PageSourceSearch

https://hypercities.com/js/core-view.js?v=timewell-no-turtle-1

js hypercities.com collected 2026-09-25 18:52:49 UTC 23,885 bytes, 584 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    const signature = maps.map((map) => map.id).join(",");
141    const changedWell = signature !== this.previewSignature;
142    this.core = core;
143    this.stack = makeCrossSection(maps, core);
144    this.selectedId = null;
145    this.hoverId = null;
146    this.container.classList.add("is-preview");
147    this.caption.hidden = true;
148    this.container.setAttribute("aria-label", `TimeWell with ${maps.length} layer${maps.length === 1 ? "" : "s"}, from ${this.stack.firstYear} to ${this.stack.lastYear}`);
149    this.viewState = this.fitView();
150    if (!this.deck) this.createDeck();
151    this.deck.setProps({ viewState: this.viewState });
152    this.previewSignature = signature;
153    if (changedWell && !this.hasIntroduced) {
154      this.hasIntroduced = true;
155      this.startAppearance();
156    } else {
157      this.progress = 1;
158      this.render();
159    }
160  }
161
162  clearPreview() {
163    if (this.locked) return;
164    if (this.appearanceFrame) cancelAnimationFrame(this.appearanceFrame);
165    this.appearanceFrame = null;
166    this.container.classList.remove("is-preview");
167    this.caption.hidden = true;
168    if (this.deck) this.deck.setProps({ layers: [] });
169    this.stack = null;
170    this.previewSignature = null;
171  }
172
173  enter(maps, core) {
174    const signature = maps.map((map) => map.id).join(",");
175    const cameFromPreview = this.previewSignature === signature && this.stack;
176    this.locked = true;
177    this.core = core;
178    this.stack = makeCrossSection(maps, core);
179    this.selectedId = null;
180    this.hoverId = null;
181    this.container.classList.remove("is-preview");
182    this.container.classList.add("is-active");
183    this.caption.hidden = true;
184    this.container.setAttribute("aria-label", `TimeWell with ${maps.length} layer${maps.length === 1 ? "" : "s"}, from ${this.stack.firstYear} to ${this.stack.lastYear}`);
185    this.viewState = this.fitView();
186    if (!this.deck) this.createDeck();
187    this.deck.setProps({ viewState: this.viewState });
188    this.previewSignature = signature;
189    if (cameFromPreview || this.hasIntroduced) {
190      this.progress = 1;
191      this.render();
192    } else {
193      this.hasIntroduced = true;
194      this.startAppearance();
195    }
196  }
197
198  createDeck() {
199    this.deck = new deck.Deck({
200      parent: this.container,
201      views: new deck.OrbitView({ id: "time-well" }),
202      controller: false,
203      viewState: this.viewState,
204      layers: []
205    });
206    this.installGestureRouting();
207    this.resizeObserver = new ResizeObserver(() => {
208      if (!this.stack) return;
209      this.viewState = this.fitView();
210      this.deck.setProps({ viewState: this.viewState });
211    });
212    this.resizeObserver.observe(this.container);
213  }
214
215  installGestureRouting() {
216    this.canvas = this.container.querySelector("canvas");
217    if (!this.canvas) return;
218    const picked = (event) => {
219      if (this.passThrough) return false;
220      const bounds = this.canvas.getBoundingClientRect();
221      const info = this.deck.pickObject({ x: event.clientX - bounds.left, y: event.clientY - bounds.top, radius: 5 });
222      return Boolean(info?.picked);
223    };
224    const passPointer = (event) => {
225      if (!this.passingGesture && event.type !== "pointerdown") return;
226      this.onMapGesture?.(event);
227      event.preventDefault();
228      event.stopImmediatePropagation();
229      if (event.type === "pointerup" || event.type === "pointercancel") this.passingGesture = false;
230    };
231    this.canvas.addEventListener("pointerdown", (event) => {
232      this.passingGesture = !picked(event);
233      if (this.passingGesture) passPointer(event);
234    }, true);
235    this.canvas.addEventListener("pointermove", (event) => {
236      if (this.passingGesture) passPointer(event);
237    }, true);
238    this.canvas.addEventListener("pointerup", passPointer, true);
239    this.canvas.addEventListener("pointercancel", passPointer, true);
240    this.canvas.addEventListener("click", (event) => {
241      if (picked(event)) return;
242      this.onMapGesture?.(event);
243      event.preventDefault();
244      event.stopImmediatePropagation();
245    }, true);
246    this.canvas.addEventListener("wheel", (event) => {
247      if (picked(event)) return;
248      this.onMapGesture?.(event);
249      event.preventDefault();
250      event.stopImmediatePropagation();
251    }, { capture: true, passive: false });
252  }
253
254  fitView() {
255    const width = Math.max(this.container.clientWidth, this.compact ? 160 : 320);
256    const height = Math.max(this.container.clientHeight, this.compact ? 200 : 320);
257    // Leave real visual air beneath the lower cap for the coordinate readout.
258    // It is part of the drilled location, not a sideways annotation.
259    const baseClearance = 92;
260    const extentMin = -baseClearance;
261    const extentMax = (this.stack?.height || 1540) + 82;
262    const scale = Math.min(width / (this.compact ? 470 : 720), height / (extentMax - extentMin)) * (this.compact ? .98 : .9);
263    return {
264      target: [0, 0, (extentMin + extentMax) / 2],
265      zoom: clamp(Math.log2(scale), this.compact ? -4.5 : -2.5, 0.5),
266      rotationX: 58,
267      rotationOrbit: 24
268    };
269  }
270
271  select(map) {
272    if (!this.stack || !map) return;
273    this.selectedId = map.id;
274    this.render();
275  }
276
277  startAppearance() {
278    if (this.appearanceFrame) cancelAnimationFrame(this.appearanceFrame);
279    this.progress = 0;
280    const startedAt = performance.now();
281    const duration = 780;
282    const animate = (now) => {
283      const raw = clamp((now - startedAt) / duration, 0, 1);
284      this.progress = 1 - (1 - raw) ** 4;
285      this.render();
286      if (raw < 1) this.appearanceFrame = requestAnimationFrame(animate);
287      else this.appearanceFrame = null;
288    };
289    this.render();
290    this.appearanceFrame = requestAnimationFrame(animate);
291  }
292
293  setHover(info) {
294    if (!this.locked) return;
295    const nextId = info.object?.map?.id || null;
296    if (nextId === this.hoverId) return;
297    this.hoverId = nextId;
298    this.onHover?.(info.object?.map || null);
299    this.render();
300  }
301
302  render() {
303    if (!this.deck || !this.stack) return;
304    const progress = this.progress ?? 1;
305    // Hovering is the invitation to a core, so it is intentionally more
306    // legible than the quieter, already-entered stack. The TimeWell remains
307    // translucent, but its own temporal lines win over the basemap beneath.
308    const previewing = !this.locked;
309    const top = (this.stack.height + 82) * progress;
310    const focusId = this.hoverId || this.selectedId;
311    const entries = this.stack.entries.map((entry) => ({
312      ...entry,
313      z: entry.z * progress,
314      polygon: entry.polygon.map(([x, y, z]) => [x, y, z * progress])
315    }));
316    if (this.compact) { this.renderCompact(entries, top); return; }
317    const labels = labelEntries(entries, this.selectedId, this.hoverId);
318    const selected = entries.find((entry) => entry.map.id === focusId);
319    const labelX = 154;
320    const temporalSpan = this.stack.span && {
321      ...this.stack.span,
322      position: [this.stack.span.position[0], this.stack.span.position[1], this.stack.span.position[2] * progress]
323    };
324    // The layer span is physically attached to the bore: its marker begins
325    // exactly above the top cap's center, then the text opens to the right.
326    const wellCaption = {
327      position: [0, 0, top + 40],
328      text: `${entries.length} LAYER${entries.length === 1 ? "" : "S"} · ${this.stack.firstYear}–${this.stack.lastYear}`
329    };
330    const coreCoordinate = this.core && {
331      // It sits at the visual base of the cylinder: centered below its lower
332      // cap rather than competing with the core itself or the year labels.
333      // OrbitView projects a purely vertical offset slightly to the right at
334      // this fixed viewing angle. The modest x counter-offset centers the
335      // readout beneath the projected lower cap, where the eye reads it.
336      position: [-64, 0, -78],
337      text: coordinateLabel(this.core)
338    };
339
340    this.deck.setProps({ layers: [
341      new deck.PolygonLayer({
342        id: "historical-strata",
343        data: entries,
344        pickable: false,
345        stroked: true,
346        filled: true,
347        getPolygon: (entry) => entry.polygon,
348        getLineColor: (entry) => entry.map.id === this.selectedId ? [...INK, 255] : entry.map.id === this.hoverId ? [...RED_BRIGHT, 255] : [...RED, previewing ? 235 : 185],
349        getFillColor: (entry) => entry.map.id === this.selectedId ? [...RED_BRIGHT, 64] : [...RED, previewing ? 50 : 28],
350        getLineWidth: (entry) => entry.map.id === this.selectedId ? 3 : entry.map.id === this.hoverId ? 2.2 : 1.15,
351        lineWidthUnits: "pixels",
352        extruded: true,
353        getElevation: (entry) => entry.map.id === this.selectedId ? 20 : entry.map.id === this.hoverId ? 13 : 7,
354        wireframe: true,
355        parameters: { depthTest: true },
356        onClick: (info) => { if (info.object) this.onSelect(info.object.map); },
357        onHover: (info) => this.setHover(info)
358      }),
359      new deck.PathLayer({
360        id: "strata-edge-hit-zones", data: entries, pickable: true,
361        getPath: (entry) => entry.polygon, getColor: [RED_BRIGHT[0], RED_BRIGHT[1], RED_BRIGHT[2], 1],
362        getWidth: 5, widthUnits: "pixels",
363        onClick: (info) => { if (info.object) this.onSelect(info.object.map); },
364        onHover: (info) => this.setHover(info),
365        parameters: { depthTest: false }
366      }),
367      new deck.ColumnLayer({
368        // A single high-resolution column gives the core true circular caps
369        // and curved walls. It intentionally replaces the former flat strip
370        // and its extra guide lines.
371        id: "bore-cylinder",
372        data: [{ position: [0, 0, 0], elevation: top }],
373        getPosition: (item) => item.position,
374        getElevation: (item) => item.elevation,
375        // A screen-sized radius keeps the well's body legible while leaving
376        // the highly variable map strata visible around the core.
377        radius: 18,
378        radiusUnits: "pixels",
379        diskResolution: 48,
380        filled: true,
381        extruded: true,
382        flatShading: false,
383        getFillColor: [...RED, previewing ? 205 : 170],
384        material: { ambient: 0.58, diffuse: 0.62, shininess: 34, specularColor: RED_LIGHT },
385        parameters: { depthTest: true }
386      }),
387      new deck.ScatterplotLayer({
388        id: "core-mouth",
389        data: [{ position: [0, 0, 0] }],
390        getPosition: (item) => item.position,
391        getRadius: 16,
392        radiusUnits: "pixels",
393        filled: false,
394        stroked: true,
395        getLineColor: [...INK, 240],
396        getLineWidth: 2,
397        parameters: { depthTest: false }
398      }),
399      new deck.TextLayer({
400        id: "well-caption-marker",
401        data: [wellCaption],
402        getPosition: (item) => item.position,
403        getText: () => "|",
404        getColor: [...RED_BRIGHT, 245],
405        getSize: 11,
406        sizeUnits: "pixels",
407        getTextAnchor: "middle",
408        getAlignmentBaseline: "bottom",
409        billboard: true,
410        fontFamily: "Avenir Next, Avenir, Helvetica Neue, sans-serif",
411        fontWeight: "500",
412        parameters: { depthTest: false }
413      }),
414      new deck.TextLayer({
415        id: "well-caption-text",
416        data: [wellCaption],
417        getPosition: (item) => item.position,
418        getText: (item) => item.text,
419        getPixelOffset: [8, 0],
420        getColor: [...RED_BRIGHT, 235],
421        getSize: 8.5,
422        sizeUnits: "pixels",
423        getTextAnchor: "start",
424        getAlignmentBaseline: "bottom",
425        billboard: true,
426        fontFamily: "Avenir Next, Avenir, Helvetica Neue, sans-serif",
427        fontWeight: "500",
428        characterSet: "0123456789 LAYERS·–",
429        parameters: { depthTest: false }
430      }),
431      ...(coreCoordinate?.text ? [new deck.TextLayer({
432        id: "core-coordinate",
433        data: [coreCoordinate],
434        getPosition: (item) => item.position,
435        getText: (item) => item.text,
436        getColor: [...INK, 182],
437        getSize: 9.5,
438        sizeUnits: "pixels",
439        getTextAnchor: "middle",
440        getAlignmentBaseline: "center",
441        billboard: true,
442        fontFamily: "ui-sans-serif, system-ui, sans-serif",
443        fontWeight: "500",
444        parameters: { depthTest: false }
445      })] : []),
446      new deck.ScatterplotLayer({
447        id: "stratum-anchors",
448        data: entries,
449        pickable: true,
450        getPosition: (entry) => [0, 0, entry.z],
451        getRadius: (entry) => entry.map.id === this.selectedId ? 7 : entry.map.id === this.hoverId ? 5.6 : 3.2,
452        radiusUnits: "pixels",
453        getFillColor: (entry) => entry.map.id === this.selectedId ? [...INK, 255] : entry.map.id === this.hoverId ? [...RED_BRIGHT, 255] : [...RED_BRIGHT, 210],
454        onClick: (info) => { if (info.object) this.onSelect(info.object.map); },
455        onHover: (info) => this.setHover(info),
456        parameters: { depthTest: false }
457      }),
458      new deck.ScatterplotLayer({
459        id: "core-hit-zones",
460        data: entries,
461        pickable: true,
462        getPosition: (entry) => [0, 0, entry.z],
463        getRadius: 13,
464        radiusUnits: "pixels",
465        getFillColor: [0, 0, 0, 1],
466        onClick: (info) => { if (info.object) this.onSelect(info.object.map); },
467        onHover: (info) => this.setHover(info),
468        parameters: { depthTest: false }
469      }),
470      new deck.LineLayer({
471        id: "year-ticks",
472        data: labels,
473        getSourcePosition: (entry) => [0, 0, entry.z],
474        getTargetPosition: (entry) => [labelX, 0, entry.z],
475        getColor: (entry) => entry.map.id === this.selectedId ? [...INK, 230] : entry.map.id === this.hoverId ? [...RED_BRIGHT, 255] : [...INK, previewing ? 170 : 110],
476        getWidth: (entry) => entry.map.id === this.selectedId ? 1.7 : entry.map.id === this.hoverId ? 1.5 : 1,
477        widthUnits: "pixels",
478        parameters: { depthTest: false }
479      }),
480      new deck.TextLayer({
481        id: "years",
482        data: labels,
483        getPosition: (entry) => [labelX, 0, entry.z],
484        getText: (entry) => String(entry.map.year),
485        getColor: (entry) => entry.map.id === this.selectedId ? [...INK, 255] : entry.map.id === this.hoverId ? [...RED_BRIGHT, 255] : [...INK, previewing ? 238 : 205],
486        getSize: (entry) => entry.map.id === this.selectedId ? 18 : entry.map.id === this.hoverId ? 16 : 13,
487        sizeUnits: "pixels",
488        getTextAnchor: "start",
489        getAlignmentBaseline: "center",
490        billboard: true,
491        fontFamily: "Palatino Linotype, Book Antiqua, Georgia, serif",
492        fontWeight: "700",
493        pickable: true,
494        onClick: (info) => { if (info.object) this.onSelect(info.object.map); },
495        onHover: (info) => this.setHover(info),
496        parameters: { depthTest: false }
497      }),
498      ...(temporalSpan ? [new deck.TextLayer({
499        id: "temporal-span",
500        data: [temporalSpan],
501        getPosition: (item) => item.position,
502        getText: (item) => item.text,
503        getColor: [...RED_BRIGHT, 220],
504        getSize: 11,
505        sizeUnits: "pixels",
506        getTextAnchor: "end",
507        getAlignmentBaseline: "center",
508        billboard: true,
509        fontFamily: "ui-sans-serif, system-ui, sans-serif",
510        parameters: { depthTest: false }
511      })] : []),
512      ...(selected ? [new deck.TextLayer({
513        id: "hovered-stratum",
514        data: [selected],
515        getPosition: (entry) => [26, 0, entry.z],
516        getText: (entry) => titleLines(entry.map.title),
517        getColor: [...RED_BRIGHT, 255],
518        getSize: (entry) => titleSize(entry.map.title),
519        sizeUnits: "pixels",
520        getTextAnchor: "start",
521        getAlignmentBaseline: "center",
522        billboard: true,
523        lineHeight: 1.08,
524        fontFamily: "ui-sans-serif, system-ui, sans-serif",
525        parameters: { depthTest: false }
526      })] : [])
527    ] });
528  }
529
530  renderCompact(entries, top) {
531    const selected = entries.find((entry) => entry.map.id === this.selectedId);
532    const bounds = [entries[0], entries.at(-1)].filter(Boolean);
533    this.deck.setProps({ layers: [
534      new deck.PolygonLayer({
535        id: "mini-strata", data: entries, pickable: false, stroked: true, filled: true,
536        getPolygon: (entry) => entry.polygon,
537        getLineColor: (entry) => entry.map.id === this.selectedId ? [...INK, 235] : [...RED_BRIGHT, 170],
538        getFillColor: (entry) => entry.map.id === this.selectedId ? [...RED_BRIGHT, 70] : [...RED, 28],
539        getLineWidth: (entry) => entry.map.id === this.selectedId ? 2 : 1,
540        lineWidthUnits: "pixels", parameters: { depthTest: false }
541      }),
542      new deck.ColumnLayer({
543        id: "mini-bore", data: [{ position: [0, 0, 0], elevation: top }],
544        getPosition: (item) => item.position, getElevation: (item) => item.elevation,
545        radius: 10, radiusUnits: "pixels", diskResolution: 40,
546        filled: true, extruded: true, getFillColor: [...RED_BRIGHT, 205],
547        parameters: { depthTest: false }
548      }),
549      new deck.ScatterplotLayer({
550        id: "mini-anchors", data: entries, pickable: false,
551        getPosition: (entry) => [0, 0, entry.z],
552        getRadius: (entry) => entry.map.id === this.selectedId ? 6 : 2.5,
553        radiusUnits: "pixels",
554        getFillColor: (entry) => entry.map.id === this.selectedId ? [...INK, 255] : [...RED_LIGHT, 185],
555        parameters: { depthTest: false }
556      }),
557      new deck.TextLayer({
558        id: "mini-years", data: selected && !bounds.some((entry) => entry.map.id === selected.map.id) ? [...bounds, selected] : bounds,
559        getPosition: (entry) => [95, 0, entry.z], getText: (entry) => String(entry.map.year),
560        getColor: (entry) => entry.map.id === this.selectedId ? [...INK, 255] : [...INK, 175],
561        getSize: (entry) => entry.map.id === this.selectedId ? 12 : 9,
562        sizeUnits: "pixels", getTextAnchor: "start", getAlignmentBaseline: "center",
563        billboard: true, fontFamily: "Palatino Linotype, Georgia, serif",
564        parameters: { depthTest: false }
565      })
566    ] });
567  }
568
569  leave() {
570    if (this.appearanceFrame) cancelAnimationFrame(this.appearanceFrame);
571    this.appearanceFrame = null;
572    this.locked = false;
573    this.compact = false;
574    this.passThrough = false;
575    this.container.classList.remove("is-active");
576    this.container.classList.remove("is-preview");
577    this.container.classList.remove("is-compact");
578    this.caption.hidden = true;
579    this.onHover?.(null);
580    if (this.deck) this.deck.setProps({ layers: [] });
581    this.stack = null;
582    this.previewSignature = null;
583  }
584}

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.