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.