1 /**
2 Film-strip knob for a flat UI.
3
4 Copyright: Guillaume Piolat 2015-2018.
5 Copyright: Ethan Reker 2017.
6 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7 */
8 module dplug.flatwidgets.flatknob;
9
10 import std.math;
11
12 import dplug.core.math;
13 import dplug.gui.element;
14 import dplug.client.params;
15
16 /**
17 * UIFilmstripKnob is a knob with a vertical RGBA image describing the knob graphics.
18 */
19 class UIFilmstripKnob : UIElement, IParameterListener
20 {
21 public:
22 nothrow:
23 @nogc:
24
25 // This will change to 1.0f at one point for consistency, so better express your knob
26 // sensivity with that.
27 enum defaultSensivity = 0.25f;
28
29 this(UIContext context, Parameter param, OwnedImage!RGBA mipmap, int numFrames, float sensitivity = 0.25)
30 {
31 super(context, flagRaw);
32 _param = param;
33 _sensitivity = sensitivity;
34 _filmstrip = mipmap;
35 _numFrames = numFrames;
36 _knobWidth = _filmstrip.w;
37 _knobHeight = _filmstrip.h / _numFrames;
38 _param.addListener(this);
39 _disabled = false;
40 _filmstripScaled = mallocNew!(OwnedImage!RGBA)();
41 _frameNthResized.reallocBuffer(_numFrames);
42 }
43
44 ~this()
45 {
46 _param.removeListener(this);
47 _filmstripScaled.destroyFree();
48 _frameNthResized.reallocBuffer(0);
49 }
50
51 override void reflow()
52 {
53 int W = position.width;
54 int H = position.height * _numFrames;
55 if (_filmstripScaled.w != W || _filmstripScaled.h != H)
56 {
57 _filmstripScaled.size(position.width, position.height * _numFrames);
58 _frameNthResized[] = false;
59 }
60 }
61
62 /// Returns: sensivity.
63 float sensivity()
64 {
65 return _sensitivity;
66 }
67
68 /// Sets sensivity.
69 float sensivity(float sensitivity)
70 {
71 return _sensitivity = sensitivity;
72 }
73
74 void disable()
75 {
76 _disabled = true;
77 }
78
79 override void onDrawRaw(ImageRef!RGBA rawMap, box2i[] dirtyRects)
80 {
81 setCurrentImage();
82 ImageRef!RGBA _currentImage = _filmstripScaled.toRef().cropImageRef(box2i(_imageX1, _imageY1, _imageX2, _imageY2));
83 foreach(dirtyRect; dirtyRects)
84 {
85 ImageRef!RGBA croppedRawIn = _currentImage.cropImageRef(dirtyRect);
86 ImageRef!RGBA croppedRawOut = rawMap.cropImageRef(dirtyRect);
87
88 int w = dirtyRect.width;
89 int h = dirtyRect.height;
90
91 for(int j = 0; j < h; ++j)
92 {
93 RGBA[] input = croppedRawIn.scanline(j);
94 RGBA[] output = croppedRawOut.scanline(j);
95
96 for(int i = 0; i < w; ++i)
97 {
98 ubyte alpha = input[i].a;
99
100 RGBA color = blendColor(input[i], output[i], alpha);
101 output[i] = color;
102 }
103 }
104
105 }
106 }
107
108 float distance(float x1, float x2, float y1, float y2)
109 {
110 return sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
111 }
112
113 void setCurrentImage()
114 {
115 float value = _param.getNormalized();
116 currentFrame = cast(int)(round(value * (_numFrames - 1)));
117
118 if(currentFrame < 0) currentFrame = 0;
119
120 _imageX1 = 0;
121 _imageY1 = (_filmstripScaled.h / _numFrames) * currentFrame;
122
123 _imageX2 = _filmstripScaled.w;
124 _imageY2 = _imageY1 + (_filmstripScaled.h / _numFrames);
125
126 cacheImage(currentFrame);
127 }
128
129 override Click onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate)
130 {
131 if (!containsPoint(x, y))
132 return Click.unhandled;
133
134 // double-click => set to default
135 if (isDoubleClick || mstate.altPressed)
136 {
137 _param.beginParamEdit();
138 if (auto fp = cast(FloatParameter)_param)
139 fp.setFromGUI(fp.defaultValue());
140 else if (auto ip = cast(IntegerParameter)_param)
141 ip.setFromGUI(ip.defaultValue());
142 else
143 assert(false);
144 _param.endParamEdit();
145 }
146
147 _normalizedValueWhileDragging = _param.getNormalized();
148
149 return Click.startDrag; // to initiate dragging
150 }
151
152 // Called when mouse drag this Element.
153 override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate)
154 {
155 if(!_disabled)
156 {
157 float displacementInHeight = cast(float)(dy) / _position.height;
158
159 float modifier = 1.0f;
160 if (mstate.shiftPressed || mstate.ctrlPressed)
161 modifier *= 0.1f;
162
163 double oldParamValue = _normalizedValueWhileDragging;
164 double newParamValue = oldParamValue - displacementInHeight * modifier * _sensitivity;
165 if (mstate.altPressed)
166 newParamValue = _param.getNormalizedDefault();
167
168 if (y > _mousePosOnLast0Cross)
169 return;
170 if (y < _mousePosOnLast1Cross)
171 return;
172
173 if (newParamValue <= 0 && oldParamValue > 0)
174 _mousePosOnLast0Cross = y;
175
176 if (newParamValue >= 1 && oldParamValue < 1)
177 _mousePosOnLast1Cross = y;
178
179 if (newParamValue < 0)
180 newParamValue = 0;
181 if (newParamValue > 1)
182 newParamValue = 1;
183
184 if (newParamValue > 0)
185 _mousePosOnLast0Cross = float.infinity;
186
187 if (newParamValue < 1)
188 _mousePosOnLast1Cross = -float.infinity;
189
190 if (newParamValue != oldParamValue)
191 {
192 if (auto fp = cast(FloatParameter)_param)
193 fp.setFromGUINormalized(newParamValue);
194 else if (auto ip = cast(IntegerParameter)_param)
195 ip.setFromGUINormalized(newParamValue);
196 else
197 assert(false);
198 _normalizedValueWhileDragging = newParamValue;
199 }
200 }
201 }
202
203 // For lazy updates
204 override void onBeginDrag()
205 {
206 _param.beginParamEdit();
207 setDirtyWhole();
208 }
209
210 override void onStopDrag()
211 {
212 _param.endParamEdit();
213 clearCrosspoints();
214 setDirtyWhole();
215 }
216
217 override void onMouseMove(int x, int y, int dx, int dy, MouseState mstate)
218 {
219 }
220
221 override void onMouseEnter()
222 {
223 _param.beginParamHover();
224 }
225
226 override void onMouseExit()
227 {
228 _param.endParamHover();
229 }
230
231 override void onParameterChanged(Parameter sender) nothrow @nogc
232 {
233 setDirtyWhole();
234 }
235
236 override void onBeginParameterEdit(Parameter sender)
237 {
238 }
239
240 override void onEndParameterEdit(Parameter sender)
241 {
242 }
243
244 override void onBeginParameterHover(Parameter sender)
245 {
246 }
247
248 override void onEndParameterHover(Parameter sender)
249 {
250 }
251
252 protected:
253
254 /// The parameter this knob is linked with.
255 Parameter _param;
256
257 OwnedImage!RGBA _filmstrip;
258 OwnedImage!RGBA _filmstripScaled;
259
260 ImageRef!RGBA _currentImage;
261
262 int _numFrames;
263 int _imageX1, _imageX2, _imageY1, _imageY2;
264 int currentFrame;
265
266 int _knobWidth;
267 int _knobHeight;
268
269 float _pushedAnimation;
270
271 /// Sensivity: given a mouse movement in 100th of the height of the knob,
272 /// how much should the normalized parameter change.
273 float _sensitivity;
274
275 float _mousePosOnLast0Cross;
276 float _mousePosOnLast1Cross;
277
278 // Normalized value last set while dragging. Necessary for integer paramerers
279 // that may round that normalized value when setting the parameter.
280 float _normalizedValueWhileDragging;
281
282 bool _disabled;
283
284 ImageResizer _resizer;
285 bool[] _frameNthResized;
286
287 void clearCrosspoints() nothrow @nogc
288 {
289 _mousePosOnLast0Cross = float.infinity;
290 _mousePosOnLast1Cross = -float.infinity;
291 }
292
293 final bool containsPoint(int x, int y) nothrow @nogc
294 {
295 vec2f center = getCenter();
296 return vec2f(x, y).distanceTo(center) < getRadius();
297 }
298
299 /// Returns: largest square centered in _position
300 final box2i getSubsquare() pure const nothrow @nogc
301 {
302 // We'll draw entirely in the largest centered square in _position.
303 box2i subSquare;
304 if (_position.width > _position.height)
305 {
306 int offset = (_position.width - _position.height) / 2;
307 int minX = offset;
308 subSquare = box2i(minX, 0, minX + _position.height, _position.height);
309 }
310 else
311 {
312 int offset = (_position.height - _position.width) / 2;
313 int minY = offset;
314 subSquare = box2i(0, minY, _position.width, minY + _position.width);
315 }
316 return subSquare;
317 }
318
319 final float getRadius() pure const nothrow @nogc
320 {
321 return getSubsquare().width * 0.5f;
322 }
323
324 final vec2f getCenter() pure const nothrow @nogc
325 {
326 box2i subSquare = getSubsquare();
327 float centerx = (subSquare.min.x + subSquare.max.x - 1) * 0.5f;
328 float centery = (subSquare.min.y + subSquare.max.y - 1) * 0.5f;
329 return vec2f(centerx, centery);
330 }
331
332 // Resize sub-image lazily. Instead of doing it in `reflow()` for all frames,
333 // we do it in a case by case basis when needed in `onDrawRaw`.
334 void cacheImage(int frame)
335 {
336 // Limitation: the source image should have identically sized sub-frames.
337 assert(_filmstrip.h % _numFrames == 0);
338
339 if (_frameNthResized[frame])
340 return;
341
342 int hsource = _filmstrip.h / _numFrames;
343 int hdest = _filmstripScaled.h / _numFrames;
344
345 // Note: in order to avoid slight sample offsets, each subframe needs to be resized separately.
346 ImageRef!RGBA source = _filmstrip.toRef.cropImageRef(rectangle(0, hsource * frame, _filmstrip.w, hsource));
347 ImageRef!RGBA dest = _filmstripScaled.toRef.cropImageRef(rectangle(0, hdest * frame, _filmstripScaled.w, hdest ));
348 _resizer.resizeImage_sRGBNoAlpha(source, dest);
349
350 _frameNthResized[frame] = true;
351 }
352 }