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 }