1 /**
2 * PBR widget: parameter value hint.
3 * Copyright: Copyright Auburn Sounds 2015-2017.
4 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
5 * Authors:   Guillaume Piolat
6 */
7 module dplug.pbrwidgets.paramhint;
8 
9 import core.stdc..string;
10 import core.atomic;
11 
12 import std.math;
13 import std.conv;
14 import std.algorithm;
15 
16 import dplug.core;
17 import dplug.gui.element;
18 import dplug.gui.bufferedelement;
19 import dplug.client.params;
20 
21 
22 /// Widget that monitors the value of a parameter and
23 /// appears whenever it change to display its value.
24 class UIParamHint : UIBufferedElement, IParameterListener
25 {
26 public:
27 nothrow:
28 @nogc:
29 
30     double fadeinDuration = 0.15f;
31     double fadeoutDuration = 0.3f;
32     double holdDuration = 0.85f;
33 
34     float textSizePx = 9.5f;
35     RGBA holeDiffuse = RGBA(90, 90, 90, 0);
36     
37     RGBA textDiffuseLow = RGBA(90, 90, 90, 0);
38     RGBA textDiffuseHigh = RGBA(42, 42, 42, 0);
39 
40     RGBA diffuseLow = RGBA(90, 90, 90, 0);
41     RGBA diffuseHigh = RGBA(245, 245, 245, 30);
42     RGBA material = RGBA(0, 255, 192, 255);
43 
44     RGBA plasticMaterial = RGBA(155, 240, 69, 255);
45     float plasticAlpha = 0.05f;
46 
47     ushort depthLow = 0;
48     ushort depthHigh = 50000;
49 
50     this(UIContext context, Parameter param, Font font)
51     {
52         super(context);
53         _param = param;
54         _param.addListener(this);
55         _font = font;
56     }
57 
58     ~this()
59     {
60         _param.removeListener(this);
61     }
62 
63     const(char)[] paramString() nothrow @nogc
64     {
65         _param.toDisplayN(_pParamStringBuffer.ptr, 128);
66         size_t len = strlen(_pParamStringBuffer.ptr);
67         string label = _param.label();
68         assert(label.length < 127);
69         _pParamStringBuffer[len] = ' ';
70         size_t totalLength = len + 1 + label.length;
71         _pParamStringBuffer[len+1..totalLength] = label[];
72         return _pParamStringBuffer[0..totalLength];
73     }
74 
75     override void onDrawBuffered(ImageRef!RGBA diffuseMap, 
76                                  ImageRef!L16 depthMap, 
77                                  ImageRef!RGBA materialMap, 
78                                  ImageRef!L8 diffuseOpacity,
79                                  ImageRef!L8 depthOpacity,
80                                  ImageRef!L8 materialOpacity) nothrow @nogc
81     {
82         int W = diffuseMap.w;
83         int H = diffuseMap.h;
84 
85         assert(_upAnimation >= 0 && _upAnimation <= 1);
86 
87         float openAnimation = std.algorithm.min(1.0f,  _upAnimation * 2.0f);
88         float moveUpAnimation = std.algorithm.max(0.0f,  _upAnimation * 2.0f - 1.0f);
89 
90         box2i fullRect = box2i(0, 0, W, H);
91 
92         float holeHeight = H * openAnimation;
93 
94         box2f plasticRect = box2f(0, 0, W, H - holeHeight);
95         materialMap.aaFillRectFloat(plasticRect.min.x, plasticRect.min.y, plasticRect.max.x, plasticRect.max.y, plasticMaterial, plasticAlpha);
96 
97 
98         box2f holeRect = box2f(0, H - holeHeight, W, H);
99 
100         if (holeRect.empty)
101             return;
102 
103         depthMap.aaFillRectFloat(holeRect.min.x, holeRect.min.y, holeRect.max.x, holeRect.max.y, L16(0));
104         diffuseMap.aaFillRectFloat(holeRect.min.x, holeRect.min.y, holeRect.max.x, holeRect.max.y, holeDiffuse);        
105 
106         ushort labelDepth = cast(ushort)(depthLow + moveUpAnimation * (depthHigh - depthLow));
107 
108         float perspectiveAnimation = 0.8f + 0.2f * moveUpAnimation;
109 
110         float labelW = W * perspectiveAnimation;
111         float labelH = H * perspectiveAnimation;
112         float labelMarginW = W - labelW;
113         float labelMarginH = H - labelH;
114 
115         box2f labelRect = box2f(labelMarginW * 0.5f, labelMarginH  * 0.5f, W - labelMarginW * 0.5f, H - labelMarginH * 0.5f);
116         box2f labelRectCrossHole = labelRect.intersection(holeRect);
117 
118         box2i ilabelRectCrossHole = box2i(cast(int)(0.5f + labelRectCrossHole.min.x), 
119                                           cast(int)(0.5f + labelRectCrossHole.min.y), 
120                                           cast(int)(0.5f + labelRectCrossHole.max.x),
121                                           cast(int)(0.5f + labelRectCrossHole.max.y));
122 
123         if (labelRect.empty)
124             return;
125 
126         // Cheating :( because depth animation isn't sufficient
127         RGBA diffuse = RGBA(cast(ubyte)(0.5f + lerp!float(diffuseLow.r, diffuseHigh.r, moveUpAnimation)),
128                             cast(ubyte)(0.5f + lerp!float(diffuseLow.g, diffuseHigh.g, moveUpAnimation)),
129                             cast(ubyte)(0.5f + lerp!float(diffuseLow.b, diffuseHigh.b, moveUpAnimation)),
130                             cast(ubyte)(0.5f + lerp!float(diffuseLow.a, diffuseHigh.a, moveUpAnimation)));
131 
132         depthMap.aaFillRectFloat(labelRectCrossHole.min.x, labelRectCrossHole.min.y, labelRectCrossHole.max.x, labelRectCrossHole.max.y, L16(labelDepth));
133         materialMap.aaFillRectFloat(labelRectCrossHole.min.x, labelRectCrossHole.min.y, labelRectCrossHole.max.x, labelRectCrossHole.max.y, material);
134         diffuseMap.aaFillRectFloat(labelRectCrossHole.min.x, labelRectCrossHole.min.y, labelRectCrossHole.max.x, labelRectCrossHole.max.y, diffuse);
135 
136         RGBA textDiffuse = RGBA(cast(ubyte)(0.5f + lerp!float(textDiffuseLow.r, textDiffuseHigh.r, moveUpAnimation)),
137                                 cast(ubyte)(0.5f + lerp!float(textDiffuseLow.g, textDiffuseHigh.g, moveUpAnimation)),
138                                 cast(ubyte)(0.5f + lerp!float(textDiffuseLow.b, textDiffuseHigh.b, moveUpAnimation)),
139                                 cast(ubyte)(0.5f + lerp!float(textDiffuseLow.a, textDiffuseHigh.a, moveUpAnimation)));
140 
141         // Draw text
142         float fontSizePx = textSizePx * perspectiveAnimation;
143         float textPositionX = ilabelRectCrossHole.width - labelRectCrossHole.center.x;
144         float fontVerticalExtent = _font.getAscent(fontSizePx) - _font.getDescent(fontSizePx);
145         float textPositionY = ilabelRectCrossHole.height - labelRect.height * 0.5f + 0.5f * _font.getHeightOfx(fontSizePx);
146         diffuseMap.crop(ilabelRectCrossHole).fillText(_font, _lastParamString, fontSizePx, 0, textDiffuse, 
147                                                       textPositionX, textPositionY,
148                                                       HorizontalAlignment.center,
149                                                       VerticalAlignment.baseline);
150 
151         // Fill opacity
152         {
153             float alpha = openAnimation;
154             ubyte balpha = cast(ubyte)(255.0f*openAnimation + 0.5f);
155             depthOpacity.fill(L8(balpha));
156             materialOpacity.fill(L8(balpha));
157             diffuseOpacity.fill(L8(balpha));
158         }
159     }
160 
161     override void onAnimate(double dt, double time) nothrow @nogc
162     {
163         bool isBeingEdited = atomicLoad(_parameterIsEdited);
164         bool wasJustChanged = cas(&_parameterChanged, true, false);
165 
166         if (isBeingEdited)
167             _timeSinceEdit = 0;
168 
169         if (isBeingEdited && wasJustChanged)
170             _lastParamString = paramString();
171 
172         float targetAnimation = _timeSinceEdit < holdDuration ? 1 : 0;
173 
174         bool animationMoved = void;
175 
176         if (_upAnimation < targetAnimation)
177         {
178             _upAnimation += dt / fadeinDuration;
179             if (_upAnimation > targetAnimation)
180                 _upAnimation = targetAnimation;
181             animationMoved = true;
182         }
183         else if (_upAnimation > targetAnimation)
184         {
185             _upAnimation -= dt / fadeoutDuration;
186             if (_upAnimation < targetAnimation)
187                 _upAnimation = targetAnimation;
188             animationMoved = true;
189         }
190         else
191             animationMoved = false;
192         
193         // redraw if:
194         // - parameter changed
195         // - animation changed
196         if ((wasJustChanged && isBeingEdited) || animationMoved)
197             setDirtyWhole();
198 
199         _timeSinceEdit += dt;
200     }
201 
202     override void onParameterChanged(Parameter sender) nothrow @nogc
203     {
204         atomicStore(_parameterChanged, true);
205     }
206 
207     override void onBeginParameterEdit(Parameter sender)
208     {
209         atomicStore(_parameterIsEdited, true);
210         atomicStore(_parameterChanged, true);
211     }
212 
213     override void onEndParameterEdit(Parameter sender)
214     {
215         atomicStore(_parameterIsEdited, false);
216     }
217 
218 private:
219     Parameter _param;
220 
221     shared(bool) _parameterIsEdited = false; // access to this is through atomic ops
222     shared(bool) _parameterChanged = false;  // access to this is through atomic ops
223 
224     float _upAnimation = 0;
225 
226     double _timeSinceEdit = double.infinity;
227     
228     const(char)[] _lastParamString;
229 
230     Font _font;
231 
232     char[256] _pParamStringBuffer;
233 }
234