1 /** 2 * PBR widget: knob. 3 * Copyright: Copyright Auburn Sounds 2015 and later. 4 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 5 * Authors: Guillaume Piolat 6 */ 7 module dplug.pbrwidgets.knob; 8 9 import std.math; 10 import std.algorithm.comparison; 11 12 import dplug.core.math; 13 import dplug.graphics.drawex; 14 15 import dplug.gui.element; 16 17 import dplug.client.params; 18 19 enum KnobStyle 20 { 21 thumb, // with a hole 22 cylinder, 23 cone, 24 ball 25 } 26 27 class UIKnob : UIElement, IParameterListener 28 { 29 public: 30 nothrow: 31 @nogc: 32 33 // This will change to 1.0f at one point for consistency, so better express your knob 34 // sensivity with that. 35 enum defaultSensivity = 0.25f; 36 37 // 38 // Modify these public members to customize knobs! 39 // 40 float knobRadius = 0.75f; 41 RGBA knobDiffuse = RGBA(233, 235, 236, 0); 42 RGBA knobMaterial = RGBA(0, 255, 128, 255); 43 KnobStyle style = KnobStyle.thumb; 44 45 // LEDs 46 int numLEDs = 7; 47 float LEDRadiusMin = 0.127f; 48 float LEDRadiusMax = 0.127f; 49 RGBA LEDDiffuseLit = RGBA(255, 140, 220, 215); 50 RGBA LEDDiffuseUnlit = RGBA(255, 140, 220, 40); 51 float LEDDistanceFromCenter = 0.8f; 52 float LEDDistanceFromCenterDragged = 0.7f; 53 ushort LEDDepth = 65000; 54 55 // trail 56 RGBA litTrailDiffuse = RGBA(230, 80, 43, 192); 57 RGBA unlitTrailDiffuse = RGBA(150, 40, 20, 8); 58 float trailRadiusMin = 0.85f; 59 float trailRadiusMax = 0.97f; 60 float trailOffsetX = 0.0f; // in ratio of knob size 61 float trailOffsetY = 0.0f; // in ratio of knob size 62 63 float trailMinAngle = -PI * 0.75f; 64 float trailBaseAngle = -PI * 0.75f; 65 float trailMaxAngle = +PI * 0.75f; 66 67 // alternate trail is for values below base angle 68 // For example, knob trails can be blue for positive values, 69 // and orange for negative. 70 RGBA litTrailDiffuseAlt = RGBA(43, 80, 230, 192); 71 bool hasAlternateTrail = false; 72 73 74 float animationTimeConstant = 40.0f; 75 76 77 78 this(UIContext context, FloatParameter param) 79 { 80 super(context); 81 _param = param; 82 _sensivity = defaultSensivity; 83 _param.addListener(this); 84 _pushedAnimation = 0; 85 clearCrosspoints(); 86 } 87 88 ~this() 89 { 90 _param.removeListener(this); 91 } 92 93 override void onAnimate(double dt, double time) nothrow @nogc 94 { 95 float target = isDragged() ? 1 : 0; 96 97 float newAnimation = lerp(_pushedAnimation, target, 1.0 - exp(-dt * animationTimeConstant)); 98 99 if (abs(newAnimation - _pushedAnimation) > 0.001f) 100 { 101 _pushedAnimation = newAnimation; 102 setDirtyWhole(); 103 } 104 } 105 106 /// Returns: sensivity. 107 float sensivity() 108 { 109 return _sensivity; 110 } 111 112 /// Sets sensivity. 113 float sensivity(float sensivity) 114 { 115 return _sensivity = sensivity; 116 } 117 118 override void onDraw(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) nothrow @nogc 119 { 120 drawTrail(diffuseMap, depthMap, materialMap, dirtyRects); 121 drawKnob(diffuseMap, depthMap, materialMap, dirtyRects); 122 } 123 124 void drawTrail(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) nothrow @nogc 125 { 126 float radius = getRadius(); 127 vec2f center = getCenter(); 128 129 float knobRadiusPx = radius * knobRadius; 130 131 vec2f trailOffset = vec2f(knobRadiusPx * trailOffsetX, knobRadiusPx * trailOffsetY); 132 133 foreach(dirtyRect; dirtyRects) 134 { 135 auto croppedDiffuse = diffuseMap.cropImageRef(dirtyRect); 136 auto croppedDepth = depthMap.cropImageRef(dirtyRect); 137 auto croppedMaterial = materialMap.cropImageRef(dirtyRect); 138 139 int bx = dirtyRect.min.x; 140 int by = dirtyRect.min.y; 141 142 // 143 // Draw trail 144 // 145 { 146 float trailCenterX = center.x - bx + trailOffset.x; 147 float trailCenterY = center.y - by + trailOffset.y; 148 149 croppedDiffuse.aaFillSector(trailCenterX, trailCenterY, 150 radius * trailRadiusMin, radius * trailRadiusMax, 151 getMinAngle, getMaxAngle, unlitTrailDiffuse); 152 153 // Eventually, use the alternative trail color 154 RGBA litTrail = litTrailDiffuse; 155 if (hasAlternateTrail && getValueAngle < getBaseAngle) 156 litTrail = litTrailDiffuseAlt; 157 158 // when dragged, trail is two times brighter 159 if (isDragged) 160 { 161 litTrail.a = cast(ubyte) std.algorithm.min(255, 2 * litTrail.a); 162 } 163 164 croppedDiffuse.aaFillSector(trailCenterX, trailCenterY, radius * trailRadiusMin, radius * trailRadiusMax, 165 min(getBaseAngle, getValueAngle), max(getBaseAngle, getValueAngle), litTrail); 166 } 167 } 168 } 169 170 void drawKnob(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) nothrow @nogc 171 { 172 float radius = getRadius(); 173 vec2f center = getCenter(); 174 175 float knobRadiusPx = radius * knobRadius; 176 177 foreach(dirtyRect; dirtyRects) 178 { 179 auto croppedDiffuse = diffuseMap.cropImageRef(dirtyRect); 180 auto croppedDepth = depthMap.cropImageRef(dirtyRect); 181 auto croppedMaterial = materialMap.cropImageRef(dirtyRect); 182 183 int bx = dirtyRect.min.x; 184 int by = dirtyRect.min.y; 185 186 // 187 // Draw knob 188 // 189 float angle = getValueAngle + PI * 0.5f; 190 float depthRadius = std.algorithm.max(knobRadiusPx * 3.0f / 5.0f, 0); 191 float depthRadius2 = std.algorithm.max(knobRadiusPx * 3.0f / 5.0f, 0); 192 193 float posEdgeX = center.x + sin(angle) * depthRadius2; 194 float posEdgeY = center.y - cos(angle) * depthRadius2; 195 196 ubyte emissive = 0; 197 if (_shouldBeHighlighted) 198 emissive = 30; 199 if (isDragged) 200 emissive = 0; 201 202 if (style == KnobStyle.thumb) 203 { 204 croppedDepth.aaSoftDisc(center.x - bx, center.y - by, depthRadius, knobRadiusPx, L16(65535)); 205 croppedDepth.aaSoftDisc(center.x - bx, center.y - by, 0, depthRadius, L16(38400)); 206 } 207 else if (style == KnobStyle.cylinder) 208 { 209 L16 depth = L16(cast(ushort)(0.5f + lerp(65535.0f, 45000.0f, _pushedAnimation)) ); 210 croppedDepth.aaSoftDisc(center.x - bx, center.y - by, knobRadiusPx - 5, knobRadiusPx, depth); 211 } 212 else if (style == KnobStyle.cone) 213 { 214 L16 depth = L16(cast(ushort)(0.5f + lerp(65535.0f, 45000.0f, _pushedAnimation)) ); 215 croppedDepth.aaSoftDisc(center.x - bx, center.y - by, 0, knobRadiusPx, depth); 216 } 217 else if (style == KnobStyle.ball) 218 { 219 L16 depth = L16(cast(ushort)(0.5f + lerp(65535.0f, 45000.0f, _pushedAnimation)) ); 220 croppedDepth.aaSoftDisc!1.2f(center.x - bx, center.y - by, 2, knobRadiusPx, depth); 221 } 222 RGBA knobDiffuseLit = knobDiffuse; 223 knobDiffuseLit.a = emissive; 224 croppedDiffuse.aaSoftDisc(center.x - bx, center.y - by, knobRadiusPx - 1, knobRadiusPx, knobDiffuseLit); 225 croppedMaterial.aaSoftDisc(center.x - bx, center.y - by, knobRadiusPx - 5, knobRadiusPx, knobMaterial); 226 227 228 // LEDs 229 for (int i = 0; i < numLEDs; ++i) 230 { 231 float disp = i * 2 * PI / numLEDs; 232 float distance = lerp(LEDDistanceFromCenter, LEDDistanceFromCenterDragged, _pushedAnimation); 233 float x = center.x + sin(angle + disp) * knobRadiusPx * distance; 234 float y = center.y - cos(angle + disp) * knobRadiusPx * distance; 235 236 float t = -1 + 2 * abs(disp - PI) / PI; 237 238 float LEDRadius = std.algorithm.max(0.0f, lerp(LEDRadiusMin, LEDRadiusMax, t)); 239 240 float smallRadius = knobRadiusPx * LEDRadius * 0.714f; 241 float largerRadius = knobRadiusPx * LEDRadius; 242 243 RGBA LEDDiffuse; 244 LEDDiffuse.r = cast(ubyte)lerp!float(LEDDiffuseUnlit.r, LEDDiffuseLit.r, _pushedAnimation); 245 LEDDiffuse.g = cast(ubyte)lerp!float(LEDDiffuseUnlit.g, LEDDiffuseLit.g, _pushedAnimation); 246 LEDDiffuse.b = cast(ubyte)lerp!float(LEDDiffuseUnlit.b, LEDDiffuseLit.b, _pushedAnimation); 247 LEDDiffuse.a = cast(ubyte)lerp!float(LEDDiffuseUnlit.a, LEDDiffuseLit.a, _pushedAnimation); 248 249 croppedDepth.aaSoftDisc(x - bx, y - by, 0, largerRadius, L16(LEDDepth)); 250 croppedDiffuse.aaSoftDisc(x - bx, y - by, 0, largerRadius, LEDDiffuse); 251 croppedMaterial.aaSoftDisc(x - bx, y - by, smallRadius, largerRadius, RGBA(128, 128, 255, defaultPhysical)); 252 } 253 } 254 } 255 256 257 258 override bool onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate) 259 { 260 if (!containsPoint(x, y)) 261 return false; 262 263 // double-click => set to default 264 if (isDoubleClick) 265 { 266 _param.beginParamEdit(); 267 _param.setFromGUI(_param.defaultValue()); 268 _param.endParamEdit(); 269 } 270 271 return true; // to initiate dragging 272 } 273 274 // Called when mouse drag this Element. 275 override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate) 276 { 277 float displacementInHeight = cast(float)(dy) / _position.height; 278 279 float modifier = 1.0f; 280 if (mstate.shiftPressed || mstate.ctrlPressed) 281 modifier *= 0.1f; 282 283 double oldParamValue = _param.getNormalized(); 284 285 double newParamValue = oldParamValue - displacementInHeight * modifier * _sensivity; 286 287 if (y > _mousePosOnLast0Cross) 288 return; 289 if (y < _mousePosOnLast1Cross) 290 return; 291 292 if (newParamValue <= 0 && oldParamValue > 0) 293 _mousePosOnLast0Cross = y; 294 295 if (newParamValue >= 1 && oldParamValue < 1) 296 _mousePosOnLast1Cross = y; 297 298 if (newParamValue < 0) 299 newParamValue = 0; 300 if (newParamValue > 1) 301 newParamValue = 1; 302 303 if (newParamValue > 0) 304 _mousePosOnLast0Cross = float.infinity; 305 306 if (newParamValue < 1) 307 _mousePosOnLast1Cross = -float.infinity; 308 309 if (newParamValue != oldParamValue) 310 _param.setFromGUINormalized(newParamValue); 311 } 312 313 // For lazy updates 314 override void onBeginDrag() 315 { 316 _param.beginParamEdit(); 317 setDirtyWhole(); 318 } 319 320 override void onStopDrag() 321 { 322 _param.endParamEdit(); 323 clearCrosspoints(); 324 setDirtyWhole(); 325 } 326 327 override void onMouseMove(int x, int y, int dx, int dy, MouseState mstate) 328 { 329 _shouldBeHighlighted = containsPoint(x, y); 330 setDirtyWhole(); 331 } 332 333 override void onMouseExit() 334 { 335 _shouldBeHighlighted = false; 336 setDirtyWhole(); 337 } 338 339 override void onParameterChanged(Parameter sender) nothrow @nogc 340 { 341 setDirtyWhole(); 342 } 343 344 override void onBeginParameterEdit(Parameter sender) 345 { 346 } 347 348 override void onEndParameterEdit(Parameter sender) 349 { 350 } 351 352 protected: 353 354 /// The parameter this knob is linked with. 355 FloatParameter _param; 356 357 float _pushedAnimation; 358 359 /// Sensivity: given a mouse movement in 100th of the height of the knob, 360 /// how much should the normalized parameter change. 361 float _sensivity; 362 363 bool _shouldBeHighlighted = false; 364 365 float _mousePosOnLast0Cross; 366 float _mousePosOnLast1Cross; 367 368 /// Exists because public angle properties are given in a 369 /// different referential, where 0 is at the top 370 static float angleConvert(float angle) nothrow @nogc pure 371 { 372 return angle + PI * 1.5f; 373 } 374 375 /// Min angle of the trail, fit for `aaFillSector`. 376 float getMinAngle() const pure nothrow @nogc 377 { 378 return angleConvert(trailMinAngle); 379 } 380 381 /// Min angle of the trail, fit for `aaFillSector`. 382 float getMaxAngle() const pure nothrow @nogc 383 { 384 return angleConvert(trailMaxAngle); 385 } 386 387 /// Max angle of the trail, fit for `aaFillSector`. 388 float getBaseAngle() const pure nothrow @nogc 389 { 390 return angleConvert(trailBaseAngle); 391 } 392 393 /// Angle of the trail, fit for `aaFillSector`. 394 float getValueAngle() nothrow @nogc 395 { 396 return lerp(getMinAngle, getMaxAngle, _param.getNormalized()); 397 } 398 399 void clearCrosspoints() nothrow @nogc 400 { 401 _mousePosOnLast0Cross = float.infinity; 402 _mousePosOnLast1Cross = -float.infinity; 403 } 404 405 final bool containsPoint(int x, int y) nothrow @nogc 406 { 407 vec2f center = getCenter(); 408 return vec2f(x, y).distanceTo(center) < getRadius(); 409 } 410 411 /// Returns: largest square centered in _position 412 final box2i getSubsquare() pure const nothrow @nogc 413 { 414 // We'll draw entirely in the largest centered square in _position. 415 box2i subSquare; 416 if (_position.width > _position.height) 417 { 418 int offset = (_position.width - _position.height) / 2; 419 int minX = offset; 420 subSquare = box2i(minX, 0, minX + _position.height, _position.height); 421 } 422 else 423 { 424 int offset = (_position.height - _position.width) / 2; 425 int minY = offset; 426 subSquare = box2i(0, minY, _position.width, minY + _position.width); 427 } 428 return subSquare; 429 } 430 431 final float getRadius() pure const nothrow @nogc 432 { 433 return getSubsquare().width * 0.5f; 434 435 } 436 437 final vec2f getCenter() pure const nothrow @nogc 438 { 439 box2i subSquare = getSubsquare(); 440 float centerx = (subSquare.min.x + subSquare.max.x - 1) * 0.5f; 441 float centery = (subSquare.min.y + subSquare.max.y - 1) * 0.5f; 442 return vec2f(centerx, centery); 443 } 444 }