1"use strict"; 2 3const DOT_SIZE = 2; 4const GRID_GAP = 12; 5const HEAT_LEVELS = 24; 6const GLOW_ALPHA = 0.8; 7const FRAME_MS = 33; 8const WAVE_LENGTH = 420; 9const WAVE_SPEED = 0.00025; 10const WAVE_SHARPNESS = 4; 11 12let pixelFields = []; 13 14document.addEventListener("DOMContentLoaded", onEffectsLoad); 15window.addEventListener("resize", onEffectsResize); 16 17function onEffectsLoad() { 18 const canvases = document.querySelectorAll("canvas[data-pixels]"); 19 if (canvases.length === 0) return; 20 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", onSchemeChange); 21 for (const canvas of canvases) { 22 const field = createField(canvas); 23 pixelFields.push(field); 24 observeField(field); 25 } 26 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { 27 for (const field of pixelFields) drawField(field, 0); 28 return; 29 } 30 requestAnimationFrame(onFrame); 31} 32 33function onEffectsResize() { 34 for (const field of pixelFields) { 35 sizeField(field); 36 drawField(field, 0); 37 } 38} 39 40function onSchemeChange() { 41 for (const field of pixelFields) { 42 field.dotStyles = buildDotStyles(field.canvas); 43 drawField(field, 0); 44 } 45} 46 47function buildDotStyles(element) { 48 const style = getComputedStyle(element); 49 const accentHex = style.getPropertyValue("--accent").trim(); 50 const strokeParts = (style.getPropertyValue("--stroke").match(/[\d.]+/g) ?? ["0", "0", "0", "0.1"]).map(Number); 51 const accentValue = parseInt(accentHex.slice(1), 16); 52 const accent = { r: (accentValue >> 16) & 255, g: (accentValue >> 8) & 255, b: accentValue & 255 }; 53 const base = { r: strokeParts[0], g: strokeParts[1], b: strokeParts[2], a: strokeParts[3] ?? 1 }; 54 const styles = []; 55 for (let i = 0; i < HEAT_LEVELS; i++) { 56 const t = i / (HEAT_LEVELS - 1); 57 const r = Math.round(base.r + (accent.r - base.r) * t); 58 const g = Math.round(base.g + (accent.g - base.g) * t); 59 const b = Math.round(base.b + (accent.b - base.b) * t); 60 const a = base.a + (GLOW_ALPHA - base.a) * t; 61 styles.push(`rgba(${r}, ${g}, ${b}, ${a.toFixed(3)})`); 62 } 63 return styles; 64} 65 66function createField(canvas) { 67 const field = { 68 canvas, 69 ctx: canvas.getContext("2d"), 70 width: 0, 71 height: 0, 72 cols: 0, 73 rows: 0, 74 offsetX: 0, 75 offsetY: 0, 76 visible: false, 77 lastFrameAt: 0, 78 dotStyles: buildDotStyles(canvas) 79 }; 80 sizeField(field); 81 return field; 82} 83 84function sizeField(field) { 85 const rect = field.canvas.getBoundingClientRect(); 86 if (rect.width < 10 || rect.height < 10) return; 87 const dpr = window.devicePixelRatio || 1; 88 field.width = rect.width; 89 field.height = rect.height; 90 field.canvas.width = Math.max(1, Math.round(rect.width * dpr)); 91 field.canvas.height = Math.max(1, Math.round(rect.height * dpr)); 92 field.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 93 field.cols = Math.max(1, Math.floor((rect.width - DOT_SIZE) / GRID_GAP) + 1); 94 field.rows = Math.max(1, Math.floor((rect.height - DOT_SIZE) / GRID_GAP) + 1); 95 field.offsetX = (rect.width - ((field.cols - 1) * GRID_GAP + DOT_SIZE)) / 2; 96 field.offsetY = (rect.height - ((field.rows - 1) * GRID_GAP + DOT_SIZE)) / 2; 97} 98 99function observeField(field) { 100 const observer = new IntersectionObserver((entries) => { 101 for (const entry of entries) { 102 field.visible = entry.isIntersecting; 103 if (!entry.isIntersecting) continue; 104 const rect = field.canvas.getBoundingClientRect(); 105 if (Math.abs(rect.width - field.width) > 1 || Math.abs(rect.height - field.height) > 1) { 106 sizeField(field); 107 drawField(field, 0); 108 } 109 } 110 }); 111 observer.observe(field.canvas); 112} 113 114function drawField(field, time) { 115 const { ctx, cols, rows } = field; 116 const phase = time * WAVE_SPEED; 117 ctx.clearRect(0, 0, field.width, field.height); 118 for (let row = 0; row < rows; row++) { 119 const y = field.offsetY + row * GRID_GAP; 120 for (let col = 0; col < cols; col++) { 121 const x = field.offsetX + col * GRID_GAP; 122 const wave = 0.5 + 0.5 * Math.sin(((x + y) / WAVE_LENGTH - phase) * Math.PI * 2); 123 const value = Math.pow(wave, WAVE_SHARPNESS); 124 const level = Math.min(HEAT_LEVELS - 1, Math.floor(value * HEAT_LEVELS)); 125 ctx.fillStyle = field.dotStyles[level]; 126 ctx.fillRect(x, y, DOT_SIZE, DOT_SIZE); 127 } 128 } 129} 130 131function onFrame(timestamp) { 132 for (const field of pixelFields) { 133 if (!field.visible) continue; 134 if (timestamp - field.lastFrameAt < FRAME_MS) continue; 135 field.lastFrameAt = timestamp; 136 drawField(field, timestamp); 137 } 138 requestAnimationFrame(onFrame); 139}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.