PageSourceSearch

https://devswarm.ai/scripts/hex-grid.js

js devswarm.ai collected 2026-10-04 02:08:16 UTC 14,124 bytes, 482 lines download raw bytes

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.