1 /**
2 On/Off switch.
3 
4 Copyright: Copyright Auburn Sounds 2015-2017.
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 Authors:   Guillaume Piolat
7 */
8 module dplug.pbrwidgets.onoffswitch;
9 
10 import std.math: exp, abs;
11 import dplug.core.math;
12 import dplug.gui.element;
13 import dplug.gui.bufferedelement;
14 import dplug.client.params;
15 
16 class UIOnOffSwitch : UIElement, IParameterListener
17 {
18 public:
19 nothrow:
20 @nogc:
21 
22     enum Orientation
23     {
24         vertical,
25         horizontal
26     }
27     @ScriptProperty RGBA diffuseOff = RGBA(230, 80, 43, 0);
28     @ScriptProperty RGBA diffuseOn = RGBA(230, 80, 43, 200);
29     @ScriptProperty RGBA material = RGBA(192, 10, 128, 255);
30     @ScriptProperty float animationTimeConstant = 10.0f;
31     @ScriptProperty ushort depthLow = 0;
32     @ScriptProperty ushort depthHigh = 30000;
33     @ScriptProperty ushort holeDepth = 0;
34     @ScriptProperty Orientation orientation = Orientation.vertical;
35     @ScriptProperty bool drawDepth = true;
36     @ScriptProperty bool drawDiffuse = true;
37     @ScriptProperty bool drawMaterial = true;
38     @ScriptProperty bool drawHole = true;     // if drawDepth && drawHole, draw Z hole
39     @ScriptProperty bool drawEmissive = true; // if drawEmissive && !drawDiffuse, draw just the emissive channel
40     
41 
42     /// Left and right border, in fraction of the widget's width.
43     /// Cannot be > 0.5f
44     @ScriptProperty float borderHorz = 0.1f;
45 
46     /// Top and bottom border, in fraction of the widget's width.*
47     /// Cannot be > 0.5f
48     @ScriptProperty float borderVert = 0.1f;
49 
50     this(UIContext context, BoolParameter param)
51     {
52         super(context, flagAnimated | flagPBR);
53         _param = param;
54         _param.addListener(this);
55         _animation = 0.0f;
56     }
57 
58     ~this()
59     {
60         _param.removeListener(this);
61     }
62 
63     override void onAnimate(double dt, double time) nothrow @nogc
64     {
65         float target = _param.valueAtomic() ? 1 : 0;
66 
67         float newAnimation = lerp(_animation, target, 1.0 - exp(-dt * animationTimeConstant));
68 
69         if (abs(newAnimation - _animation) > 0.001f)
70         {
71             _animation = newAnimation;
72             setDirtyWhole();
73         }
74     }
75 
76     override void onDrawPBR(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) nothrow @nogc
77     {
78         // The switch is in a subrect
79         int width = _position.width;
80         int height = _position.height;
81 
82         box2i switchRect = box2i( cast(int)(0.5f + width * borderHorz),
83                                   cast(int)(0.5f + height * borderVert),
84                                   cast(int)(0.5f + width * (1 - borderHorz)),
85                                   cast(int)(0.5f + height * (1 - borderVert)) );
86 
87         ubyte red    = cast(ubyte)(lerp!float(diffuseOff.r, diffuseOn.r, _animation));
88         ubyte green  = cast(ubyte)(lerp!float(diffuseOff.g, diffuseOn.g, _animation));
89         ubyte blue   = cast(ubyte)(lerp!float(diffuseOff.b, diffuseOn.b, _animation));
90         int emissive = cast(ubyte)(lerp!float(diffuseOff.a, diffuseOn.a, _animation));
91 
92         if (isMouseOver || isDragged)
93             emissive += 40;
94 
95         if (emissive > 255)
96             emissive = 255;
97 
98         // Workaround issue https://issues.dlang.org/show_bug.cgi?id=23076
99         // Regular should not be inlined here.
100         static float lerpfloat(float a, float b, float t) pure nothrow @nogc
101         {
102             pragma(inline, false);
103             return t * b + (1 - t) * a;
104         }
105 
106         L16 depthA = L16(cast(short)(lerpfloat(depthHigh, depthLow, _animation)));
107         L16 depthB = L16(cast(short)(lerpfloat(depthLow, depthHigh, _animation)));
108 
109         RGBA diffuseColor = RGBA(red, green, blue, cast(ubyte)emissive);
110 
111         foreach(dirtyRect; dirtyRects)
112         {
113             auto cDepth = depthMap.cropImageRef(dirtyRect);
114 
115             // Write a plain color in the diffuse and material map.
116             box2i validRect = dirtyRect.intersection(switchRect);
117             if (!validRect.empty)
118             {
119                 ImageRef!RGBA cDiffuse = diffuseMap.cropImageRef(validRect);
120                 if (drawDiffuse)
121                 {
122                     cDiffuse.fillAll(diffuseColor);
123                 }
124                 else if (drawEmissive)
125                 {
126                     cDiffuse.fillRectAlpha(0, 0, cDiffuse.w, cDiffuse.h, diffuseColor.a);
127                 }
128                 if (drawMaterial)
129                     materialMap.cropImageRef(validRect).fillAll(material);
130             }
131 
132             // dig a hole
133             if (drawDepth)
134             {
135                 if (drawHole)
136                     cDepth.fillAll(L16(holeDepth));
137 
138                 if (orientation == Orientation.vertical)
139                     verticalSlope(cDepth, switchRect, depthA, depthB);
140                 else
141                     horizontalSlope(cDepth, switchRect, depthA, depthB);
142             }
143         }
144     }
145 
146     override Click onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate)
147     {
148         if (!_canBeDragged)
149         {
150             // inside gesture, refuse new clicks that could call
151             // excess beginParamEdit()/endParamEdit()
152             return Click.unhandled;
153         }
154 
155         // ALT + click => set it to default
156         if (mstate.altPressed) // reset on ALT + click
157         {
158             _param.beginParamEdit();
159             _param.setFromGUI(_param.defaultValue());
160         }
161         else
162         {
163             // Any click => invert
164             // Note: double-click doesn't reset to default, would be annoying
165             _param.beginParamEdit();
166             _param.setFromGUI(!_param.value());
167         }
168         _canBeDragged = false;
169         return Click.startDrag;
170     }
171 
172     override void onMouseEnter()
173     {
174         _param.beginParamHover();
175         setDirtyWhole();
176     }
177 
178     override void onMouseExit()
179     {
180          _param.endParamHover();
181         setDirtyWhole();
182     }
183 
184     override void onStopDrag()
185     {
186         // End parameter edit at end of dragging, even if no parameter change happen,
187         // so that touch automation restore previous parameter value at the end of the mouse 
188         // gesture.
189         _param.endParamEdit();
190         _canBeDragged = true;
191     }
192 
193     override void onParameterChanged(Parameter sender) nothrow @nogc
194     {
195         setDirtyWhole();
196     }
197 
198     override void onBeginParameterEdit(Parameter sender)
199     {
200     }
201 
202     override void onEndParameterEdit(Parameter sender)
203     {
204     }
205 
206     override void onBeginParameterHover(Parameter sender)
207     {
208     }
209 
210     override void onEndParameterHover(Parameter sender)
211     {
212     }
213 
214 protected:
215 
216     /// The parameter this switch is linked with.
217     BoolParameter _param;
218 
219     /// To prevent multiple-clicks having an adverse effect on automation.
220     bool _canBeDragged = true;
221 
222 private:
223     float _animation;
224 }
225 
226 private:
227 void fillRectAlpha(bool CHECKED=true, V)(auto ref V v, int x1, int y1, int x2, int y2, ubyte alpha) nothrow @nogc
228 if (isWritableView!V && is(RGBA : ViewColor!V))
229 {
230     sort2(x1, x2);
231     sort2(y1, y2);
232     static if (CHECKED)
233     {
234         if (x1 >= v.w || y1 >= v.h || x2 <= 0 || y2 <= 0 || x1==x2 || y1==y2) return;
235         if (x1 <    0) x1 =   0;
236         if (y1 <    0) y1 =   0;
237         if (x2 >= v.w) x2 = v.w;
238         if (y2 >= v.h) y2 = v.h;
239     }
240 
241     foreach (y; y1..y2)
242     {
243         RGBA[] scan = v.scanline(y);
244         foreach (x; x1..x2)
245         {
246             scan[x].a = alpha;
247         }
248     }
249 }