PageSourceSearch

https://hypercities.com/js/core-view.js?v=well-controls-3

js hypercities.com collected 2026-10-03 21:38:52 UTC 24,847 bytes, 605 lines download raw bytes

1// The temporal core uses the book-cover red family: dark in depth, the
2// cover colour at interaction points, and a restrained light on its surface.
3const RED = [159, 40, 37];
4const RED_BRIGHT = [218, 56, 51];
5const RED_LIGHT = [239, 113, 106];
6const INK = [241, 237, 228];
7
8const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
9
10function metresPerLongitude(latitude) {
11  return 111320 * Math.cos(latitude * Math.PI / 180);
12}
13
14function titleLines(title, target = 24) {
15  const words = String(title || "Untitled historical map").trim().split(/\s+/);
16  const lines = [];
17  let line = "";
18  for (const word of words) {
19    if (line && `${line} ${word}`.length > target) {
20      lines.push(line);
21      line = word;
22    } else line = line ? `${line} ${word}` : word;
23  }
24  if (line) lines.push(line);
25  return lines.join("\n");
26}
27
28function titleSize(title) {
29  const lines = titleLines(title).split("\n").length;
30  return lines > 3 ? 8.5 : lines > 2 ? 9.5 : lines > 1 ? 10.5 : 12;
31}
32
33function coordinateLabel(core) {
34  if (!core || !Number.isFinite(core.lat) || !Number.isFinite(core.lng)) return "";
35  // deck.gl's compact default glyph atlas is intentionally ASCII-only here.
36  // Keeping this navigational notation plain makes the live readout reliable.
37  const latitude = `${core.lat < 0 ? "S" : "N"} ${Math.abs(core.lat).toFixed(4)}`;
38  const longitude = `${core.lng < 0 ? "W" : "E"} ${Math.abs(core.lng).toFixed(4)}`;
39  return `${latitude} / ${longitude}`;
40}
41
42/*
43 * This is a core cross-section, not a second map. Each plate retains the
44 * footprint's aspect ratio and the drilled point's relative position within
45 * it. Geographic truth remains in the MapLibre field, which shows the full,
46 * unscaled bounding box when a plate is selected.
47 */
48function makeCrossSection(maps, core) {
49  const firstYear = maps[0]?.year || 0;
50  const lastYear = maps.at(-1)?.year || firstYear + 1;
51  const raw = maps.map((map, index) => {
52    const [west, south, east, north] = map.bbox;
53    const width = Math.max((east - west) * metresPerLongitude(core.lat), 1);
54    const height = Math.max((north - south) * 110540, 1);
55    return {
56      map,
57      index,
58      area: Math.log10(width * height),
59      aspect: clamp(width / height, 0.28, 3.6),
60      coreX: clamp((core.lng - west) / (east - west), 0, 1),
61      coreY: clamp((core.lat - south) / (north - south), 0, 1),
62      yearGap: index ? Math.max(0, map.year - maps[index - 1].year) : 0
63    };
64  });
65  const leastArea = Math.min(...raw.map((entry) => entry.area));
66  const greatestArea = Math.max(...raw.map((entry) => entry.area));
67  const areaSpan = Math.max(greatestArea - leastArea, 0.1);
68  const largestGap = Math.max(...raw.map((entry) => entry.yearGap), 1);
69  const gapLog = Math.log1p(largestGap);
70  const spacing = raw.map((entry, index) => index ? 1 + 3.6 * Math.log1p(entry.yearGap) / gapLog : 0);
71  const totalSpacing = Math.max(spacing.reduce((total, value) => total + value, 0), 1);
72  let travelled = 0;
73  const height = 1540;
74  const entries = raw.map((entry, index) => {
75    if (index) travelled += spacing[index];
76    const areaFactor = 0.62 + ((entry.area - leastArea) / areaSpan) * 0.82;
77    const base = 90 * areaFactor;
78    const width = base * Math.sqrt(entry.aspect);
79    const depth = base / Math.sqrt(entry.aspect);
80    const z = maps.length === 1 ? 770 : 90 + travelled / totalSpacing * 1360;
81    const west = -entry.coreX * width;
82    const east = (1 - entry.coreX) * width;
83    const south = -entry.coreY * depth;
84    const north = (1 - entry.coreY) * depth;
85    return {
86      ...entry,
87      z,
88      polygon: [[west, south, z], [east, south, z], [east, north, z], [west, north, z], [west, south, z]]
89    };
90  });
91  const spanYears = Math.max(0, lastYear - firstYear);
92
93  return {
94    firstYear,
95    lastYear,
96    height,
97    entries,
98    // The one red temporal annotation names the whole excavation, not every
99    // interval between maps. Individual years remain attached to their plates.
100    span: spanYears ? { position: [-84, 0, (entries[0].z + entries.at(-1).z) / 2], text: `${spanYears} years` } : null
101  };
102}
103
104function labelEntries(entries, selectedId, hoverId) {
105  const step = Math.max(1, Math.ceil(entries.length / 8));
106  const important = new Set([0, entries.length - 1]);
107  entries.forEach((entry, index) => {
108    if (index % step === 0 || entry.map.id === selectedId || entry.map.id === hoverId) important.add(index);
109  });
110  return entries.filter((_, index) => important.has(index));
111}
112
113export class CoreView {
114  constructor({ onSelect, onHover, onMapGesture }) {
115    this.onSelect = onSelect;
116    this.onHover = onHover;
117    this.onMapGesture = onMapGesture;
118    this.container = document.querySelector("#core-renderer");
119    this.caption = document.querySelector("#well-caption");
120    this.compact = false;
121  }
122
123  setCompact(compact) {
124    const next = Boolean(compact && this.locked);
125    const changed = next !== this.compact || this.container.classList.contains("is-compact") !== next;
126    this.compact = next;
127    this.container.classList.toggle("is-compact", next);
128    if (changed && this.stack && this.deck) {
129      this.viewState = this.fitView();
130      this.deck.setProps({ viewState: this.viewState });
131      this.render();
132    }
133  }
134
135  setPassThrough(passThrough) { this.passThrough = Boolean(passThrough); }
136
137  preview(maps, core) {
138    if (this.locked) return;
139    if (!maps.length) { this.clearPreview(); return; }
140    this.compact = window.matchMedia("(max-width: 780px)").matches;
141    this.container.classList.toggle("is-compact", this.compact);
142    const signature = maps.map((map) => map.id).join(",");
143    const changedWell = signature !== this.previewSignature;
144    this.core = core;
145    this.stack = makeCrossSection(maps, core);
146    this.selectedId = null;
147    this.hoverId = null;
148    this.container.classList.add("is-preview");
149    this.caption.hidden = true;
150    this.container.setAttribute("aria-label", `TimeWell with ${maps.length} layer${maps.length === 1 ? "" : "s"}, from ${this.stack.firstYear} to ${this.stack.lastYear}`);
151    this.viewState = this.fitView();
152    if (!this.deck) this.createDeck();
153    this.deck.setProps({ viewState: this.viewState });
154    this.previewSignature = signature;
155    if (changedWell && !this.hasIntroduced) {
156      this.hasIntroduced = true;
157      this.startAppearance();
158    } else {
159      this.progress = 1;
160      this.render();
161    }
162  }
163
164  clearPreview() {
165    if (this.locked) return;
166    if (this.appearanceFrame) cancelAnimationFrame(this.appearanceFrame);
167    this.appearanceFrame = null;
168    this.container.classList.remove("is-preview");
169    this.caption.hidden = true;
170    if (this.deck) this.deck.setProps({ layers: [] });
171    this.stack = null;
172    this.previewSignature = null;
173  }
174
175  enter(maps, core) {
176    const signature = maps.map((map) => map.id).join(",");
177    const cameFromPreview = this.previewSignature === signature && this.stack;
178    this.locked = true;
179    this.core = core;
180    this.stack = makeCrossSection(maps, core);
181    this.selectedId = null;
182    this.hoverId = null;
183    this.container.classList.remove("is-preview");
184    this.container.classList.add("is-active");
185    this.caption.hidden = true;
186    this.container.setAttribute("aria-label", `TimeWell with ${maps.length} layer${maps.length === 1 ? "" : "s"}, from ${this.stack.firstYear} to ${this.stack.lastYear}`);
187    this.viewState = this.fitView();
188    if (!this.deck) this.createDeck();
189    this.deck.setProps({ viewState: this.viewState });
190    this.previewSignature = signature;
191    if (cameFromPreview || this.hasIntroduced) {
192      this.progress = 1;
193      this.render();
194    } else {
195      this.hasIntroduced = true;
196      this.startAppearance();
197    }
198  }
199
200  createDeck() {
201    this.deck = new deck.Deck({
202      parent: this.container,
203      views: new deck.OrbitView({ id: "time-well" }),
204      controller: false,
205      viewState: this.viewState,
206      onAfterRender: () => this.positionSizeControl(),
207      layers: []
208    });
209    this.installGestureRouting();
210    this.resizeObserver = new ResizeObserver(() => {
211      if (!this.stack) return;
212      this.viewState = this.fitView();
213      this.deck.setProps({ viewState: this.viewState });
214    });
215    this.resizeObserver.observe(this.container);
216  }
217
218  positionSizeControl() {
219    const button = document.querySelector("#timewell-size");
220    if (!button) return;
221    if (this.compact || !this.locked) {
222      button.style.removeProperty("left");
223      button.style.removeProperty("top");
224      return;
225    }
226    const viewport = this.deck?.getViewports()[0];
227    if (!viewport) return;
228    // Attach to the projected coordinate label, not the canvas's empty corner.
229    const [x, y] = viewport.project([-64, 0, -78]);
230    const rect = this.container.getBoundingClientRect();
231    const halfWidth = button.offsetWidth / 2;
232    button.style.left = `${Math.max(halfWidth + 8, Math.min(window.innerWidth - halfWidth - 8, rect.left + x))}px`;
233    button.style.top = `${Math.min(window.innerHeight - 52, rect.top + y + 12)}px`;
234  }
235
236  installGestureRouting() {
237    this.canvas = this.container.querySelector("canvas");
238    if (!this.canvas) return;
239    const picked = (event) => {
240      if (this.passThrough) return false;
241      const bounds = this.canvas.getBoundingClientRect();
242      const info = this.deck.pickObject({ x: event.clientX - bounds.left, y: event.clientY - bounds.top, radius: 5 });
243      return Boolean(info?.picked);
244    };
245    const passPointer = (event) => {
246      if (!this.passingGesture && event.type !== "pointerdown") return;
247      this.onMapGesture?.(event);
248      event.preventDefault();
249      event.stopImmediatePropagation();
250      if (event.type === "pointerup" || event.type === "pointercancel") this.passingGesture = false;
251    };
252    this.canvas.addEventListener("pointerdown", (event) => {
253      this.passingGesture = !picked(event);
254      if (this.passingGesture) passPointer(event);
255    }, true);
256    this.canvas.addEventListener("pointermove", (event) => {
257      if (this.passingGesture) passPointer(event);
258    }, true);
259    this.canvas.addEventListener("pointerup", passPointer, true);
260    this.canvas.addEventListener("pointercancel", passPointer, true);
261    this.canvas.addEventListener("click", (event) => {
262      if (picked(event)) return;
263      this.onMapGesture?.(event);
264      event.preventDefault();
265      event.stopImmediatePropagation();
266    }, true);
267    this.canvas.addEventListener("wheel", (event) => {
268      if (picked(event)) return;
269      this.onMapGesture?.(event);
270      event.preventDefault();
271      event.stopImmediatePropagation();
272    }, { capture: true, passive: false });
273  }
274
275  fitView() {
276    const width = Math.max(this.container.clientWidth, this.compact ? 160 : 320);
277    const height = Math.max(this.container.clientHeight, this.compact ? 200 : 320);
278    // Leave real visual air beneath the lower cap for the coordinate readout.
279    // It is part of the drilled location, not a sideways annotation.
280    const baseClearance = 92;
281    const extentMin = -baseClearance;
282    const extentMax = (this.stack?.height || 1540) + 82;
283    const scale = Math.min(width / (this.compact ? 470 : 720), height / (extentMax - extentMin)) * (this.compact ? .98 : .9);
284    return {
285      target: [0, 0, (extentMin + extentMax) / 2],
286      zoom: clamp(Math.log2(scale), this.compact ? -4.5 : -2.5, 0.5),
287      rotationX: 58,
288      rotationOrbit: 24
289    };
290  }
291
292  select(map) {
293    if (!this.stack || !map) return;
294    this.selectedId = map.id;
295    this.render();
296  }
297
298  startAppearance() {
299    if (this.appearanceFrame) cancelAnimationFrame(this.appearanceFrame);
300    this.progress = 0;
301    const startedAt = performance.now();
302    const duration = 780;
303    const animate = (now) => {
304      const raw = clamp((now - startedAt) / duration, 0, 1);
305      this.progress = 1 - (1 - raw) ** 4;
306      this.render();
307      if (raw < 1) this.appearanceFrame = requestAnimationFrame(animate);
308      else this.appearanceFrame = null;
309    };
310    this.render();
311    this.appearanceFrame = requestAnimationFrame(animate);
312  }
313
314  setHover(info) {
315    if (!this.locked) return;
316    const nextId = info.object?.map?.id || null;
317    if (nextId === this.hoverId) return;
318    this.hoverId = nextId;
319    this.onHover?.(info.object?.map || null);
320    this.render();
321  }
322
323  render() {
324    if (!this.deck || !this.stack) return;
325    const progress = this.progress ?? 1;
326    // Hovering is the invitation to a core, so it is intentionally more
327    // legible than the quieter, already-entered stack. The TimeWell remains
328    // translucent, but its own temporal lines win over the basemap beneath.
329    const previewing = !this.locked;
330    const top = (this.stack.height + 82) * progress;
331    const focusId = this.hoverId || this.selectedId;
332    const entries = this.stack.entries.map((entry) => ({
333      ...entry,
334      z: entry.z * progress,
335      polygon: entry.polygon.map(([x, y, z]) => [x, y, z * progress])
336    }));
337    if (this.compact) { this.renderCompact(entries, top); return; }
338    const labels = labelEntries(entries, this.selectedId, this.hoverId);
339    const selected = entries.find((entry) => entry.map.id === focusId);
340    const labelX = 154;
341    const temporalSpan = this.stack.span && {
342      ...this.stack.span,
343      position: [this.stack.span.position[0], this.stack.span.position[1], this.stack.span.position[2] * progress]
344    };
345    // The layer span is physically attached to the bore: its marker begins
346    // exactly above the top cap's center, then the text opens to the right.
347    const wellCaption = {
348      position: [0, 0, top + 40],
349      text: `${entries.length} LAYER${entries.length === 1 ? "" : "S"} · ${this.stack.firstYear}–${this.stack.lastYear}`
350    };
351    const coreCoordinate = this.core && {
352      // It sits at the visual base of the cylinder: centered below its lower
353      // cap rather than competing with the core itself or the year labels.
354      // OrbitView projects a purely vertical offset slightly to the right at
355      // this fixed viewing angle. The modest x counter-offset centers the
356      // readout beneath the projected lower cap, where the eye reads it.
357      position: [-64, 0, -78],
358      text: coordinateLabel(this.core)
359    };
360
361    this.deck.setProps({ layers: [
362      new deck.PolygonLayer({
363        id: "historical-strata",
364        data: entries,
365        pickable: false,
366        stroked: true,
367        filled: true,
368        getPolygon: (entry) => entry.polygon,
369        getLineColor: (entry) => entry.map.id === this.selectedId ? [...INK, 255] : entry.map.id === this.hoverId ? [...RED_BRIGHT, 255] : [...RED, previewing ? 235 : 185],
370        getFillColor: (entry) => entry.map.id === this.selectedId ? [...RED_BRIGHT, 64] : [...RED, previewing ? 50 : 28],
371        getLineWidth: (entry) => entry.map.id === this.selectedId ? 3 : entry.map.id === this.hoverId ? 2.2 : 1.15,
372        lineWidthUnits: "pixels",
373        extruded: true,
374        getElevation: (entry) => entry.map.id === this.selectedId ? 20 : entry.map.id === this.hoverId ? 13 : 7,
375        wireframe: true,
376        parameters: { depthTest: true },
377        onClick: (info) => { if (info.object) this.onSelect(info.object.map); },
378        onHover: (info) => this.setHover(info)
379      }),
380      new deck.PathLayer({
381        id: "strata-edge-hit-zones", data: entries, pickable: true,
382        getPath: (entry) => entry.polygon, getColor: [RED_BRIGHT[0], RED_BRIGHT[1], RED_BRIGHT[2], 1],
383        getWidth: 5, widthUnits: "pixels",
384        onClick: (info) => { if (info.object) this.onSelect(info.object.map); },
385        onHover: (info) => this.setHover(info),
386        parameters: { depthTest: false }
387      }),
388      new deck.ColumnLayer({
389        // A single high-resolution column gives the core true circular caps
390        // and curved walls. It intentionally replaces the former flat strip
391        // and its extra guide lines.
392        id: "bore-cylinder",
393        data: [{ position: [0, 0, 0], elevation: top }],
394        getPosition: (item) => item.position,
395        getElevation: (item) => item.elevation,
396        // A screen-sized radius keeps the well's body legible while leaving
397        // the highly variable map strata visible around the core.
398        radius: 18,
399        radiusUnits: "pixels",
400        diskResolution: 48,
401        filled: true,
402        extruded: true,
403        flatShading: false,
404        getFillColor: [...RED, previewing ? 205 : 170],
405        material: { ambient: 0.58, diffuse: 0.62, shininess: 34, specularColor: RED_LIGHT },
406        parameters: { depthTest: true }
407      }),
408      new deck.ScatterplotLayer({
409        id: "core-mouth",
410        data: [{ position: [0, 0, 0] }],
411        getPosition: (item) => item.position,
412        getRadius: 16,
413        radiusUnits: "pixels",
414        filled: false,
415        stroked: true,
416        getLineColor: [...INK, 240],
417        getLineWidth: 2,
418        parameters: { depthTest: false }
419      }),
420      new deck.TextLayer({
421        id: "well-caption-marker",
422        data: [wellCaption],
423        getPosition: (item) => item.position,
424        getText: () => "|",
425        getColor: [...RED_BRIGHT, 245],
426        getSize: 11,
427        sizeUnits: "pixels",
428        getTextAnchor: "middle",
429        getAlignmentBaseline: "bottom",
430        billboard: true,
431        fontFamily: "Avenir Next, Avenir, Helvetica Neue, sans-serif",
432        fontWeight: "500",
433        parameters: { depthTest: false }
434      }),
435      new deck.TextLayer({
436        id: "well-caption-text",
437        data: [wellCaption],
438        getPosition: (item) => item.position,
439        getText: (item) => item.text,
440        getPixelOffset: [8, 0],
441        getColor: [...RED_BRIGHT, 235],
442        getSize: 6.8,
443        sizeUnits: "pixels",
444        getTextAnchor: "start",
445        getAlignmentBaseline: "bottom",
446        billboard: true,
447        fontFamily: "Avenir Next, Avenir, Helvetica Neue, sans-serif",
448        fontWeight: "500",
449        characterSet: "0123456789 LAYERS·–",
450        parameters: { depthTest: false }
451      }),
452      ...(coreCoordinate?.text ? [new deck.TextLayer({
453        id: "core-coordinate",
454        data: [coreCoordinate],
455        getPosition: (item) => item.position,
456        getText: (item) => item.text,
457        getColor: [...INK, 182],
458        getSize: 9.5,
459        sizeUnits: "pixels",
460        getTextAnchor: "middle",
461        getAlignmentBaseline: "center",
462        billboard: true,
463        fontFamily: "ui-sans-serif, system-ui, sans-serif",
464        fontWeight: "500",
465        parameters: { depthTest: false }
466      })] : []),
467      new deck.ScatterplotLayer({
468        id: "stratum-anchors",
469        data: entries,
470        pickable: true,
471        getPosition: (entry) => [0, 0, entry.z],
472        getRadius: (entry) => entry.map.id === this.selectedId ? 7 : entry.map.id === this.hoverId ? 5.6 : 3.2,
473        radiusUnits: "pixels",
474        getFillColor: (entry) => entry.map.id === this.selectedId ? [...INK, 255] : entry.map.id === this.hoverId ? [...RED_BRIGHT, 255] : [...RED_BRIGHT, 210],
475        onClick: (info) => { if (info.object) this.onSelect(info.object.map); },
476        onHover: (info) => this.setHover(info),
477        parameters: { depthTest: false }
478      }),
479      new deck.ScatterplotLayer({
480        id: "core-hit-zones",
481        data: entries,
482        pickable: true,
483        getPosition: (entry) => [0, 0, entry.z],
484        getRadius: 13,
485        radiusUnits: "pixels",
486        getFillColor: [0, 0, 0, 1],
487        onClick: (info) => { if (info.object) this.onSelect(info.object.map); },
488        onHover: (info) => this.setHover(info),
489        parameters: { depthTest: false }
490      }),
491      new deck.LineLayer({
492        id: "year-ticks",
493        data: labels,
494        getSourcePosition: (entry) => [0, 0, entry.z],
495        getTargetPosition: (entry) => [labelX, 0, entry.z],
496        getColor: (entry) => entry.map.id === this.selectedId ? [...INK, 230] : entry.map.id === this.hoverId ? [...RED_BRIGHT, 255] : [...INK, previewing ? 170 : 110],
497        getWidth: (entry) => entry.map.id === this.selectedId ? 1.7 : entry.map.id === this.hoverId ? 1.5 : 1,
498        widthUnits: "pixels",
499        parameters: { depthTest: false }
500      }),
501      new deck.TextLayer({
502        id: "years",
503        data: labels,
504        getPosition: (entry) => [labelX, 0, entry.z],
505        getText: (entry) => String(entry.map.year),
506        getColor: (entry) => entry.map.id === this.selectedId ? [...INK, 255] : entry.map.id === this.hoverId ? [...RED_BRIGHT, 255] : [...INK, previewing ? 238 : 205],
507        getSize: (entry) => entry.map.id === this.selectedId ? 18 : entry.map.id === this.hoverId ? 16 : 13,
508        sizeUnits: "pixels",
509        getTextAnchor: "start",
510        getAlignmentBaseline: "center",
511        billboard: true,
512        fontFamily: "Palatino Linotype, Book Antiqua, Georgia, serif",
513        fontWeight: "700",
514        pickable: true,
515        onClick: (info) => { if (info.object) this.onSelect(info.object.map); },
516        onHover: (info) => this.setHover(info),
517        parameters: { depthTest: false }
518      }),
519      ...(temporalSpan ? [new deck.TextLayer({
520        id: "temporal-span",
521        data: [temporalSpan],
522        getPosition: (item) => item.position,
523        getText: (item) => item.text,
524        getColor: [...RED_BRIGHT, 220],
525        getSize: 11,
526        sizeUnits: "pixels",
527        getTextAnchor: "end",
528        getAlignmentBaseline: "center",
529        billboard: true,
530        fontFamily: "ui-sans-serif, system-ui, sans-serif",
531        parameters: { depthTest: false }
532      })] : []),
533      ...(selected ? [new deck.TextLayer({
534        id: "hovered-stratum",
535        data: [selected],
536        getPosition: (entry) => [-34, 0, entry.z],
537        getText: (entry) => titleLines(entry.map.title),
538        getColor: [...RED_BRIGHT, 255],
539        getSize: (entry) => titleSize(entry.map.title),
540        sizeUnits: "pixels",
541        getTextAnchor: "end",
542        getAlignmentBaseline: "center",
543        billboard: true,
544        lineHeight: 1.08,
545        fontFamily: "ui-sans-serif, system-ui, sans-serif",
546        parameters: { depthTest: false }
547      })] : [])
548    ] });
549  }
550
551  renderCompact(entries, top) {
552    const selected = entries.find((entry) => entry.map.id === this.selectedId);
553    const bounds = [entries[0], entries.at(-1)].filter(Boolean);
554    this.deck.setProps({ layers: [
555      new deck.PolygonLayer({
556        id: "mini-strata", data: entries, pickable: false, stroked: true, filled: true,
557        getPolygon: (entry) => entry.polygon,
558        getLineColor: (entry) => entry.map.id === this.selectedId ? [...INK, 235] : [...RED_BRIGHT, 170],
559        getFillColor: (entry) => entry.map.id === this.selectedId ? [...RED_BRIGHT, 70] : [...RED, 28],
560        getLineWidth: (entry) => entry.map.id === this.selectedId ? 2 : 1,
561        lineWidthUnits: "pixels", parameters: { depthTest: false }
562      }),
563      new deck.ColumnLayer({
564        id: "mini-bore", data: [{ position: [0, 0, 0], elevation: top }],
565        getPosition: (item) => item.position, getElevation: (item) => item.elevation,
566        radius: 10, radiusUnits: "pixels", diskResolution: 40,
567        filled: true, extruded: true, getFillColor: [...RED_BRIGHT, 205],
568        parameters: { depthTest: false }
569      }),
570      new deck.ScatterplotLayer({
571        id: "mini-anchors", data: entries, pickable: false,
572        getPosition: (entry) => [0, 0, entry.z],
573        getRadius: (entry) => entry.map.id === this.selectedId ? 6 : 2.5,
574        radiusUnits: "pixels",
575        getFillColor: (entry) => entry.map.id === this.selectedId ? [...INK, 255] : [...RED_LIGHT, 185],
576        parameters: { depthTest: false }
577      }),
578      new deck.TextLayer({
579        id: "mini-years", data: selected && !bounds.some((entry) => entry.map.id === selected.map.id) ? [...bounds, selected] : bounds,
580        getPosition: (entry) => [95, 0, entry.z], getText: (entry) => String(entry.map.year),
581        getColor: (entry) => entry.map.id === this.selectedId ? [...INK, 255] : [...INK, 175],
582        getSize: (entry) => entry.map.id === this.selectedId ? 12 : 9,
583        sizeUnits: "pixels", getTextAnchor: "start", getAlignmentBaseline: "center",
584        billboard: true, fontFamily: "Palatino Linotype, Georgia, serif",
585        parameters: { depthTest: false }
586      })
587    ] });
588  }
589
590  leave() {
591    if (this.appearanceFrame) cancelAnimationFrame(this.appearanceFrame);
592    this.appearanceFrame = null;
593    this.locked = false;
594    this.compact = false;
595    this.passThrough = false;
596    this.container.classList.remove("is-active");
597    this.container.classList.remove("is-preview");
598    this.container.classList.remove("is-compact");
599    this.caption.hidden = true;
600    this.onHover?.(null);
601    if (this.deck) this.deck.setProps({ layers: [] });
602    this.stack = null;
603    this.previewSignature = null;
604  }
605}

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.