vendor: 14 bytes, lines 1-2
1(() => { 2 //
2dunbar-design-repository/assets/js/tester.js 3 document.addEventListener("DOMContentLoaded", () => { 4 console.log("tester.js loaded"); 5 const tester = document.getElementById("tester"); 6 const timelineSection = document.getElementById("timeline"); 7 const timelineEntryMarkers = [...document.querySelectorAll(".timeline-entry-marker")]; 8 const timelineEntrySections = [...document.querySelectorAll("#timeline .timeline-section[data-slug]")]; 9 const fontWeight = document.getElementById("fontWeight"); 10 const fontWidth = document.getElementById("fontWidth"); 11 const xHeight = document.getElementById("xHeight"); 12 const styleSelect = document.getElementById("style"); 13 const resetBtn = document.getElementById("resetBtn"); 14 const fontWeightValue = document.getElementById("fontWeightValue"); 15 const fontWidthValue = document.getElementById("fontWidthValue"); 16 const xHeightValue = document.getElementById("xHeightValue"); 17 if (!tester || !fontWeight || !fontWidth || !xHeight || !resetBtn) { 18 console.warn("Tester elements missing"); 19 return; 20 } 21 const testerWordListPaths = [ 22 "/words/dunbar.txt" 23 ]; 24 const fallbackPresetTexts = [ 25 "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 26 "abcdefghijklmnopqrstuvwxyz", 27 "0123456789", 28 ".,:;!?&@#%*()[]{}+-=/", 29 "The quick brown fox jumps over the lazy dog.", 30 "Various alternate characters." 31 ]; 32 let presetTexts = fallbackPresetTexts; 33 let testerHasUserText = false; 34 let scrollTextStep = 0; 35 let scrollTextFrameRequested = false; 36 const scrollTextStepCount = 4; 37 const weightStops = [100, 300, 500, 700, 800, 900]; 38 tester.addEventListener("keydown", (e) => { 39 if (e.key === "Enter") { 40 e.preventDefault(); 41 } 42 }); 43 tester.addEventListener("paste", (e) => { 44 e.preventDefault(); 45 testerHasUserText = true; 46 const text = (e.clipboardData || window.clipboardData).getData("text").replace(/[\r\n]+/g, " "); 47 document.execCommand("insertText", false, text); 48 }); 49 function parseWordList(data) { 50 return data.replace(/^\/\/.+$/mg, "").replace(/(^|\n)\/\*[^]*?\*\/($|\n)/g, "\n").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).flatMap((line) => line.split("|").map((word) => word.trim()).filter(Boolean)); 51 } 52 async function loadPresetTexts() { 53 try { 54 const lists = await Promise.all( 55 testerWordListPaths.map(async (path) => { 56 const response = await fetch(path); 57 if (!response.ok) { 58 throw new Error(`Unable to load ${path}`); 59 } 60 return response.text(); 61 }) 62 ); 63 const nextPresetTexts = lists.flatMap(parseWordList); 64 if (nextPresetTexts.length) { 65 presetTexts = nextPresetTexts; 66 } 67 } catch (error) { 68 console.warn("Using fallback tester presets:", error); 69 } 70 } 71 function getRandomPreset(currentText = "") { 72 if (presetTexts.length < 2) return presetTexts[0] || ""; 73 let nextText = currentText; 74 let attempts = 0; 75 while (nextText === currentText && attempts < 8) { 76 nextText = presetTexts[Math.floor(Math.random() * presetTexts.length)]; 77 attempts += 1; 78 } 79 return nextText; 80 } 81 function syncStyleDropdownToWeight() { 82 if (!styleSelect) return; 83 const weight = Number(fontWeight.value); 84 const nearestWeight = weightStops.reduce((nearest, current) => { 85 return Math.abs(current - weight) < Math.abs(nearest - weight) ? current : nearest; 86 }, weightStops[0]); 87 styleSelect.value = String(nearestWeight); 88 } 89 function syncWeightToStyleDropdown() { 90 if (!styleSelect) return; 91 fontWeight.value = styleSelect.value; 92 } 93 function updateValueLabels() { 94 if (fontWeightValue) fontWeightValue.textContent = fontWeight.value; 95 if (fontWidthValue) fontWidthValue.textContent = fontWidth.value; 96 if (xHeightValue) xHeightValue.textContent = xHeight.value; 97 } 98 function updateTesterStyles() { 99 tester.style.fontWeight = fontWeight.value; 100 tester.style.fontVariationSettings = `"wght" ${fontWeight.value}, "wdth" ${fontWidth.value}, "XHGT" ${xHeight.value}`;
101 updateValueLabels(); 102 console.log("fontVariationSettings:", tester.style.fontVariationSettings); 103 } 104 function autoResizeText() { 105 const text = tester.textContent.trim(); 106 const isMobile = window.matchMedia("(max-width: 640px)").matches; 107 const baseSize = isMobile ? 44 : 90; 108 const minSize = isMobile ? 14 : 18; 109 const maxSize = isMobile ? 72 : 320; 110 const availableWidth = tester.offsetWidth * 0.95; 111 if (!text.length) { 112 tester.style.fontSize = `${baseSize}px`; 113 return; 114 } 115 tester.style.fontSize = `${baseSize}px`; 116 const range = document.createRange(); 117 range.selectNodeContents(tester); 118 const rect = range.getBoundingClientRect(); 119 if (!rect.width) return; 120 let scale = availableWidth / rect.width; 121 if (text.length <= 2) { 122 scale *= 1.1; 123 } else if (text.length <= 6) { 124 scale *= 1.05; 125 } 126 const newSize = Math.max(minSize, Math.min(baseSize * scale, maxSize)); 127 tester.style.fontSize = `${newSize}px`; 128 } 129 function updateBaseline() { 130 const marker = document.createElement("span"); 131 marker.textContent = "x"; 132 marker.style.display = "inline-block"; 133 marker.style.padding = "0"; 134 marker.style.margin = "0"; 135 marker.style.lineHeight = "1"; 136 marker.style.visibility = "hidden"; 137 tester.appendChild(marker); 138 const markerRect = marker.getBoundingClientRect(); 139 const testerRect = tester.getBoundingClientRect(); 140 const baseline = markerRect.bottom - testerRect.top; 141 tester.style.setProperty("--baseline", `${baseline}px`); 142 marker.remove(); 143 } 144 function refreshTesterLayout() { 145 updateTesterStyles(); 146 autoResizeText(); 147 updateBaseline(); 148 } 149 function setRandomTesterText() { 150 tester.textContent = getRandomPreset(tester.textContent.trim()); 151 refreshTesterLayout(); 152 } 153 function updateTesterTextOnScroll() { 154 scrollTextFrameRequested = false; 155 if (!timelineSection || testerHasUserText || !timelineEntryMarkers.length) return; 156 const timelineRect = timelineSection.getBoundingClientRect(); 157 const viewportHeight = window.innerHeight || document.documentElement.clientHeight; 158 if (timelineRect.bottom <= 0 || timelineRect.top >= viewportHeight) { 159 return; 160 } 161 const passedEntryCount = timelineEntrySections.length ? timelineEntrySections.filter((section) => { 162 return section.getBoundingClientRect().top <= viewportHeight * 0.5; 163 }).length : Math.round( 164 Math.min( 165 1, 166 Math.max(0, (viewportHeight - timelineRect.top) / (viewportHeight + timelineRect.height)) 167 ) * timelineEntryMarkers.length 168 ); 169 const nextStep = Math.min( 170 scrollTextStepCount, 171 Math.floor(passedEntryCount * scrollTextStepCount / timelineEntryMarkers.length) 172 ); 173 if (nextStep !== scrollTextStep) { 174 scrollTextStep = nextStep; 175 setRandomTesterText(); 176 } 177 } 178 function requestTesterTextScrollUpdate() { 179 if (scrollTextFrameRequested) return; 180 scrollTextFrameRequested = true; 181 window.requestAnimationFrame(updateTesterTextOnScroll); 182 } 183 tester.textContent = getRandomPreset(); 184 loadPresetTexts().then(() => { 185 if (!testerHasUserText) { 186 setRandomTesterText(); 187 } 188 }); 189 tester.addEventListener("beforeinput", () => { 190 testerHasUserText = true; 191 }); 192 tester.addEventListener("input", () => { 193 testerHasUserText = true; 194 refreshTesterLayout(); 195 }); 196 fontWeight.addEventListener("input", () => { 197 syncStyleDropdownToWeight(); 198 refreshTesterLayout(); 199 }); 200 fontWidth.addEventListener("input", refreshTesterLayout); 201 xHeight.addEventListener("input", refreshTesterLayout); 202 fontWeight.addEventListener("change", refreshTesterLayout); 203 fontWidth.addEventListener("change", refreshTesterLayout); 204 xHeight.addEventListener("change", refreshTesterLayout); 205 if (styleSelect) { 206 styleSelect.addEventListener("change", () => { 207 syncWeightToStyleDropdown(); 208 refreshTesterLayout(); 209 }); 210 } 211 resetBtn.addEventListener("click", () => { 212 testerHasUserText = false;
213 scrollTextStep = 0; 214 fontWeight.value = 500; 215 fontWidth.value = 100; 216 xHeight.value = 460; 217 if (styleSelect) styleSelect.value = "500"; 218 setRandomTesterText(); 219 }); 220 window.addEventListener("scroll", requestTesterTextScrollUpdate, { passive: true }); 221 window.addEventListener("resize", () => { 222 refreshTesterLayout(); 223 requestTesterTextScrollUpdate(); 224 }); 225 refreshTesterLayout(); 226 }); 227 228
228// dunbar-design-repository/assets/js/feature.js 229 document.addEventListener("DOMContentLoaded", () => { 230 console.log("feature.js loaded"); 231 function initIntroVariableDemo(demo) { 232 const svg = demo.querySelector(".intro-variable-svg"); 233 const plot = demo.querySelector(".intro-variable-plot"); 234 const letters = [...demo.querySelectorAll(".intro-variable-letter")]; 235 const word = demo.querySelector(".intro-variable-word"); 236 const guideLayer = demo.querySelector(".intro-variable-guides"); 237 const sequenceLetters = [...demo.dataset.introSequence || ""]; 238 const isSequentialLetterDemo = sequenceLetters.length > 0 && letters.length === 1; 239 if (!svg || !plot || !letters.length || !word || !guideLayer) return null; 240 const holdDuration = 1400; 241 const transitionDuration = 900; 242 const letterSwitchPause = 2e3; 243 const ranges = { 244 wght: [100, 900], 245 wdth: [75, 100], 246 XHGT: [353, 574] 247 }; 248 const capHeightValue = 746; 249 const widthStops = [ 250 { value: 75, label: "Compressed" }, 251 { value: 82, label: "Condensed" }, 252 { value: 90, label: "Narrow" }, 253 { value: 100, label: "Regular" } 254 ]; 255 const xheightStops = [ 256 { value: 353, label: "Low" }, 257 { value: 525, label: "Text" }, 258 { value: 574, label: "Tall" } 259 ]; 260 const weightStops = [ 261 { value: 100, label: "Hairline" }, 262 { value: 300, label: "Light" }, 263 { value: 350, label: "Book" }, 264 { value: 400, label: "Regular" }, 265 { value: 500, label: "Medium" }, 266 { value: 700, label: "Bold" }, 267 { value: 800, label: "Extra Bold" }, 268 { value: 900, label: "Ultra" } 269 ]; 270 const layout = { 271 plotY: 455, 272 baselineY: 0, 273 capHeightY: -395, 274 weightValueY: 22, 275 xheightValueOffset: -8, 276 capHeightValueOffset: 16, 277 capHeightLineOffset: 23, 278 xheightScale: 0.72, 279 widthValueY: -410, 280 widthGuideTopY: -405, 281 labelY: 58, 282 labelLineHeight: 15, 283 contentLeft: isSequentialLetterDemo ? -110 : -260, 284 contentRightPadding: isSequentialLetterDemo ? 110 : 260 285 }; 286 const initialStates = makeStateSet(); 287 let sequenceIndex = 0; 288 let sequenceChanges = 0; 289 if (isSequentialLetterDemo) { 290 letters[0].textContent = sequenceLetters[sequenceIndex]; 291 } 292 const states = letters.map((el, i) => { 293 const current = initialStates[i]; 294 return { 295 el, 296 current, 297 from: { ...current }, 298 target: { ...current } 299 }; 300 }); 301 const axisX = createSvgEl("line", ["intro-axis-line"]); 302 const axisY = createSvgEl("line", ["intro-axis-line"]); 303 const capLine = createSvgEl("line", ["intro-axis-line"]); 304 const baselineLine = createSvgEl("line", ["intro-baseline-line"]); 305 const originLine = createSvgEl("line", ["intro-origin-line"]); 306 const widthAxisLabel = createSvgEl("text", ["intro-axis-label"]); 307 const capHeightAxisLabel = createSvgEl("text", ["intro-axis-label"]); 308 const xheightAxisLabel = createSvgEl("text", ["intro-axis-label", "intro-xheight-axis-label"]); 309 const weightAxisLabel = createSvgEl("text", ["intro-axis-label"]); 310 guideLayer.appendChild(axisX); 311 guideLayer.appendChild(axisY); 312 guideLayer.appendChild(capLine); 313 guideLayer.appendChild(baselineLine); 314 guideLayer.appendChild(originLine); 315 guideLayer.appendChild(widthAxisLabel); 316 guideLayer.appendChild(capHeightAxisLabel); 317 guideLayer.appendChild(xheightAxisLabel); 318 guideLayer.appendChild(weightAxisLabel); 319 const guides = letters.map((letter, i) => { 320 const group = createSvgEl("g"); 321 const isCapHeightGuide = !isSequentialLetterDemo && i === 0; 322 const widthLine = createSvgEl("line", ["intro-guide-line"]); 323 const xheightLine = createSvgEl("line", [ 324 "intro-guide-line", 325 isCapHeightGuide ? "intro-capheight-guide" : "intro-xheight-guide" 326 ]); 327 const widthValue = createSvgEl("text", ["intro-guide-text", "intro-value-text"]); 328 const xheightValue = createSvgEl("text", ["intro-guide-text", "intro-value-text"]); 329 const weightValue = createSvgEl("text", ["intro-guide-text", "intro-value-text"]); 330 const label = createSvgEl("text", ["intro-letter-label"]); 331 const labelBox = createSvgEl("rect", ["intro-letter-label-box"]); 332 const widthLabel = createSvgEl("tspan", ["intro-label-row"]); 333 const xheightLabel = createSvgEl("tspan", ["intro-label-row"]); 334 const weightLabel = createSvgEl("tspan", ["intro-label-row"]); 335 group.appendChild(widthLine); 336 group.appendChild(xheightLine); 337 group.appendChild(widthValue); 338 group.appendChild(xheightValue); 339 group.appendChild(weightValue); 340 group.appendChild(labelBox); 341 label.appendChild(widthLabel); 342 label.appendChild(xheightLabel); 343 label.appendChild(weightLabel); 344 group.appendChild(label); 345 guideLayer.appendChild(group); 346 return { 347 widthLine, 348 xheightLine, 349 widthValue, 350 xheightValue, 351 weightValue, 352 labelBox, 353 label, 354 widthLabel, 355 xheightLabel, 356 weightLabel, 357 isCapHeightGuide 358 }; 359 }); 360 let phase = "hold"; 361 let phaseStart = performance.now(); 362 let isRunning = false;
363 let animationFrameId = null; 364 function makeStateSet(previousStates = []) { 365 const stateProfiles = [ 366 { wght: [100, 170], wdth: [92, 100], XHGT: [353, 390] }, 367 { wght: [850, 900], wdth: [82, 90], XHGT: [540, 574] }, 368 { wght: [420, 680], wdth: [75, 77], XHGT: [420, 500] }, 369 { wght: [680, 800], wdth: [94, 100], XHGT: [353, 410] }, 370 { wght: [260, 340], wdth: [81, 83], XHGT: [430, 500] }, 371 { wght: [430, 560], wdth: [94, 100], XHGT: [520, 574] } 372 ]; 373 const nextStates = []; 374 const profiles = stateProfiles.slice(0, letters.length); 375 profiles.forEach((profile) => { 376 nextStates.push(makeDistinctState(nextStates, profile)); 377 }); 378 while (nextStates.length < letters.length) { 379 nextStates.push(makeDistinctState(nextStates)); 380 } 381 return orderStatesForContrast(nextStates, previousStates); 382 } 383 function makeDistinctState(existingStates, profile = ranges) { 384 let candidate = makeRandomState(profile); 385 let attempts = 0; 386 while (!isDistinctState(candidate, existingStates) && attempts < 80) { 387 candidate = makeRandomState(profile); 388 attempts += 1; 389 } 390 return candidate; 391 } 392 function makeRandomState(profile = ranges) { 393 return { 394 wght: random(...profile.wght), 395 wdth: random(...profile.wdth), 396 XHGT: random(...profile.XHGT) 397 }; 398 } 399 function isDistinctState(candidate, existingStates) { 400 return existingStates.every((state) => { 401 const wghtDistance = Math.abs(candidate.wght - state.wght); 402 const wdthDistance = Math.abs(candidate.wdth - state.wdth); 403 const xhgtDistance = Math.abs(candidate.XHGT - state.XHGT); 404 if (wghtDistance < 140 && wdthDistance < 7 && xhgtDistance < 60) { 405 return false; 406 } 407 return getStateDistance(candidate, state) > 0.34; 408 }); 409 } 410 function getStateDistance(a, b) { 411 const wght = (a.wght - b.wght) / (ranges.wght[1] - ranges.wght[0]); 412 const wdth = (a.wdth - b.wdth) / (ranges.wdth[1] - ranges.wdth[0]); 413 const XHGT = (a.XHGT - b.XHGT) / (ranges.XHGT[1] - ranges.XHGT[0]); 414 return Math.sqrt(wght * wght + wdth * wdth + XHGT * XHGT); 415 } 416 function orderStatesForContrast(nextStates, previousStates = []) { 417 let bestOrder = shuffle(nextStates); 418 let bestScore = getSequenceContrastScore(bestOrder, previousStates); 419 for (let i = 0; i < 220; i += 1) { 420 const candidate = shuffle(nextStates); 421 const score = getSequenceContrastScore(candidate, previousStates); 422 if (score > bestScore) { 423 bestOrder = candidate; 424 bestScore = score; 425 } 426 } 427 return bestOrder; 428 } 429 function getSequenceContrastScore(sequence, previousStates = []) { 430 const neighborDistances = sequence.slice(1).map((state, i) => { 431 return getStateDistance(sequence[i], state); 432 }); 433 const transitionDistances = sequence.map((state, i) => { 434 if (!previousStates[i]) return 1; 435 return getStateDistance(previousStates[i], state); 436 }); 437 const minNeighborDistance = Math.min(...neighborDistances, 1); 438 const minTransitionDistance = Math.min(...transitionDistances); 439 const averageNeighborDistance = getAverage(neighborDistances); 440 const averageTransitionDistance = getAverage(transitionDistances); 441 const weakNeighborCount = neighborDistances.filter((distance) => distance < 0.42).length; 442 const weakTransitionCount = transitionDistances.filter((distance) => distance < 0.48).length; 443 return minNeighborDistance * 6 + minTransitionDistance * 7 + averageNeighborDistance + averageTransitionDistance - weakNeighborCount * 3 - weakTransitionCount * 4; 444 } 445 function getAverage(values) { 446 if (!values.length) return 0; 447 return values.reduce((sum, value) => sum + value, 0) / values.length; 448 } 449 function applyState(el, state) { 450 el.style.fontVariationSettings = getFontVariationSettings(state); 451 } 452 function getFontVariationSettings(state) { 453 return ` 454 "wght" ${Math.round(state.wght)}, 455 "wdth" ${Math.round(state.wdth)}, 456 "XHGT" ${Math.round(state.XHGT)} 457 `; 458 } 459 function getXHeightY(state) { 460 return layout.baselineY - state.XHGT / ranges.XHGT[1] * Math.abs(layout.capHeightY) * layout.xheightScale; 461 } 462 function syncGuideLabelFontSizes() { 463 const labelRowGap = Number.parseFloat( 464 getComputedStyle(demo).getPropertyValue("--intro-label-row-gap") 465 ); 466 if (Number.isFinite(labelRowGap) && labelRowGap > 0) { 467 layout.labelLineHeight = labelRowGap; 468 }
469 const viewBoxWidth = svg.viewBox.baseVal.width; 470 const renderedWidth = svg.getBoundingClientRect().width; 471 if (!viewBoxWidth || !renderedWidth) return; 472 const svgScale = renderedWidth / viewBoxWidth; 473 const labels = guideLayer.querySelectorAll( 474 ".intro-origin-text, .intro-axis-label, .intro-guide-text, .intro-letter-label" 475 ); 476 labels.forEach((label) => { 477 label.style.removeProperty("font-size"); 478 const cssFontSize = Number.parseFloat(getComputedStyle(label).fontSize); 479 if (Number.isFinite(cssFontSize) && cssFontSize > 0) { 480 label.style.fontSize = `${cssFontSize / svgScale}px`; 481 } 482 }); 483 } 484 function updateGuides() { 485 let boxes = []; 486 try { 487 boxes = states.map((state, i) => { 488 const start2 = word.getStartPositionOfChar(i); 489 const end = word.getEndPositionOfChar(i); 490 const width = end.x - start2.x; 491 if (Number.isFinite(width) && width > 0) { 492 return { x: start2.x, width }; 493 } 494 const box = state.el.getBBox(); 495 if (box.width > 0) return box; 496 return word.getExtentOfChar(i); 497 }); 498 } catch { 499 return; 500 } 501 if (!boxes.length) return; 502 const lastBox = boxes[boxes.length - 1]; 503 const wordRight = lastBox.x + lastBox.width; 504 const viewBoxWidth = svg.viewBox.baseVal.width || svg.clientWidth; 505 const contentLeft = layout.contentLeft; 506 const contentRight = wordRight + layout.contentRightPadding; 507 const plotX = (viewBoxWidth - (contentRight - contentLeft)) / 2 - contentLeft; 508 const svgScale = svg.getBoundingClientRect().width / viewBoxWidth || 1; 509 const rootFontSize = Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16; 510 const letterLabelPaddingX = rootFontSize * 0.8 / svgScale; 511 const letterLabelPaddingY = rootFontSize * 0.5 / svgScale; 512 plot.setAttribute("transform", `translate(${plotX} ${layout.plotY})`); 513 widthAxisLabel.setAttribute("x", -6); 514 widthAxisLabel.setAttribute("y", layout.capHeightY - 10); 515 widthAxisLabel.setAttribute("text-anchor", "end"); 516 widthAxisLabel.textContent = "Width"; 517 let widthLabelCapHeightY = layout.widthGuideTopY; 518 try { 519 widthLabelCapHeightY = widthAxisLabel.getBBox().y; 520 } catch { 521 } 522 axisX.setAttribute("x1", layout.contentLeft); 523 axisX.setAttribute("x2", wordRight + layout.contentRightPadding); 524 axisX.setAttribute("y1", layout.baselineY); 525 axisX.setAttribute("y2", layout.baselineY); 526 axisY.setAttribute("x1", 0); 527 axisY.setAttribute("x2", 0); 528 axisY.setAttribute("y1", widthLabelCapHeightY); 529 axisY.setAttribute("y2", layout.labelY + layout.labelLineHeight * 2); 530 originLine.setAttribute("x1", 0); 531 originLine.setAttribute("x2", 0); 532 originLine.setAttribute("y1", widthLabelCapHeightY); 533 originLine.setAttribute("y2", layout.labelY + layout.labelLineHeight * 2); 534 capLine.setAttribute("x1", 0); 535 capLine.setAttribute("x2", wordRight); 536 capLine.setAttribute("y1", layout.capHeightY + layout.capHeightLineOffset); 537 capLine.setAttribute("y2", layout.capHeightY + layout.capHeightLineOffset); 538 baselineLine.setAttribute("x1", 0); 539 baselineLine.setAttribute("x2", wordRight); 540 baselineLine.setAttribute("y1", layout.baselineY); 541 baselineLine.setAttribute("y2", layout.baselineY); 542 capHeightAxisLabel.setAttribute("x", -6); 543 capHeightAxisLabel.setAttribute("y", layout.capHeightY + layout.capHeightValueOffset); 544 capHeightAxisLabel.setAttribute("text-anchor", "end"); 545 capHeightAxisLabel.textContent = "Cap height"; 546 xheightAxisLabel.setAttribute("x", -6); 547 xheightAxisLabel.setAttribute( 548 "y", 549 (layout.capHeightY + layout.capHeightValueOffset + layout.weightValueY) / 2 550 ); 551 xheightAxisLabel.setAttribute("text-anchor", "end"); 552 xheightAxisLabel.textContent = isSequentialLetterDemo && sequenceIndex === 0 ? "" : "x-HEIGHT"; 553 weightAxisLabel.setAttribute("x", -6); 554 weightAxisLabel.setAttribute("y", layout.weightValueY); 555 weightAxisLabel.setAttribute("text-anchor", "end"); 556 weightAxisLabel.textContent = "Weight";
557 states.forEach((state, i) => { 558 const guide = guides[i]; 559 const box = boxes[i]; 560 if (!guide || !box) return; 561 const isCapHeightGuide = isSequentialLetterDemo ? sequenceIndex === 0 : guide.isCapHeightGuide; 562 guide.xheightLine.classList.toggle("intro-capheight-guide", isCapHeightGuide); 563 guide.xheightLine.classList.toggle("intro-xheight-guide", !isCapHeightGuide); 564 const left = box.x; 565 const right = box.x + box.width; 566 const center = box.x + box.width / 2; 567 const xheightY = isCapHeightGuide ? layout.capHeightY + layout.capHeightLineOffset : getXHeightY(state.current); 568 const xheightGuideY = isCapHeightGuide ? xheightY : xheightY - 2; 569 guide.widthLine.setAttribute("x1", right); 570 guide.widthLine.setAttribute("x2", right); 571 guide.widthLine.setAttribute("y1", widthLabelCapHeightY); 572 guide.widthLine.setAttribute("y2", layout.labelY + layout.labelLineHeight * 2); 573 guide.xheightLine.setAttribute("x1", left); 574 guide.xheightLine.setAttribute("x2", right); 575 guide.xheightLine.setAttribute("y1", xheightGuideY); 576 guide.xheightLine.setAttribute("y2", xheightGuideY); 577 guide.widthValue.setAttribute("x", center); 578 guide.widthValue.setAttribute("y", layout.widthValueY); 579 guide.widthValue.setAttribute("text-anchor", "middle"); 580 guide.widthValue.textContent = Math.round(box.width); 581 const xheightValueY = isCapHeightGuide ? layout.capHeightY + layout.capHeightValueOffset : xheightY + layout.xheightValueOffset; 582 guide.xheightValue.setAttribute("x", center); 583 guide.xheightValue.setAttribute("y", xheightValueY); 584 guide.xheightValue.setAttribute("text-anchor", "middle"); 585 guide.xheightValue.textContent = isCapHeightGuide ? capHeightValue : Math.round(state.current.XHGT); 586 guide.weightValue.setAttribute("x", center); 587 guide.weightValue.setAttribute("y", layout.weightValueY); 588 guide.weightValue.setAttribute("text-anchor", "middle"); 589 guide.weightValue.textContent = Math.round(state.current.wght); 590 guide.label.setAttribute("x", center); 591 guide.label.setAttribute("y", layout.labelY); 592 guide.label.setAttribute("text-anchor", "middle"); 593 guide.widthLabel.setAttribute("x", center); 594 guide.widthLabel.setAttribute("dy", 0); 595 guide.widthLabel.textContent = getNearestLabel(state.current.wdth, widthStops); 596 guide.xheightLabel.setAttribute("x", center); 597 guide.xheightLabel.setAttribute("dy", layout.labelLineHeight); 598 guide.xheightLabel.textContent = getNearestLabel(state.current.XHGT, xheightStops); 599 guide.weightLabel.setAttribute("x", center); 600 guide.weightLabel.setAttribute("dy", layout.labelLineHeight); 601 guide.weightLabel.textContent = getNearestLabel(state.current.wght, weightStops); 602 const labelBounds = guide.label.getBBox(); 603 guide.labelBox.setAttribute("x", labelBounds.x - letterLabelPaddingX); 604 guide.labelBox.setAttribute("y", labelBounds.y - letterLabelPaddingY); 605 guide.labelBox.setAttribute("width", labelBounds.width + letterLabelPaddingX * 2); 606 guide.labelBox.setAttribute("height", labelBounds.height + letterLabelPaddingY * 2); 607 }); 608 } 609 function animate(now) { 610 if (!isRunning) return; 611 const elapsed = now - phaseStart; 612 if (phase === "letter-switch-pause") { 613 states.forEach((state) => applyState(state.el, state.current)); 614 updateGuides(); 615 if (elapsed >= letterSwitchPause) { 616 sequenceIndex = (sequenceIndex + 1) % sequenceLetters.length; 617 letters[0].textContent = sequenceLetters[sequenceIndex]; 618 sequenceChanges = 0; 619 phase = "hold"; 620 phaseStart = now; 621 } 622 animationFrameId = requestAnimationFrame(animate); 623 return; 624 } 625 if (phase === "hold") {
626 states.forEach((state) => applyState(state.el, state.current)); 627 updateGuides(); 628 if (elapsed >= holdDuration) { 629 const nextStates = isSequentialLetterDemo ? [makeDistinctState([states[0].current])] : makeStateSet(states.map((state) => state.current)); 630 states.forEach((state) => { 631 state.from = { ...state.current }; 632 state.target = nextStates.shift(); 633 }); 634 phase = "transition"; 635 phaseStart = now; 636 } 637 animationFrameId = requestAnimationFrame(animate); 638 return; 639 } 640 if (phase === "transition") { 641 const progress = Math.min(elapsed / transitionDuration, 1); 642 const eased = easeInOutCubic(progress); 643 states.forEach((state) => { 644 state.current = { 645 wght: mix(state.from.wght, state.target.wght, eased), 646 wdth: mix(state.from.wdth, state.target.wdth, eased), 647 XHGT: mix(state.from.XHGT, state.target.XHGT, eased) 648 }; 649 applyState(state.el, state.current); 650 }); 651 updateGuides(); 652 if (progress >= 1) { 653 if (isSequentialLetterDemo) { 654 sequenceChanges += 1; 655 if (sequenceChanges === 2) { 656 phase = "letter-switch-pause"; 657 phaseStart = now; 658 animationFrameId = requestAnimationFrame(animate); 659 return; 660 } 661 } 662 phase = "hold"; 663 phaseStart = now; 664 } 665 } 666 animationFrameId = requestAnimationFrame(animate); 667 } 668 function start() { 669 if (isRunning) return; 670 isRunning = true; 671 phaseStart = performance.now(); 672 animationFrameId = requestAnimationFrame(animate); 673 } 674 function stop() { 675 isRunning = false; 676 if (animationFrameId) { 677 cancelAnimationFrame(animationFrameId); 678 animationFrameId = null; 679 } 680 } 681 function refreshGuides() { 682 syncGuideLabelFontSizes(); 683 states.forEach((state) => applyState(state.el, state.current)); 684 updateGuides(); 685 } 686 function scheduleGuidesRefresh() { 687 requestAnimationFrame(() => { 688 requestAnimationFrame(refreshGuides); 689 }); 690 } 691 function initialize() { 692 refreshGuides(); 693 const introSection = demo.closest("#intro") || demo; 694 const introObserver = new IntersectionObserver( 695 (entries) => { 696 entries.forEach((entry) => { 697 if (entry.isIntersecting) { 698 start(); 699 } else { 700 stop(); 701 } 702 }); 703 }, 704 { 705 threshold: 0.1 706 } 707 ); 708 introObserver.observe(introSection); 709 } 710 if (document.fonts?.ready) { 711 document.fonts.ready.then(initialize); 712 document.fonts.addEventListener?.("loadingdone", scheduleGuidesRefresh); 713 } else { 714 initialize(); 715 } 716 window.addEventListener("load", scheduleGuidesRefresh, { once: true }); 717 if (window.ResizeObserver) { 718 const resizeObserver = new ResizeObserver(scheduleGuidesRefresh); 719 resizeObserver.observe(svg); 720 } 721 return { element: demo, start, stop }; 722 } 723 function createSvgEl(tag, classNames = []) { 724 const el = document.createElementNS("http://www.w3.org/2000/svg", tag); 725 classNames.forEach((name) => el.classList.add(name)); 726 return el; 727 } 728 function random(min, max) { 729 return min + Math.random() * (max - min); 730 } 731 function shuffle(items) { 732 const shuffled = [...items]; 733 for (let i = shuffled.length - 1; i > 0; i -= 1) { 734 const j = Math.floor(Math.random() * (i + 1)); 735 [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; 736 } 737 return shuffled; 738 } 739 function mix(a, b, t) { 740 return a + (b - a) * t; 741 } 742 function easeInOutCubic(t) { 743 return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; 744 } 745 function getNearestLabel(value, stops) { 746 let closest = stops[0]; 747 stops.forEach((stop) => { 748 if (Math.abs(stop.value - value) < Math.abs(closest.value - value)) { 749 closest = stop; 750 } 751 }); 752 return closest.label; 753 } 754 document.querySelectorAll("[data-intro-variable-demo]").forEach((demo) => { 755 initIntroVariableDemo(demo); 756 }); 757 const featureDemos = []; 758 function initTypeDemo(demo, settings) { 759 const svg = demo.querySelector(".type-svg"); 760 const textArea = demo.querySelector(".type-text"); 761 const word = demo.querySelector(".word"); 762 const wordText = demo.querySelector(".word-text"); 763 const guides = demo.querySelector(".guides"); 764 const axisValue = demo.querySelector(".axis-value"); 765 const buttons = demo.querySelectorAll(".controls button"); 766 const xheightGuide = demo.querySelector(".xheight-guide"); 767 const baselineGuide = demo.querySelector(".baseline-guide"); 768 if (!axisValue || buttons.length === 0) return null; 769 if (!word && !wordText) return null; 770 const activeWord = wordText || word; 771 const stops = settings.stops; 772 let stopIndex = 0; 773 let pauseUntil = 0; 774 const pauseDuration = 700; 775 let currentValue = stops[0]; 776 let isPausedByHover = false;
777 let isRunning = false; 778 let animationFrameId = null; 779 const speed = settings.speed || 1; 780 let guideLines = []; 781 const yTop = 70; 782 const yBottom = 340; 783 function createWidthGuides() { 784 if (!guides || !word) return; 785 guides.innerHTML = ""; 786 guideLines = []; 787 const text = word.textContent.trim(); 788 for (let i = 0; i < text.length; i++) { 789 const line = document.createElementNS( 790 "http://www.w3.org/2000/svg", 791 "line" 792 ); 793 line.setAttribute("y1", yTop); 794 line.setAttribute("y2", yBottom); 795 line.setAttribute("class", "guide-line"); 796 guides.appendChild(line); 797 guideLines.push(line); 798 } 799 } 800 function updateWidthGuides() { 801 if (settings.axis !== "wdth") return; 802 if (!word || !guides || guideLines.length === 0) return; 803 const text = word.textContent.trim(); 804 for (let i = 0; i < text.length; i++) { 805 if (!guideLines[i]) continue; 806 const endPosition = word.getEndPositionOfChar(i); 807 guideLines[i].setAttribute("x1", endPosition.x); 808 guideLines[i].setAttribute("x2", endPosition.x); 809 } 810 } 811 function updateXHeightGuides() { 812 if (settings.axis !== "XHGT") return; 813 if (!xheightGuide || !baselineGuide) return; 814 const baselineY = settings.baselineY || 205; 815 const minXHeight = Math.min(...settings.stops); 816 const maxXHeight = Math.max(...settings.stops); 817 const lowGuideY = settings.lowGuideY || 145; 818 const tallGuideY = settings.tallGuideY || 85; 819 const progress = (currentValue - minXHeight) / (maxXHeight - minXHeight); 820 const xheightY = lowGuideY + (tallGuideY - lowGuideY) * progress; 821 baselineGuide.setAttribute("y1", baselineY); 822 baselineGuide.setAttribute("y2", baselineY); 823 xheightGuide.setAttribute("y1", xheightY); 824 xheightGuide.setAttribute("y2", xheightY); 825 } 826 function setAxisValue(value) { 827 currentValue = value; 828 const xhgt = settings.axis === "XHGT" ? currentValue : settings.xhgt; 829 const wdth = settings.axis === "wdth" ? currentValue : settings.wdth; 830 activeWord.style.fontVariationSettings = ` 831 "XHGT" ${xhgt}, 832 "wght" ${settings.wght}, 833 "wdth" ${wdth} 834 `; 835 axisValue.textContent = Math.round(currentValue); 836 updateActiveButton(); 837 requestAnimationFrame(() => { 838 updateWidthGuides(); 839 updateXHeightGuides(); 840 }); 841 } 842 function updateActiveButton() { 843 buttons.forEach((button) => { 844 button.classList.remove("active"); 845 const buttonValue = Number(button.dataset.value); 846 if (Math.round(currentValue) === buttonValue) { 847 button.classList.add("active"); 848 } 849 }); 850 } 851 function animate(timestamp) { 852 if (!isRunning) return; 853 if (!isPausedByHover) { 854 if (timestamp < pauseUntil) { 855 animationFrameId = requestAnimationFrame(animate); 856 return; 857 } 858 const targetValue = stops[stopIndex]; 859 if (Math.abs(currentValue - targetValue) <= speed) { 860 currentValue = targetValue; 861 setAxisValue(currentValue); 862 pauseUntil = timestamp + pauseDuration; 863 stopIndex++; 864 if (stopIndex >= stops.length) { 865 stopIndex = 0; 866 } 867 } else { 868 const moveDirection = currentValue < targetValue ? 1 : -1; 869 currentValue += moveDirection * speed; 870 setAxisValue(currentValue); 871 } 872 } 873 animationFrameId = requestAnimationFrame(animate); 874 } 875 function start() { 876 if (isRunning) return; 877 isRunning = true; 878 demo.classList.add("is-active-demo"); 879 animationFrameId = requestAnimationFrame(animate); 880 } 881 function stop() { 882 isRunning = false; 883 demo.classList.remove("is-active-demo"); 884 if (animationFrameId) { 885 cancelAnimationFrame(animationFrameId); 886 animationFrameId = null; 887 } 888 } 889 buttons.forEach((button) => { 890 button.addEventListener("click", () => { 891 const value = Number(button.dataset.value); 892 const currentStopIndex = stops.indexOf(value); 893 stopIndex = currentStopIndex + 1; 894 if (stopIndex >= stops.length || currentStopIndex === -1) { 895 stopIndex = 0; 896 } 897 pauseUntil = performance.now() + pauseDuration; 898 setAxisValue(value); 899 }); 900 });
901 [svg, textArea, ...buttons].filter(Boolean).forEach((el) => { 902 el.addEventListener("mouseenter", () => { 903 isPausedByHover = true; 904 }); 905 el.addEventListener("mouseleave", () => { 906 isPausedByHover = false; 907 }); 908 }); 909 document.fonts.ready.then(() => { 910 createWidthGuides(); 911 setAxisValue(currentValue); 912 }); 913 return { 914 element: demo, 915 start, 916 stop 917 }; 918 } 919 function initParagraphDemo(demo, settings) { 920 const layers = [...demo.querySelectorAll(".paragraph-demo-layer")]; 921 const buttons = demo.querySelectorAll(".paragraph-controls button"); 922 if (layers.length === 0 || buttons.length === 0) return null; 923 function wrapText(el) { 924 if (!el.dataset.originalText) { 925 el.dataset.originalText = el.textContent; 926 } 927 const rawText = el.dataset.originalText.replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim(); 928 el.innerHTML = ""; 929 const words = rawText.split(" "); 930 words.forEach((word, wordIndex) => { 931 const wordUnit = document.createElement("span"); 932 wordUnit.className = "word-unit"; 933 [...word].forEach((char, charIndex) => { 934 const letter = document.createElement("span"); 935 letter.className = "char-letter"; 936 letter.textContent = char; 937 wordUnit.appendChild(letter); 938 if (charIndex < word.length - 1) { 939 const letterSpace = document.createElement("span"); 940 letterSpace.className = "letter-space"; 941 wordUnit.appendChild(letterSpace); 942 } 943 }); 944 el.appendChild(wordUnit); 945 if (wordIndex < words.length - 1) { 946 const wordSpace = document.createElement("span"); 947 wordSpace.className = "word-space"; 948 wordSpace.textContent = "\xA0"; 949 wordSpace.classList.add(`word-space-${wordIndex + 1}`); 950 el.appendChild(wordSpace); 951 } 952 }); 953 } 954 layers.forEach((layer, index) => { 955 const heading = layer.querySelector(".heading-demo-text"); 956 const paragraph = layer.querySelector(".paragraph-demo-text"); 957 const state = settings.states[index]; 958 if (!heading || !paragraph || !state) return; 959 wrapText(heading); 960 wrapText(paragraph);
961 [heading, paragraph].forEach((el) => { 962 if (state.fontFamily) { 963 el.style.fontFamily = state.fontFamily; 964 } 965 if (state.fontVariationSettings) { 966 el.style.fontVariationSettings = state.fontVariationSettings; 967 } 968 }); 969 }); 970 const holdDuration = settings.holdDuration || 2400; 971 let stateIndex = 0; 972 let timeoutId = null; 973 let isPausedByHover = false; 974 let isRunning = false; 975 function updateActiveButton() { 976 buttons.forEach((button) => { 977 button.classList.toggle( 978 "active", 979 Number(button.dataset.state) === stateIndex 980 ); 981 }); 982 } 983 function showState(index) { 984 if (!layers[index]) return; 985 stateIndex = index; 986 layers.forEach((layer, layerIndex) => { 987 layer.classList.toggle("active", layerIndex === stateIndex); 988 }); 989 updateActiveButton(); 990 } 991 function clearAutoTimer() { 992 if (timeoutId) { 993 window.clearTimeout(timeoutId); 994 timeoutId = null; 995 } 996 } 997 function scheduleNext() { 998 clearAutoTimer(); 999 if (!isRunning || isPausedByHover) return; 1000 timeoutId = window.setTimeout(() => { 1001 showState((stateIndex + 1) % layers.length); 1002 scheduleNext(); 1003 }, holdDuration); 1004 } 1005 buttons.forEach((button) => { 1006 button.addEventListener("click", () => { 1007 const nextIndex = Number(button.dataset.state); 1008 if (Number.isNaN(nextIndex) || !layers[nextIndex]) return; 1009 showState(nextIndex); 1010 scheduleNext(); 1011 }); 1012 }); 1013 demo.addEventListener("mouseenter", () => { 1014 isPausedByHover = true; 1015 clearAutoTimer(); 1016 }); 1017 demo.addEventListener("mouseleave", () => { 1018 isPausedByHover = false; 1019 scheduleNext(); 1020 }); 1021 function start() { 1022 if (isRunning) return; 1023 isRunning = true; 1024 demo.classList.add("is-active-demo"); 1025 showState(stateIndex); 1026 scheduleNext(); 1027 } 1028 function stop() { 1029 isRunning = false; 1030 demo.classList.remove("is-active-demo"); 1031 clearAutoTimer(); 1032 } 1033 document.fonts.ready.then(() => { 1034 showState(0); 1035 }); 1036 return { 1037 element: demo, 1038 start, 1039 stop 1040 }; 1041 } 1042 function initSvgSlideshowDemo(demo, settings = {}) { 1043 const desktopSlides = [...demo.querySelectorAll(".svg-slideshow > .svg-slide")]; 1044 const mobileSlides = [...demo.querySelectorAll(".svg-slideshow-mobile > .svg-slide")]; 1045 const buttons = [...demo.querySelectorAll(".controls button")]; 1046 const mobileQuery = window.matchMedia("(max-width: 640px)"); 1047 if (!desktopSlides.length && !mobileSlides.length) return null; 1048 let currentIndex = 0; 1049 let intervalId = null; 1050 let isPausedByHover = false; 1051 const interval = settings.interval || 1400; 1052 function getSlides() { 1053 return mobileQuery.matches && mobileSlides.length ? mobileSlides : desktopSlides; 1054 } 1055 function showSlide(index) { 1056 const slides = getSlides(); 1057 currentIndex = index % slides.length; 1058 [...desktopSlides, ...mobileSlides].forEach((slide) => { 1059 slide.classList.remove("active"); 1060 }); 1061 slides.forEach((slide, i) => { 1062 slide.classList.toggle("active", i === currentIndex); 1063 }); 1064 buttons.forEach((button, i) => { 1065 button.classList.toggle("active", i === currentIndex); 1066 }); 1067 } 1068 function nextSlide() { 1069 const nextIndex = (currentIndex + 1) % getSlides().length; 1070 showSlide(nextIndex); 1071 } 1072 function start() { 1073 if (intervalId) return; 1074 demo.classList.add("is-active-demo"); 1075 intervalId = window.setInterval(() => { 1076 if (!isPausedByHover) { 1077 nextSlide(); 1078 } 1079 }, interval); 1080 } 1081 function stop() { 1082 demo.classList.remove("is-active-demo"); 1083 if (intervalId) { 1084 window.clearInterval(intervalId); 1085 intervalId = null; 1086 } 1087 } 1088 buttons.forEach((button, index) => { 1089 button.addEventListener("click", () => { 1090 showSlide(index); 1091 }); 1092 }); 1093 demo.addEventListener("mouseenter", () => { 1094 isPausedByHover = true; 1095 }); 1096 demo.addEventListener("mouseleave", () => { 1097 isPausedByHover = false;
1098 }); 1099 demo.addEventListener("focusin", () => { 1100 isPausedByHover = true; 1101 }); 1102 demo.addEventListener("focusout", () => { 1103 isPausedByHover = false; 1104 }); 1105 mobileQuery.addEventListener("change", () => showSlide(currentIndex)); 1106 showSlide(0); 1107 return { 1108 element: demo, 1109 start, 1110 stop 1111 }; 1112 } 1113 function initGlyphSlideshowDemo(demo, settings = {}) { 1114 const slides = [...demo.querySelectorAll(".glyph-slide")]; 1115 const slideButtons = [...demo.querySelectorAll(".glyph-slideshow-controls > button")]; 1116 if (!slides.length) return null; 1117 let currentIndex = 0; 1118 let intervalId = null; 1119 let toggleTimeoutId = null; 1120 let isPausedByHover = false; 1121 const interval = settings.interval || 2400; 1122 const toggleDelay = settings.toggleDelay || interval / 2; 1123 function setGlyphFeature(slide, state) { 1124 const display = slide.querySelector(".glyph-display"); 1125 const toggleButtons = [...slide.querySelectorAll(".glyph-toggle button")]; 1126 if (!display) return; 1127 const feature = slide.dataset.feature; 1128 display.classList.toggle("feature-off", state === "off"); 1129 display.classList.toggle("feature-on", state === "on"); 1130 display.style.fontFeatureSettings = state === "on" && feature ? `"${feature}" 1` : "normal"; 1131 toggleButtons.forEach((button) => { 1132 button.classList.toggle("active", button.dataset.state === state); 1133 }); 1134 } 1135 function clearAutoToggle() { 1136 if (toggleTimeoutId) { 1137 window.clearTimeout(toggleTimeoutId); 1138 toggleTimeoutId = null; 1139 } 1140 } 1141 function scheduleAutoToggle() { 1142 clearAutoToggle(); 1143 const activeSlide = slides[currentIndex]; 1144 setGlyphFeature(activeSlide, "off"); 1145 toggleTimeoutId = window.setTimeout(() => { 1146 if (!isPausedByHover) { 1147 setGlyphFeature(activeSlide, "on"); 1148 } 1149 }, toggleDelay); 1150 } 1151 function showSlide(index) { 1152 currentIndex = index; 1153 slides.forEach((slide, i) => { 1154 slide.classList.toggle("active", i === currentIndex); 1155 if (i !== currentIndex) { 1156 setGlyphFeature(slide, "off"); 1157 } 1158 }); 1159 slideButtons.forEach((button, i) => { 1160 button.classList.toggle("active", i === currentIndex); 1161 }); 1162 scheduleAutoToggle(); 1163 } 1164 function nextSlide() { 1165 const nextIndex = (currentIndex + 1) % slides.length; 1166 showSlide(nextIndex); 1167 } 1168 slides.forEach((slide) => { 1169 const toggleButtons = [...slide.querySelectorAll(".glyph-toggle button")]; 1170 toggleButtons.forEach((button) => { 1171 button.addEventListener("click", () => { 1172 clearAutoToggle(); 1173 setGlyphFeature(slide, button.dataset.state); 1174 }); 1175 }); 1176 setGlyphFeature(slide, "off"); 1177 }); 1178 slideButtons.forEach((button, index) => { 1179 button.addEventListener("click", () => { 1180 showSlide(index); 1181 }); 1182 }); 1183 function start() { 1184 if (intervalId) return; 1185 demo.classList.add("is-active-demo"); 1186 scheduleAutoToggle(); 1187 intervalId = window.setInterval(() => { 1188 if (!isPausedByHover) { 1189 nextSlide(); 1190 } 1191 }, interval); 1192 } 1193 function stop() { 1194 demo.classList.remove("is-active-demo"); 1195 clearAutoToggle(); 1196 if (intervalId) { 1197 window.clearInterval(intervalId); 1198 intervalId = null; 1199 } 1200 } 1201 demo.addEventListener("mouseenter", () => { 1202 isPausedByHover = true; 1203 clearAutoToggle(); 1204 }); 1205 demo.addEventListener("mouseleave", () => { 1206 isPausedByHover = false; 1207 scheduleAutoToggle(); 1208 }); 1209 demo.addEventListener("focusin", () => { 1210 isPausedByHover = true; 1211 clearAutoToggle(); 1212 }); 1213 demo.addEventListener("focusout", () => { 1214 isPausedByHover = false; 1215 scheduleAutoToggle(); 1216 }); 1217 showSlide(0); 1218 return { 1219 element: demo, 1220 start, 1221 stop 1222 }; 1223 } 1224 const widthDemo = document.querySelector(".width-demo"); 1225 const xheightDemo = document.querySelector(".xheight-demo"); 1226 if (widthDemo) { 1227 const demo = initTypeDemo(widthDemo, { 1228 axis: "wdth", 1229 stops: [75, 82, 100], 1230 speed: 0.4, 1231 xhgt: 353, 1232 wght: 400, 1233 wdth: 75 1234 }); 1235 if (demo) featureDemos.push(demo); 1236 } 1237 if (xheightDemo) { 1238 const demo = initTypeDemo(xheightDemo, { 1239 axis: "XHGT", 1240 stops: [353, 574], 1241 speed: 4, 1242 xhgt: 353, 1243 wght: 400, 1244 wdth: 100, 1245 // adjust these if the guide line feels visually off 1246 baselineY: 205, 1247 lowGuideY: 100, 1248 tallGuideY: 35 1249 }); 1250 if (demo) featureDemos.push(demo); 1251 } 1252 const demoObserver = new IntersectionObserver( 1253 (entries) => {
1254 entries.forEach((entry) => { 1255 const matchingDemo = featureDemos.find( 1256 (demo) => demo.element === entry.target 1257 ); 1258 if (!matchingDemo) return; 1259 if (entry.isIntersecting) { 1260 matchingDemo.start(); 1261 } else { 1262 matchingDemo.stop(); 1263 } 1264 }); 1265 }, 1266 { 1267 threshold: 0.35, 1268 rootMargin: "-10% 0px -10% 0px" 1269 } 1270 ); 1271 const paragraphDemo = document.querySelector(".paragraph-demo"); 1272 if (paragraphDemo) { 1273 const demo = initParagraphDemo(paragraphDemo, { 1274 fadeDuration: 600, 1275 holdDuration: 2e3, 1276 states: [ 1277 { 1278 label: "Text", 1279 fontFamily: "'DunbarSeriesVF-mini'", 1280 fontVariationSettings: "var(--DunbarTextRegular_mini)" 1281 }, 1282 { 1283 label: "Low", 1284 fontFamily: "'DunbarSeriesVF-mini'", 1285 fontVariationSettings: "var(--DunbarTextRegularUnadjusted_mini)" 1286 } 1287 ] 1288 }); 1289 if (demo) featureDemos.push(demo); 1290 } 1291 const svgSlideshowDemo = document.querySelector(".svg-slideshow-demo"); 1292 if (svgSlideshowDemo) { 1293 const demo = initSvgSlideshowDemo(svgSlideshowDemo, { 1294 interval: 1400 1295 }); 1296 if (demo) featureDemos.push(demo); 1297 } 1298 const xheightCapsDemo = document.querySelector(".xheight-caps-demo"); 1299 if (xheightCapsDemo) { 1300 const demo = initTypeDemo(xheightCapsDemo, { 1301 axis: "XHGT", 1302 stops: [574, 353], 1303 speed: 4, 1304 xhgt: 574, 1305 wght: 400, 1306 wdth: 100, 1307 baselineY: 205, 1308 lowGuideY: 145, 1309 tallGuideY: 85 1310 }); 1311 if (demo) featureDemos.push(demo); 1312 } 1313 const glyphSlideshowDemo = document.querySelector(".glyph-slideshow-demo"); 1314 if (glyphSlideshowDemo) { 1315 const demo = initGlyphSlideshowDemo(glyphSlideshowDemo, { 1316 interval: 2600, 1317 toggleDelay: 1300 1318 }); 1319 if (demo) featureDemos.push(demo); 1320 } 1321 featureDemos.forEach((demo) => { 1322 demoObserver.observe(demo.element); 1323 }); 1324 window.featureAnimations = { 1325 stopAll() { 1326 featureDemos.forEach((demo) => demo.stop()); 1327 } 1328 }; 1329 }); 1330 1331
1331// dunbar-design-repository/assets/js/timeline.js 1332 document.addEventListener("DOMContentLoaded", () => { 1333 const markers = [...document.querySelectorAll(".timeline-entry-marker[data-target]")]; 1334 const sectionMarkers = [...document.querySelectorAll(".section-marker[data-section-target]")]; 1335 const panels = [...document.querySelectorAll(".timeline-content > .timeline-panel[data-slug]")]; 1336 const sections = [...document.querySelectorAll(".timeline-section[data-slug]")]; 1337 const timelineTester = document.querySelector("#timeline .tester"); 1338 const timelineContent = document.querySelector("#timeline .timeline-content"); 1339 const commitMarkers = [...document.querySelectorAll(".timeline-marker--commit")]; 1340 const mobileTimelineQuery = window.matchMedia("(max-width: 850px)"); 1341 const body = document.body; 1342 const intro = document.getElementById("intro"); 1343 const feature = document.getElementById("feature"); 1344 const article = document.getElementById("article"); 1345 const timeline = document.getElementById("timeline"); 1346 const footer = document.getElementById("footer"); 1347 if (!window.location.hash) { 1348 if ("scrollRestoration" in history) history.scrollRestoration = "manual"; 1349 window.scrollTo({ top: 0, left: 0, behavior: "auto" }); 1350 } 1351 const scrollTopTriggers = [ 1352 ...document.querySelectorAll(".btn-top, #site-title") 1353 ]; 1354 const sectionHashIds = /* @__PURE__ */ new Set(["intro", "feature", "article", "timeline", "colophon"]); 1355 let activeSectionId = ""; 1356 scrollTopTriggers.forEach((trigger) => { 1357 trigger.addEventListener("click", (event) => { 1358 event.preventDefault(); 1359 window.scrollTo({ 1360 top: 0, 1361 behavior: "smooth" 1362 }); 1363 }); 1364 }); 1365 function getActiveSectionHash() { 1366 const hashSectionId = activeSectionId === "footer" ? "colophon" : activeSectionId; 1367 const sectionId = sectionHashIds.has(hashSectionId) ? hashSectionId : "article"; 1368 return sectionHashIds.has(sectionId) ? `#${sectionId}` : "#intro"; 1369 } 1370 function updateSectionHash(sectionId, method = "replaceState") { 1371 const hashSectionId = sectionId === "footer" ? "colophon" : sectionId; 1372 if (!sectionHashIds.has(hashSectionId)) return; 1373 if (document.body.classList.contains("lightbox-open") || document.body.classList.contains("lightbox-open")) return; 1374 const hash = `#${hashSectionId}`; 1375 if (window.location.hash === hash) return; 1376 history[method]("", document.title, hash); 1377 } 1378 function syncSectionHashFromBody() { 1379 const sectionId = [...sectionHashIds].find((id) => body.classList.contains(`section-${id}`)); 1380 if (sectionId) updateSectionHash(sectionId); 1381 } 1382 function setActiveSectionHash(method = "pushState") { 1383 const hash = getActiveSectionHash(); 1384 if (window.location.hash === hash) return; 1385 history[method]("", document.title, hash); 1386 } 1387 if (!markers.length || !panels.length || !sections.length) return; 1388 let introInView = false; 1389 let featureInView = false; 1390 let articleInView = false; 1391 let timelineInView = false; 1392 let footerInView = false; 1393 let sectionStateFrame = null; 1394 let commitScrollFrame = null; 1395 function updateScrollActiveCommit() { 1396 commitScrollFrame = null; 1397 if (!mobileTimelineQuery.matches || !body.classList.contains("timeline-list-in-view")) return; 1398 if (!timelineContent || !commitMarkers.length) return; 1399 const rect = timelineContent.getBoundingClientRect(); 1400 const viewportAnchor = window.innerHeight * 0.45; 1401 const progress = Math.min(1, Math.max(0, (viewportAnchor - rect.top) / Math.max(rect.height, 1))); 1402 const activeIndex = Math.round(progress * (commitMarkers.length - 1)); 1403 commitMarkers.forEach((marker, index) => { 1404 marker.classList.toggle("scroll-active", index === activeIndex); 1405 }); 1406 } 1407 function scheduleScrollActiveCommit() { 1408 if (commitScrollFrame) return; 1409 commitScrollFrame = window.requestAnimationFrame(updateScrollActiveCommit); 1410 } 1411 function setActive(slug) {
1412 markers.forEach((marker) => { 1413 marker.classList.toggle("active", marker.dataset.target === slug); 1414 marker.classList.remove("show-all"); 1415 }); 1416 panels.forEach((panel) => { 1417 panel.classList.toggle("active", panel.dataset.slug === slug); 1418 panel.classList.remove("show-all"); 1419 }); 1420 } 1421 function clearActive() { 1422 markers.forEach((marker) => marker.classList.remove("active")); 1423 panels.forEach((panel) => panel.classList.remove("active")); 1424 } 1425 function showAllTimeline() { 1426 markers.forEach((marker) => { 1427 marker.classList.remove("active"); 1428 marker.classList.add("show-all"); 1429 }); 1430 panels.forEach((panel) => { 1431 panel.classList.remove("active"); 1432 panel.classList.add("show-all"); 1433 }); 1434 } 1435 function clearShowAllTimeline() { 1436 markers.forEach((marker) => marker.classList.remove("show-all")); 1437 panels.forEach((panel) => panel.classList.remove("show-all")); 1438 } 1439 function setSectionNavActive(sectionId) { 1440 sectionMarkers.forEach((marker) => { 1441 marker.classList.toggle( 1442 "active", 1443 marker.dataset.sectionTarget === sectionId 1444 ); 1445 }); 1446 body.classList.toggle("timeline-expanded", sectionId === "timeline"); 1447 body.classList.toggle("timeline-folded", sectionId !== "timeline"); 1448 if (sectionId !== "timeline") { 1449 clearActive(); 1450 } 1451 } 1452 function setMode(mode) { 1453 const sectionId = mode.replace("section-", ""); 1454 if (body.classList.contains(mode)) { 1455 activeSectionId = sectionId; 1456 updateSectionHash(sectionId); 1457 return; 1458 } 1459 body.classList.remove("section-intro", "section-feature", "section-article", "section-timeline", "section-footer"); 1460 if (mode) body.classList.add(mode); 1461 activeSectionId = sectionId; 1462 setSectionNavActive(sectionId); 1463 updateSectionHash(sectionId); 1464 } 1465 function isSectionCurrent(element) { 1466 if (!element) return false; 1467 const rect = element.getBoundingClientRect(); 1468 const targetY = window.innerHeight * 0.45; 1469 return rect.top <= targetY && rect.bottom > targetY; 1470 } 1471 function updateFooterInView() { 1472 if (!footer) { 1473 footerInView = false; 1474 return false; 1475 } 1476 const rect = footer.getBoundingClientRect(); 1477 const enterFooter = rect.top < window.innerHeight && rect.bottom > 0; 1478 const stayInFooter = rect.top < window.innerHeight + 96 && rect.bottom > -96; 1479 footerInView = footerInView ? stayInFooter : enterFooter; 1480 return footerInView; 1481 } 1482 function resolveArticleTimelineMode() { 1483 sectionStateFrame = null; 1484 if (introInView || featureInView) return; 1485 if (updateFooterInView()) { 1486 setMode("section-footer"); 1487 return; 1488 } 1489 timelineInView = isSectionCurrent(timeline); 1490 if (isSectionCurrent(article)) { 1491 setMode("section-article"); 1492 showAllTimeline(); 1493 clearActive(); 1494 return; 1495 } 1496 if (timelineInView) { 1497 setMode("section-timeline"); 1498 } 1499 } 1500 function scheduleArticleTimelineMode() { 1501 if (sectionStateFrame) return; 1502 sectionStateFrame = window.requestAnimationFrame(() => { 1503 resolveArticleTimelineMode(); 1504 syncSectionHashFromBody(); 1505 }); 1506 } 1507 function getFirstTimelineSlug() { 1508 return sections[0]?.dataset.slug; 1509 } 1510 sectionMarkers.forEach((marker) => { 1511 marker.addEventListener("click", (event) => { 1512 event.preventDefault(); 1513 const id = marker.dataset.sectionTarget; 1514 const target = document.getElementById(id); 1515 if (!target) return; 1516 if (id === "timeline") { 1517 setMode("section-timeline"); 1518 const firstSlug = getFirstTimelineSlug(); 1519 if (firstSlug) {
1520 setActive(firstSlug); 1521 scrollToSlug(firstSlug); 1522 } 1523 return; 1524 } 1525 window.scrollTo({ 1526 top: target.offsetTop - 80, 1527 behavior: "smooth" 1528 }); 1529 }); 1530 }); 1531 function scrollToSlug(slug, behavior = "smooth") { 1532 const section = document.querySelector(`.timeline-section[data-slug="${slug}"]`); 1533 if (!section) return; 1534 const top = section.getBoundingClientRect().top + window.scrollY - window.innerHeight * 0.35; 1535 window.scrollTo({ 1536 top, 1537 behavior 1538 }); 1539 } 1540 markers.forEach((marker) => { 1541 marker.addEventListener("click", (event) => { 1542 event.preventDefault(); 1543 event.stopPropagation(); 1544 setMode("section-timeline"); 1545 setActive(marker.dataset.target); 1546 scrollToSlug(marker.dataset.target); 1547 }); 1548 marker.addEventListener("keydown", (event) => { 1549 if (event.key === "Enter" || event.key === " ") { 1550 event.preventDefault(); 1551 setMode("section-timeline");
1552 setActive(marker.dataset.target); 1553 scrollToSlug(marker.dataset.target); 1554 } 1555 }); 1556 }); 1557 const sectionObserver = new IntersectionObserver( 1558 (entries) => { 1559 if (introInView || featureInView || updateFooterInView() || isSectionCurrent(article)) return; 1560 setMode("section-timeline"); 1561 const visible = entries.filter((entry) => entry.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio); 1562 if (!visible.length) return; 1563 clearShowAllTimeline(); 1564 setActive(visible[0].target.dataset.slug); 1565 }, 1566 { 1567 rootMargin: "-35% 0px -35% 0px", 1568 threshold: [0.25, 0.5, 0.75] 1569 } 1570 ); 1571 sections.forEach((section) => sectionObserver.observe(section)); 1572 if (timeline) { 1573 const timelineObserver = new IntersectionObserver( 1574 (entries) => { 1575 if (updateFooterInView()) return; 1576 timelineInView = entries[0].isIntersecting && isSectionCurrent(timeline); 1577 if (timelineInView && !introInView && !featureInView && !footerInView) { 1578 setMode("section-timeline"); 1579 } 1580 }, 1581 { threshold: 0.01 } 1582 ); 1583 timelineObserver.observe(timeline); 1584 } 1585 if (timelineContent) { 1586 const timelineListObserver = new IntersectionObserver( 1587 (entries) => { 1588 const isVisible = entries[0].isIntersecting; 1589 body.classList.toggle("timeline-list-in-view", isVisible); 1590 if (isVisible) { 1591 scheduleScrollActiveCommit(); 1592 } else { 1593 commitMarkers.forEach((marker) => marker.classList.remove("scroll-active")); 1594 } 1595 }, 1596 { threshold: 0.01 } 1597 ); 1598 timelineListObserver.observe(timelineContent); 1599 } 1600 if (timelineTester) { 1601 const timelineTesterObserver = new IntersectionObserver( 1602 (entries) => { 1603 body.classList.toggle("timeline-tester-in-view", entries[0].isIntersecting); 1604 }, 1605 { threshold: 0.01 } 1606 ); 1607 timelineTesterObserver.observe(timelineTester); 1608 } 1609 window.addEventListener("scroll", scheduleArticleTimelineMode, { passive: true }); 1610 window.addEventListener("scroll", scheduleScrollActiveCommit, { passive: true }); 1611 window.addEventListener("resize", scheduleArticleTimelineMode); 1612 window.addEventListener("resize", scheduleScrollActiveCommit); 1613 mobileTimelineQuery.addEventListener("change", () => { 1614 if (!mobileTimelineQuery.matches) { 1615 body.classList.remove("timeline-list-in-view"); 1616 commitMarkers.forEach((marker) => marker.classList.remove("scroll-active")); 1617 } 1618 }); 1619 if (intro) { 1620 const introObserver = new IntersectionObserver( 1621 (entries) => { 1622 introInView = entries[0].isIntersecting; 1623 body.classList.toggle("section-intro", introInView); 1624 if (introInView) { 1625 setMode("section-intro"); 1626 clearShowAllTimeline(); 1627 clearActive(); 1628 } else { 1629 body.classList.remove("section-intro"); 1630 scheduleArticleTimelineMode(); 1631 } 1632 }, 1633 { threshold: 0.01 } 1634 ); 1635 introObserver.observe(intro); 1636 } 1637 if (feature) { 1638 const featureObserver = new IntersectionObserver( 1639 (entries) => { 1640 featureInView = entries[0].isIntersecting; 1641 if (featureInView) { 1642 setMode("section-feature"); 1643 showAllTimeline(); 1644 clearActive(); 1645 } else { 1646 clearShowAllTimeline(); 1647 if (window.featureAnimations) { 1648 window.featureAnimations.stopAll(); 1649 } 1650 scheduleArticleTimelineMode(); 1651 } 1652 }, 1653 { threshold: 0.01 } 1654 ); 1655 featureObserver.observe(feature); 1656 } 1657 if (article) { 1658 const articleObserver = new IntersectionObserver( 1659 (entries) => { 1660 articleInView = entries[0].isIntersecting; 1661 if (articleInView && isSectionCurrent(article)) { 1662 setMode("section-article"); 1663 showAllTimeline(); 1664 clearActive(); 1665 } else { 1666 clearShowAllTimeline(); 1667 } 1668 }, 1669 { threshold: 0.01 } 1670 ); 1671 articleObserver.observe(article); 1672 } 1673 if (footer) { 1674 const footerObserver = new IntersectionObserver( 1675 () => { 1676 if (updateFooterInView()) { 1677 setMode("section-footer"); 1678 return; 1679 } 1680 scheduleArticleTimelineMode(); 1681 }, 1682 { threshold: 0.01 } 1683 ); 1684 footerObserver.observe(footer); 1685 } 1686 function openSectionFromHash() { 1687 const hashId = window.location.hash.replace("#", ""); 1688 const timelineMarker = markers.find((marker) => marker.dataset.target === hashId); 1689 if (timelineMarker) { 1690 setMode("section-timeline");
1691 setActive(hashId); 1692 requestAnimationFrame(() => scrollToSlug(hashId, "auto")); 1693 return; 1694 } 1695 const sectionId = hashId; 1696 if (!sectionHashIds.has(sectionId)) return; 1697 const targetId = sectionId === "colophon" ? "footer" : sectionId; 1698 const target = document.getElementById(targetId); 1699 if (!target) return; 1700 setMode(`section-${targetId}`); 1701 window.scrollTo({ 1702 top: targetId === "timeline" ? target.offsetTop : target.offsetTop - 80, 1703 behavior: "auto" 1704 }); 1705 } 1706 function handleHashState() { 1707 openSectionFromHash(); 1708 } 1709 window.addEventListener("hashchange", handleHashState); 1710 window.addEventListener("popstate", handleHashState); 1711 handleHashState(); 1712 }); 1713 1714
1714// dunbar-design-repository/assets/js/main.js 1715 console.log("main.js loaded"); 1716 window.addEventListener("load", () => { 1717 }); 1718})(); 1719//# sourceMappingURL=bundle.js.map
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.