1 /**
2 * Film-strip slider.
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 
9 module dplug.flatwidgets.flatslider;
10 
11 import std.math;
12 import std.algorithm.comparison;
13 
14 import dplug.core.math;
15 import dplug.graphics.drawex;
16 import dplug.gui.bufferedelement;
17 import dplug.client.params;
18 
19 class UIFilmstripSlider : UIElement, IParameterListener
20 {
21 public:
22 nothrow:
23 @nogc:
24 
25     this(UIContext context, FloatParameter param, OwnedImage!RGBA mipmap, int numFrames, float sensitivity = 0.25)
26     {
27         super(context);
28         _param = param;
29         _sensivity = sensitivity;
30         _filmstrip = mipmap;
31         _numFrames = numFrames;
32         _knobWidth = _filmstrip.w;
33         _knobHeight = _filmstrip.h / _numFrames;
34         _disabled = false;
35     }
36 
37     ~this()
38     {
39         _param.removeListener(this);
40     }
41 
42     /// Returns: sensivity.
43     float sensivity()
44     {
45         return _sensivity;
46     }
47 
48     /// Sets sensivity.
49     float sensivity(float sensivity)
50     {
51         return _sensivity = sensivity;
52     }
53 
54     override void onAnimate(double dt, double time) nothrow @nogc
55     {
56         float target = isDragged() ? 1 : 0;
57         float newAnimation = lerp(_pushedAnimation, target, 1.0 - exp(-dt * 30));
58         if (abs(newAnimation - _pushedAnimation) > 0.001f)
59         {
60             _pushedAnimation = newAnimation;
61             setDirtyWhole();
62         }
63     }
64 
65     override void onDraw(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) nothrow @nogc
66     {
67         setCurrentImage();
68         auto _currentImage = _filmstrip.crop(box2i(_imageX1, _imageY1, _imageX2, _imageY2));
69         foreach(dirtyRect; dirtyRects){
70 
71             auto croppedDiffuseIn = _currentImage.crop(dirtyRect);
72             auto croppedDiffuseOut = diffuseMap.crop(dirtyRect);
73 
74             int w = dirtyRect.width;
75             int h = dirtyRect.height;
76 
77             for(int j = 0; j < h; ++j){
78 
79                 RGBA[] input = croppedDiffuseIn.scanline(j);
80                 RGBA[] output = croppedDiffuseOut.scanline(j);
81 
82 
83                 for(int i = 0; i < w; ++i){
84                     ubyte alpha = input[i].a;
85 
86                     RGBA color = RGBA.op!q{.blend(a, b, c)} (input[i], output[i], alpha);
87                     output[i] = color;
88                 }
89             }
90 
91         }
92     }
93 
94     void setCurrentImage()
95     {
96         float value = _param.getNormalized();
97         currentFrame = cast(int)(round(value * (_numFrames - 1)));
98 
99         if(currentFrame < 0) currentFrame = 0;
100         if(currentFrame > 59) currentFrame = 59;
101 
102         _imageX1 = 0;
103         _imageY1 = (_filmstrip.h / _numFrames) * currentFrame;
104 
105         _imageX2 = _filmstrip.w;
106         _imageY2 = _imageY1 + (_filmstrip.h / _numFrames);
107 
108     }
109 
110     override bool onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate)
111     {
112         // double-click => set to default
113         if (isDoubleClick)
114         {
115             _param.beginParamEdit();
116             if (auto p = cast(FloatParameter)_param)
117                 p.setFromGUI(p.defaultValue());
118             else if (auto p = cast(IntegerParameter)_param)
119                 p.setFromGUI(p.defaultValue());
120             else
121                 assert(false); // only integer and float parameters supported
122             _param.endParamEdit();
123         }
124 
125         return true; // to initiate dragging
126     }
127 
128     // Called when mouse drag this Element.
129     override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate)
130     {
131         // FUTURE: replace by actual trail height instead of total height
132         if(!_disabled)
133         {
134             float displacementInHeight = cast(float)(dy) / _position.height; 
135 
136             float modifier = 1.0f;
137             if (mstate.shiftPressed || mstate.ctrlPressed)
138                 modifier *= 0.1f;
139 
140             double oldParamValue = _param.getNormalized() + _draggingDebt;
141             double newParamValue = oldParamValue - displacementInHeight * modifier * _sensivity;
142 
143             if (y > _mousePosOnLast0Cross)
144                 return;
145             if (y < _mousePosOnLast1Cross)
146                 return;
147 
148             if (newParamValue <= 0 && oldParamValue > 0)
149                 _mousePosOnLast0Cross = y;
150 
151             if (newParamValue >= 1 && oldParamValue < 1)
152                 _mousePosOnLast1Cross = y;
153 
154             if (newParamValue < 0)
155                 newParamValue = 0;
156             if (newParamValue > 1)
157                 newParamValue = 1;
158 
159             if (newParamValue > 0)
160                 _mousePosOnLast0Cross = float.infinity;
161 
162             if (newParamValue < 1)
163                 _mousePosOnLast1Cross = -float.infinity;
164 
165             if (newParamValue != oldParamValue)
166             {
167                 if (auto p = cast(FloatParameter)_param)
168                 {
169                     p.setFromGUINormalized(newParamValue);
170                 }
171                 /*else if (auto p = cast(IntegerParameter)_param)
172                 {
173                     p.setFromGUINormalized(newParamValue);
174                     _draggingDebt = newParamValue - p.getNormalized();
175                 }*/
176                 else
177                     assert(false); // only integer and float parameters supported
178             }
179             setDirtyWhole();
180         }
181     }
182 
183     // For lazy updates
184     override void onBeginDrag()
185     {
186         _param.beginParamEdit();
187         setDirtyWhole();
188     }
189 
190     override void onStopDrag()
191     {
192         _param.endParamEdit();
193         setDirtyWhole();
194         _draggingDebt = 0.0f;
195     }
196 
197     override void onMouseEnter()
198     {
199     }
200 
201     override void onMouseExit()
202     {
203     }
204 
205     override void onParameterChanged(Parameter sender)
206     {
207         setDirtyWhole();
208     }
209 
210     override void onBeginParameterEdit(Parameter sender)
211     {
212     }
213 
214     override void onEndParameterEdit(Parameter sender)
215     {
216     }
217 
218     void disable()
219     {
220         _disabled = true;
221     }
222 
223 protected:
224 
225     /// The parameter this switch is linked with.
226     Parameter _param;
227 
228     OwnedImage!RGBA _filmstrip;
229     int _numFrames;
230     int _imageX1, _imageX2, _imageY1, _imageY2;
231     int currentFrame;
232 
233     int _knobWidth;
234     int _knobHeight;
235 
236     /// Sensivity: given a mouse movement in 100th of the height of the knob,
237     /// how much should the normalized parameter change.
238     float _sensivity;
239 
240     float  _pushedAnimation;
241 
242     float _mousePosOnLast0Cross;
243     float _mousePosOnLast1Cross;
244 
245     // Exists because small mouse drags for integer parameters may not 
246     // lead to a parameter value change, hence a need to accumulate those drags.
247     float _draggingDebt = 0.0f;
248 
249     bool _disabled;
250 
251     void clearCrosspoints()
252     {
253         _mousePosOnLast0Cross = float.infinity;
254         _mousePosOnLast1Cross = -float.infinity;
255     }
256 }