1// TripGeo hexagon logo - stained glass edition. The mark is a ring of six pinwheel panels round a 2// centre hexagon (the inner triangles are gone), and behind it all a globe turns, so each of the 3// seven is a coloured pane of glass with the earth seen through it - a double exposure of the 4// hexagon and the world. The centre pane is a lighter blend of the six colours round it. 5// 6// The logo draws at once as a still (glass panels over an empty globe with its grid), then fetches 7// a small vector outline of the land (data/earth-land.json - Natural Earth 110m, thinned) and only 8// starts turning once that has arrived. The palette still slowly melts from one set of colours to 9// the next, as before. The old hexagon spin is switched off (see M.hexSpin) but left in place. 10// 11// The clock is a module-level singleton seeded from sessionStorage and every frame is a pure 12// function of it, so a logo that appears on another page - home to blog, or after a reload - picks 13// the animation up exactly where the last one left off instead of restarting. 14window.tgHexLogo = (function () { 15 'use strict'; 16 17 const SIZE = 400, CX = SIZE / 2, CY = SIZE / 2, RO = 156; // RO: the outer hexagon 18 const NS = 'http://www.w3.org/2000/svg'; 19 const DEG = Math.PI / 180; 20 21 const OUTER = ['#F5A623', '#7AC943', '#29ABE2', '#1B75BC', '#9B51E0', '#EC2E8A']; 22 23 /* Never white, never near-white: no colour is allowed to get lighter than this (HSL lightness, %). 24 The seams between the pieces are the only white in the mark, and they are the gaps, not a colour. */ 25 const MAX_LIGHT = 72, MIN_LIGHT = 12; 26 27 /* Tuning. Periods are in the comments so it is easy to re-pitch. */ 28 const M = { 29 globeRate: 9, // deg/s the earth turns (~40s a revolution), west to east 30 globeTilt: 20, // deg the north pole leans towards the viewer 31 globeLon: 10, // deg - the longitude facing us in the still (and at clock zero) 32 globeScale: 0.96, // globe radius as a fraction of the outer hexagon's radius 33 cornerRound: 0.16, // radius of the rounded outer corners, as a fraction of the same 34 35 oceanDark: 12, // % lightness the sea sits below the pane's own colour... 36 landLight: 14, // ...and the land above it, so the continents shine through the glass 37 gridLight: 22, // the grid lines, lighter again but faint (gridOpacity) 38 gridOpacity: 0.30, 39 40 centreSat: 1.5, // how strongly the centre keeps the blended hue when the six disagree 41 centreLight: 14, // % lightness the centre sits above the panes' average, so it reads light 42 43 hueSwing: 8, // deg - a small extra sway either side of whatever palette is showing 44 hueRate: 0.055, // (~114s), so even a held palette is never quite still 45 lightAmp: 5, // +/- % lightness in the wave that runs around the six panes 46 lightRate: 0.40, // rad/s (~16s) 47 lightLag: 0.85, // phase step from one pane to the next 48 49 hexSpin: false, // the old turning of the whole hexagon - off for now 50 spinDrift: 0.9, // deg/s - steady drift of the mark, when hexSpin is on 51 spinSwing: 22, // deg - sine on top, so it eases, stops and turns back 52 spinRate: 0.060, // rad/s (~105s) 53 54 paletteHold: 22, // s - each palette stays put this long... 55 paletteFade: 18 // s - ...then melts into the next one over this long 56 }; 57 58 /* ---------------------------------------------------------------- clock 59 Seconds since the first logo of this browser-tab session was drawn. Kept in sessionStorage so 60 it survives a reload, and read through performance.now() so it stays smooth within a load. */ 61 const CLOCK_KEY = 'tgHexLogoClock'; 62 const perfStart = performance.now(); 63 let baseSeconds = 0; 64 65 (function seedClock() { 66 const now = Date.now(); 67 try { 68 const saved = parseFloat(sessionStorage.getItem(CLOCK_KEY)); 69 if (isFinite(saved) && saved > 0 && saved <= now) { 70 baseSeconds = (now - saved) / 1000; 71 return; 72 }
73 sessionStorage.setItem(CLOCK_KEY, String(now)); 74 } catch (e) { 75 // Private mode or storage blocked - the clock just starts at zero for this load. 76 } 77 })(); 78 79 const clock = () => baseSeconds + (performance.now() - perfStart) / 1000; 80 81 /* ---------------------------------------------------------------- colour */ 82 function hexToHsl(hex) { 83 const n = parseInt(hex.slice(1), 16); 84 const r = ((n >> 16) & 255) / 255, g = ((n >> 8) & 255) / 255, b = (n & 255) / 255; 85 const max = Math.max(r, g, b), min = Math.min(r, g, b), l = (max + min) / 2; 86 if (max === min) return [0, 0, l * 100]; 87 const c = max - min; 88 let h; 89 if (max === r) h = ((g - b) / c) % 6; 90 else if (max === g) h = (b - r) / c + 2; 91 else h = (r - g) / c + 4; 92 h *= 60; 93 if (h < 0) h += 360; 94 return [h, (c / (1 - Math.abs(2 * l - 1))) * 100, l * 100]; 95 } 96 97 /* The palettes: six [hue, saturation %, lightness %] colours, one per pane, in the order they sit 98 round the mark. The first is the brand palette and the order below is the order they play in. */ 99 const ramp = (h0, h1, s, l0, l1) => [0, 1, 2, 3, 4, 5].map(function (k) { 100 const u = k / 5; 101 return [(h0 + (h1 - h0) * u + 360) % 360, s, l0 + (l1 - l0) * u]; 102 }); 103 104 const PALETTES = [ 105 { name: 'default', outer: OUTER.map(hexToHsl) }, 106 { name: 'vivid', outer: [[32, 100, 50], [92, 100, 44], [196, 100, 47], [222, 100, 52], [284, 100, 62], [334, 100, 58]] }, 107 { name: 'shades', outer: ramp(206, 200, 62, 56, 30) }, 108 { name: 'fire', outer: [[352, 80, 32], [6, 90, 40], [16, 96, 47], [27, 100, 50], [38, 100, 50], [48, 100, 48]] }, 109 { name: 'ice', outer: [[232, 56, 32], [220, 68, 40], [208, 78, 48], [198, 82, 54], [190, 74, 58], [204, 56, 60]] }, 110 { name: 'monochrome', outer: ramp(215, 215, 8, 56, 28) }, 111 { name: 'neon', outer: [[312, 100, 58], [268, 100, 64], [200, 100, 52], [160, 100, 46], [84, 100, 48], [50, 100, 50]] }, 112 { name: 'forest', outer: [[160, 50, 26], [140, 42, 30], [120, 42, 36], [98, 46, 42], [74, 44, 42], [40, 44, 36]] }, 113 { name: 'sunset', outer: [[252, 50, 32], [282, 56, 38], [320, 68, 46], [346, 78, 52], [8, 88, 52], [30, 94, 50]] } 114 ]; 115 116 const smooth = u => u * u * (3 - 2 * u); 117 118 // Hue takes the short way round the wheel; saturation and lightness blend straight across. 119 function lerpHsl(a, b, u) { 120 const dh = ((b[0] - a[0] + 540) % 360) - 180; 121 return [(a[0] + dh * u + 360) % 360, a[1] + (b[1] - a[1]) * u, a[2] + (b[2] - a[2]) * u]; 122 } 123 124 function palette(t) { 125 const span = M.paletteHold + M.paletteFade; 126 const pos = t / span, n = PALETTES.length; 127 const i = Math.floor(pos) % n; 128 const within = (pos - Math.floor(pos)) * span; 129 const a = PALETTES[i]; 130 if (within <= M.paletteHold) return a; 131 132 const b = PALETTES[(i + 1) % n]; 133 const u = smooth((within - M.paletteHold) / M.paletteFade); 134 return { outer: a.outer.map(function (c, k) { return lerpHsl(c, b.outer[k], u); }) }; 135 } 136 137 // A pane's colour with the hue sway and the brightness wave applied, as [h, s, l] 138 function tint(hsl, k, t, hueShift, wave) { 139 return [(hsl[0] + hueShift + 360) % 360, hsl[1], 140 hsl[2] + (wave ? M.lightAmp * Math.sin(t * M.lightRate - k * M.lightLag) : 0)]; 141 } 142 143 /* The centre's colour, made from the six round it. A straight RGB average of a rainbow is a muddy 144 grey, so instead the hues are averaged as directions round the colour wheel (weighted by how 145 saturated each is), and the result keeps the panes' average saturation and lightness. For the 146 brand colours that lands on a light lavender. If the hues all but cancel, the saturation drops with 147 them, so the blend fades towards a neutral rather than flicking between hues. */ 148 function blend(cols) { 149 let x = 0, y = 0, s = 0, l = 0; 150 cols.forEach(function (c) { 151 x += c[1] * Math.cos(c[0] * DEG); y += c[1] * Math.sin(c[0] * DEG); 152 s += c[1]; l += c[2]; 153 }); 154 const n = cols.length, pull = s ? Math.hypot(x, y) / s : 0; // 0 = hues cancel, 1 = all agree 155 const h = (Math.atan2(y, x) / DEG + 360) % 360; 156 return [h, (s / n) * Math.min(1, M.centreSat * Math.sqrt(pull)), l / n + M.centreLight]; 157 } 158 159 // As a CSS colour, lightness nudged by `dl` and held inside the limits so it never goes white 160 function css(hsl, dl) { 161 let l = hsl[2] + (dl || 0); 162 if (l < MIN_LIGHT) l = MIN_LIGHT; else if (l > MAX_LIGHT) l = MAX_LIGHT; 163 return 'hsl(' + hsl[0].toFixed(1) + ',' + hsl[1].toFixed(1) + '%,' + l.toFixed(1) + '%)'; 164 } 165 166 /* ---------------------------------------------------------------- ring geometry 167 Outer hexagon of circumradius RO, inner hexagon of R = RO * (1 - ring). Each cut runs from an 168 inner vertex to the outer hexagon, swept round into the pinwheel, which gives the six trapezium 169 panes; the inner hexagon is a solid pane of its own. Every corner of the outer hexagon falls 170 inside one pane, so rounding it there rounds the whole mark. */ 171 const f = n => Math.round(n * 100) / 100; 172 const f1 = n => Math.round(n * 10) / 10; 173 174 const pt = (r, k) => { 175 const a = -Math.PI / 3 * k; // negative: SVG y grows downwards 176 return [CX + r * Math.cos(a), CY + r * Math.sin(a)]; 177 }; 178 179 function panels(ring, reversed) { 180 const R = RO * (1 - ring), d = RO - R; 181 const V = [0, 1, 2, 3, 4, 5].map(k =>
181 pt(R, k)); 182 const W = [0, 1, 2, 3, 4, 5].map(k => pt(RO, k)); 183 const U = [0, 1, 2, 3, 4, 5].map(k => { 184 const a = V[k], b = V[(k + 1) % 6]; 185 return [(b[0] - a[0]) / R, (b[1] - a[1]) / R]; // unit, along inner edge k 186 }); 187 const E = k => [W[k][0] + d * U[k][0], W[k][1] + d * U[k][1]]; 188 189 // Cut each outer corner back by rc along both edges and bow it round. The short side of the 190 // corner is only R long, so a very deep ring gets a smaller rounding. 191 const rc = Math.min(RO * M.cornerRound, R * 0.8); 192 const toward = (w, p) => { 193 const len = Math.hypot(p[0] - w[0], p[1] - w[1]); 194 return [w[0] + (p[0] - w[0]) * rc / len, w[1] + (p[1] - w[1]) * rc / len]; 195 }; 196 197 const out = []; 198 for (let k = 0; k < 6; k++) { 199 const n = (k + 1) % 6; 200 let pts = [V[k], V[n], E(n), toward(W[n], E(n)), [W[n][0], W[n][1], 'q'], toward(W[n], E(k)), E(k)]; 201 // Mirror only the ring, never the globe - a mirrored earth would be the wrong way round 202 if (reversed) pts = pts.map(p => [SIZE - p[0], p[1], p[2]]); 203 out.push(pts); 204 } 205 const centre = reversed ? V.map(p => [SIZE - p[0], p[1]]) : V; 206 return { panes: out, centre: centre }; 207 } 208 209 // Straight sides, except that a point marked 'q' is the control point of a curve to the next one 210 function toPath(pts) { 211 let d = 'M' + f(pts[0][0]) + ' ' + f(pts[0][1]); 212 for (let i = 1; i < pts.length; i++) { 213 const p = pts[i]; 214 if (p[2] === 'q') { const e = pts[++i]; d += 'Q' + f(p[0]) + ' ' + f(p[1]) + ' ' + f(e[0]) + ' ' + f(e[1]); } 215 else d += 'L' + f(p[0]) + ' ' + f(p[1]); 216 } 217 return d + 'Z'; 218 } 219 220 /* ---------------------------------------------------------------- the earth 221 The land arrives as rings of [lon, lat, lon, lat, ...] in degrees. Each ring is kept as its 222 longitudes in radians plus the sine and cosine of its latitudes, so a frame is only a handful 223 of multiplications a point. The grid is built here and needs no download. */ 224 function prepare(flat) { 225 const n = flat.length / 2; 226 const lon = new Float64Array(n), sl = new Float64Array(n), cl = new Float64Array(n); 227 for (let i = 0; i < n; i++) { 228 lon[i] = flat[i * 2] * DEG; 229 const la = flat[i * 2 + 1] * DEG; 230 sl[i] = Math.sin(la); cl[i] = Math.cos(la); 231 } 232 return { n: n, lon: lon, sl: sl, cl: cl }; 233 } 234 235 const GRID = (function () { 236 const lines = []; 237 for (let lo = -180; lo < 180; lo += 30) { 238 const a = []; 239 for (let la = -90; la <= 90; la += 5) a.push(lo, la); 240 lines.push(prepare(a)); 241 } 242 for (let la = -60; la <= 60; la += 30) { 243 const a = []; 244 for (let lo = -180; lo <= 180; lo += 5) a.push(lo, la); 245 lines.push(prepare(a)); 246 } 247 return lines; 248 })(); 249 250 let land = null; // prepared rings, once loaded 251 let landRequested = false; 252 253 // The data file sits beside this script's folder and carries the same cache-busting version 254 const SCRIPT_SRC = document.currentScript && document.currentScript.src; 255 function landUrl() { 256 try { 257 const u = new URL('../data/earth-land.json', SCRIPT_SRC || location.href); 258 if (SCRIPT_SRC) u.search = new URL(SCRIPT_SRC).search; 259 return u.href; 260 } catch (e) { 261 return '/data/earth-land.json'; 262 } 263 } 264 265 function loadLand() { 266 if (landRequested || !window.fetch) return; 267 landRequested = true; 268 fetch(landUrl()) 269 .then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); }) 270 .then(function (rings) { 271 land = rings.map(prepare); 272 const t = clock(); 273 instances.forEach(function (i) { i.draw(t); }); 274 stills.forEach(function (i) { if (i.host.isConnected) i.draw(t); }); 275 stills.clear(); 276 start(); 277 }) 278 .catch(function (e) { 279 // No land, no spin - the still logo stays up, which is a perfectly good logo 280 console.warn('HexLogo: could not load the earth outline', e); 281 }); 282 } 283
284 /* Orthographic view of the globe with (lon0, lat0) facing us. Returns x and y on the unit disc 285 (y up) and z, the depth - a point is on the near side when z > 0. */ 286 function view(g, lon0, s0, c0) { 287 const n = g.n, x = new Float64Array(n), y = new Float64Array(n), z = new Float64Array(n); 288 for (let i = 0; i < n; i++) { 289 const dl = g.lon[i] - lon0, cd = Math.cos(dl); 290 x[i] = g.cl[i] * Math.sin(dl); 291 y[i] = c0 * g.sl[i] - s0 * g.cl[i] * cd; 292 z[i] = s0 * g.sl[i] + c0 * g.cl[i] * cd; 293 } 294 return { n: n, x: x, y: y, z: z }; 295 } 296 297 /* A closed ring clipped to the near side. Where it goes over the horizon it is cut there, and it 298 comes back along the rim the short way to where it next reappears - so a continent sliding 299 round the edge stays a filled shape instead of collapsing or flooding the disc. */ 300 function clipRing(v, G, out) { 301 const n = v.n; 302 let s = -1; 303 for (let i = 0; i < n; i++) if (v.z[i] > 0) { s = i; break; } 304 if (s < 0) return; // wholly on the far side 305 306 const X = (x) => f1(CX + G * x), Y = (y) => f1(CY - G * y); 307 let d = 'M' + X(v.x[s]) + ' ' + Y(v.y[s]), exitA = 0; 308 for (let j = 1; j <= n; j++) { 309 const p = (s + j - 1) % n, q = (s + j) % n; 310 const pv = v.z[p] > 0, qv = v.z[q] > 0; 311 if (pv !== qv) { 312 const u = v.z[p] / (v.z[p] - v.z[q]); 313 const a = Math.atan2(v.y[p] + u * (v.y[q] - v.y[p]), v.x[p] + u * (v.x[q] - v.x[p])); 314 if (pv) { 315 exitA = a; 316 d += 'L' + X(Math.cos(a)) + ' ' + Y(Math.sin(a)); 317 } else { 318 const da = ((a - exitA + 3 * Math.PI) % (2 * Math.PI)) - Math.PI; 319 const steps = Math.max(1, Math.ceil(Math.abs(da) / 0.12)); 320 for (let k = 1; k <= steps; k++) { 321 const b = exitA + da * k / steps; 322 d += 'L' + X(Math.cos(b)) + ' ' + Y(Math.sin(b)); 323 } 324 } 325 } 326 if (qv && j < n) d += 'L' + X(v.x[q]) + ' ' + Y(v.y[q]); 327 } 328 out.push(d + 'Z'); 329 } 330 331 // An open line (the grid), drawn only where it is on the near side 332 function clipLine(v, G, out) { 333 const X = (x) => f1(CX + G * x), Y = (y) => f1(CY - G * y); 334 let d = '', drawing = false; 335 for (let i = 0; i < v.n; i++) { 336 const vis = v.z[i] > 0; 337 if (vis) { 338 if (!drawing && i > 0) { 339 const p = i - 1, u = v.z[p] / (v.z[p] - v.z[i]); 340 const a = Math.atan2(v.y[p] + u * (v.y[i] - v.y[p]), v.x[p] + u * (v.x[i] - v.x[p])); 341 d += 'M' + X(Math.cos(a)) + ' ' + Y(Math.sin(a)) + 'L'; 342 } else d += drawing ? 'L' : 'M'; 343 d += X(v.x[i]) + ' ' + Y(v.y[i]); 344 drawing = true; 345 } else if (drawing) { 346 const p = i - 1, u = v.z[p] / (v.z[p] - v.z[i]); 347 const a = Math.atan2(v.y[p] + u * (v.y[i] - v.y[p]), v.x[p] + u * (v.x[i] - v.x[p])); 348 d += 'L' + X(Math.cos(a)) + ' ' + Y(Math.sin(a)); 349 drawing = false; 350 } 351 } 352 if (d) out.push(d); 353 } 354 355 /* ---------------------------------------------------------------- view */ 356 let uid = 0; 357 358 function el(name, attrs) { 359 const e = document.createElementNS(NS, name); 360 for (const k in attrs) e.setAttribute(k, attrs[k]); 361 return e; 362 } 363 364 function build(host, o) { 365 // The padding is the static logo's, which keeps the drawn size exactly where it was 366 const pad = Math.max(o.seamWidth, 4) + 2; 367 const id = 'tghx' + (++uid) + '-'; 368 const geo = panels(o.ringDepth, o.reversed), shapes = geo.panes; 369 const G = RO * M.globeScale; // globe radius 370 371 const svg = el('svg', { 372 xmlns: NS,
373 viewBox: (-pad) + ' ' + (-pad) + ' ' + (SIZE + pad * 2) + ' ' + (SIZE + pad * 2), 374 role: 'img', 375 'aria-label': o.label 376 }); 377 378 /* Shared pieces, drawn once a frame and shown six times - once through each pane. They carry 379 no fill of their own, so each <use> colours them for its own pane. */ 380 const defs = el('defs', {}); 381 const landPath = el('path', { id: id + 'land' }); 382 const gridPath = el('path', { id: id + 'grid', fill: 'none', 'stroke-width': 1.4, 'stroke-linecap': 'round' }); 383 const disc = el('circle', { id: id + 'disc', cx: CX, cy: CY, r: f(G) }); 384 defs.appendChild(landPath); 385 defs.appendChild(gridPath); 386 defs.appendChild(disc); 387 388 // Light on the upper left, dark at the limb, so the globe reads as a ball behind the glass 389 const shade = el('radialGradient', { id: id + 'shade', cx: '0.38', cy: '0.32', r: '0.75' }); 390 [['0', '#ffffff', 0.22], ['0.45', '#ffffff', 0], ['0.8', '#000000', 0.12], ['1', '#000000', 0.32]] 391 .forEach(function (s) { 392 shade.appendChild(el('stop', { offset: s[0], 'stop-color': s[1], 'stop-opacity': s[2] })); 393 }); 394 defs.appendChild(shade); 395 396 // Seven panes of glass: the six trapeziums, then the centre hexagon (index 6) 397 const glassShapes = shapes.concat([geo.centre]); 398 399 const clips = []; 400 for (let k = 0; k < 7; k++) { 401 const cp = el('clipPath', { id: id + 'c' + k }); 402 const p = el('path', { d: toPath(glassShapes[k]) }); 403 cp.appendChild(p); 404 defs.appendChild(cp); 405 clips.push(p); 406 } 407 svg.appendChild(defs); 408 409 const panes = []; 410 for (let k = 0; k < 7; k++) { 411 const g = el('g', { 'clip-path': 'url(#' + id + 'c' + k + ')' }); 412 const glass = el('path', { d: toPath(glassShapes[k]) }); 413 const sea = el('use', { href: '#' + id + 'disc' }); 414 const earth = el('use', { href: '#' + id + 'land' }); 415 const grid = el('use', { href: '#' + id + 'grid', 'stroke-opacity': M.gridOpacity }); 416 const ball = el('use', { href: '#' + id + 'disc', fill: 'url(#' + id + 'shade)' }); 417 g.appendChild(glass); g.appendChild(sea); g.appendChild(earth); g.appendChild(grid); g.appendChild(ball); 418 svg.appendChild(g); 419 panes.push({ glass: glass, sea: sea, earth: earth, grid: grid }); 420 } 421 422 // The seams (leading, in stained glass terms) go over the top of everything 423 const gSeam = el('g', { fill: 'none', stroke: o.seamColor, 'stroke-width': f(o.seamWidth), 'stroke-linejoin': 'round' }); 424 const seams = shapes.map(function (s) { const p = el('path', { d: toPath(s) }); gSeam.appendChild(p); return p; }); 425 svg.appendChild(gSeam); 426 427 host.textContent = ''; 428 host.appendChild(svg); 429 430 const moving = () => o.animate && !reduced; 431 432 return { 433 host: host, 434 draw: function (t) { 435 const live = moving(); 436 437 if (M.hexSpin && live) { 438 const spin = t * M.spinDrift + M.spinSwing * Math.sin(t * M.spinRate); 439 const rot = 'rotate(' + f(spin) + ' ' + CX + ' ' + CY + ')'; 440 clips.forEach(function (p) { p.setAttribute('transform', rot); }); 441 panes.forEach(function (p) { p.glass.setAttribute('transform', rot); }); 442 seams.forEach(function (p) { p.setAttribute('transform', rot); }); 443 } 444 445 // The globe only turns once the land is here; until then it holds at the still's view 446 const lon0 = (live && land ? M.globeLon - M.globeRate * t : M.globeLon) * DEG; 447 const s0 = Math.sin(M.globeTilt * DEG), c0 = Math.cos(M.globeTilt * DEG); 448 449 const gridD = []; 450 GRID.forEach(function (g) { clipLine(view(g, lon0, s0, c0), G, gridD); }); 451 gridPath.setAttribute('d', gridD.join('')); 452 453 if (land) { 454 const landD = [];
455 land.forEach(function (g) { clipRing(view(g, lon0, s0, c0), G, landD); }); 456 landPath.setAttribute('d', landD.join('')); 457 } 458 459 // A still logo (Animate off, or reduced motion) stays in the brand colours 460 const colours = live ? palette(t) : PALETTES[0]; 461 const hue = live ? M.hueSwing * Math.sin(t * M.hueRate) : 0; 462 const tints = []; 463 for (let k = 0; k < 7; k++) { 464 // The centre pane (6) is a lighter blend of the six colours round it 465 const c = k < 6 ? tint(colours.outer[k], k, t, hue, live) : blend(tints), p = panes[k]; 466 tints.push(c); 467 p.glass.setAttribute('fill', css(c, 0)); 468 p.sea.setAttribute('fill', css(c, -M.oceanDark)); 469 p.earth.setAttribute('fill', css(c, M.landLight)); 470 p.grid.setAttribute('stroke', css(c, M.gridLight)); 471 } 472 } 473 }; 474 } 475 476 /* ---------------------------------------------------------------- loop 477 One rAF for every logo on the page, and it only runs once the land has loaded. Instances drop 478 out when their host leaves the DOM, which is how a Blazor page change tidies up - the component 479 needs no disposal call. */ 480 const instances = new Set(); // animated logos 481 const stills = new Set(); // still logos waiting for the land, to be drawn once when it lands 482 let rafId = 0; 483 let reduced = false; 484 485 const motionQuery = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; 486 if (motionQuery) { 487 reduced = motionQuery.matches; 488 const onMotionChange = function (e) { 489 reduced = e.matches; 490 if (reduced) instances.forEach(function (i) { i.draw(clock()); }); 491 else start(); 492 }; 493 if (motionQuery.addEventListener) motionQuery.addEventListener('change', onMotionChange); 494 else if (motionQuery.addListener) motionQuery.addListener(onMotionChange); 495 } 496 497 function tick() { 498 rafId = 0; 499 const t = clock(); 500 instances.forEach(function (inst) { 501 if (!inst.host.isConnected) instances.delete(inst); 502 else inst.draw(t); 503 }); 504 start(); 505 } 506 507 function start() { 508 if (!rafId && !reduced && land && instances.size) rafId = requestAnimationFrame(tick); 509 } 510 511 /* ---------------------------------------------------------------- api */ 512 function render(element, options) { 513 if (!element) return; 514 const o = options || {}; 515 const opt = { 516 ringDepth: typeof o.ringDepth === 'number' ? o.ringDepth : 0.55, 517 seamWidth: typeof o.seamWidth === 'number' ? o.seamWidth : 5, 518 seamColor: o.seamColor || '#ffffff', 519 reversed: !!o.reversed, 520 label: o.label || 'TripGeo', 521 animate: o.animate !== false 522 }; 523 524 // Already drawn with these options (by adopt(), from the prerendered HTML): leave it running 525 const sig = JSON.stringify(opt); 526 if (element.__tgHex && element.__tgHex.sig === sig && element.__tgHex.host.firstChild) return; 527 528 if (element.__tgHex) { instances.delete(element.__tgHex); stills.delete(element.__tgHex); } 529 530 const inst = build(element, opt); 531 inst.sig = sig; 532 element.__tgHex = inst; 533 inst.draw(clock()); // the still, now, so the box never flashes empty 534 535 if (opt.animate) instances.add(inst); 536 else if (!land) stills.add(inst); 537 loadLand(); 538 start(); 539 } 540 541 // Blazor does not need this - see the loop above - but it is here for anything that wants to 542 // stop a logo without removing it from the page. 543 function dispose(element) { 544 if (element && element.__tgHex) { 545 instances.delete(element.__tgHex); 546 stills.delete(element.__tgHex); 547 element.__tgHex = null; 548 } 549 } 550 551 // The six pane colours at time t as [hue, saturation, lightness] - handy for checking a palette 552 // without waiting for it to come round. 553 function colours(seconds) { 554 const p = palette(seconds); 555 return { outer: p.outer, names: PALETTES.map(function (x) { return x.name; }) }; 556 } 557 558 /* ---------------------------------------------------------------- early paint 559 The logo box arrives in the prerendered HTML carrying its options as data-* attributes (see 560 HexLogo.razor). Drawing from those here, as soon as this script runs, puts the logo on screen 561 with the page instead of about a second later when the WebAssembly app has started and called 562 render() itself. Both draw from the same clock with the same options, so when the app does take 563 over the frame it shows is the one already on screen. */ 564 function optionsFrom(el) { 565 const d = el.dataset, num = function (v) { const n = parseFloat(v); return isFinite(n) ? n : undefined; }; 566 return { 567 ringDepth: num(d.ringDepth), 568 seamWidth: num(d.seamWidth), 569 seamColor: d.seamColor || undefined, 570 reversed: d.reversed === 'true', 571 label: d.label || undefined, 572 animate: d.animate !== 'false' 573 }; 574 } 575 576 // Draw every logo box under `root` that has not been drawn yet 577 function adopt(root) { 578 if (!root || root.nodeType !== 1) return; 579 const hosts = root.matches && root.matches('[data-tg-hexlogo]') ? [root] : []; 580 const inside = root.querySelectorAll ? root.querySelectorAll('[data-tg-hexlogo]') : []; 581 for (let i = 0; i < inside.length; i++) hosts.push(inside[i]);
582 hosts.forEach(function (h) { if (!h.__tgHex) render(h, optionsFrom(h)); }); 583 } 584 585 // When the WebAssembly app takes over it replaces the prerendered logo box with a fresh, empty one. 586 // A mutation observer runs before the browser next paints, so redrawing the new box here means 587 // there is no frame with the logo missing. It only needs to run while the app is starting. 588 if (window.MutationObserver && document.documentElement) { 589 const watcher = new MutationObserver(function (records) { 590 records.forEach(function (r) { r.addedNodes.forEach(adopt); }); 591 }); 592 watcher.observe(document.documentElement, { childList: true, subtree: true }); 593 setTimeout(function () { watcher.disconnect(); }, 60000); 594 } 595 596 adopt(document.documentElement); 597 document.addEventListener('DOMContentLoaded', function () { adopt(document.documentElement); }); 598 599 return { render: render, dispose: dispose, clock: clock, colours: colours }; 600})();
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.