Coverage for paintmaskanimator/hsv_adjust_dialog.py: 100%

65 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 04:50 +0000

1"""使用色の色相・彩度・明度調整(Issue #22)。 

2 

3使用色パネルで選んだ色をまとめてずらす。スライダーを動かすあいだはキャンバス 

4の表示だけを差し替えて見せ(``canvas.preview_color_remap``)、OKで初めて全コマ 

5へ適用する。元の絵は確定するまで変わらない。 

6""" 

7from PySide6.QtCore import Qt, Signal 

8from PySide6.QtGui import QColor 

9from PySide6.QtWidgets import ( 

10 QDialog, 

11 QDialogButtonBox, 

12 QHBoxLayout, 

13 QLabel, 

14 QSlider, 

15 QVBoxLayout, 

16) 

17 

18from .i18n import tr 

19 

20#: スライダーの範囲。色相は一周、彩度・明度は±100%。 

21HUE_RANGE = (-180, 180) 

22SATURATION_RANGE = (-100, 100) 

23VALUE_RANGE = (-100, 100) 

24 

25 

26def shifted_color(rgb, hue, saturation, value): 

27 """``rgb`` を HSV 空間でずらした RGB タプルを返す。 

28 

29 :param hue: 色相の回転(度)。一周を超えたぶんは回り込む。 

30 :param saturation: 彩度の増減(%)。 

31 :param value: 明度の増減(%)。 

32 """ 

33 color = QColor(*(int(channel) for channel in rgb[:3])) 

34 current_hue = color.hue() 

35 # 無彩色(hue が -1)は色相を持たないので、回しても色は付かない。 

36 new_hue = ( 

37 -1 if current_hue < 0 

38 else int((current_hue + int(hue)) % 360) 

39 ) 

40 new_saturation = max(0, min(255, color.saturation() + round(saturation * 2.55))) 

41 new_value = max(0, min(255, color.value() + round(value * 2.55))) 

42 shifted = QColor.fromHsv( 

43 new_hue if new_hue >= 0 else 0, 

44 0 if new_hue < 0 else new_saturation, 

45 new_value, 

46 ) 

47 return shifted.red(), shifted.green(), shifted.blue() 

48 

49 

50def shifted_mapping(rgbs, hue, saturation, value): 

51 """``{元の RGB: ずらした RGB}``。変わらない色と白は含めない。""" 

52 mapping = {} 

53 for rgb in rgbs: 

54 source = tuple(int(channel) for channel in rgb[:3]) 

55 if source == (255, 255, 255): 

56 continue 

57 destination = shifted_color(source, hue, saturation, value) 

58 if destination != source: 

59 mapping[source] = destination 

60 return mapping 

61 

62 

63class HsvAdjustDialog(QDialog): 

64 """色相・彩度・明度のスライダー。動かすたび ``previewChanged`` を出す。""" 

65 

66 #: ``{元の RGB: ずらした RGB}``。 

67 previewChanged = Signal(object) 

68 

69 def __init__(self, rgbs, parent=None): 

70 super().__init__(parent) 

71 self.setWindowTitle(tr("色相・彩度・明度")) 

72 self.setModal(True) 

73 self.resize(420, 190) 

74 self._rgbs = [tuple(int(channel) for channel in rgb[:3]) for rgb in rgbs] 

75 

76 layout = QVBoxLayout(self) 

77 note = QLabel( 

78 tr("選択中の{count}色をまとめて調整します。" 

79 "OKで、選択レイヤーのすべてのコマへ適用します。").format( 

80 count=len(self._rgbs) 

81 ) 

82 ) 

83 note.setWordWrap(True) 

84 layout.addWidget(note) 

85 

86 self.hue = self._add_slider(layout, tr("色相"), HUE_RANGE, "°") 

87 self.saturation = self._add_slider( 

88 layout, tr("彩度"), SATURATION_RANGE, "%" 

89 ) 

90 self.value = self._add_slider(layout, tr("明度"), VALUE_RANGE, "%") 

91 

92 buttons = QDialogButtonBox( 

93 QDialogButtonBox.StandardButton.Ok 

94 | QDialogButtonBox.StandardButton.Cancel 

95 ) 

96 buttons.accepted.connect(self.accept) 

97 buttons.rejected.connect(self.reject) 

98 layout.addWidget(buttons) 

99 

100 def _add_slider(self, layout, label, value_range, suffix): 

101 row = QHBoxLayout() 

102 caption = QLabel(label) 

103 caption.setFixedWidth(42) 

104 slider = QSlider(Qt.Orientation.Horizontal) 

105 slider.setRange(*value_range) 

106 slider.setValue(0) 

107 readout = QLabel(f"0{suffix}") 

108 readout.setFixedWidth(52) 

109 readout.setAlignment(Qt.AlignmentFlag.AlignRight) 

110 slider.valueChanged.connect( 

111 lambda current: readout.setText(f"{current:+d}{suffix}") 

112 ) 

113 slider.valueChanged.connect(self._emit_preview) 

114 row.addWidget(caption) 

115 row.addWidget(slider, 1) 

116 row.addWidget(readout) 

117 layout.addLayout(row) 

118 return slider 

119 

120 def _emit_preview(self): 

121 self.previewChanged.emit(self.mapping()) 

122 

123 def mapping(self): 

124 return shifted_mapping( 

125 self._rgbs, 

126 self.hue.value(), 

127 self.saturation.value(), 

128 self.value.value(), 

129 )