sandbox/studio: knob now has large metallic disc filling the body

Reading the close-up: the knob is dominated by a big metallic chrome
disc that occupies most of its surface. The dark part is just a thin
outer rim. The indicator is a small dark notch on the disc that
rotates with the value (the disc itself stays vertically lit).

- Thin dark rim around the outside (~3px @ 56).
- Large metallic disc fills the inside (radial gradient, brushed-metal
  striations from center outward, top crescent highlight).
- Indicator is a small dark triangular notch on the disc, base near
  the center, tip toward the rim, rotated to the value angle.
- Amber arc kept outside everything as a value-progress meter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jose Luis Montañes
2026-05-01 18:06:08 +02:00
parent baca3c2df1
commit 249e2a2b96

View File

@@ -448,96 +448,112 @@ function setupCanvas(canvas, size) {
function drawKnob(ctx, size, norm) { function drawKnob(ctx, size, norm) {
const cx = size / 2, cy = size / 2 + 1; const cx = size / 2, cy = size / 2 + 1;
const outerR = size * 0.46; // chrome-bezel outer edge const totalR = size * 0.46; // outer edge of the whole knob
const ringW = size * 0.075; // metallic ring width (~4px @ 56) const rimW = size * 0.045; // dark rim thickness (thin)
const dialR = outerR - ringW; // dark dial radius const discR = totalR - rimW; // metallic disc fills most of inside
const ringMid = (outerR + dialR) / 2;
ctx.clearRect(0, 0, size, size); ctx.clearRect(0, 0, size, size);
// (1) drop shadow under the whole knob — gives it weight on the panel // (1) drop shadow under the whole knob
ctx.save(); ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.7)'; ctx.shadowColor = 'rgba(0,0,0,0.7)';
ctx.shadowBlur = 6; ctx.shadowBlur = 6;
ctx.shadowOffsetY = 2; ctx.shadowOffsetY = 2;
ctx.fillStyle = '#000'; ctx.fillStyle = '#000';
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, outerR + 0.5, 0, Math.PI * 2); ctx.arc(cx, cy, totalR + 0.5, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
ctx.restore(); ctx.restore();
// (2) amber value arc OUTSIDE the bezel (faint track + bright active portion) // (2) amber value arc OUTSIDE everything (faint full track + bright active)
const startA = Math.PI * 0.78, endA = Math.PI * 2.22; const startA = Math.PI * 0.78, endA = Math.PI * 2.22;
ctx.lineCap = 'round'; ctx.lineCap = 'round';
ctx.lineWidth = 1.5; ctx.lineWidth = 1.5;
ctx.strokeStyle = 'rgba(232,160,80,0.10)'; ctx.strokeStyle = 'rgba(232,160,80,0.10)';
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, outerR + 3, startA, endA); ctx.arc(cx, cy, totalR + 4, startA, endA);
ctx.stroke(); ctx.stroke();
ctx.strokeStyle = '#e8a050'; ctx.strokeStyle = '#e8a050';
ctx.shadowBlur = 3; ctx.shadowBlur = 3;
ctx.shadowColor = 'rgba(232,160,80,0.55)'; ctx.shadowColor = 'rgba(232,160,80,0.55)';
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, outerR + 3, startA, startA + norm * (endA - startA)); ctx.arc(cx, cy, totalR + 4, startA, startA + norm * (endA - startA));
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
// (3) METALLIC RING — vertical gradient (light top, dark bottom). // (3) thin DARK RIM around the outside (the body of the knob)
// This is the "chrome bezel" the reference shows. const rim = ctx.createLinearGradient(0, cy - totalR, 0, cy + totalR);
const ringGrad = ctx.createLinearGradient(0, cy - outerR, 0, cy + outerR); rim.addColorStop(0, '#2a2826');
ringGrad.addColorStop(0, '#c4beb2'); rim.addColorStop(0.5, '#0a0908');
ringGrad.addColorStop(0.30, '#807a72'); rim.addColorStop(1, '#040404');
ringGrad.addColorStop(0.55, '#3a3631'); ctx.strokeStyle = rim;
ringGrad.addColorStop(0.85, '#1c1a17'); ctx.lineWidth = rimW;
ringGrad.addColorStop(1, '#0a0908');
ctx.strokeStyle = ringGrad;
ctx.lineWidth = ringW;
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, ringMid, 0, Math.PI * 2); ctx.arc(cx, cy, totalR - rimW / 2, 0, Math.PI * 2);
ctx.stroke(); ctx.stroke();
// (4) thin bright highlight on the very top of the bezel // (4) BIG METALLIC DISC — fills most of the inside (the "circulito metalico
ctx.strokeStyle = 'rgba(255,255,255,0.22)'; // que ocupa casi todo"). Radial gradient, brushed-metal feel.
ctx.lineWidth = 0.6; const disc = ctx.createRadialGradient(
ctx.beginPath(); cx, cy - discR * 0.25, 0,
ctx.arc(cx, cy, outerR - 0.3, Math.PI * 1.18, Math.PI * 1.82); cx, cy + discR * 0.4, discR * 1.4
ctx.stroke();
// (5) inner shadow where the dial sinks below the bezel
ctx.strokeStyle = 'rgba(0,0,0,0.7)';
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.arc(cx, cy, dialR + 0.4, 0, Math.PI * 2);
ctx.stroke();
// (6) dial body — pure charcoal/black, NO warm tint
const dial = ctx.createRadialGradient(
cx - dialR * 0.35, cy - dialR * 0.5, 0,
cx, cy, dialR * 1.1
); );
dial.addColorStop(0, '#2a2a28'); disc.addColorStop(0, '#b6b0a6'); // lit upper-center
dial.addColorStop(0.5, '#141312'); disc.addColorStop(0.45, '#8a847a');
dial.addColorStop(1, '#040404'); disc.addColorStop(0.85, '#46423d');
ctx.fillStyle = dial; disc.addColorStop(1, '#2a2722'); // bottom dark
ctx.fillStyle = disc;
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, dialR, 0, Math.PI * 2); ctx.arc(cx, cy, discR, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
// (7) faint glossy crescent on the dial's upper half // (5) brushed-metal radial striations (very subtle, deterministic)
ctx.strokeStyle = 'rgba(255,255,255,0.07)'; ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, discR - 0.5, 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)})`;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(a) * discR, cy + Math.sin(a) * discR);
ctx.stroke();
}
ctx.restore();
// (6) thin shadow line where disc meets rim (depth)
ctx.strokeStyle = 'rgba(0,0,0,0.55)';
ctx.lineWidth = 0.7; ctx.lineWidth = 0.7;
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, dialR - 1, Math.PI * 1.18, Math.PI * 1.82); ctx.arc(cx, cy, discR + 0.4, 0, Math.PI * 2);
ctx.stroke(); ctx.stroke();
// (8) indicator tick — cream, from mid to dial edge // (7) bright highlight crescent on the top edge of the disc
const ang = startA + norm * (endA - startA); ctx.strokeStyle = 'rgba(255,255,255,0.30)';
ctx.strokeStyle = 'rgba(245,235,215,0.92)'; ctx.lineWidth = 0.8;
ctx.lineWidth = 1.7;
ctx.lineCap = 'round';
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(cx + Math.cos(ang) * dialR * 0.5, cy + Math.sin(ang) * dialR * 0.5); ctx.arc(cx, cy, discR - 0.5, Math.PI * 1.18, Math.PI * 1.82);
ctx.lineTo(cx + Math.cos(ang) * dialR * 0.88, cy + Math.sin(ang) * dialR * 0.88);
ctx.stroke(); ctx.stroke();
// (8) small DARK NOTCH indicator on the disc, rotates with value
const ang = startA + norm * (endA - startA);
const tipR = discR * 0.86;
const baseR = discR * 0.55;
const perp = ang + Math.PI / 2;
const half = size * 0.028;
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)';
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();
} }
document.querySelectorAll('canvas.hw-knob').forEach((canvas) => { document.querySelectorAll('canvas.hw-knob').forEach((canvas) => {