sandbox/studio: realistic brushed-aluminium knob (multi-layer lighting)
Previous version was too flat. Real metal needs concentric brushing (not radial), a strong horizontal specular band, multiple light sources and a defined bevel. Disc rendering now layers: - Radial base gradient (matte foundation, slightly dark at edge) - Concentric brushing rings (lathe-turned aluminium, alternating bright/dark with deterministic pseudo-random alpha) - Vertical lighting overlay (light-from-above gradient) - Horizontal specular band on the upper portion (light source reflection) - Subtle rim light from below (typical of softbox-lit studio gear) - Bright bevel crescent on the upper edge + dark crescent on the lower - Inset shadow line where disc meets the dark rim Indicator notch refined to read as engraved (dark groove + thin bright lower lip catching light on the inner edge). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -491,69 +491,117 @@ function drawKnob(ctx, size, norm) {
|
||||
ctx.arc(cx, cy, totalR - rimW / 2, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// (4) BIG METALLIC DISC — fills most of the inside (the "circulito metalico
|
||||
// que ocupa casi todo"). Radial gradient, brushed-metal feel.
|
||||
const disc = ctx.createRadialGradient(
|
||||
cx, cy - discR * 0.25, 0,
|
||||
cx, cy + discR * 0.4, discR * 1.4
|
||||
);
|
||||
disc.addColorStop(0, '#b6b0a6'); // lit upper-center
|
||||
disc.addColorStop(0.45, '#8a847a');
|
||||
disc.addColorStop(0.85, '#46423d');
|
||||
disc.addColorStop(1, '#2a2722'); // bottom dark
|
||||
ctx.fillStyle = disc;
|
||||
// (4a) METAL — base radial gradient (matte foundation, slightly dark at edge)
|
||||
const baseGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, discR);
|
||||
baseGrad.addColorStop(0, '#787268');
|
||||
baseGrad.addColorStop(0.7, '#5a544c');
|
||||
baseGrad.addColorStop(1, '#36322d');
|
||||
ctx.fillStyle = baseGrad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, discR, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// (5) brushed-metal radial striations (very subtle, deterministic)
|
||||
// (4b) CONCENTRIC BRUSHING — lathe-turned aluminium look (signature on
|
||||
// Moog/Roland synths). Pseudo-random alpha per ring, alternating
|
||||
// bright/dark bands, deterministic so the rings don't shimmer.
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, discR - 0.5, 0, Math.PI * 2);
|
||||
ctx.arc(cx, cy, discR - 0.3, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
for (let i = 0; i < 96; i++) {
|
||||
const a = (i / 96) * Math.PI * 2;
|
||||
const alpha = 0.025 + ((i * 9301 + 49297) % 233) / 233 * 0.04;
|
||||
ctx.strokeStyle = `rgba(255, 240, 220, ${alpha.toFixed(3)})`;
|
||||
for (let r = discR; r > 0.5; r -= 0.55) {
|
||||
const seed = Math.floor(r * 17.31);
|
||||
const n = ((seed * 9301 + 49297) % 233) / 233;
|
||||
const a = 0.04 + n * 0.10;
|
||||
ctx.strokeStyle = n > 0.55
|
||||
? `rgba(255, 245, 225, ${a.toFixed(3)})`
|
||||
: `rgba(10, 8, 6, ${(a * 0.85).toFixed(3)})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, cy);
|
||||
ctx.lineTo(cx + Math.cos(a) * discR, cy + Math.sin(a) * discR);
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// (6) thin shadow line where disc meets rim (depth)
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.55)';
|
||||
// (4c) VERTICAL LIGHTING — light from above. Strong on top, dark below.
|
||||
const vert = ctx.createLinearGradient(0, cy - discR, 0, cy + discR);
|
||||
vert.addColorStop(0, 'rgba(255, 248, 230, 0.32)');
|
||||
vert.addColorStop(0.40, 'rgba(255, 248, 230, 0.04)');
|
||||
vert.addColorStop(0.55, 'rgba(0, 0, 0, 0.05)');
|
||||
vert.addColorStop(1, 'rgba(0, 0, 0, 0.45)');
|
||||
ctx.fillStyle = vert;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, discR, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// (4d) HORIZONTAL SPECULAR BAND — the bright reflection of the light
|
||||
// source running across the upper part of the disc.
|
||||
const band = ctx.createLinearGradient(0, cy - discR * 0.65, 0, cy - discR * 0.05);
|
||||
band.addColorStop(0, 'rgba(255, 252, 240, 0)');
|
||||
band.addColorStop(0.5, 'rgba(255, 252, 240, 0.42)');
|
||||
band.addColorStop(1, 'rgba(255, 252, 240, 0)');
|
||||
ctx.fillStyle = band;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, discR, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// (4e) RIM LIGHT FROM BELOW — subtle glow on the bottom edge (typical of
|
||||
// studio gear photographed under softboxes).
|
||||
const rimLight = ctx.createLinearGradient(0, cy + discR * 0.55, 0, cy + discR);
|
||||
rimLight.addColorStop(0, 'rgba(255, 230, 200, 0)');
|
||||
rimLight.addColorStop(1, 'rgba(255, 230, 200, 0.14)');
|
||||
ctx.fillStyle = rimLight;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, discR, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// (5) BEVEL — bright crescent on the upper edge
|
||||
ctx.strokeStyle = 'rgba(255, 252, 240, 0.55)';
|
||||
ctx.lineWidth = 0.9;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, discR - 0.5, Math.PI * 1.10, Math.PI * 1.90);
|
||||
ctx.stroke();
|
||||
|
||||
// (5b) BEVEL — dark crescent on the lower edge (shadow side of the bevel)
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.50)';
|
||||
ctx.lineWidth = 0.9;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, discR - 0.5, Math.PI * 0.10, Math.PI * 0.90);
|
||||
ctx.stroke();
|
||||
|
||||
// (6) inset shadow line where disc meets rim
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.65)';
|
||||
ctx.lineWidth = 0.7;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, discR + 0.4, 0, Math.PI * 2);
|
||||
ctx.arc(cx, cy, discR + 0.3, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// (7) bright highlight crescent on the top edge of the disc
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.30)';
|
||||
ctx.lineWidth = 0.8;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, discR - 0.5, Math.PI * 1.18, Math.PI * 1.82);
|
||||
ctx.stroke();
|
||||
|
||||
// (8) small DARK NOTCH indicator on the disc, rotates with value
|
||||
// (7) INDICATOR — small engraved dark notch on the disc, rotates with value.
|
||||
// Drawn as a tapered triangle with a subtle bottom highlight to look
|
||||
// like it's incised into the metal rather than printed on top.
|
||||
const ang = startA + norm * (endA - startA);
|
||||
const tipR = discR * 0.86;
|
||||
const baseR = discR * 0.55;
|
||||
const baseR = discR * 0.50;
|
||||
const perp = ang + Math.PI / 2;
|
||||
const half = size * 0.028;
|
||||
const half = size * 0.026;
|
||||
const tipX = cx + Math.cos(ang) * tipR;
|
||||
const tipY = cy + Math.sin(ang) * tipR;
|
||||
const bX = cx + Math.cos(ang) * baseR;
|
||||
const bY = cy + Math.sin(ang) * baseR;
|
||||
ctx.fillStyle = 'rgba(8,7,6,0.95)';
|
||||
// dark fill (the engraved groove)
|
||||
ctx.fillStyle = 'rgba(6, 5, 4, 0.92)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(tipX, tipY);
|
||||
ctx.lineTo(bX + Math.cos(perp) * half, bY + Math.sin(perp) * half);
|
||||
ctx.lineTo(bX - Math.cos(perp) * half, bY - Math.sin(perp) * half);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
// tiny bright lower lip (light catches the inner edge of the groove)
|
||||
ctx.strokeStyle = 'rgba(255, 250, 230, 0.25)';
|
||||
ctx.lineWidth = 0.4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(bX + Math.cos(perp) * half, bY + Math.sin(perp) * half);
|
||||
ctx.lineTo(tipX, tipY);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
document.querySelectorAll('canvas.hw-knob').forEach((canvas) => {
|
||||
|
||||
Reference in New Issue
Block a user