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.