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", i18n("Mutate"), "tools/scripts")
83 action.triggered.connect(self.mutatemutate)
84
85 def mutate(self):
86 '''
87 Mutates current brush/color/etc. settings by some user-configurable amount.
88 Configurable settings are some percentage of a hard maximum amount for usability tuning.
89 Mutation is triggered *manually* by the artist via action, hotkey, or button,
90 whenever some randomness or brush/color variation is desired.
91 '''
92 window = Krita.instance().activeWindow()
93 if window == None:
94 return
95 view = window.activeView()
96 if view == None:
97 return
98 if view.document() == None:
99 return
100
101 #Brush mutations...
102 newSize = view.brushSize() + calculate_mutation(clamp(sizeMutMax()[0], sizeMutMax()[1], view.brushSize()) * sizeMutMax()[2], nSizeMut)
103 view.setBrushSize(clamp(1, 1000, newSize))
104
105 newRotation = view.brushRotation() + calculate_mutation(rotationMutMax, nRotationMut)
106 view.setBrushRotation(newRotation)
107
108 newOpacity = view.paintingOpacity() + calculate_mutation(opacityMutMax, nOpacityMut)
109 view.setPaintingOpacity(clamp(0.01, 1, newOpacity))
110
111 newFlow = view.paintingFlow() + calculate_mutation(flowMutMax, nFlowMut)
112 view.setPaintingFlow(clamp(0.01, 1, newFlow))
113
114 #Color mutations...
115 managedColorFG = view.foregroundColor()
116 canvasColorFG = managedColorFG.colorForCanvas(view.canvas())
117
118 mutatedNormalizedHue = canvasColorFG.hueF() + calculate_mutation(hueMutMax, nHueMut)
119 mutatedNormalizedSaturation = clamp(0.01, 1, canvasColorFG.saturationF() + calculate_mutation(saturationMutMax, nSaturationMut))
120 mutatedNormalizedValue = clamp(0, 1, canvasColorFG.valueF() + calculate_mutation(valueMutMax, nValueMut))
121
122 canvasColorFG.setHsvF(mutatedNormalizedHue, mutatedNormalizedSaturation, mutatedNormalizedValue)
123 view.setForeGroundColor(ManagedColor.fromQColor(canvasColorFG))
124
125 # Low-priority canvas-floating message...
126 view.showFloatingMessage(i18n("Settings mutated!"), QIcon(), 1000, 2)
127
128
129def calculate_mutation(mutationMax, nScale):
130 '''
131 mutationMax <- maximum possible mutation value.
132 nScale <- normalized (0.0..1.0) percentage (float).
133 Returns a randomized mutation value within range from -mutationMax..mutationMax, scaled by nScale.
134 '''
135 # return random.uniform(0, math.pi * 2) * mutationMax * nScale # Linear distribution (Evenly random.)
136 return math.sin(random.uniform(0, math.pi * 2)) * mutationMax * nScale # Sine distribution (Randomness biased towards more extreme mutations.)
137
138
139def clamp(minimum, maximum, input):
140 '''
141 Clamp input to some value between the minimum and maximum values.
142 Used to keep values within expected ranges.
143 '''
144 return min(maximum, max(input, minimum))
145
146
147#GUI
148class MutatorDocker(DockWidget):
149 ''' MutatorDocker - Krita DockWidget
150 This class handles the GUI elements that assign mutation values.
151 Can be found inside Krita's Settings>Dockers menu.
152 '''
153 def __init__(self):
154 super().__init__()
155
156 self.setWindowTitle(i18n("Mutator"))
157
158 # Create body, set widget and setup layout...
159 body = QWidget(self)
160 self.setWidget(body)
161 body.setLayout(QVBoxLayout())
162 body.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred))
163
164 # Create mutation amount sliders...
165 mutationSettings = QWidget()
166 body.layout().addWidget(mutationSettings)
167 mutationSettings.setLayout(QVBoxLayout())
168
169 sizeMutSlider = SliderSpinBox().widget() # Size
170 sizeMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global brush size."))
171 sizeMutSlider.setRange(0,100)
172 sizeMutSlider.setPrefix(i18n("Size Mutation: "))
173 sizeMutSlider.setSuffix("%")
174 sizeMutSlider.valueChanged.connect(self.update_size_mutupdate_size_mut)
175 sizeMutSlider.setValue(int(nSizeMut * 100))
176 mutationSettings.layout().addWidget(sizeMutSlider)
177
178 rotationMutSlider = SliderSpinBox().widget() # Rotation
179 rotationMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global brush rotation."))
180 rotationMutSlider.setRange(0, 100)
181 rotationMutSlider.setPrefix(i18n("Rotation Mutation: "))
182 rotationMutSlider.setSuffix("%")
183 rotationMutSlider.valueChanged.connect(self.update_rotation_mutupdate_rotation_mut)
184 rotationMutSlider.setValue(int(nRotationMut * 100))
185 mutationSettings.layout().addWidget(rotationMutSlider)
186
187 opacityMutSlider = SliderSpinBox().widget() # Opacity
188 opacityMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global brush opacity."))
189 opacityMutSlider.setRange(0, 100)
190 opacityMutSlider.setPrefix(i18n("Opacity Mutation: "))
191 opacityMutSlider.setSuffix("%")
192 opacityMutSlider.valueChanged.connect(self.update_opacity_mutupdate_opacity_mut)
193 opacityMutSlider.setValue(int(nOpacityMut * 100))
194 mutationSettings.layout().addWidget(opacityMutSlider)
195
196 flowMutSlider = SliderSpinBox().widget() # Flow
197 flowMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global brush flow."))
198 flowMutSlider.setRange(0, 100)
199 flowMutSlider.setPrefix(i18n("Flow Mutation: "))
200 flowMutSlider.setSuffix("%")
201 flowMutSlider.valueChanged.connect(self.update_flow_mutupdate_flow_mut)
202 flowMutSlider.setValue(int(nFlowMut * 100))
203 mutationSettings.layout().addWidget(flowMutSlider)
204
205 hueMutSlider = SliderSpinBox().widget() # FGC Hue
206 hueMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global foreground color hue."))
207 hueMutSlider.setRange(0, 100)
208 hueMutSlider.setPrefix(i18n("Hue Mutation: "))
209 hueMutSlider.setSuffix("%")
210 hueMutSlider.valueChanged.connect(self.update_fgc_hue_mutupdate_fgc_hue_mut)
211 hueMutSlider.setValue(int(nHueMut * 100))
212 mutationSettings.layout().addWidget(hueMutSlider)
213
214 saturationMutSlider = SliderSpinBox().widget() # FGC Saturation
215 saturationMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global foreground color saturation."))
216 saturationMutSlider.setRange(0, 100)
217 saturationMutSlider.setPrefix(i18n("Saturation Mutation: "))
218 saturationMutSlider.setSuffix("%")
219 saturationMutSlider.valueChanged.connect(self.update_fgc_saturation_mutupdate_fgc_saturation_mut)
220 saturationMutSlider.setValue(int(nSaturationMut * 100))
221 mutationSettings.layout().addWidget(saturationMutSlider)
222
223 valueMutSlider = SliderSpinBox().widget() # FGC Value
224 valueMutSlider.setToolTip(i18n("Controls the degree to which mutation affects Krita's global foreground color value."))
225 valueMutSlider.setRange(0, 100)
226 valueMutSlider.setPrefix(i18n("Value Mutation: "))
227 valueMutSlider.setSuffix("%")
228 valueMutSlider.valueChanged.connect(self.update_fgc_value_mutupdate_fgc_value_mut)
229 valueMutSlider.setValue(int(nValueMut * 100))
230 mutationSettings.layout().addWidget(valueMutSlider)
231
232 # Spacer
233 body.layout().addStretch()
234
235 # Create mutate button...
236 mutateButton = QPushButton(i18n("Mutate"))
237 mutateButton.setToolTip(i18n("Invokes the \"Mutate\" action, which randomly mutates various global brush and color settings based on the mutation settings configured above."))
238 mutateButton.clicked.connect(self.trigger_mutatetrigger_mutate)
239 body.layout().addWidget(mutateButton)
240
241
242 # Slider event handlers...
243 # Note: Sliders range from 0-100%, but global mutation state is normalized from 0.0-1.0.
244 def update_size_mut(self, value):
245 global nSizeMut
246 nSizeMut = value / 100
247
248
249 def update_rotation_mut(self, value):
250 global nRotationMut
251 nRotationMut = value / 100
252
253
254 def update_opacity_mut(self, value):
255 global nOpacityMut
256 nOpacityMut = value / 100
257
258
259 def update_flow_mut(self, value):
260 global nFlowMut
261 nFlowMut = value / 100
262
263
264 def update_fgc_hue_mut(self, value):
265 global nHueMut
266 nHueMut = value / 100
267
268
270 global nSaturationMut
271 nSaturationMut = value / 100
272
273
274 def update_fgc_value_mut(self, value):
275 global nValueMut
276 nValueMut = value / 100
277
278
279 def trigger_mutate(self):
280 Krita.instance().action("mutate").activate(QAction.Trigger)
281
282
283 def canvasChanged(self, canvas): # Unused
284 pass
285
286
287# Krita boilerplate.
288Krita.instance().addExtension(Mutator(Krita.instance()))
289Krita.instance().addDockWidgetFactory(DockWidgetFactory("mutatorDocker", DockWidgetFactory.DockPosition.DockRight, MutatorDocker))
static Krita * instance()
instance retrieve the singleton instance of the Application object.
Definition Krita.cpp:396
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:274
update_rotation_mut(self, value)
Definition mutator.py:249
update_fgc_hue_mut(self, value)
Definition mutator.py:264
update_opacity_mut(self, value)
Definition mutator.py:254
update_fgc_saturation_mut(self, value)
Definition mutator.py:269
canvasChanged(self, canvas)
Definition mutator.py:283
createActions(self, window)
Definition mutator.py:78
__init__(self, parent)
Definition mutator.py:70
clamp(minimum, maximum, input)
Definition mutator.py:139
calculate_mutation(mutationMax, nScale)
Definition mutator.py:129