Krita Source Code Documentation
Loading...
Searching...
No Matches
mutator.py
Go to the documentation of this file.
1'''
2Licensed under the MIT License.
3
4Copyright (c) 2018 Eoin O'Neill <eoinoneill1991@gmail.com>
5Copyright (c) 2018 Emmet O'Neill <emmetoneill.pdx@gmail.com>
6
7Permission is hereby granted, free of charge, to any person obtaining a copy
8of this software and associated documentation files (the "Software"), to deal
9in the Software without restriction, including without limitation the rights
10to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11copies of the Software, and to permit persons to whom the Software is
12furnished to do so, subject to the following conditions:
13
14The above copyright notice and this permission notice shall be included in all
15copies or substantial portions of the Software.
16
17THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23SOFTWARE.
24'''
25
26
27import math, random
28try:
29 from PyQt6.QtGui import QIcon
30 from PyQt6.QtWidgets import QWidget, QVBoxLayout, QSizePolicy, QPushButton
31 from PyQt6.QtGui import QAction
32except:
33 from PyQt5.QtGui import QIcon
34 from PyQt5.QtWidgets import QWidget, QAction, QVBoxLayout, QSizePolicy, QPushButton
35from krita import Krita, Extension, DockWidget, DockWidgetFactory, SliderSpinBox, ManagedColor
36from builtins import i18n
37
38# Global mutation settings...
39# (Typically normalized within 0.0-1.0 range.
40# Controlled via sliders within MutatorDocker GUI.)
41nSizeMut = 0.5
42nRotationMut = 1.0
43nOpacityMut = 0.1
44nFlowMut = 0.1
45nHueMut = 0.2
46nSaturationMut = 0.2
47nValueMut = 0.1
48
49
50# Usability-tuned maximum mutation values "constants"...
51# (Think of these as the *largest possible mutation* for each parameter
52# when the slider is set to 100%. Can be modified to user taste!)
54 #(lowThreshold, highThreshold, scale)
55 return (10, 400, 0.25)
56rotationMutMax = 180
57opacityMutMax = 0.3
58flowMutMax = 0.3
59hueMutMax = 0.125
60saturationMutMax = 0.3
61valueMutMax = 0.25
62
63
64class Mutator(Extension):
65 ''' Mutator Class - Krita Extension
66 The Mutator Krita extension script randomly mutates some of the artist's
67 key brush and color settings by some configurable amount.
68 (When the extension is active settings can be configured in Krita's GUI using sliders in the MutatorDocker.)
69 '''
70 def __init__(self,parent):
71 super().__init__(parent)
72
73
74 def setup(self):
75 pass
76
77
78 def createActions(self, window):
79 '''
80 Adds an "action" to the Krita menus, which connects to the mutate function.
81 '''
82 action = window.createAction("mutate", "Mutate", "tools/scripting")
83 action.triggered.connect(self.mutatemutate)
84
85
86 def mutate(self):
87 '''
88 Mutates current brush/color/etc. settings by some user-configurable amount.
89 Configurable settings are some percentage of a hard maximum amount for usability tuning.
90 Mutation is triggered *manually* by the artist via action, hotkey, or button,
91 whenever some randomness or brush/color variation is desired.
92 '''
93 window = Krita.instance().activeWindow()
94 if window == None:
95 return
96 view = window.activeView()
97 if view == None:
98 return
99 if view.document() == None:
100 return
101
102 #Brush mutations...
103 newSize = view.brushSize() + calculate_mutation(clamp(sizeMutMax()[0], sizeMutMax()[1], view.brushSize()) * sizeMutMax()[2], nSizeMut)
104 view.setBrushSize(clamp(1, 1000, newSize))
105
106 newRotation = view.brushRotation() + calculate_mutation(rotationMutMax, nRotationMut)
107 view.setBrushRotation(newRotation)
108
109 newOpacity = view.paintingOpacity() + calculate_mutation(opacityMutMax, nOpacityMut)
110 view.setPaintingOpacity(clamp(0.01, 1, newOpacity))
111
112 newFlow = view.paintingFlow() + calculate_mutation(flowMutMax, nFlowMut)
113 view.setPaintingFlow(clamp(0.01, 1, newFlow))
114
115 #Color mutations...
116 managedColorFG = view.foregroundColor()
117 canvasColorFG = managedColorFG.colorForCanvas(view.canvas())
118
119 mutatedNormalizedHue = canvasColorFG.hueF() + calculate_mutation(hueMutMax, nHueMut)
120 mutatedNormalizedSaturation = clamp(0.01, 1, canvasColorFG.saturationF() + calculate_mutation(saturationMutMax, nSaturationMut))
121 mutatedNormalizedValue = clamp(0, 1, canvasColorFG.valueF() + calculate_mutation(valueMutMax, nValueMut))
122
123 canvasColorFG.setHsvF(mutatedNormalizedHue, mutatedNormalizedSaturation, mutatedNormalizedValue)
124 view.setForeGroundColor(ManagedColor.fromQColor(canvasColorFG))
125
126 # Low-priority canvas-floating message...
127 view.showFloatingMessage(i18n("Settings mutated!"), QIcon(), 1000, 2)
128
129
130def calculate_mutation(mutationMax, nScale):
131 '''
132 mutationMax <- maximum possible mutation value.
133 nScale <- normalized (0.0..1.0) percentage (float).
134 Returns a randomized mutation value within range from -mutationMax..mutationMax, scaled by nScale.
135 '''
136 # return random.uniform(0, math.pi * 2) * mutationMax * nScale # Linear distribution (Evenly random.)
137 return math.sin(random.uniform(0, math.pi * 2)) * mutationMax * nScale # Sine distribution (Randomness biased towards more extreme mutations.)
138
139
140def clamp(minimum, maximum, input):
141 '''
142 Clamp input to some value between the minimum and maximum values.
143 Used to keep values within expected ranges.
144 '''
145 return min(maximum, max(input, minimum))
146
147
148#GUI
149class MutatorDocker(DockWidget):
150 ''' MutatorDocker - Krita DockWidget
151 This class handles the GUI elements that assign mutation values.
152 Can be found inside Krita's Settings>Dockers menu.
153 '''
154 def __init__(self):
155 super().__init__()
156
157 self.setWindowTitle(i18n("Mutator"))
158
159 # Create body, set widget and setup layout...
160 body = QWidget(self)
161 self.setWidget(body)
162 body.setLayout(QVBoxLayout())
163 body.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred))
164
165 # Create mutation amount sliders...
166 mutationSettings = QWidget()
167 body.layout().addWidget(mutationSettings)
168 mutationSettings.setLayout(QVBoxLayout())
169
170 sizeMutSlider = SliderSpinBox().widget() # Size
171 sizeMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global brush size."))
172 sizeMutSlider.setRange(0,100)
173 sizeMutSlider.setPrefix(i18n("Size Mutation: "))
174 sizeMutSlider.setSuffix("%")
175 sizeMutSlider.valueChanged.connect(self.update_size_mutupdate_size_mut)
176 sizeMutSlider.setValue(int(nSizeMut * 100))
177 mutationSettings.layout().addWidget(sizeMutSlider)
178
179 rotationMutSlider = SliderSpinBox().widget() # Rotation
180 rotationMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global brush rotation."))
181 rotationMutSlider.setRange(0, 100)
182 rotationMutSlider.setPrefix(i18n("Rotation Mutation: "))
183 rotationMutSlider.setSuffix("%")
184 rotationMutSlider.valueChanged.connect(self.update_rotation_mutupdate_rotation_mut)
185 rotationMutSlider.setValue(int(nRotationMut * 100))
186 mutationSettings.layout().addWidget(rotationMutSlider)
187
188 opacityMutSlider = SliderSpinBox().widget() # Opacity
189 opacityMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global brush opacity."))
190 opacityMutSlider.setRange(0, 100)
191 opacityMutSlider.setPrefix(i18n("Opacity Mutation: "))
192 opacityMutSlider.setSuffix("%")
193 opacityMutSlider.valueChanged.connect(self.update_opacity_mutupdate_opacity_mut)
194 opacityMutSlider.setValue(int(nOpacityMut * 100))
195 mutationSettings.layout().addWidget(opacityMutSlider)
196
197 flowMutSlider = SliderSpinBox().widget() # Flow
198 flowMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global brush flow."))
199 flowMutSlider.setRange(0, 100)
200 flowMutSlider.setPrefix(i18n("Flow Mutation: "))
201 flowMutSlider.setSuffix("%")
202 flowMutSlider.valueChanged.connect(self.update_flow_mutupdate_flow_mut)
203 flowMutSlider.setValue(int(nFlowMut * 100))
204 mutationSettings.layout().addWidget(flowMutSlider)
205
206 hueMutSlider = SliderSpinBox().widget() # FGC Hue
207 hueMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global foreground color hue."))
208 hueMutSlider.setRange(0, 100)
209 hueMutSlider.setPrefix(i18n("Hue Mutation: "))
210 hueMutSlider.setSuffix("%")
211 hueMutSlider.valueChanged.connect(self.update_fgc_hue_mutupdate_fgc_hue_mut)
212 hueMutSlider.setValue(int(nHueMut * 100))
213 mutationSettings.layout().addWidget(hueMutSlider)
214
215 saturationMutSlider = SliderSpinBox().widget() # FGC Saturation
216 saturationMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global foreground color saturation."))
217 saturationMutSlider.setRange(0, 100)
218 saturationMutSlider.setPrefix(i18n("Saturation Mutation: "))
219 saturationMutSlider.setSuffix("%")
220 saturationMutSlider.valueChanged.connect(self.update_fgc_saturation_mutupdate_fgc_saturation_mut)
221 saturationMutSlider.setValue(int(nSaturationMut * 100))
222 mutationSettings.layout().addWidget(saturationMutSlider)
223
224 valueMutSlider = SliderSpinBox().widget() # FGC Value
225 valueMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global foreground color value."))
226 valueMutSlider.setRange(0, 100)
227 valueMutSlider.setPrefix(i18n("Value Mutation: "))
228 valueMutSlider.setSuffix("%")
229 valueMutSlider.valueChanged.connect(self.update_fgc_value_mutupdate_fgc_value_mut)
230 valueMutSlider.setValue(int(nValueMut * 100))
231 mutationSettings.layout().addWidget(valueMutSlider)
232
233 # Spacer
234 body.layout().addStretch()
235
236 # Create mutate button...
237 mutateButton = QPushButton(i18n("Mutate"))
238 mutateButton.setToolTip(i18n("Invokes the \"Mutate\" action, which randomly mutates various global brush and color settings based on the mutation settings configured above."))
239 mutateButton.clicked.connect(self.trigger_mutatetrigger_mutate)
240 body.layout().addWidget(mutateButton)
241
242
243 # Slider event handlers...
244 # Note: Sliders range from 0-100%, but global mutation state is normalized from 0.0-1.0.
245 def update_size_mut(self, value):
246 global nSizeMut
247 nSizeMut = value / 100
248
249
250 def update_rotation_mut(self, value):
251 global nRotationMut
252 nRotationMut = value / 100
253
254
255 def update_opacity_mut(self, value):
256 global nOpacityMut
257 nOpacityMut = value / 100
258
259
260 def update_flow_mut(self, value):
261 global nFlowMut
262 nFlowMut = value / 100
263
264
265 def update_fgc_hue_mut(self, value):
266 global nHueMut
267 nHueMut = value / 100
268
269
271 global nSaturationMut
272 nSaturationMut = value / 100
273
274
275 def update_fgc_value_mut(self, value):
276 global nValueMut
277 nValueMut = value / 100
278
279
280 def trigger_mutate(self):
281 Krita.instance().action("mutate").activate(QAction.Trigger)
282
283
284 def canvasChanged(self, canvas): # Unused
285 pass
286
287
288# Krita boilerplate.
289Krita.instance().addExtension(Mutator(Krita.instance()))
290Krita.instance().addDockWidgetFactory(DockWidgetFactory("mutatorDocker", DockWidgetFactory.DockPosition.DockRight, MutatorDocker))
static Krita * instance()
instance retrieve the singleton instance of the Application object.
Definition Krita.cpp:390
static ManagedColor * fromQColor(const QColor &qcolor, Canvas *canvas=0)
fromQColor is the (approximate) reverse of colorForCanvas()
update_fgc_value_mut(self, value)
Definition mutator.py:275
update_rotation_mut(self, value)
Definition mutator.py:250
update_fgc_hue_mut(self, value)
Definition mutator.py:265
update_opacity_mut(self, value)
Definition mutator.py:255
update_fgc_saturation_mut(self, value)
Definition mutator.py:270
canvasChanged(self, canvas)
Definition mutator.py:284
createActions(self, window)
Definition mutator.py:78
__init__(self, parent)
Definition mutator.py:70
clamp(minimum, maximum, input)
Definition mutator.py:140
calculate_mutation(mutationMax, nScale)
Definition mutator.py:130