diff --git a/pavebank/canvas.js b/pavebank/canvas.js index 9d212c3..3de9c57 100644 --- a/pavebank/canvas.js +++ b/pavebank/canvas.js @@ -1,7 +1,7 @@ // ========================================================================= // SECTION 1: GLOBAL DEFAULTS // ========================================================================= - + const LINEGRID_DEFAULTS = { // All values in rem — 1rem = 50px baseline colRem: 2.2, // column width (110px @ 50px/rem) @@ -25,8 +25,8 @@ lineColor: '#f5f5f5', dpr: Math.min(window.devicePixelRatio || 1, 2), }; - - + + // ========================================================================= // SECTION 2: LINEGRID CLASS // @@ -43,12 +43,12 @@ // 4. Wire into _tick() following existing pattern // 5. Use _snapY(y) for all y positions to stay on background grid // ========================================================================= - + class LineGrid { constructor(container, options = {}) { this.container = typeof container === 'string' ? document.querySelector(container) : container; const cfg = Object.assign({}, LINEGRID_DEFAULTS, options); - + // Grid — all rem-based, actual pixel values computed in resize() this.COL_REM = cfg.colRem; this.GAP_REM = cfg.gapRem; @@ -65,41 +65,41 @@ this.MAX_H = 0; this.RADIUS = 0; this.BG_OPACITY = cfg.bgOpacity; - + // Cube this.SIZE_RATIO = cfg.sizeRatio; this.STROKE = cfg.stroke; this.THRESHOLD = cfg.threshold; - + // Hover this.TRAIL_LEN = cfg.trailLen; this.LERP = cfg.lerp; this.VEL_DECAY = cfg.velDecay; this.VEL_SCALE = cfg.velScale; - + // Rhombus wave this.WAVE_SPEED = cfg.waveSpeed; - + // Flip this.FLIP_SPEED = cfg.flipSpeed; this.FLIP_FOCAL = cfg.flipFocal; - + // Colors this._bgVar = options.bgColor && options.bgColor.startsWith('--') ? options.bgColor : null; this._lineVar = options.lineColor && options.lineColor.startsWith('--') ? options.lineColor : null; this.BG_COLOR = this._bgVar ? this._readVar(this._bgVar) : (cfg.bgColor || '#222222'); this.LINE_COLOR = this._lineVar ? this._hexToRgb(this._readVar(this._lineVar)) : this._hexToRgb(cfg.lineColor || '#f5f5f5'); - + // System this.DPR = cfg.dpr; - + // Foreground config this.foregroundType = options.foregroundType || null; this.shapePath = options.shapePath || null; this.shapeViewBox = options.shapeViewBox || { w: 1450, h: 770 }; this.flipPaths = options.flipPaths || []; this.flipBounds = options.flipBounds || []; - + // Runtime state this.W = 0; this.H = 0; this.COL_W = 0; this.sCX = 0; this.sCY = 0; this.LINES = 0; @@ -114,11 +114,11 @@ this.prevSmoothX = -1; this.prevSmoothY = -1; this.velocity = 0; this._rafId = null; - + // Rhombus state this.rhombusLines = null; this.revealProgress = 0; // 0 = hidden, 1 = fully revealed — drive from GSAP - + // Flip state this.flipDatas = []; this.flipHalfW = []; @@ -126,7 +126,7 @@ this.flipIdx = 0; this.flipLastSign = 1; this.flipAngle = 0; - + // Gyroscope state this.gyroRings = null; this.GYRO_SLOW = options.gyroSlow || 0.75; @@ -144,26 +144,26 @@ this.globeRevealX = 0; // 0=center → 1=right (W*0.712) this.plinthReveal = 0; // 0=hidden → 1=full width this.arcReveal = 0; // 0=hidden → 1=visible (bottom→top mask) - + // Canvas this.canvas = document.createElement('canvas'); this.canvas.style.cssText = `display:block;width:100%;height:100%;background:${this.BG_COLOR};`; this.container.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d'); - + // Offscreen this.faceCanvas = document.createElement('canvas'); this.fCtx = this.faceCanvas.getContext('2d'); - + this.CUBE_EDGES = [[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]]; - + this._onMouseMove = this._onMouseMove.bind(this); this._onMouseLeave = this._onMouseLeave.bind(this); this._onResize = this._onResize.bind(this); this.canvas.addEventListener('mousemove', this._onMouseMove); this.canvas.addEventListener('mouseleave', this._onMouseLeave); window.addEventListener('resize', this._onResize); - + window.addEventListener('load', () => { this.resize(); if (this.foregroundType === 'globe') this._initGlobe(); @@ -172,9 +172,9 @@ LineGrid._instances.add(this); }); } - + // ── Events ────────────────────────────────────────────────────────────── - + _onMouseMove(e) { const rect = this.canvas.getBoundingClientRect(); this.targetX = e.clientX - rect.left; @@ -188,24 +188,24 @@ this.velocity = 0; this.trail.length = 0; } _onResize() { this.resize(); } - + // ── Resize ────────────────────────────────────────────────────────────── - + resize() { const style = window.getComputedStyle(this.container); const W = this.container.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); const H = this.container.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom); this.W = W; this.H = H > 0 ? H : window.innerHeight; - + this.canvas.style.width = this.W + 'px'; this.canvas.style.height = this.H + 'px'; this.canvas.width = this.W * this.DPR; this.canvas.height = this.H * this.DPR; this.ctx.scale(this.DPR, this.DPR); - + this.faceCanvas.width = this.W * this.DPR; this.faceCanvas.height = this.H * this.DPR; - + // ── Rem → px conversion ───────────────────────────────────────────── // Re-read rem on every resize so dynamic root font-size changes // are always reflected immediately across all canvas measurements @@ -220,18 +220,18 @@ // Store rem for use in _renderGlobe / _drawGlobeDecor this._rem = rem; // ──────────────────────────────────────────────────────────────────── - + this.sCX = this.W / 2; this.sCY = this.H / 2; this.LINES = Math.ceil(this.H / this.LINE_GAP); - + this.colXCache = []; this.energy = []; for (let col = 0; col < this.COLS; col++) { this.colXCache.push(col * (this.COL_W + this.GAP)); this.energy.push(new Float32Array(this.LINES).fill(0)); } - + if (this.foregroundType === 'rhombus' && this.shapePath) { this._buildRhombusLines(); } @@ -239,13 +239,13 @@ this._buildFlipShapes(); } } - + // ── Shared helpers ─────────────────────────────────────────────────────── - + _rotateY(v,a){const[x,y,z]=v;return[x*Math.cos(a)+z*Math.sin(a),y,-x*Math.sin(a)+z*Math.cos(a)];} _rotateX(v,a){const[x,y,z]=v;return[x,y*Math.cos(a)-z*Math.sin(a),y*Math.sin(a)+z*Math.cos(a)];} _project(v,focal){const[x,y,z]=v,s=focal/(focal+z+focal*0.5);return[this.sCX+x*s,this.sCY+y*s];} - + _getFracCol(x) { for (let c = 0; c < this.COLS; c++) { const cx = this.colXCache[c]; @@ -253,20 +253,20 @@ } return x < 0 ? 0 : this.COLS - 1; } - + // ALL foreground renderers must use _snapY() for y positions _snapY(y) { return Math.round(y / this.LINE_GAP) * this.LINE_GAP; } - + _readVar(varName) { return getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); } - + refreshColors() { if (this._bgVar) this.BG_COLOR = this._readVar(this._bgVar); if (this._lineVar) this.LINE_COLOR = this._hexToRgb(this._readVar(this._lineVar)); this.canvas.style.background = this.BG_COLOR; } - + _hexToRgb(hex) { if (!hex) return '245,245,245'; hex = hex.trim().replace('#', ''); @@ -276,7 +276,7 @@ const b = parseInt(hex.slice(4,6), 16); return `${r},${g},${b}`; } - + _drawRoundedBottom(x, y, w, h, r) { if (w <= 0 || h <= 0) return; const safeR = Math.min(r, h/2, w/2); @@ -286,11 +286,11 @@ c.lineTo(x+safeR,y+h); c.quadraticCurveTo(x,y+h,x,y+h-safeR); c.lineTo(x,y); c.closePath(); c.fill(); } - + // ========================================================================= // FOREGROUND: CUBE // ========================================================================= - + _renderCube(SIZE, FOCAL) { const {W,H,DPR,STROKE} = this; const fCtx = this.fCtx; @@ -314,7 +314,7 @@ }); fCtx.restore(); } - + _drawCubeLines(getAlpha, SIZE) { const {COLS,COL_W,LINE_GAP,RADIUS,THRESHOLD,H} = this; for (let col=0;colv/DPR; - + let x0=Infinity,x1=-Infinity,y0=Infinity,y1=-Infinity; for(let py=0;py{ const px=Math.round(Math.max(0,Math.min(W*DPR-1,cx*DPR))); const py=Math.round(Math.max(0,Math.min(H*DPR-1,cy*DPR))); return d[(py*stride+px)*4+3]/255; }; - + const lines=[]; for(let col=0;col reveal) continue; - + // How far into the reveal this line is (0=just appeared, 1=fully in) // Lines near center are "more revealed" than lines near edge const lineReveal = reveal === 0 ? 0 : Math.min(1, (reveal - dist) / Math.max(reveal, 0.001)); - + // Breathing wave — only active once fully revealed const phase = t * WAVE_SPEED - distH * 0.5; const sine = (Math.sin(phase * Math.PI * 2) + 1) / 2; const breathH = baseH * 0.4 + sine * baseH * 0.6; - + // During reveal: grow from MIN_H to baseH. After reveal: breathe. const revealH = MIN_H + lineReveal * (baseH - MIN_H); const lh = reveal < 1 ? revealH : breathH; - + this.ctx.fillStyle=`rgba(${this.LINE_COLOR},1)`; this._drawRoundedBottom(lx,y,rx-lx,Math.max(MIN_H,lh),RADIUS); } } } - + // ========================================================================= // FOREGROUND: FLIP (two SVG paths that Y-axis rotate and swap) // ========================================================================= - + _buildFlipShapes() { const {W,H,DPR} = this; this.flipDatas = []; this.flipHalfW = []; this.flipHalfH = []; - + this.flipPaths.forEach((pathStr, i) => { const b = this.flipBounds[i]; const shapeCX = (b.x1 + b.x2) / 2; const shapeCY = (b.y1 + b.y2) / 2; const offX = this.sCX - shapeCX; const offY = this.sCY - shapeCY; - + const oc = document.createElement('canvas'); oc.width = W * DPR; oc.height = H * DPR; const oc2d = oc.getContext('2d'); @@ -473,54 +473,54 @@ oc2d.fillStyle = 'white'; oc2d.fill(new Path2D(pathStr)); oc2d.restore(); - + this.flipDatas.push(oc2d.getImageData(0, 0, W * DPR, H * DPR)); this.flipHalfW.push((b.x2 - b.x1) / 2 * 0.8); this.flipHalfH.push((b.y2 - b.y1) / 2 * 0.8); }); } - + _drawFlipLines() { if(this.flipDatas.length < 2) return; const {W,H,DPR,COLS,COL_W,LINE_GAP,RADIUS,THRESHOLD,MIN_H,MAX_H,FLIP_FOCAL} = this; - + this.flipAngle += this.FLIP_SPEED; const cosA = Math.cos(this.flipAngle); const sinA = Math.sin(this.flipAngle); - + const currentSign = cosA >= 0 ? 1 : -1; if (currentSign !== this.flipLastSign) { this.flipIdx = 1 - this.flipIdx; this.flipLastSign = currentSign; } - + const data = this.flipDatas[this.flipIdx]; const halfW = this.flipHalfW[this.flipIdx]; const halfH = this.flipHalfH[this.flipIdx]; const stride = W * DPR; - + const getAlpha = (x, y) => { const px = Math.round(Math.max(0, Math.min(W*DPR-1, x*DPR))); const py = Math.round(Math.max(0, Math.min(H*DPR-1, y*DPR))); return data.data[(py*stride+px)*4+3] / 255; }; - + const edgeZ = halfW * sinA; const perspScale = FLIP_FOCAL / (FLIP_FOCAL + edgeZ); const screenHalfW = halfW * Math.abs(cosA) * perspScale; if (screenHalfW < 0.5) return; - + const scanL = Math.floor(this.sCX - halfW); const scanR = Math.ceil(this.sCX + halfW); const scaleX = screenHalfW / halfW; - + for (let col = 0; col < COLS; col++) { const colX = this.colXCache[col]; const colEnd = colX + COL_W; const projLeft = this.sCX - screenHalfW; const projRight = this.sCX + screenHalfW; if (colEnd < projLeft || colX > projRight) continue; - + for (let y = 0; y <= H; y += LINE_GAP) { let leftShapeX = -1, rightShapeX = -1; for (let sx = scanL; sx <= scanR; sx++) { @@ -530,30 +530,30 @@ if (getAlpha(sx, y) > THRESHOLD) { rightShapeX = sx; break; } } if (leftShapeX === -1 || rightShapeX === -1) continue; - + const screenLeft = this.sCX + (leftShapeX - this.sCX) * scaleX; const screenRight = this.sCX + (rightShapeX - this.sCX) * scaleX; const leftX = Math.max(screenLeft, colX); const rightX = Math.min(screenRight, colEnd); if (rightX <= leftX) continue; - + const midX = (leftX + rightX) / 2; const ndx = (midX - this.sCX) / Math.max(screenHalfW, 1); const ndy = (y - this.sCY) / halfH; const d = Math.sqrt(ndx*ndx + ndy*ndy); const t = Math.max(0, 1 - d); const lh = MIN_H + t * (MAX_H - MIN_H); - + this.ctx.fillStyle = `rgba(${this.LINE_COLOR},1)`; this._drawRoundedBottom(leftX, y, rightX - leftX, lh, RADIUS); } } } - + // ========================================================================= // FOREGROUND: GYRO (4 rings rotating on different axes with axis drift) // ========================================================================= - + _initGyro() { const s = this.GYRO_SLOW; this.gyroRings = [ @@ -572,26 +572,26 @@ ring.axisZ = axisZTargets[i]; }); } - + _renderGyro() { if (!this.gyroRings) return; const { W, H, DPR } = this; const RING_R = Math.min(W, H) * 0.45 * this.gyroScale; if (RING_R < 0.5) return null; - + const STEPS = 180; const fCtx = this.fCtx; const reveal = this.gyroReveal; - + fCtx.clearRect(0, 0, W*DPR, H*DPR); fCtx.save(); fCtx.scale(DPR, DPR); fCtx.strokeStyle = 'white'; fCtx.lineWidth = this.GYRO_STROKE; fCtx.lineCap = 'round'; - + const FOCAL = 900; - + this.gyroRings.forEach(ring => { // Y spin accumulates always ring.angle += ring.speed; @@ -599,12 +599,12 @@ ring.axisX += ring.driftX; ring.axisZ += ring.driftZ; } - + const startAxisX = Math.PI / 2; const displayAxisX = startAxisX + (ring.axisX - startAxisX) * reveal; const displayAxisZ = ring.axisZ * reveal; const displayAngle = ring.angle; - + fCtx.beginPath(); for (let st = 0; st <= STEPS; st++) { const a = (st / STEPS) * Math.PI * 2; @@ -625,24 +625,24 @@ fCtx.closePath(); fCtx.stroke(); }); - + fCtx.restore(); return RING_R; } - + _drawGyroLines(RING_R) { const { W, H, DPR, COLS, COL_W, LINE_GAP, RADIUS, THRESHOLD } = this; const data = this.fCtx.getImageData(0, 0, W*DPR, H*DPR); const stride = Math.round(W * DPR); - + const getAlpha = (x, y) => { const px = Math.round(Math.max(0, Math.min(W*DPR-1, x*DPR))); const py = Math.round(Math.max(0, Math.min(H*DPR-1, y*DPR))); return data.data[(py*stride+px)*4+3] / 255; }; - + const GYRO_THRESHOLD = THRESHOLD; - + const getSegments = (colX, colEnd, y) => { const segs = []; let inSeg = false, start = -1; for (let px = colX; px <= colEnd; px++) { @@ -653,7 +653,7 @@ if (inSeg) segs.push([start, colEnd]); return segs; }; - + for (let col = 0; col < COLS; col++) { const colX = this.colXCache[col], colEnd = colX + COL_W; for (let y = 0; y <= H; y += LINE_GAP) { @@ -670,11 +670,11 @@ } } } - + // ========================================================================= // FOREGROUND: GLOBE // ========================================================================= - + _initGlobe() { const self = this; fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json') @@ -705,7 +705,7 @@ }; }); } - + _drawGlobeDecor(fCtx, GLOBE_R, GLOBE_CY, GLOBE_CX) { const sCX = GLOBE_CX !== undefined ? GLOBE_CX : this.sCX; const sCY = GLOBE_CY; @@ -714,37 +714,37 @@ const ARC_R = GLOBE_R + rem * 0.56; const ARC_STROKE = rem * 0.12; const TICK_SPAN = rem * 0.2; - + // Laurel arcs — reveal bottom→top via clip mask if (this.arcReveal > 0) { const arcBottom = sCY + ARC_R + TICK_SPAN + 10; const arcTop = sCY - ARC_R - TICK_SPAN - 10; const clipTop = arcBottom - this.arcReveal * (arcBottom - arcTop); - + fCtx.save(); fCtx.beginPath(); fCtx.rect(0, clipTop, this.W, arcBottom - clipTop + 10); fCtx.clip(); - + fCtx.strokeStyle = color; fCtx.lineWidth = ARC_STROKE; fCtx.lineCap = 'round'; - + fCtx.beginPath(); fCtx.arc(sCX, sCY, ARC_R, Math.PI * 0.62, Math.PI * 1.18, false); fCtx.stroke(); this._drawArcTick(fCtx, sCX, sCY, ARC_R, Math.PI * 0.62, 0.07, TICK_SPAN, ARC_STROKE); this._drawArcTick(fCtx, sCX, sCY, ARC_R, Math.PI * 1.18, -0.07, TICK_SPAN, ARC_STROKE); - + fCtx.beginPath(); fCtx.arc(sCX, sCY, ARC_R, Math.PI * 1.82, Math.PI * 2.38, false); fCtx.stroke(); this._drawArcTick(fCtx, sCX, sCY, ARC_R, Math.PI * 1.82, 0.07, TICK_SPAN, ARC_STROKE); this._drawArcTick(fCtx, sCX, sCY, ARC_R, Math.PI * 2.38, -0.07, TICK_SPAN, ARC_STROKE); - + fCtx.restore(); } - + // Plinth — width grows from center outward if (this.plinthReveal > 0) { const PLINTH_GAP = rem * 0.44; @@ -753,13 +753,13 @@ const PLINTH_H = rem * 0.36; const PLINTH_STEP_W = GLOBE_R * 0.85 * this.plinthReveal; const PLINTH_STEP_H = rem * 0.22; - + fCtx.fillStyle = color; fCtx.fillRect(sCX - PLINTH_STEP_W/2, PLINTH_Y, PLINTH_STEP_W, PLINTH_STEP_H); fCtx.fillRect(sCX - PLINTH_W/2, PLINTH_Y + PLINTH_STEP_H, PLINTH_W, PLINTH_H); } } - + _drawArcTick(o, cx, cy, r, angle, dAngle, tickSpan, lineWidth) { const innerR = r - tickSpan, outerR = r + tickSpan; const x1 = cx + innerR * Math.cos(angle), y1 = cy + innerR * Math.sin(angle); @@ -767,7 +767,7 @@ o.lineWidth = lineWidth; o.beginPath(); o.moveTo(x1,y1); o.lineTo(x2,y2); o.stroke(); } - + _renderGlobe() { if (!this.globeReady) return; const { W, H, DPR } = this; @@ -775,26 +775,26 @@ const GLOBE_R_FULL = Math.min(W, H) * 0.36 * 0.8; const GLOBE_R = GLOBE_R_FULL * this.globeRevealScale; // scale from 0 if (GLOBE_R < 0.5) return null; - + // X position lerps from center → right const GLOBE_CX = this.sCX + (W * 0.712 - this.sCX) * this.globeRevealX; const GLOBE_CY = this.sCY - rem * 0.6; const FOCAL = H * 1.1; const fCtx = this.fCtx; - + fCtx.clearRect(0, 0, W*DPR, H*DPR); fCtx.save(); fCtx.scale(DPR, DPR); fCtx.fillStyle = 'white'; - + const cosA = Math.cos(this.globeAngle), sinA = Math.sin(this.globeAngle); const cosT = Math.cos(this.GLOBE_TILT), sinT = Math.sin(this.GLOBE_TILT); - + this.shapeMinX = GLOBE_CX - GLOBE_R_FULL - 10; this.shapeMaxX = GLOBE_CX + GLOBE_R_FULL + 10; this._globeCY = GLOBE_CY; this._globeCX = GLOBE_CX; - + this.globeCountries.forEach(ring => { const pts = []; let anyVisible = false; @@ -816,7 +816,7 @@ if (!behind) anyVisible = true; } if (!anyVisible) return; - + fCtx.beginPath(); let penDown = false; pts.forEach(({ sx, sy, behind }) => { @@ -827,17 +827,17 @@ if (penDown) fCtx.closePath(); fCtx.fill(); }); - + fCtx.restore(); - + fCtx.save(); fCtx.scale(DPR, DPR); this._drawGlobeDecor(fCtx, GLOBE_R_FULL, GLOBE_CY, GLOBE_CX); fCtx.restore(); - + return GLOBE_R_FULL; } - + _drawGlobeLines(GLOBE_R) { if (!this.globeReady) return; const { W, H, DPR, COLS, COL_W, LINE_GAP, RADIUS, THRESHOLD } = this; @@ -845,13 +845,13 @@ const stride = Math.round(W * DPR); const globeCY = this._globeCY || this.sCY; const globeCX = this._globeCX || this.sCX; - + const getAlpha = (x, y) => { const px = Math.round(Math.max(0, Math.min(W*DPR-1, x*DPR))); const py = Math.round(Math.max(0, Math.min(H*DPR-1, y*DPR))); return data.data[(py*stride+px)*4+3] / 255; }; - + const getSegments = (colX, colEnd, y) => { const segs = []; let inSeg = false, start = -1; for (let px = colX; px <= colEnd; px++) { @@ -862,7 +862,7 @@ if (inSeg) segs.push([start, colEnd]); return segs; }; - + for (let col = 0; col < COLS; col++) { const colX = this.colXCache[col], colEnd = colX + COL_W; if (colEnd < this.shapeMinX || colX > this.shapeMaxX) continue; @@ -884,22 +884,22 @@ } } } - + // ========================================================================= // MAIN LOOP // ========================================================================= - + _tick(ts) { if(this.lastTs!==null) this.waveTime+=(ts-this.lastTs)/1000; this.lastTs=ts; this.angle+=0.009; - + const {W,H,DPR,COLS,COL_W,GAP,LINE_GAP,LINES,RADIUS, SIZE_RATIO,TRAIL_LEN,LERP,BG_OPACITY,VEL_DECAY,VEL_SCALE} = this; const BG_OPACITY_LINE = this.LINE_COLOR; const SIZE=H*SIZE_RATIO, FOCAL=H*1.1; const ctx=this.ctx; - + if(this.targetX>=0){ this.smoothX+=(this.targetX-this.smoothX)*LERP; this.smoothY+=(this.targetY-this.smoothY)*LERP; @@ -911,14 +911,14 @@ } this.velocity*=VEL_DECAY; const velStrength=Math.min(this.velocity*VEL_SCALE,1); - + if(velStrength>0.01&&this.smoothX>=0){ this.trail.push({x:this.smoothX,y:this.smoothY,v:velStrength}); if(this.trail.length>TRAIL_LEN) this.trail.shift(); } else { if(this.trail.length>0) this.trail.shift(); } - + this.trail.forEach((pt,ti)=>{ const strength=Math.pow(ti/this.trail.length,2)*pt.v; for(let col=0;col0; if(this.foregroundType==='cube'){ this._renderCube(SIZE,FOCAL); @@ -950,24 +950,24 @@ return data.data[(py*stride+px)*4+3]/255; }; } - + ctx.fillStyle=this.BG_COLOR; ctx.fillRect(0,0,W,H); - + const fracCol =this.smoothX>=0?this._getFracCol(this.smoothX):-1; const hoverLineIdx=this.smoothY>=0?this.smoothY/LINE_GAP:-1; - + for(let col=0;col=0&&velStrength>0.01) ?Math.exp(-Math.pow(col-fracCol,2)*1.2)*velStrength:0; - + let y=0,li=0; while(y<=H){ const e=this.energy[col][li]||0; - + ctx.fillStyle=`rgba(${BG_OPACITY_LINE},${BG_OPACITY})`; this._drawRoundedBottom(colX,y,COL_W,1,RADIUS); - + if(e>0.01){ ctx.fillStyle=`rgba(${BG_OPACITY_LINE},1)`; this._drawRoundedBottom(colX,y,COL_W,1+e*4,RADIUS); @@ -989,7 +989,7 @@ y+=LINE_GAP; li++; } } - + if(this.foregroundType==='cube') this._drawCubeLines(getAlpha,SIZE); if(this.foregroundType==='rhombus') this._drawRhombus(); if(this.foregroundType==='flip') this._drawFlipLines(); @@ -1004,10 +1004,10 @@ const gr = this._renderGlobe(); if (gr) this._drawGlobeLines(gr); } - + this._rafId=requestAnimationFrame((ts)=>this._tick(ts)); } - + destroy() { cancelAnimationFrame(this._rafId); this.canvas.removeEventListener('mousemove', this._onMouseMove); @@ -1017,30 +1017,30 @@ LineGrid._instances.delete(this); } } - + // Static instance registry LineGrid._instances = new Set(); - + // Refresh colors on all active instances at once. LineGrid.refreshAll = () => { LineGrid._instances.forEach(instance => instance.refreshColors()); }; - - + + // ========================================================================= // SECTION 3: CANVAS INSTANCES // bgColor / lineColor accept a CSS var name ('--dark') or a hex value ('#222'). // To update colors on scroll/interaction: LineGrid.refreshAll() // Globe scroll velocity: grid1.GLOBE_SPEED = BASE + velocity // ========================================================================= - + // Canvas 1 — rotating globe (right-aligned), laurel arcs + plinth const grid1 = new LineGrid('#section-1', { foregroundType: 'globe', bgColor: '--dark', lineColor: '--white', }); - + // Canvas 2 — gyroscope rings // Reveal sequence: // const tl = gsap.timeline({ paused: true }); @@ -1052,7 +1052,7 @@ bgColor: '--dark', lineColor: '--white', }); - + // Canvas 3 — animated rhombus // Reveal: gsap.to(grid3, { revealProgress: 1, duration: 1.2, ease: 'power2.out' }) const grid3 = new LineGrid('#section-3', { @@ -1063,7 +1063,7 @@ bgColor: '--dark', // ← manually set lineColor: '--white', // ← manually set }); - + // Canvas 4 — background + hover only new LineGrid('#section-4', { foregroundType: null,