1 /**
2 * Film-strip knob for a flat UI.
3 * Copyright: Copyright Auburn Sounds 2015
4 * Copyright: Copyright Cut Through Recordings 2017
5 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 * Authors:   Guillaume Piolat, Ethan Reker
7 */
8 module dplug.flatwidgets.flatknob;
9 
10 import std.math;
11 import std.algorithm.comparison;
12 
13 import dplug.core.math;
14 import dplug.graphics.drawex;
15 
16 import dplug.gui.element;
17 
18 import dplug.client.params;
19 
20 /**
21 * 
22 */
23 class UIFilmstripKnob : UIElement, IParameterListener
24 {
25 public:
26 nothrow:
27 @nogc:
28 
29     // This will change to 1.0f at one point for consistency, so better express your knob
30     // sensivity with that.
31     enum defaultSensivity = 0.25f;
32 
33     this(UIContext context, FloatParameter param, OwnedImage!RGBA mipmap, int numFrames, float sensitivity = 0.25)
34     {
35         super(context);
36         _param = param;
37         _sensitivity = sensitivity;
38         _filmstrip = mipmap;
39         _numFrames = numFrames;
40         _knobWidth = _filmstrip.w;
41         _knobHeight = _filmstrip.h / _numFrames;
42         _param.addListener(this);
43         _disabled = false;
44 
45     }
46 
47     ~this()
48     {
49         _param.removeListener(this);
50     }
51 
52     /// Returns: sensivity.
53     float sensivity()
54     {
55         return _sensitivity;
56     }
57 
58     /// Sets sensivity.
59     float sensivity(float sensitivity)
60     {
61         return _sensitivity = sensitivity;
62     }
63 
64     void disable()
65     {
66         _disabled = true;
67     }
68 
69     override void onDraw(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) nothrow @nogc
70     {
71         setCurrentImage();
72         auto _currentImage = _filmstrip.crop(box2i(_imageX1, _imageY1, _imageX2, _imageY2));
73         foreach(dirtyRect; dirtyRects){
74 
75             auto croppedDiffuseIn = _currentImage.crop(dirtyRect);
76             auto croppedDiffuseOut = diffuseMap.crop(dirtyRect);
77 
78             int w = dirtyRect.width;
79             int h = dirtyRect.height;
80 
81             for(int j = 0; j < h; ++j){
82 
83                 RGBA[] input = croppedDiffuseIn.scanline(j);
84                 RGBA[] output = croppedDiffuseOut.scanline(j);
85 
86 
87                 for(int i = 0; i < w; ++i){
88                     ubyte alpha = input[i].a;
89 
90                     RGBA color = RGBA.op!q{.blend(a, b, c)} (input[i], output[i], alpha);
91                     output[i] = color;
92                 }
93             }
94 
95         }
96     }
97 
98     float distance(float x1, float x2, float y1, float y2)
99     {
100         return sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
101     }
102 
103     void setCurrentImage()
104     {
105         float value = _param.getNormalized();
106         currentFrame = cast(int)(round(value * (_numFrames - 1)));
107 
108         if(currentFrame < 0) currentFrame = 0;
109 
110         _imageX1 = 0;
111         _imageY1 = (_filmstrip.h / _numFrames) * currentFrame;
112 
113         _imageX2 = _filmstrip.w;
114         _imageY2 = _imageY1 + (_filmstrip.h / _numFrames);
115 
116     }
117 
118     override bool onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate)
119     {
120         if (!containsPoint(x, y))
121             return false;
122 
123         // double-click => set to default
124         if (isDoubleClick)
125         {
126             _param.beginParamEdit();
127             _param.setFromGUI(_param.defaultValue());
128             _param.endParamEdit();
129         }
130 
131         return true; // to initiate dragging
132     }
133 
134     // Called when mouse drag this Element.
135     override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate)
136     {
137         if(!_disabled)
138         {
139             float displacementInHeight = cast(float)(dy) / _position.height;
140 
141             float modifier = 1.0f;
142             if (mstate.shiftPressed || mstate.ctrlPressed)
143                 modifier *= 0.1f;
144 
145             double oldParamValue = _param.getNormalized();
146 
147             double newParamValue = oldParamValue - displacementInHeight * modifier * _sensitivity;
148 
149             if (y > _mousePosOnLast0Cross)
150                 return;
151             if (y < _mousePosOnLast1Cross)
152                 return;
153 
154             if (newParamValue <= 0 && oldParamValue > 0)
155                 _mousePosOnLast0Cross = y;
156 
157             if (newParamValue >= 1 && oldParamValue < 1)
158                 _mousePosOnLast1Cross = y;
159 
160             if (newParamValue < 0)
161                 newParamValue = 0;
162             if (newParamValue > 1)
163                 newParamValue = 1;
164 
165             if (newParamValue > 0)
166                 _mousePosOnLast0Cross = float.infinity;
167 
168             if (newParamValue < 1)
169                 _mousePosOnLast1Cross = -float.infinity;
170 
171             if (newParamValue != oldParamValue)
172                 _param.setFromGUINormalized(newParamValue);
173             setDirtyWhole();
174         }
175     }
176 
177     // For lazy updates
178     override void onBeginDrag()
179     {
180         _param.beginParamEdit();
181         setDirtyWhole();
182     }
183 
184     override void onStopDrag()
185     {
186         _param.endParamEdit();
187         clearCrosspoints();
188         setDirtyWhole();
189     }
190 
191     override void onMouseMove(int x, int y, int dx, int dy, MouseState mstate)
192     {
193         
194     }
195 
196     override void onMouseExit()
197     {
198         //_shouldBeHighlighted = false;
199     }
200 
201     override void onParameterChanged(Parameter sender) nothrow @nogc
202     {
203         setDirtyWhole();
204     }
205 
206     override void onBeginParameterEdit(Parameter sender)
207     {
208     }
209 
210     override void onEndParameterEdit(Parameter sender)
211     {
212     }
213 
214 protected:
215 
216     /// The parameter this knob is linked with.
217     FloatParameter _param;
218 
219     OwnedImage!RGBA _filmstrip;
220     OwnedImage!RGBA _faderFilmstrip;
221     OwnedImage!RGBA _knobGreenFilmstrip;
222     ImageRef!RGBA _currentImage;
223 
224     int _numFrames;
225     int _imageX1, _imageX2, _imageY1, _imageY2;
226     int currentFrame;
227 
228     int _knobWidth;
229     int _knobHeight;
230 
231     float _pushedAnimation;
232 
233     /// Sensivity: given a mouse movement in 100th of the height of the knob,
234     /// how much should the normalized parameter change.
235     float _sensitivity;
236 
237     float _mousePosOnLast0Cross;
238     float _mousePosOnLast1Cross;
239 
240     bool _disabled;
241 
242     void clearCrosspoints() nothrow @nogc
243     {
244         _mousePosOnLast0Cross = float.infinity;
245         _mousePosOnLast1Cross = -float.infinity;
246     }
247 
248     final bool containsPoint(int x, int y) nothrow @nogc
249     {
250         vec2f center = getCenter();
251         return vec2f(x, y).distanceTo(center) < getRadius();
252     }
253 
254     /// Returns: largest square centered in _position
255     final box2i getSubsquare() pure const nothrow @nogc
256     {
257         // We'll draw entirely in the largest centered square in _position.
258         box2i subSquare;
259         if (_position.width > _position.height)
260         {
261             int offset = (_position.width - _position.height) / 2;
262             int minX = offset;
263             subSquare = box2i(minX, 0, minX + _position.height, _position.height);
264         }
265         else
266         {
267             int offset = (_position.height - _position.width) / 2;
268             int minY = offset;
269             subSquare = box2i(0, minY, _position.width, minY + _position.width);
270         }
271         return subSquare;
272     }
273 
274     final float getRadius() pure const nothrow @nogc
275     {
276         return getSubsquare().width * 0.5f;
277 
278     }
279 
280     final vec2f getCenter() pure const nothrow @nogc
281     {
282         box2i subSquare = getSubsquare();
283         float centerx = (subSquare.min.x + subSquare.max.x - 1) * 0.5f;
284         float centery = (subSquare.min.y + subSquare.max.y - 1) * 0.5f;
285         return vec2f(centerx, centery);
286     }
287 }