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
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
1"""使用色の色相・彩度・明度調整(Issue #22)。
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)
18from .i18n import tr
20#: スライダーの範囲。色相は一周、彩度・明度は±100%。
21HUE_RANGE = (-180, 180)
22SATURATION_RANGE = (-100, 100)
23VALUE_RANGE = (-100, 100)
26def shifted_color(rgb, hue, saturation, value):
27 """``rgb`` を HSV 空間でずらした RGB タプルを返す。
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()
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
63class HsvAdjustDialog(QDialog):
64 """色相・彩度・明度のスライダー。動かすたび ``previewChanged`` を出す。"""
66 #: ``{元の RGB: ずらした RGB}``。
67 previewChanged = Signal(object)
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]
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)
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, "%")
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)
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
120 def _emit_preview(self):
121 self.previewChanged.emit(self.mapping())
123 def mapping(self):
124 return shifted_mapping(
125 self._rgbs,
126 self.hue.value(),
127 self.saturation.value(),
128 self.value.value(),
129 )