mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Overdrive laser demo, deep link support, and split-compare fix
Demo: - Laser-etched "Paul Bakaus" signature drawn in real-time on dark surface, adapted from pbakaus/shaders laser-precision - Two-canvas architecture: persistent burn canvas + cleared-per-frame spark overlay to prevent pixel accumulation - Smooth quadraticCurveTo rendering for fluid signature strokes - Sparks with motion trails, multi-layer tip glow Infrastructure: - Add init() support for command demos — JS can now execute after demo HTML is inserted into the DOM - Fix split-compare retriggerAnimations to not destroy canvas elements (clone-and-replace for CSS-only demos, individual retrigger when canvas is present) - Add deep linking to commands (#cmd-overdrive etc.) with hash tracking and initial load support - Remove auto-#hero hash — hero is the default state Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
8bb0017752
commit
577c9c872c
@@ -71,7 +71,7 @@ For each issue, document:
|
||||
- **Impact**: How it affects users
|
||||
- **WCAG/Standard**: Which standard it violates (if applicable)
|
||||
- **Recommendation**: How to fix it
|
||||
- **Suggested command**: Which command to use (prefer: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /critique, /colorize — or other installed skills you're sure exist)
|
||||
- **Suggested command**: Which command to use (prefer: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive — or other installed skills you're sure exist)
|
||||
|
||||
#### Critical Issues
|
||||
[Issues that block core functionality or violate WCAG A]
|
||||
@@ -108,7 +108,7 @@ Create actionable plan:
|
||||
|
||||
### Suggested Commands for Fixes
|
||||
|
||||
Map issues to available commands. Prefer these: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /critique, /colorize. You may also suggest other installed skills you're sure exist, but never invent commands.
|
||||
Map issues to available commands. Prefer these: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive. You may also suggest other installed skills you're sure exist, but never invent commands.
|
||||
|
||||
Examples:
|
||||
- "Use `/normalize` to align with design system (addresses N theming issues)"
|
||||
|
||||
@@ -102,7 +102,7 @@ For each issue:
|
||||
- **What**: Name the problem clearly
|
||||
- **Why it matters**: How this hurts users or undermines goals
|
||||
- **Fix**: What to do about it (be concrete)
|
||||
- **Command**: Which command to use (prefer: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /critique, /colorize — or other installed skills you're sure exist)
|
||||
- **Command**: Which command to use (prefer: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive — or other installed skills you're sure exist)
|
||||
|
||||
### Minor Observations
|
||||
Quick notes on smaller issues worth addressing.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderCommandDemo } from "../demo-renderer.js";
|
||||
import { renderCommandDemo, initCommandDemo } from "../demo-renderer.js";
|
||||
import { initSplitCompare } from "../effects/split-compare.js";
|
||||
import { commandProcessSteps, commandCategories, commandRelationships } from "../data.js";
|
||||
|
||||
@@ -164,6 +164,7 @@ function setupDesktopScrollSpy(commands) {
|
||||
const cmd = commands.find(c => c.id === cmdId);
|
||||
if (cmd) {
|
||||
updateTerminal(cmd, terminalContent, commands);
|
||||
history.replaceState(null, '', `#cmd-${cmdId}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -183,6 +184,7 @@ function setupDesktopScrollSpy(commands) {
|
||||
const cmd = commands.find(c => c.id === cmdId);
|
||||
if (cmd) {
|
||||
updateTerminal(cmd, terminalContent, commands);
|
||||
history.replaceState(null, '', `#cmd-${cmdId}`);
|
||||
}
|
||||
|
||||
e.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
@@ -223,6 +225,7 @@ function updateTerminal(cmd, container, allCommands) {
|
||||
|
||||
});
|
||||
}
|
||||
initCommandDemo(cmd.id, container);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -293,6 +296,7 @@ function setupMobileInteractions(commands) {
|
||||
maxPosition: 90
|
||||
});
|
||||
}
|
||||
if (commands[0]) initCommandDemo(commands[0].id, demoArea);
|
||||
|
||||
// Pill click/tap handler
|
||||
pills.forEach(pill => {
|
||||
@@ -334,6 +338,7 @@ function setupMobileInteractions(commands) {
|
||||
|
||||
});
|
||||
}
|
||||
initCommandDemo(cmdId, demoArea);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,19 @@
|
||||
import { getCommandDemo } from './demos/commands/index.js';
|
||||
import { getSkillDemo } from './demos/skills/index.js';
|
||||
|
||||
/**
|
||||
* Initialize a command demo's JS after its HTML has been inserted into the DOM.
|
||||
* Call this after innerHTML is set and split compare is initialized.
|
||||
*/
|
||||
export function initCommandDemo(commandId, container) {
|
||||
const demo = getCommandDemo(commandId);
|
||||
if (demo && typeof demo.init === 'function') {
|
||||
const demoArea = container.querySelector('.split-after .split-content') || container;
|
||||
console.log('[initCommandDemo]', commandId, 'demoArea:', demoArea);
|
||||
demo.init(demoArea);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a command demo with split-screen comparison
|
||||
*/
|
||||
|
||||
@@ -1,33 +1,296 @@
|
||||
// Overdrive command demo - shows standard rectangular layout vs organic, non-standard composition
|
||||
// Overdrive command demo - laser-etched signature on a premium dark surface
|
||||
// Laser effect adapted from pbakaus/shaders laser-precision
|
||||
|
||||
export default {
|
||||
id: 'overdrive',
|
||||
caption: 'Standard rectangular layout → Organic, technically ambitious composition',
|
||||
caption: 'Static flat card → Laser-etched signature effect',
|
||||
|
||||
before: `
|
||||
<div style="width: 100%; max-width: 240px; padding: 20px 16px; text-align: center; background: #f5f5f5; border: 1px solid #e0e0e0; border-radius: 8px;">
|
||||
<div style="width: 48px; height: 48px; background: #ddd; border-radius: 50%; margin: 0 auto 12px;"></div>
|
||||
<div style="font-size: 14px; font-weight: 600; color: #333; margin-bottom: 4px;">Sarah Chen</div>
|
||||
<div style="font-size: 12px; color: #888; margin-bottom: 16px;">Design Lead</div>
|
||||
<div style="display: flex; justify-content: center; gap: 24px; padding-top: 12px; border-top: 1px solid #e0e0e0;">
|
||||
<div style="text-align: center;"><div style="font-size: 14px; font-weight: 600; color: #333;">142</div><div style="font-size: 10px; color: #999;">Projects</div></div>
|
||||
<div style="text-align: center;"><div style="font-size: 14px; font-weight: 600; color: #333;">38</div><div style="font-size: 10px; color: #999;">Reviews</div></div>
|
||||
<div style="width: 100%; height: 100%; min-height: 200px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; font-family: system-ui, sans-serif;">
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<div style="font-size: 12px; color: #666; font-style: italic; line-height: 1.6; max-width: 220px; margin-bottom: 16px;">It's time to spark your imagination. Welcome to the Impeccable Community.</div>
|
||||
<div style="font-size: 12px; color: #aaa;">Paul Bakaus</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
after: `
|
||||
<div style="width: 100%; max-width: 240px; padding: 20px 16px; position: relative; overflow: hidden; background: var(--color-paper); border-radius: 24px 24px 8px 8px;">
|
||||
<div style="position: absolute; top: -30px; right: -20px; width: 100px; height: 100px; background: color-mix(in oklch, var(--color-accent) 10%, transparent); border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%; filter: blur(1px);"></div>
|
||||
<div style="position: absolute; bottom: -15px; left: -10px; width: 70px; height: 70px; background: color-mix(in oklch, var(--color-accent) 6%, transparent); border-radius: 60% 40% 30% 70% / 50% 60% 40% 50%;"></div>
|
||||
<div style="position: relative; z-index: 1;">
|
||||
<div style="width: 48px; height: 48px; background: var(--color-accent); border-radius: 40% 60% 55% 45% / 55% 40% 60% 45%; margin: 0 auto 12px; display: flex; align-items: center; justify-content: center; color: white; font-size: 15px; font-weight: 600; font-family: 'Cormorant Garamond', serif;">SC</div>
|
||||
<div style="font-size: 0.9375rem; font-weight: 600; color: var(--color-ink); margin-bottom: 2px; font-family: 'Cormorant Garamond', serif;">Sarah Chen</div>
|
||||
<div style="font-size: 0.6875rem; color: var(--color-ash); margin-bottom: 16px; letter-spacing: 0.04em; font-family: 'Instrument Sans', sans-serif;">Design Lead</div>
|
||||
<div style="display: flex; justify-content: center; gap: 20px; padding-top: 12px; border-top: 1px solid var(--color-mist);">
|
||||
<div style="text-align: center;"><div style="font-size: 0.875rem; font-weight: 600; color: var(--color-ink); font-variant-numeric: tabular-nums;">142</div><div style="font-size: 0.5625rem; color: var(--color-ash); letter-spacing: 0.06em;">Projects</div></div>
|
||||
<div style="text-align: center;"><div style="font-size: 0.875rem; font-weight: 600; color: var(--color-ink); font-variant-numeric: tabular-nums;">38</div><div style="font-size: 0.5625rem; color: var(--color-ash); letter-spacing: 0.06em;">Reviews</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<canvas class="od-burn" style="position: absolute; inset: 0; width: 100%; height: 100%; background: #0e0d0b;"></canvas>
|
||||
<canvas class="od-sparks" style="position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none;"></canvas>
|
||||
<div style="position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; pointer-events: none; text-align: center;">
|
||||
<p style="font-family: 'Cormorant Garamond', serif; font-size: 1.1rem; font-style: italic; font-weight: 400; color: rgba(240,230,210,0.85); line-height: 1.5; max-width: 260px; margin: 0 0 24px;">It's time to spark your imagination.<br>Welcome to the Impeccable Community.</p>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
|
||||
init(container) {
|
||||
const burnCanvas = container.querySelector('.od-burn');
|
||||
const sparkCanvas = container.querySelector('.od-sparks');
|
||||
if (!burnCanvas || !sparkCanvas) return;
|
||||
|
||||
const rect = burnCanvas.parentElement.getBoundingClientRect();
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
|
||||
// Size both canvases
|
||||
for (const c of [burnCanvas, sparkCanvas]) {
|
||||
c.width = Math.round(rect.width * dpr);
|
||||
c.height = Math.round(rect.height * dpr);
|
||||
}
|
||||
|
||||
const ctx = burnCanvas.getContext('2d'); // persistent burn trails
|
||||
const sCtx = sparkCanvas.getContext('2d'); // cleared each frame
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
sCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
const w = rect.width;
|
||||
const h = rect.height;
|
||||
|
||||
// Fill background
|
||||
ctx.fillStyle = '#0e0d0b';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// ── Signature path ──
|
||||
function buildSignaturePath() {
|
||||
const pts = [];
|
||||
function bez(x0,y0, cx1,cy1, cx2,cy2, x1,y1, n) {
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const t = i / n, mt = 1-t;
|
||||
pts.push({
|
||||
x: mt*mt*mt*x0 + 3*mt*mt*t*cx1 + 3*mt*t*t*cx2 + t*t*t*x1,
|
||||
y: mt*mt*mt*y0 + 3*mt*mt*t*cy1 + 3*mt*t*t*cy2 + t*t*t*y1
|
||||
});
|
||||
}
|
||||
}
|
||||
// P
|
||||
bez(6,44, 5,32, 4,18, 8,8, 14);
|
||||
bez(8,8, 16,5, 26,7, 26,16, 12);
|
||||
bez(26,16, 26,22, 18,26, 14,28, 10);
|
||||
// a
|
||||
bez(14,28, 18,22, 23,20, 26,22, 8);
|
||||
bez(26,22, 29,24, 28,30, 24,32, 6);
|
||||
bez(24,32, 28,34, 30,30, 32,28, 5);
|
||||
// u
|
||||
bez(32,28, 34,36, 38,40, 42,32, 8);
|
||||
bez(42,32, 44,26, 47,24, 48,28, 6);
|
||||
// l
|
||||
bez(48,28, 49,16, 50,6, 53,8, 10);
|
||||
bez(53,8, 55,14, 56,28, 58,32, 8);
|
||||
// B
|
||||
bez(66,44, 66,32, 67,16, 70,8, 14);
|
||||
bez(70,8, 78,4, 83,10, 79,18, 12);
|
||||
bez(79,18, 84,15, 87,24, 80,30, 12);
|
||||
bez(80,30, 78,34, 80,36, 84,32, 5);
|
||||
// akaus
|
||||
bez(84,32, 89,24, 94,22, 97,26, 8);
|
||||
bez(97,26, 99,30, 96,34, 100,30, 5);
|
||||
bez(100,30, 101,20, 102,14, 104,16, 8);
|
||||
bez(104,16, 106,24, 109,28, 107,32, 6);
|
||||
bez(107,32, 105,36, 110,36, 113,30, 6);
|
||||
bez(113,30, 118,24, 122,22, 125,28, 8);
|
||||
bez(125,28, 128,36, 133,38, 137,30, 8);
|
||||
bez(137,30, 139,26, 142,24, 144,28, 5);
|
||||
bez(144,28, 154,24, 170,22, 195,28, 16);
|
||||
|
||||
// Same scaling as the static preview version
|
||||
const rawW = 200;
|
||||
const scale = (w * 0.7) / rawW;
|
||||
const ox = (w - rawW * scale) / 2;
|
||||
const oy = h * 0.52;
|
||||
return pts.map(p => ({ x: p.x * scale + ox, y: p.y * scale * 0.75 + oy }));
|
||||
}
|
||||
|
||||
const sigPoints = buildSignaturePath();
|
||||
const segLengths = [];
|
||||
let totalLength = 0;
|
||||
for (let i = 1; i < sigPoints.length; i++) {
|
||||
const dx = sigPoints[i].x - sigPoints[i-1].x;
|
||||
const dy = sigPoints[i].y - sigPoints[i-1].y;
|
||||
segLengths.push(Math.sqrt(dx*dx + dy*dy));
|
||||
totalLength += segLengths[segLengths.length - 1];
|
||||
}
|
||||
|
||||
function posAt(dist) {
|
||||
let d = 0;
|
||||
for (let i = 0; i < segLengths.length; i++) {
|
||||
if (d + segLengths[i] >= dist) {
|
||||
const t = segLengths[i] > 0 ? (dist - d) / segLengths[i] : 0;
|
||||
const p0 = sigPoints[i], p1 = sigPoints[i+1];
|
||||
return { x: p0.x + (p1.x - p0.x) * t, y: p0.y + (p1.y - p0.y) * t };
|
||||
}
|
||||
d += segLengths[i];
|
||||
}
|
||||
return sigPoints[sigPoints.length - 1];
|
||||
}
|
||||
|
||||
// ── State ──
|
||||
let drawnLength = 0;
|
||||
const drawSpeed = totalLength / 3.0;
|
||||
let prevTip = sigPoints[0];
|
||||
let sparks = [];
|
||||
let phase = 'drawing';
|
||||
let phaseTimer = 0;
|
||||
let lastTime = 0;
|
||||
|
||||
// Track all drawn points for smooth rendering
|
||||
const drawnPts = [];
|
||||
|
||||
function drawBurnTrail() {
|
||||
if (drawnPts.length < 2) return;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
// Draw smooth path through all points using quadratic curves
|
||||
function strokeSmooth(color, width) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(drawnPts[0].x, drawnPts[0].y);
|
||||
for (let i = 1; i < drawnPts.length - 1; i++) {
|
||||
const mx = (drawnPts[i].x + drawnPts[i+1].x) / 2;
|
||||
const my = (drawnPts[i].y + drawnPts[i+1].y) / 2;
|
||||
ctx.quadraticCurveTo(drawnPts[i].x, drawnPts[i].y, mx, my);
|
||||
}
|
||||
const last = drawnPts[drawnPts.length - 1];
|
||||
ctx.lineTo(last.x, last.y);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
strokeSmooth('rgba(180, 100, 30, 0.12)', 5);
|
||||
strokeSmooth('rgba(220, 140, 50, 0.3)', 2.5);
|
||||
strokeSmooth('rgba(255, 210, 130, 0.7)', 1.2);
|
||||
strokeSmooth('rgba(255, 248, 235, 0.6)', 0.4);
|
||||
}
|
||||
|
||||
function emitSparks(x, y, count) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 50 + Math.random() * 140;
|
||||
sparks.push({
|
||||
x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed,
|
||||
life: 0.15 + Math.random() * 0.35, maxLife: 0.15 + Math.random() * 0.35,
|
||||
size: 0.3 + Math.random() * 1.0, bright: Math.random() > 0.4
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function draw(timestamp) {
|
||||
if (!document.contains(burnCanvas)) return;
|
||||
|
||||
// Actual frame delta time
|
||||
if (!lastTime) lastTime = timestamp;
|
||||
const dt = Math.min(0.05, (timestamp - lastTime) / 1000);
|
||||
lastTime = timestamp;
|
||||
|
||||
switch (phase) {
|
||||
case 'drawing':
|
||||
drawnLength += drawSpeed * dt;
|
||||
if (drawnLength >= totalLength) {
|
||||
drawnLength = totalLength;
|
||||
phase = 'holding';
|
||||
phaseTimer = 0;
|
||||
emitSparks(prevTip.x, prevTip.y, 8);
|
||||
}
|
||||
const tip = posAt(drawnLength);
|
||||
drawnPts.push({ x: tip.x, y: tip.y });
|
||||
prevTip = tip;
|
||||
if (Math.random() < 0.4) emitSparks(tip.x, tip.y, 1);
|
||||
// Redraw full smooth trail (clear burn canvas first)
|
||||
ctx.fillStyle = '#0e0d0b';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
drawBurnTrail();
|
||||
break;
|
||||
|
||||
case 'holding':
|
||||
phaseTimer += dt;
|
||||
if (phaseTimer >= 3.5) { phase = 'fading'; phaseTimer = 0; }
|
||||
break;
|
||||
|
||||
case 'fading':
|
||||
phaseTimer += dt;
|
||||
ctx.fillStyle = 'rgba(14, 13, 11, 0.04)';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
if (phaseTimer >= 2.0) {
|
||||
ctx.fillStyle = '#0e0d0b';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
drawnLength = 0; prevTip = sigPoints[0];
|
||||
sparks = []; drawnPts.length = 0;
|
||||
phase = 'drawing'; phaseTimer = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Update sparks
|
||||
for (let i = sparks.length - 1; i >= 0; i--) {
|
||||
const s = sparks[i];
|
||||
s.x += s.vx * dt; s.y += s.vy * dt;
|
||||
s.vx *= 0.94; s.vy *= 0.94; s.vy += 100 * dt;
|
||||
s.life -= dt;
|
||||
if (s.life <= 0) sparks.splice(i, 1);
|
||||
}
|
||||
|
||||
// Draw sparks + tip on overlay (cleared each frame)
|
||||
sCtx.clearRect(0, 0, w, h);
|
||||
|
||||
for (const s of sparks) {
|
||||
const t = s.life / s.maxLife;
|
||||
const r = s.size * (0.3 + t * 0.7);
|
||||
// Spark trail
|
||||
const speed = Math.sqrt(s.vx*s.vx + s.vy*s.vy);
|
||||
if (speed > 20) {
|
||||
const tl = speed * 0.01;
|
||||
sCtx.beginPath();
|
||||
sCtx.moveTo(s.x, s.y);
|
||||
sCtx.lineTo(s.x - s.vx/speed * tl, s.y - s.vy/speed * tl);
|
||||
sCtx.strokeStyle = s.bright
|
||||
? `rgba(255,255,240,${(t*0.4).toFixed(3)})`
|
||||
: `rgba(255,180,60,${(t*0.3).toFixed(3)})`;
|
||||
sCtx.lineWidth = r * 0.5;
|
||||
sCtx.lineCap = 'round';
|
||||
sCtx.stroke();
|
||||
}
|
||||
sCtx.beginPath();
|
||||
sCtx.arc(s.x, s.y, r, 0, Math.PI * 2);
|
||||
sCtx.fillStyle = s.bright
|
||||
? `rgba(255,255,255,${(t*0.85).toFixed(3)})`
|
||||
: `rgba(255,200,80,${(t*0.75).toFixed(3)})`;
|
||||
sCtx.fill();
|
||||
}
|
||||
|
||||
// Draw laser tip on overlay
|
||||
if (phase === 'drawing' && drawnLength < totalLength) {
|
||||
const tip = posAt(drawnLength);
|
||||
const fl = 0.85 + Math.random() * 0.15;
|
||||
|
||||
// Wide heat bloom
|
||||
const g0 = sCtx.createRadialGradient(tip.x, tip.y, 0, tip.x, tip.y, 35);
|
||||
g0.addColorStop(0, `rgba(255,100,20,${0.15*fl})`);
|
||||
g0.addColorStop(0.4, `rgba(200,60,10,${0.05*fl})`);
|
||||
g0.addColorStop(1, 'rgba(150,40,10,0)');
|
||||
sCtx.fillStyle = g0; sCtx.beginPath(); sCtx.arc(tip.x, tip.y, 35, 0, Math.PI*2); sCtx.fill();
|
||||
|
||||
// Amber corona
|
||||
const g1 = sCtx.createRadialGradient(tip.x, tip.y, 0, tip.x, tip.y, 16);
|
||||
g1.addColorStop(0, `rgba(255,180,60,${0.45*fl})`);
|
||||
g1.addColorStop(0.5, `rgba(255,140,40,${0.15*fl})`);
|
||||
g1.addColorStop(1, 'rgba(200,80,20,0)');
|
||||
sCtx.fillStyle = g1; sCtx.beginPath(); sCtx.arc(tip.x, tip.y, 16, 0, Math.PI*2); sCtx.fill();
|
||||
|
||||
// White-hot core
|
||||
const g2 = sCtx.createRadialGradient(tip.x, tip.y, 0, tip.x, tip.y, 6);
|
||||
g2.addColorStop(0, `rgba(255,255,255,${0.95*fl})`);
|
||||
g2.addColorStop(0.3, `rgba(255,250,240,${0.7*fl})`);
|
||||
g2.addColorStop(0.6, `rgba(255,220,160,${0.3*fl})`);
|
||||
g2.addColorStop(1, 'rgba(255,180,80,0)');
|
||||
sCtx.fillStyle = g2; sCtx.beginPath(); sCtx.arc(tip.x, tip.y, 6, 0, Math.PI*2); sCtx.fill();
|
||||
|
||||
// Overexposed center
|
||||
const g3 = sCtx.createRadialGradient(tip.x, tip.y, 0, tip.x, tip.y, 2.5);
|
||||
g3.addColorStop(0, `rgba(255,255,255,${fl})`);
|
||||
g3.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
sCtx.fillStyle = g3; sCtx.beginPath(); sCtx.arc(tip.x, tip.y, 2.5, 0, Math.PI*2); sCtx.fill();
|
||||
}
|
||||
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,10 +48,27 @@ export function initSplitCompare(container, options = {}) {
|
||||
}
|
||||
|
||||
function retriggerAnimations() {
|
||||
// Find all elements with animations in the "after" content and re-trigger them
|
||||
// Re-trigger CSS animations in the "after" content.
|
||||
// If there's a canvas (e.g. overdrive shader), we can't clone-and-replace
|
||||
// because that destroys JS-driven animations. In that case, retrigger
|
||||
// individual elements. Otherwise, use the fast clone approach.
|
||||
const afterContent = splitAfter.querySelector('.split-content');
|
||||
if (afterContent) {
|
||||
// Clone and replace to restart all CSS animations
|
||||
if (!afterContent) return;
|
||||
|
||||
const hasCanvas = afterContent.querySelector('canvas, .od-burn, .od-sparks');
|
||||
if (hasCanvas) {
|
||||
// Safe path: retrigger CSS animations individually, skip canvas
|
||||
afterContent.querySelectorAll('*').forEach(el => {
|
||||
if (el.tagName === 'CANVAS') return;
|
||||
const anim = getComputedStyle(el).animationName;
|
||||
if (anim && anim !== 'none') {
|
||||
el.style.animation = 'none';
|
||||
el.offsetHeight;
|
||||
el.style.animation = '';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fast path: clone and replace to restart all CSS animations
|
||||
const clone = afterContent.cloneNode(true);
|
||||
afterContent.parentNode.replaceChild(clone, afterContent);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,21 @@ export function initHashTracking() {
|
||||
let ticking = false;
|
||||
|
||||
function updateHash() {
|
||||
// Don't override command deep links while user is in the commands section
|
||||
if (currentHash.startsWith('cmd-')) {
|
||||
const cmdEl = document.getElementById(currentHash);
|
||||
if (cmdEl) {
|
||||
const rect = cmdEl.getBoundingClientRect();
|
||||
// Only clear the cmd hash if user scrolled well away from commands section
|
||||
if (rect.top > window.innerHeight * 2 || rect.bottom < -window.innerHeight) {
|
||||
currentHash = '';
|
||||
} else {
|
||||
ticking = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scrollY = window.scrollY;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const triggerPoint = scrollY + viewportHeight * 0.3;
|
||||
@@ -38,6 +53,9 @@ export function initHashTracking() {
|
||||
}
|
||||
});
|
||||
|
||||
// Don't set #hero — it's the default state, no hash needed
|
||||
if (activeSection === 'hero') activeSection = '';
|
||||
|
||||
if (activeSection !== currentHash) {
|
||||
currentHash = activeSection;
|
||||
if (activeSection) {
|
||||
@@ -59,17 +77,23 @@ export function initHashTracking() {
|
||||
|
||||
// Handle initial hash on page load - instant jump
|
||||
if (window.location.hash) {
|
||||
const target = document.querySelector(window.location.hash);
|
||||
const hash = window.location.hash.slice(1);
|
||||
const target = document.getElementById(hash);
|
||||
if (target) {
|
||||
currentHash = hash;
|
||||
setTimeout(() => {
|
||||
const offset = 40;
|
||||
const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset;
|
||||
window.scrollTo({ top: targetPosition, behavior: 'auto' });
|
||||
|
||||
// If it's a command deep link, activate it
|
||||
if (hash.startsWith('cmd-') && target.classList.contains('manual-entry')) {
|
||||
target.click();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
// No hash — don't set one on initial load
|
||||
}
|
||||
|
||||
// Initial check
|
||||
updateHash();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user