1/* eslint-disable no-redeclare */ 2/** 3 * Hex Grid Hero Background 4 * 5 * Structured hex grid rendered to canvas with elliptical fade, purple path 6 * edges, and a radial fade overlay. Static layers are composited to an 7 * offscreen canvas; animation frames just blit that image + draw effects. 8 * 9 * Grid data: 10 * grid[col][row] â hex object { col, row, cx, cy, verts } 11 * getEdge(col, row, side) â edge object { x1, y1, x2, y2 } 12 * 13 * Feature flag HEX_HERO_MODE: 14 * "canvas" â full hex grid + paths + wave animation (default) 15 * "image" â no canvas, original AVIF background shows 16 * "hybrid" â AVIF visible + transparent canvas with lightning on paths 17 * 18 * Flat-top hexagons, odd-q offset coordinates. 19 */ 20;(function () { 21 "use strict" 22 23 // --- Feature flag --- 24 var HEX_HERO_MODE = "canvas" 25 26 // --- Bail early --- 27 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return 28 if (window.location.search.indexOf("no-hex-grid") !== -1) return 29 // Routes whose hero renders the animated hex-grid field. 30 var HEX_GRID_ROUTES = ["/", "/features"] 31 var route = document.body.closest("[data-route]") 32 if (!route) route = document.querySelector("[data-route]") 33 if (route && HEX_GRID_ROUTES.indexOf(route.getAttribute("data-route")) === -1) 34 return 35 if (HEX_HERO_MODE === "image") return 36 37 // Parse hex color to [r,g,b] 38 function parseHex(h) { 39 return [ 40 parseInt(h.slice(1, 3), 16), 41 parseInt(h.slice(3, 5), 16), 42 parseInt(h.slice(5, 7), 16), 43 ] 44 } 45 function rgbToHex(r, g, b) { 46 return ( 47 "#" + 48 [r, g, b] 49 .map(function (v) { 50 return Math.round(v).toString(16).padStart(2, "0") 51 }) 52 .join("") 53 ) 54 } 55 function lerpColor(rgb1, rgb2, t) { 56 t = Math.max(0, Math.min(1, t)) 57 return rgbToHex( 58 rgb1[0] + (rgb2[0] - rgb1[0]) * t, 59 rgb1[1] + (rgb2[1] - rgb1[1]) * t, 60 rgb1[2] + (rgb2[2] - rgb1[2]) * t 61 ) 62 } 63 64 // --- Palette â derived from the --m-* token layer (marketing-overrides.css) 65 // so the canvas art follows the theme instead of baking retired hexes. 66 // Fill/path are value steps between the field and the accent, mirroring the 67 // desktop derivation model. Fallbacks keep the art alive if a token is 68 // missing (e.g. the grid playground opened via file://). 69 function readTokenHex(name, fallback) { 70 var v = getComputedStyle(document.documentElement) 71 .getPropertyValue(name) 72 .trim() 73 return /^#[0-9a-fA-F]{6}$/.test(v) ? v : fallback 74 } 75 var BG = readTokenHex("--m-bg", "#0b0a11") 76 var ACCENT = readTokenHex("--m-accent", "#5f2aff") 77 var BG_RGB = parseHex(BG) 78 var ACCENT_RGB = parseHex(ACCENT) 79 var HEX_FILL = lerpColor(BG_RGB, ACCENT_RGB, 0.3) 80 var PATH_COLOR = lerpColor(BG_RGB, ACCENT_RGB, 0.75) 81 var HEX_RGB = parseHex(HEX_FILL) 82 83 // --- Config --- 84 var HEX_RADIUS = 26 85 var GAP = 7 86 var OX = 1 87 var OY = 50 88 89 // Fade params 90 var fadeInnerStop = 0.3 91 var fadeRXPct = 50 92 var fadeRYPct = 50 93 94 // --- State --- 95 var canvas, ctx 96 var dpr = Math.min(window.devicePixelRatio || 1, 2) 97 var W = 0, 98 H = 0 99 var cols = 0, 100 rows = 0 101 var hexR = HEX_RADIUS 102 var hexW, hexH 103 var offsetX = 0, 104 offsetY = 0 105 var grid = [] 106 var resizeTimer = null 107 108 // Fade ellipse center/radii (set by buildGrid, read by effects) 109 var fadeCX = 0, 110 fadeCY = 0, 111 fadeRX = 1, 112 fadeRY = 1 113 114 // Path segments resolved from hex-grid-paths.js 115 var allSegments = [] 116 117 // Offscreen canvas for static layer 118 var offCanvas = null, 119 offCtx = null 120 121 // --- Hex geometry (flat-top) --- 122 function computeHexMetrics() { 123 hexR = HEX_RADIUS 124 hexW = hexR * 2 125 hexH = Math.sqrt(3) * hexR 126 } 127 128 function hexToPixel(col, row) { 129 var x = col * hexR * 1.5 + offsetX 130 var y = row * hexH + (col % 2 === 0 ? 0 : hexH * 0.5) + offsetY 131 return [x, y] 132 } 133 134 function hexVertex(cx, cy, i) { 135 var angle = (Math.PI / 3) * i 136 return [cx + hexR * Math.cos(angle), cy + hexR * Math.sin(angle)] 137 } 138 139 // --- Fixed reference dimensions (match AVIF / compare-hero canvas) --- 140 var REF_W = 1440 141 var REF_H = 1008 142 143 // --- Build grid data structure --- 144 function buildGrid() { 145 computeHexMetrics() 146 // Fixed grid size â always the same number of hexagons regardless of viewport 147 cols = Math.ceil(REF_W / (hexR * 1.5)) + 2 148 rows = Math.ceil(REF_H / hexH) + 2 149 150 // Center the fixed-size grid in the actual viewport 151 var gridPixelW = (cols - 1) * hexR * 1.5 + hexW 152 var gridPixelH = (rows - 1) * hexH + hexH 153 offsetX = (W - gridPixelW) / 2 + hexR + OX 154 offsetY = (H - gridPixelH) / 2 + hexH * 0.5 + OY 155 156 // Elliptical fade center: 13th row, middle column 157 var centerCol = Math.floor(cols / 2) 158 var centerRow = 12 159 var cc = hexToPixel(centerCol, centerRow) 160 fadeCX = cc[0] 161 fadeCY = cc[1] 162 fadeRX = REF_W * (fadeRXPct / 100) 163 fadeRY = REF_H * (fadeRYPct / 100) 164 165 grid = [] 166 for (var c = 0; c < cols; c++) { 167 grid[c] = [] 168 for (var r = 0; r < rows; r++) { 169 var center = hexToPixel(c, r) 170 var verts = [] 171 for (var v = 0; v < 6; v++) { 172 verts.push(hexVertex(center[0], center[1], v)) 173 } 174 grid[c][r] = { 175 col: c, 176 row: r, 177 cx: center[0], 178 cy: center[1], 179 verts: verts, 180 } 181 } 182 } 183 } 184 185 // --- Edge access --- 186 function getEdge(col, row, side) { 187 if (!grid[col] || !grid[col][row]) return null 188 var hex = grid[col][row] 189 var v1 = hex.verts[side] 190 var v2 = hex.verts[(side + 1) % 6] 191 return { x1: v1[0], y1: v1[1], x2: v2[0], y2: v2[1] } 192 } 193 194 // --- Resolve path edges --- 195 // hexGridPaths stores center-relative offsets [dcol, drow, side]. 196 // At runtime we resolve them to absolute grid indices using the
197 // current viewport's center column and row 12 (the fade center). 198 function resolvePaths() { 199 allSegments = [] 200 var paths = window.hexGridPaths 201 if (!paths) return 202 var centerCol = Math.floor(cols / 2) 203 var centerRow = 12 204 for (var i = 0; i < paths.length; i++) { 205 var p = paths[i] 206 var c = centerCol + p[0] 207 var r = centerRow + p[1] 208 var edge = getEdge(c, r, p[2]) 209 if (edge) { 210 allSegments.push([edge.x1, edge.y1, edge.x2, edge.y2]) 211 } 212 } 213 } 214 215 // --- Rendering (static layer) --- 216 // scrollScale: 0â1 multiplier that shrinks each hex toward its center. 217 // When 1.0, output is identical to the cached offscreen blit. 218 function renderStatic(targetCtx, scrollScale) { 219 if (scrollScale === undefined) scrollScale = 1.0 220 targetCtx.clearRect(0, 0, W, H) 221 222 // In hybrid mode, keep canvas transparent (no bg fill, no hex fills) 223 if (HEX_HERO_MODE === "hybrid") return 224 225 // Background 226 targetCtx.fillStyle = BG 227 targetCtx.fillRect(0, 0, W, H) 228 229 // Hex fills with elliptical fade 230 var inset = GAP / 2 231 var scale = Math.max(0.1, (hexR - inset) / hexR) * scrollScale 232 233 for (var c = 0; c < cols; c++) { 234 for (var r = 0; r < rows; r++) { 235 var hex = grid[c][r] 236 237 // Elliptical distance â fade 238 var dx = (hex.cx - fadeCX) / fadeRX 239 var dy = (hex.cy - fadeCY) / fadeRY 240 var dist = Math.sqrt(dx * dx + dy * dy) 241 var fadeT = Math.max( 242 0, 243 Math.min(1, (dist - fadeInnerStop) / (1 - fadeInnerStop)) 244 ) 245 246 targetCtx.fillStyle = lerpColor(HEX_RGB, BG_RGB, fadeT) 247 targetCtx.beginPath() 248 var v0x = hex.cx + (hex.verts[0][0] - hex.cx) * scale 249 var v0y = hex.cy + (hex.verts[0][1] - hex.cy) * scale 250 targetCtx.moveTo(v0x, v0y) 251 for (var v = 1; v < 6; v++) { 252 var vx = hex.cx + (hex.verts[v][0] - hex.cx) * scale 253 var vy = hex.cy + (hex.verts[v][1] - hex.cy) * scale 254 targetCtx.lineTo(vx, vy) 255 } 256 targetCtx.closePath() 257 targetCtx.fill() 258 } 259 } 260 261 // Path edges with glow + crisp passes 262 if (allSegments.length > 0) { 263 targetCtx.save() 264 targetCtx.lineCap = "round" 265 targetCtx.lineJoin = "round" 266 targetCtx.strokeStyle = PATH_COLOR 267 268 for (var gi = 0; gi < allSegments.length; gi++) { 269 var seg = allSegments[gi] 270 var mx = (seg[0] + seg[2]) / 2 271 var my = (seg[1] + seg[3]) / 2 272 var pdx = (mx - fadeCX) / fadeRX 273 var pdy = (my - fadeCY) / fadeRY 274 var pdist = Math.sqrt(pdx * pdx + pdy * pdy) 275 var pathAlpha = 276 1 - 277 Math.max( 278 0, 279 Math.min(1, (pdist - fadeInnerStop) / (1 - fadeInnerStop)) 280 ) 281 // Fade path edges proportionally as hexes shrink 282 pathAlpha *= scrollScale 283 if (pathAlpha <= 0) continue 284 285 // Glow pass 286 targetCtx.lineWidth = GAP + 2 287 targetCtx.globalAlpha = 0.25 * pathAlpha 288 targetCtx.shadowColor = PATH_COLOR 289 targetCtx.shadowBlur = 6 290 targetCtx.beginPath() 291 targetCtx.moveTo(seg[0], seg[1]) 292 targetCtx.lineTo(seg[2], seg[3]) 293 targetCtx.stroke() 294 295 // Crisp pass 296 targetCtx.shadowBlur = 0 297 targetCtx.globalAlpha = pathAlpha 298 targetCtx.lineWidth = Math.max(GAP * 0.7, 2.0) 299 targetCtx.beginPath() 300 targetCtx.moveTo(seg[0], seg[1]) 301 targetCtx.lineTo(seg[2], seg[3]) 302 targetCtx.stroke() 303 } 304 targetCtx.restore() 305 } 306
307 // Fade-to-dark radial overlay 308 drawFadeOverlay(targetCtx) 309 } 310 311 function drawFadeOverlay(targetCtx) { 312 var rx = fadeRX, 313 ry = fadeRY, 314 cx = fadeCX, 315 cy = fadeCY 316 targetCtx.save() 317 var grad = targetCtx.createRadialGradient( 318 cx, 319 cy, 320 0, 321 cx, 322 cy, 323 Math.max(rx, ry) 324 ) 325 var bgRgb = BG_RGB.join(",") 326 grad.addColorStop(0, "rgba(" + bgRgb + ",0)") 327 grad.addColorStop(fadeInnerStop, "rgba(" + bgRgb + ",0)") 328 grad.addColorStop(1, "rgba(" + bgRgb + ",1)") 329 targetCtx.fillStyle = grad 330 targetCtx.translate(cx, cy) 331 targetCtx.scale(1, ry / rx) 332 targetCtx.translate(-cx, -cy) 333 targetCtx.fillRect(cx - rx - 10, cy - rx - 10, rx * 2 + 20, rx * 2 + 20) 334 targetCtx.restore() 335 } 336 337 // --- Offscreen canvas management --- 338 function buildOffscreen() { 339 if (!offCanvas) { 340 offCanvas = document.createElement("canvas") 341 offCtx = offCanvas.getContext("2d") 342 } 343 offCanvas.width = W * dpr 344 offCanvas.height = H * dpr 345 offCtx.setTransform(dpr, 0, 0, dpr, 0, 0) 346 renderStatic(offCtx) 347 } 348 349 // Blit static layer to main canvas 350 function blitStatic() { 351 ctx.setTransform(1, 0, 0, 1, 0, 0) 352 if (HEX_HERO_MODE === "hybrid") { 353 ctx.clearRect(0, 0, canvas.width, canvas.height) 354 } else { 355 ctx.drawImage(offCanvas, 0, 0) 356 } 357 ctx.setTransform(dpr, 0, 0, dpr, 0, 0) 358 } 359 360 // --- Canvas setup --- 361 function setupCanvas() { 362 var section = canvas.parentElement 363 W = section.offsetWidth 364 H = section.offsetHeight 365 canvas.width = W * dpr 366 canvas.height = H * dpr 367 canvas.style.width = W + "px" 368 canvas.style.height = H + "px" 369 ctx.setTransform(dpr, 0, 0, dpr, 0, 0) 370 } 371 372 function fullRebuild() { 373 setupCanvas() 374 buildGrid() 375 resolvePaths() 376 buildOffscreen() 377 blitStatic() 378 } 379 380 // Sync local state to the window.hexGrid API object so external 381 // consumers (effects script, playground) always see current values. 382 function syncApi() { 383 var api = window.hexGrid 384 if (!api) return 385 api.grid = grid 386 api.allSegments = allSegments 387 api.fadeCX = fadeCX 388 api.fadeCY = fadeCY 389 api.fadeRX = fadeRX 390 api.fadeRY = fadeRY 391 api.fadeInnerStop = fadeInnerStop 392 api.W = W 393 api.H = H 394 api.offCanvas = offCanvas 395 api.GAP = GAP 396 api.HEX_RADIUS = HEX_RADIUS 397 } 398 399 function onResize() { 400 clearTimeout(resizeTimer) 401 resizeTimer = setTimeout(function () { 402 fullRebuild() 403 syncApi() 404 // Notify effects script to recompute distances 405 if (window.hexGridEffects && window.hexGridEffects.onResize) { 406 window.hexGridEffects.onResize() 407 } 408 }, 200) 409 } 410 411 // --- Init --- 412 function init() { 413 canvas = document.getElementById("hex-grid-canvas") 414 if (!canvas) return 415 ctx = canvas.getContext("2d") 416 417 if (HEX_HERO_MODE === "hybrid") { 418 canvas.parentElement.classList.add("hex-grid-hybrid") 419 } else { 420 canvas.parentElement.classList.add("hex-grid-active") 421 } 422 423 fullRebuild() 424 window.addEventListener("resize", onResize) 425 426 // Rebuild the grid with optional parameter overrides (used by playground) 427 function rebuild(params) { 428 if (params) { 429 if (params.radius !== undefined) HEX_RADIUS = params.radius 430 if (params.gap !== undefined) GAP = params.gap 431 if (params.offsetX !== undefined) OX = params.offsetX 432 if (params.offsetY !== undefined) OY = params.offsetY 433 if (params.fadeInnerStop !== undefined) 434 fadeInnerStop = params.fadeInnerStop 435 if (params.fadeRXPct !== undefined) fadeRXPct = params.fadeRXPct 436 if (params.fadeRYPct !== undefined) fadeRYPct = params.fadeRYPct 437 } 438 fullRebuild() 439 syncApi() 440 // Notify effects script to recompute 441 if (window.hexGridEffects && window.hexGridEffects.onResize) { 442 window.hexGridEffects.onResize() 443 } 444 } 445 446 // Expose grid API for effects script and other consumers 447 window.hexGrid = { 448 grid: grid, 449 getEdge: getEdge, 450 render: blitStatic, 451 allSegments: allSegments, 452 fadeCX: fadeCX, 453 fadeCY: fadeCY, 454 fadeRX: fadeRX, 455 fadeRY: fadeRY, 456 fadeInnerStop: fadeInnerStop, 457 W: W, 458 H: H, 459 ctx: ctx, 460 mode: HEX_HERO_MODE, 461 blitStatic: blitStatic, 462 renderStaticToCtx: function (targetCtx, scrollScale) { 463 renderStatic(targetCtx, scrollScale) 464 }, 465 offCanvas: offCanvas, 466 dpr: dpr, 467 HEX_RGB: HEX_RGB, 468 BG_RGB: BG_RGB, 469 GAP: GAP, 470 HEX_RADIUS: HEX_RADIUS, 471 lerpColor: lerpColor, 472 parseHex: parseHex, 473 rebuild: rebuild, 474 } 475 } 476 477 if (document.readyState === "loading") { 478 document.addEventListener("DOMContentLoaded", init) 479 } else { 480 init() 481 } 482})()
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.