diff --git a/suites-experimental/scrollytelling/DESIGN.md b/suites-experimental/scrollytelling/DESIGN.md new file mode 100644 index 000000000..e72efe92c --- /dev/null +++ b/suites-experimental/scrollytelling/DESIGN.md @@ -0,0 +1,41 @@ +# Scrollytelling Workload: Layout & Design Decision Document + +This document records the concise design decisions and architectural rules for the **Project A-101: The Homestead Dossier** scrollytelling benchmark workload in Speedometer. + +## Executive Design Decision + +The workload models a 1950s/1960s mid-century architectural journal, technical drafting blueprint, and precision print instruction manual (such as the classic 1968 Canon Autozoom 518 instruction manual). To maintain visual harmony and benchmark performance consistency, all layout components, CSS styles, Canvas procedural renderings, and SVG overlays must follow these foundational rules: + +### Aesthetic Reference: Inverted 1968 Canon Autozoom 518 Graphic & Layout Style + +Our visual identity adapts the utilitarian yet refined late-1960s Japanese industrial technical print design of the Canon Autozoom 518 manual, inverted for a negative dark-mode drafting void (`#000000` pure black background, `#111111` drafting canvas, `#ffffff` primary white text and linework). The typography establishes a strict hierarchy combining bold, condensed mid-century display lettering with clean, airy geometric sans-serifs (resembling Futura/Helvetica) and typewriter monospace engineering tables. Section headers and step sequences (`1`, `2`, `3`) leap from the dark canvas in high-contrast **crimson-ruby accent red** (`#D12B3E`), paired with crisp red leader-line callouts (`---→`) targeting mechanical and structural components. The layout balances technical precision and human warmth through asymmetric grids, slanted geometric shapes, rounded rectangular diagram frames, and high-contrast duotone architectural line art. + +### 1. 1950s Black & White Blueprint / Planning Design with Organic Watercolor Accents + +- **Pure Black Background & B&W UI**: The UI theme is negative blueprint drafting (light on dark void). Body background is `#000000`. Inactive cards and drafting canvas are `#111111`, active cards are `#0a0a0a`. Primary drafting ink and typography is `#ffffff`. Secondary lines are `#cccccc` and `#777777`. +- **Organic Watercolor Accents**: Watercolor brush assets (`bw_watercolor_texture.webp`, `watercolor_stroke_*.webp`) are allowed exclusively as gradual animation coloring elements (revealed via circular focus, slide wipe, or fade wash as scroll progress advances). +- **Allowed Backgrounds**: Only B&W topographic maps (`topo_map_*.webp`) and planning paper dot grids (`planning_paper_dot_grid.webp`) blended with `screen` / `color-dodge` / `overlay` (and inverted where necessary) to render as white linework on black. + +### 2. Slightly Slanted Borders & Geometric Shapes + +- Containers, headers, step cards, title blocks, and callout badges must be enclosed in slightly slanted geometric shapes (-2.0° to +2.0° tilt) with asymmetric polygon borders or trapezoidal outlines. (Note: Paragraph text is explicitly exempt from slanting and must remain vertical). + +### 3. Irregular & Independent Drop Shadows (Shadow Angle ≠ Box Angle) + +- **Irregularity**: Every section, card, title block, and text badge must cast a drop shadow at an irregular distance and angle (varying between bottom-right, bottom-left, top-right, and top-left across stages). +- **Independent Shadow Tilt**: A drop shadow must NEVER have the same rotation angle as the box casting it. + - In DOM/CSS, drop shadows are constructed as separate `::before` or `::after` pseudo-elements (`background: var(--shadow-solid, #222222)`) with an independent rotation variable (`--shadow-tilt`) that differs from the box rotation (`--graphic-tilt` or `--step-tilt`). + - In Canvas and SVG, drop shadows are rendered as distinct underlying geometry rotated independently from the foreground box. + +### 4. Vertical Standard Paragraph Text (No Separate Boxes or Animations) + +- **Standard Text Styling & Vertical Alignment**: Paragraph text (`.step-paragraph-item`, `.step-description`) must NEVER be broken into visually separate boxes, cards, or tabs. Whether a stage has a single paragraph or multiple paragraphs, all paragraph text must use standard text styling with clean vertical alignment (0° rotation, straight vertical layout). +- **Zero Layout Animations**: Paragraph text must have 0 layout animations and 0 transforms (no translating on hover/active, no scaling, no rotating, and no drop shadows or background boxes). + +### 5. Three-Tier Typography Hierarchy + +1. **Display / Titles / Badges**: Bold mid-century industrial display fonts (`Impact`, `Arial Black`, `Futura`). +2. **Narrative Body Text / Paragraphs**: Clean, effortlessly readable standard sans-serif / system fonts (`system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`) with comfortable line height (`1.65`) and normal weight (`400`) for all paragraph text (`.step-description`). +3. **Technical Specifications & Metadata**: Typewriter monospace fonts (`Courier New`, `Courier`, monospace) exclusively for engineering specifications (`.step-specs`), title blocks, leader callouts, diorama tab headers (`.paragraph-mech-tab`), and table of contents items. + +- All procedural line art features hand-drafted jitter and cross-hatching in white/cream/light gray tones. diff --git a/suites-experimental/scrollytelling/README.md b/suites-experimental/scrollytelling/README.md new file mode 100644 index 000000000..1c08c887f --- /dev/null +++ b/suites-experimental/scrollytelling/README.md @@ -0,0 +1,23 @@ +# Scrollytelling Workload (Project A-101: The Homestead Dossier) + +An experimental scrollytelling benchmark workload for Speedometer that evaluates browser performance during interactive scroll-driven visual storytelling. + +## Overview + +This workload models a 1950s/1960s mid-century architectural dossier and technical blueprint (`#000000` pure black background with high-contrast B&W geometry and procedural line art). As the user scrolls through 17 narrative stages covering American homestead evolution and geotechnical engineering analysis, the benchmark measures layout, Canvas 2D rendering, SVG overlay positioning, and DOM interaction performance. + +## Architecture & Mechanics + +- **Scroll Engine**: Uses `scrollama` to track stage progression and trigger deterministic scroll animations (`src/engine-scrollama.js`). +- **Graphics & Rendering**: Procedural Canvas 2D linework, monochrome cross-hatching, and organic watercolor animation reveals (`src/graphics.js`). +- **Layout & DOM**: 17 stages with 5 narrative phases each, featuring sticky SVG leader callouts, slanted borders, and upright body typography (`src/content.js`, `src/main.js`, `src/styles.css`). +- **Benchmark Determinism**: Intercepts `window.requestAnimationFrame` to synchronously flush visual states before `page.layout()` measurements (`index.html`). + +## Building the Workload + +To build the bundle using Rollup: + +```bash +npm install +npm run build +``` diff --git a/suites-experimental/scrollytelling/STYLE.md b/suites-experimental/scrollytelling/STYLE.md new file mode 100644 index 000000000..2e230cd97 --- /dev/null +++ b/suites-experimental/scrollytelling/STYLE.md @@ -0,0 +1,79 @@ +# Speedometer Scrollytelling Workload — Style & Architectural Requirements + +When modifying or extending the `suites-experimental/scrollytelling` workload, all engineers, agents, and subagents must strictly adhere to the following 9 architectural and graphic design rules to maintain visual harmony and benchmark timing precision. + +--- + +## 1. Core Aesthetic & Palette: 1950s Black & White Blueprint / Planning Design + +- **Background Texture**: Pure black drafting background (`#000000` body background, `#111111` drafting canvas/inactive cards, `#0a0a0a` active panels). +- **Allowed Background Assets**: Only use B&W topographic maps (`topo_map_*.webp`) and our standalone tiling SVG drafting grid (`drafting_grid_tiling.svg`) blended with `screen` / `color-dodge` / `overlay` (and inverted where necessary) so linework appears crisp white/light gray on black. + +--- + +## 2. Strict Black & White Geometry & Ink Palette (Zero Dark-on-Dark Text) + +- All structural wireframes, text, cards, borders, badges, and procedural line art remain strictly high-contrast black and white. +- **Primary Text & Drawing Ink**: `#ffffff` (pure white) or `var(--ink-primary)`. +- **Secondary Lines & Hatching**: `#cccccc` and `#777777` or `var(--ink-secondary)`. +- **Active Highlights & Cards**: `#0a0a0a` (deep black/slate) or `#000000` (pure black) backgrounds with `#ffffff` text and borders. +- **NEVER** use dark or muted text over dark gray/dark backgrounds. Every label, subtitle, specification, and TOC link must remain bright and legible at all times. + +--- + +## 3. Slightly Slanted Borders & Geometric Shapes + +- Every layout container, header, step card (`.step`), drawing frame (`.graphic-sticky-wrapper`), timeline point (`.timeline-point`), and SVG/Canvas title block or callout badge must feature intentional, slightly slanted geometric angles (`-2.0°` to `+2.0°`) and asymmetric outlines. +- **Exemption**: Standard paragraph body text (`.step-paragraph-item`, `.step-description`) is explicitly exempt from slanting and must remain vertical. + +--- + +## 4. Independent & Irregular Drop Shadow Angles (The "Collage / Diorama" Rule) + +- **Irregularity per Element**: Drop shadow distances and offset angles must vary irregularly per text block, per animation stage, and per card. +- **Angle Independence (`Shadow Angle != Box Angle`)**: A drop shadow must **never** share the exact same rotation angle as the box casting it. + - **In CSS**: Implement shadows using positioned pseudo-elements (`::before` or `::after` with `background: var(--shadow-solid, #222222); z-index: -1;`) rotated at a slightly different angle (`--shadow-tilt`) than the box itself (`--step-tilt`, `--tab-tilt`). + - **In Canvas & SVG**: Draw drop shadows as separate underlying polygons or rectangles with an independent rotation transformation before drawing the foreground element. + +--- + +## 5. Three-Tier Typography Hierarchy + +1. **Display / Titles / Badges**: Bold mid-century industrial display fonts (`Impact`, `Arial Black`, `Futura`). +2. **Narrative Body Text / Paragraphs**: Clean, effortlessly readable standard sans-serif / system fonts (`system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`) with comfortable line height (`1.65`) and normal weight (`400`). +3. **Technical Specifications & Metadata**: Typewriter monospace fonts (`Courier New`, `Courier`, monospace) exclusively for engineering specifications (`.step-specs`), title blocks, leader callouts, Table of Contents items, and bottom timeline year markers. + +--- + +## 6. Vertical Standard Paragraph Text & 100% Full Opacity at All Times + +- **Standard Text Styling & Vertical Alignment**: Paragraph text must never be broken into visually separate boxes, cards, or tabs. All paragraph text must use standard text styling with clean vertical alignment (`0°` rotation, straight vertical layout). +- **100% Full Opacity Guarantee**: Section cards (`.step`) and all text elements (`.step-meta`, `.step-title`, `.step-paragraph-item`, `.step-specs`, `.spec-label`, `.toc-item`, `.toc-num`, `.timeline-point`) must **never be grayed out or dimmed** when inactive. Section text must maintain `opacity: 1 !important;` and bright white/light-gray text colors at all times. + +--- + +## 7. Zero CSS Transitions / Layout Animations (Speedometer Benchmark Accuracy) + +- In the Speedometer synchronous test harness, `window.requestAnimationFrame` is intercepted and flushed synchronously before measuring `page.layout()`. +- When CSS transitions or animations are present, DOM elements get trapped in asynchronous intermediate states, corrupting benchmark frame timing and layout snapshots. +- **Rule**: All CSS transitions and inline JS animations must remain disabled globally (`transition: none !important; animation: none !important;`). All state updates (active card highlighting, TOC collapsing, timeline visibility) must occur instantaneously and synchronously. + +--- + +## 8. Dynamic Animation-Specific Procedural Color Highlights (Zero Permanent Brush Backgrounds) + +- **No Static Bitmap Brush Backgrounds**: Permanent watercolor brush background textures (`bw_watercolor_texture.webp`, `wcWash`, `wc1`, `wc2`, `wc3`) must never be rendered over stage backgrounds or animations. +- **Dynamic Procedural Highlights**: Color accents are permitted exclusively as dynamic, animation-specific HTML5 Canvas 2D fills and gradients that animate from `0.0` as scroll progress advances (e.g., foliage emerald canopies, oak log carpentry, aquifer sapphire water tables, kiln-fired red brick terracotta walls, golden sunlight rays, hydraulic sluice cyan water, laser transit neon beams, and geothermal radiant convection loops). + +--- + +## 9. Tiling Drafting Grid Asset & Interactive Navigation Overlays + +- **Tiling Grid Asset**: Use our standalone SVG grid asset (`drafting_grid_tiling.svg` with large ~1cm intersection dots and fine ~1mm dotted connecting lines) tiled via CSS `background-repeat: repeat` and Canvas 2D `IMAGE_SOURCES.grid`. +- **Collapsible TOC & Bottom Timeline Overlays**: Both navigation overlays must remain hidden at the top of the page (`.is-hidden`) and appear synchronously without CSS transitions after scrolling past the header title threshold (~300px). + +--- + +## Onboarding & Best Practices: The `/learn` Command + +When onboarding to or modifying this scrollytelling benchmark workload, we strongly recommend that all engineers, agents, and subagents use the `/learn` slash command to familiarize themselves with these 9 architectural rules, benchmark constraints, and styling guardrails before making code changes or submitting pull requests. diff --git a/suites-experimental/scrollytelling/dist/app.js b/suites-experimental/scrollytelling/dist/app.js new file mode 100644 index 000000000..45673cb6d --- /dev/null +++ b/suites-experimental/scrollytelling/dist/app.js @@ -0,0 +1 @@ +const e=[{id:"step-1",year:"1780",title:"Early Settlement",subtitle:"Timber Clearing & Rudimentary Shelter",paragraphs:["The rugged New England frontier in 1780 presented a formidable wilderness of virgin old-growth hemlock, white oak, and sugar maple. Early homesteaders arrived with rudimentary hand tools, clearing dense canopy and wrestling with glacial till to carve out a small, survivable clearing in the forest. Every square foot of cleared land required immense physical labor, transforming the untamed woodland into the beginnings of a permanent agrarian settlement.","Construction began immediately by felling massive timber trees to fashion hand-hewn logs. Broadaxes and adzes were used to square the timbers, which were carefully notched and interlocked at the corners with a dovetail log corner notch without iron nails. Mud, clay, and dried woodland moss were painstakingly rammed into the chinks between logs to create a windbreak against the punishing sub-zero winters and gale-force winds.","Survival during these formative decades relied entirely on localized natural resources. A shallow dug well tapped the immediate surface water table, while a massive dry-laid fieldstone hearth section served as the sole source of thermal heating, illumination, and daily cooking. This initial 320-square-foot cabin represented a fragile human foothold in an otherwise untamed ecological landscape.","The perimeter of the clearing was delineated by split-rail worm fences crafted from durable cedar and chestnut logs. These interlocking zigzag barriers kept wild grazing animals away from early subsistence vegetable patches without requiring post-hole digging in the rocky, frost-heaved soil.","Throughout the harsh winter months, the central stone hearth acted as the structural and social nucleus of the homestead. Heat radiated from the massive fieldstone chimney stack, warming the sleeping loft above and preserving dried herbs, smoked provisions, and root vegetables stored in the crawlspace below."],description:"The rugged New England frontier in 1780 presented a formidable wilderness of virgin old-growth hemlock, white oak, and sugar maple. Early homesteaders arrived with rudimentary hand tools, clearing dense canopy and wrestling with glacial till to carve out a small, survivable clearing in the forest. Every square foot of cleared land required immense physical labor, transforming the untamed woodland into the beginnings of a permanent agrarian settlement.\n\nConstruction began immediately by felling massive timber trees to fashion hand-hewn logs. Broadaxes and adzes were used to square the timbers, which were carefully notched and interlocked at the corners with a dovetail log corner notch without iron nails. Mud, clay, and dried woodland moss were painstakingly rammed into the chinks between logs to create a windbreak against the punishing sub-zero winters and gale-force winds.\n\nSurvival during these formative decades relied entirely on localized natural resources. A shallow dug well tapped the immediate surface water table, while a massive dry-laid fieldstone hearth section served as the sole source of thermal heating, illumination, and daily cooking. This initial 320-square-foot cabin represented a fragile human foothold in an otherwise untamed ecological landscape.",specs:[{label:"Material",value:"Old-Growth Timber"},{label:"Foundation",value:"Direct Soil / Log Sills"},{label:"Tools",value:"Hand Axe, Broadaxe, Adze"},{label:"Footprint",value:"320 sq. ft."}]},{id:"step-2",year:"1795",title:"Mortise-and-Tenon Framing",subtitle:"Heavy Timber Joinery & Structural Rigidity",paragraphs:["As the initial settlement matured, builders sought greater structural longevity and architectural volume by transitioning from rough log construction to sophisticated heavy timber framing. Seasoned white oak and eastern white pine were selected for their exceptional structural rigidity and resistance to decay across multi-story spans.","Master housewrights employed precision hand tools including T-handled auger bits for mortise boring deep circular cavities, followed by heavy framing chisels to clear rectangular mortise slots. Post and girt connections were painstakingly scribed and fitted on the ground to ensure exact geometric alignment before erection.","Each structural joint relied on precision joinery rather than metal fasteners. Tenons carved at the ends of connecting girts and summer beams slid snugly into corresponding mortise pockets, creating a robust framework capable of supporting heavy second-story live loads and resisting lateral wind shear.","To lock the joinery permanently in place, master craftsmen utilized draw-boring techniques with tapered wooden treenail pegs. Offset holes were drilled through the mortise and tenon, and seasoned hickory pegs were driven home, pulling the joint tightly together under tremendous mechanical tension.","The raising of the timber bents was a communal undertaking requiring block and tackle pulleys, wooden pikes, and coordinated manpower. Once raised and pegged, the self-supporting timber cage stood braced against New England storm gales, forming an enduring skeleton that would endure centuries."],description:"As the initial settlement matured, builders sought greater structural longevity and architectural volume by transitioning from rough log construction to sophisticated heavy timber framing. Seasoned white oak and eastern white pine were selected for their exceptional structural rigidity and resistance to decay across multi-story spans.\n\nMaster housewrights employed precision hand tools including T-handled auger bits for mortise boring deep circular cavities, followed by heavy framing chisels to clear rectangular mortise slots. Post and girt connections were painstakingly scribed and fitted on the ground to ensure exact geometric alignment before erection.\n\nEach structural joint relied on precision joinery rather than metal fasteners. Tenons carved at the ends of connecting girts and summer beams slid snugly into corresponding mortise pockets, creating a robust framework capable of supporting heavy second-story live loads and resisting lateral wind shear.",specs:[{label:"Joinery",value:"Mortise-and-Tenon"},{label:"Fasteners",value:"Draw-Bored Treenails"},{label:"Tooling",value:"T-Handle Auger Bit"},{label:"Timber",value:"Seasoned White Oak"}]},{id:"step-3",year:"1810",title:"Geology & Landscape",subtitle:"Bedrock Adaptation & Natural Drainage",paragraphs:["As agricultural operations expanded, builders sought permanent subterranean storage by engaging directly with the site's underlying geology. Deep excavations for enlarged root cellars exposed dense veins of metamorphic granite bedrock and impervious clay subsoils, requiring structural adaptation rather than resistance.","Dry-laid fieldstone footings were laboriously assembled right atop the granite glacial erratic footing ledge cleared from surrounding arable meadows. Master stonemasons keyed these massive granite foundations directly onto the bedrock shelf without mortar, relying on gravity and precise geometric friction to resist heave from seasonal freeze-thaw cycles and heavy frost penetration.","Subterranean cutaway profiling revealed the complex stratification of topsoil, glacial till, and bedrock shelves. By understanding these natural geological horizons, builders anchored the bearing walls directly over structural ledges capable of distributing multi-ton loads without differential settlement.","To protect timber sills from rot and groundwater inundation, homesteaders engineered gravity perimeter trench drainage around foundation walls. By mapping the natural topography and subterranean hydrological flow, they diverted seasonal runoff away from the cellar walls, establishing an enduring relationship between the domestic footprint and the natural contour of the land.","The cellar floor itself was stepped and leveled directly into the granite shelf, creating cool, stable thermal zones (+420m ASL elevation) where root crops and preserved provisions remained at a steady 50°F year-round, shielded from surface frost."],description:"As agricultural operations expanded, builders sought permanent subterranean storage by engaging directly with the site's underlying geology. Deep excavations for enlarged root cellars exposed dense veins of metamorphic granite bedrock and impervious clay subsoils, requiring structural adaptation rather than resistance.\n\nDry-laid fieldstone footings were laboriously assembled right atop the granite glacial erratic footing ledge cleared from surrounding arable meadows. Master stonemasons keyed these massive granite foundations directly onto the bedrock shelf without mortar, relying on gravity and precise geometric friction to resist heave from seasonal freeze-thaw cycles and heavy frost penetration.\n\nTo protect timber sills from rot and groundwater inundation, homesteaders engineered gravity perimeter trench drainage around foundation walls. By mapping the natural topography and subterranean hydrological flow, they diverted seasonal runoff away from the cellar walls, establishing an enduring relationship between the domestic footprint and the natural contour of the land.",specs:[{label:"Substratum",value:"Granite Bedrock"},{label:"Footing",value:"Dry-Laid Fieldstone"},{label:"Drainage",value:"Gravity Trenching"},{label:"Elevation",value:"+420m ASL"}]},{id:"step-4",year:"1835",title:"Watercourse & Millwright",subtitle:"Hydraulic Power & Mechanical Transmission",paragraphs:["The presence of a perennial stream running along the valley contour prompted the construction of a mechanized watercourse and timber milling facility. By damming the brook and excavating a regulated millrace, homesteaders harnessed hydraulic potential to replace exhaustive manual sawing with automated kinetic power.","At the heart of the hydraulic works stood a wooden overshot waterwheel bucket profile engineered by specialized millwrights. Water delivered via an elevated wooden flume poured into carefully angled buckets, maximizing gravitational torque and rotational momentum across the central oak axle.","Flume flow and water velocity were controlled precisely through vertical iron-bound sluice gates. By raising or lowering the weir gate with mechanical rack-and-pinion levers, the miller adjusted hydraulic output in real-time to match seasonal water volume and sawing load variations.","Rotational kinetic energy from the waterwheel was transmitted inland through an intricate train featuring a trundle lantern crown gear and heavy wooden drive shafts. Masterfully carved applewood cog teeth meshed smoothly to step up rotational speed and redirect torque 90 degrees upward into the mill floor.","This mechanical gearing powered vertical reciprocating saw blades and heavy rotating granite millstones. The automated output transformed local forestry and grain harvesting, multiplying productivity twelvefold and turning the homestead into a regional agricultural hub."],description:"The presence of a perennial stream running along the valley contour prompted the construction of a mechanized watercourse and timber milling facility. By damming the brook and excavating a regulated millrace, homesteaders harnessed hydraulic potential to replace exhaustive manual sawing with automated kinetic power.\n\nAt the heart of the hydraulic works stood a wooden overshot waterwheel bucket profile engineered by specialized millwrights. Water delivered via an elevated wooden flume poured into carefully angled buckets, maximizing gravitational torque and rotational momentum across the central oak axle.\n\nRotational kinetic energy from the waterwheel was transmitted inland through an intricate train featuring a trundle lantern crown gear and heavy wooden drive shafts. Masterfully carved applewood cog teeth meshed smoothly to step up rotational speed and redirect torque 90 degrees upward into the mill floor.",specs:[{label:"Power Source",value:"16-Blade Overshot Wheel"},{label:"Transmission",value:"Trundle Crown Gears"},{label:"Head Height",value:"14-Foot Drop"},{label:"Output",value:"12 Horsepower Equiv."}]},{id:"step-5",year:"1860",title:"Industrial Revolution",subtitle:"Steam-Milled Lumber & Iron Reinforcement",paragraphs:["By the mid-nineteenth century, the arrival of steam-powered sawmills and regional rail networks revolutionized rural building practices. Heavy post-and-beam timber framing gave way to balloon framing, utilizing standardized, steam-milled dimensioned lumber attached with mass-produced cut wire nails. This industrial shift accelerated construction speed and allowed for more flexible interior layouts.","Standardized 2x4 framing members arrived by rail, where each steam-milled 2x4 balloon framing stud on sill plate rose continuously from the foundation all the way to the roof rafters. This continuous framing method allowed for rapid assembly by smaller work crews using machine-cut nails, transforming residential construction from a specialized craft into an industrial assembly process.","Industrial metallurgy provided significant structural reinforcement and spatial expansion. Wrought iron tie-rods, cast-iron lintels, and masonry reinforcements enabled taller, multi-story additions with expansive window openings that flooded interior parlors with natural light and improved ventilation across the entire homestead.","Across long floor spans, a wrought iron tie-rod with turnbuckle was anchored through exterior plates. By tightening these metal rods, builders counteracted outward structural spreading and locked the framing rigidly under heavy second-story loads.","Technological modernization extended deep into homestead infrastructure with the introduction of mechanized water pumping systems. Hand-driven and steam-powered mechanical pumps drew pristine water from deep aquifers directly into indoor copper plumbing, fundamentally transforming domestic labor, sanitation hygiene, and the daily rhythm of agrarian life."],description:"By the mid-nineteenth century, the arrival of steam-powered sawmills and regional rail networks revolutionized rural building practices. Heavy post-and-beam timber framing gave way to balloon framing, utilizing standardized, steam-milled dimensioned lumber attached with mass-produced cut wire nails. This industrial shift accelerated construction speed and allowed for more flexible interior layouts.\n\nIndustrial metallurgy provided significant structural reinforcement and spatial expansion. Wrought iron tie-rods, cast-iron lintels, and masonry reinforcements enabled taller, multi-story additions with expansive window openings that flooded interior parlors with natural light and improved ventilation across the entire homestead.\n\nTechnological modernization extended deep into homestead infrastructure with the introduction of mechanized water pumping systems. Hand-driven and steam-powered mechanical pumps drew pristine water from deep aquifers directly into indoor copper plumbing, fundamentally transforming domestic labor, sanitation hygiene, and the daily rhythm of agrarian life.",specs:[{label:"Framing",value:"Balloon Framing (2x4)"},{label:"Reinforcement",value:"Wrought Iron Tie-Rods"},{label:"Utilities",value:"Steam Mechanized Pump"},{label:"Footprint",value:"1,200 sq. ft."}]},{id:"step-6",year:"1885",title:"Victorian Expansion",subtitle:"Ornamental Millwork & Gas Lighting Infrastructure",paragraphs:["As agricultural wealth reached its zenith in the late nineteenth century, the homestead underwent a decorative Victorian expansion. Aesthetic tastes shifted toward ornate Queen Anne detailing, prominent wraparound verandas, and complex multi-gabled roof profiles that expressed domestic pride and craftsmanship.","High-speed steam-powered spindle lathes in regional millwork shops produced intricate decorative components, such as a spindle lathe turned porch baluster profile. Elaborate turned balusters, gingerbread gable scrollwork, and chamfered porch columns were shipped by rail and assembled to transform the exterior facade with rich architectural shadow lines.","The exterior skin was enhanced with patterned fish-scale cedar shingles, decorative frieze boards, and projected bay windows. These multi-faceted fenestrations captured morning and afternoon sunlight while creating elegant interior alcoves for parlor seating and botanical conservatories.","Interior modernization took a giant leap forward with the installation of a centralized gas illumination network fed by a threaded black-iron gas pipe manifold. Heavy threaded black-iron gas pipes were plumbed through basement walls and floor cavities, distributing fuel safely to wall-mounted brass sconces and overhead chandeliers.","The soft glow of gas lighting extended active domestic life far beyond twilight hours. Combined with ornamental plaster ceiling medallions and embossed wallcoverings, the homestead achieved an unprecedented level of interior refinement and technological comfort."],description:"As agricultural wealth reached its zenith in the late nineteenth century, the homestead underwent a decorative Victorian expansion. Aesthetic tastes shifted toward ornate Queen Anne detailing, prominent wraparound verandas, and complex multi-gabled roof profiles that expressed domestic pride and craftsmanship.\n\nHigh-speed steam-powered spindle lathes in regional millwork shops produced intricate decorative components, such as a spindle lathe turned porch baluster profile. Elaborate turned balusters, gingerbread gable scrollwork, and chamfered porch columns were shipped by rail and assembled to transform the exterior facade with rich architectural shadow lines.\n\nInterior modernization took a giant leap forward with the installation of a centralized gas illumination network fed by a threaded black-iron gas pipe manifold. Heavy threaded black-iron gas pipes were plumbed through basement walls and floor cavities, distributing fuel safely to wall-mounted brass sconces and overhead chandeliers.",specs:[{label:"Millwork",value:"Spindle Lathe Balusters"},{label:"Lighting",value:"Black-Iron Gas Manifold"},{label:"Siding",value:"Fish-Scale Shingles"},{label:"Style",value:"Victorian / Queen Anne"}]},{id:"step-7",year:"1900",title:"Gradual Farm Improvements",subtitle:"Agrarian Complex & Stone Boundary Walls",paragraphs:["At the turn of the twentieth century, decades of agricultural prosperity culminated in the transformation of the isolated homestead into a sophisticated, multi-building agrarian complex. The domestic dwelling was flanked by towering timber frame barns, specialized granaries, and livestock outbuildings designed for maximum operational efficiency.","Miles of dry-stone boundary walls, built from generations of frost-heaved fieldstones, crisscrossed the rolling landscape where a dry-stone boundary wall with pollinator cavity demarcated pasture lands. Master stonemasons assembled these walls without mortar, deliberately incorporating internal gaps for wildlife.","These enduring stone enclosures not only organized the agrarian layout and contained grazing herds, but also created vital microhabitats for native small mammals, songbirds, and beneficial solitary bees that pollinated the adjacent fruit orchards.","Water management reached a new level of sophistication through structured irrigation channels regulated by an adjustable irrigation sluice weir gate. Precision wooden weir gates allowed farmers to divert exact volumes of stream water into gravity-fed orchard furrows.","By harnessing and directing nearby perennial streams through engineered granite culverts, farmers established thriving heirloom orchards and expansive grain fields, sustaining a resilient, self-sufficient rural economy across 45 cultivated acres."],description:"At the turn of the twentieth century, decades of agricultural prosperity culminated in the transformation of the isolated homestead into a sophisticated, multi-building agrarian complex. The domestic dwelling was flanked by towering timber frame barns, specialized granaries, and livestock outbuildings designed for maximum operational efficiency.\n\nMiles of dry-stone boundary walls, built from generations of frost-heaved fieldstones, crisscrossed the rolling landscape where a dry-stone boundary wall with pollinator cavity demarcated pasture lands. Master stonemasons assembled these walls without mortar, deliberately incorporating internal gaps for wildlife.\n\nWater management reached a new level of sophistication through structured irrigation channels regulated by an adjustable irrigation sluice weir gate. Precision wooden weir gates allowed farmers to divert exact volumes of stream water into gravity-fed orchard furrows.",specs:[{label:"Outbuildings",value:"Timber Barn & Granary"},{label:"Enclosure",value:"Dry-Stone Boundary Walls"},{label:"Irrigation",value:"Sluice Weir Gates"},{label:"Acreage",value:"45 Acres Cultivated"}]},{id:"step-8",year:"1925",title:"Electrification & Heating",subtitle:"Knob-and-Tube Wiring & Hydronic Radiators",paragraphs:["The arrival of rural electrification in 1925 marked one of the most profound technological milestones in the homestead's history. Power lines strung from regional hydroelectric generators brought clean, continuous electrical current right to the farmhouse service entrance.","Electricians routed copper conductors through ceiling and floor joists supported by each porcelain knob-and-tube insulator. These glazed ceramic tubes and cleats maintained safe physical separation between live wires and dry wooden framing, preventing electrical spark arcs while powering overhead lighting circuits.","In tandem with electrical illumination, central heating replaced scattered wood-burning parlor stoves. A high-capacity coal-fired boiler installed in the cellar circulated heated water through closed-loop iron piping to cast-iron hot water radiators positioned under windows throughout the house.","The fluted, multi-column shape of each cast-iron hot water radiator fin profile maximized surface area for convective air currents and gentle thermal radiation. This hydronic system delivered draft-free, uniform warmth across every room, stabilizing interior temperatures during harsh winter freezes.","On the exterior clapboard siding near the service entry, an external watt-hour induction meter disk spun continuously inside its glass enclosure. This spinning electromechanical disc tracked cumulative power usage, connecting the rural dwelling directly to the modern energy economy."],description:"The arrival of rural electrification in 1925 marked one of the most profound technological milestones in the homestead's history. Power lines strung from regional hydroelectric generators brought clean, continuous electrical current right to the farmhouse service entrance.\n\nElectricians routed copper conductors through ceiling and floor joists supported by each porcelain knob-and-tube insulator. These glazed ceramic tubes and cleats maintained safe physical separation between live wires and dry wooden framing, preventing electrical spark arcs while powering overhead lighting circuits.\n\nThe fluted, multi-column shape of each cast-iron hot water radiator fin profile maximized surface area for convective air currents and gentle thermal radiation. This hydronic system delivered draft-free, uniform warmth across every room, stabilizing interior temperatures during harsh winter freezes.",specs:[{label:"Electrical",value:"Knob-and-Tube Wiring"},{label:"Heating",value:"Hydronic Cast Radiators"},{label:"Metering",value:"Spinning Induction Disk"},{label:"Voltage",value:"110V AC Single-Phase"}]},{id:"step-9",year:"1950",title:"Decline & Abandonment",subtitle:"Weathering, Roof Sag & Botanical Reclamation",paragraphs:["Following post-war economic shifts and the rapid industrialization of agriculture, the historic homestead experienced a profound transition as cultivation ceased and human occupants departed. Left without regular maintenance, the structure entered a prolonged period of architectural weathering and environmental exposure.","Winter freeze-thaw cycles and heavy snow accumulation initiated structural compromise across the roofline, visible in weathered timber siding erosion & rafter sag curve. Untreated timber rafters sagged downward under persistent moisture loads, creating a pronounced deflection that dislodged slate shingles from their battens.","On the exterior walls, decades of wind-driven rain caused severe grain erosion across the cedar siding. Lime mortar joints between fieldstones leached away under acidic rainfall, leaving exterior foundation walls vulnerable to moisture intrusion and differential frost settlement.","In the absence of human intervention, ecological succession rapidly reclaimed the built environment as a pioneer tree root masonry fracture formed along the footing. Pioneer saplings and sugar maples took root in the accumulated leaf litter along the foundation, driving sturdy roots into masonry joints.","Wild grapevines, poison ivy, and Virginia creeper climbed the siding, enveloping cracked windowpanes and framing bents in a dense botanical canopy. The decaying architecture slowly merged back into the surrounding forest ecosystem, entering a state of romantic structural repose."],description:"Following post-war economic shifts and the rapid industrialization of agriculture, the historic homestead experienced a profound transition as cultivation ceased and human occupants departed. Left without regular maintenance, the structure entered a prolonged period of architectural weathering and environmental exposure.\n\nWinter freeze-thaw cycles and heavy snow accumulation initiated structural compromise across the roofline, visible in weathered timber siding erosion & rafter sag curve. Untreated timber rafters sagged downward under persistent moisture loads, creating a pronounced deflection that dislodged slate shingles from their battens.\n\nIn the absence of human intervention, ecological succession rapidly reclaimed the built environment as a pioneer tree root masonry fracture formed along the footing. Pioneer saplings and sugar maples took root in the accumulated leaf litter along the foundation, driving sturdy roots into masonry joints.",specs:[{label:"Structural State",value:"Compromised / Rafter Sag"},{label:"Weathering",value:"Severe Mortar Leaching"},{label:"Overgrowth",value:"Pioneer Root Fractures"},{label:"Human Occupancy",value:"0 (Abandoned)"}]},{id:"step-10",year:"1965",title:"Emergency Shoring & Stabilization",subtitle:"Screw-Jack Columns & Structural Halts",paragraphs:["Alarmed by the imminent threat of catastrophic collapse, local historical preservationists and structural engineers intervened in 1965 to execute emergency stabilization measures. Their primary goal was to arrest ongoing structural deflection and weatherproof the vulnerable building envelope before the harsh winter season.","In the damp root cellar, engineers inserted an adjustable screw-jack steel lally column beneath the sagging main summer beams and girders. By turning the threaded collars with heavy wrenches, teams applied upward jacking force to halt downward deflection and stabilize the compromised timber structure.","Where historic oak sill plates had rotted entirely from ground contact, temporary reinforced concrete pads were poured onto the basement dirt floor. The screw-jack columns transferred multi-ton dead loads directly onto these new concrete footings, bypassing the deteriorated perimeter masonry.","On the exterior, structural stabilization strapping was applied across bulging masonry sections, while utility service was tracked by an external watt-hour induction meter disk. Modern weatherproof service panels were installed to provide safe temporary site power for restoration machinery and diagnostic illumination.","Compromised sections of the sagging roof structure were covered with heavy tar canvas sheeting and timber battens. This rapid weatherproofing intervention successfully sealed out rain and snowmelt, preserving the historic timber skeleton while comprehensive architectural restoration plans were prepared."],description:"Alarmed by the imminent threat of catastrophic collapse, local historical preservationists and structural engineers intervened in 1965 to execute emergency stabilization measures. Their primary goal was to arrest ongoing structural deflection and weatherproof the vulnerable building envelope before the harsh winter season.\n\nIn the damp root cellar, engineers inserted an adjustable screw-jack steel lally column beneath the sagging main summer beams and girders. By turning the threaded collars with heavy wrenches, teams applied upward jacking force to halt downward deflection and stabilize the compromised timber structure.\n\nOn the exterior, structural stabilization strapping was applied across bulging masonry sections, while utility service was tracked by an external watt-hour induction meter disk. Modern weatherproof service panels were installed to provide safe temporary site power for restoration machinery and diagnostic illumination.",specs:[{label:"Shoring",value:"Screw-Jack Lally Columns"},{label:"Load Transfer",value:"Temporary Concrete Pads"},{label:"Weatherproofing",value:"Heavy Tar Canvas Sheeting"},{label:"Status",value:"Emergency Stabilization"}]},{id:"step-11",year:"1980",title:"Modernization Planning",subtitle:"Blueprint Overlays & Hydraulic Jacking",paragraphs:["A new chapter of formal preservation commenced in 1980 as conservation engineers and historic architects initiated a rigorous, scientific restoration program. Detailed structural assessments and crisp technical blueprint overlays (A-101) were generated to map every timber framing member and masonry defect with millimeter precision.","To correct two centuries of differential foundation settlement, teams positioned a 12-ton hydraulic jack ram under joists and bearing girders. Working in synchronized increments monitored by laser transit realignment, the jacks gently lifted the sagging floors back to true horizontal datum without overstressing historic joints.","Where historic timber girders exhibited excessive deflection under modern design criteria, concealed structural steel C-channel grids and steel lintels were inserted parallel to the original oak beams. This hidden structural reinforcement carried all heavy floor live loads while preserving the authentic visual aesthetic of the exposed 18th-century timbers.","Deteriorated sections of the white oak sill plates were surgically excavated and repaired using a pressure injection epoxy resin splice. Liquid epoxy resins infused under pressure bonded period-correct hardwood prosthetics to sound ancient timber, restoring full compressive and tensile strength.","This meticulous conservation methodology successfully bridged historical preservation integrity with modern residential building codes. The stabilized and reinforced shell now stood fully capable of supporting contemporary domestic living standards while celebrating its authentic material history."],description:"A new chapter of formal preservation commenced in 1980 as conservation engineers and historic architects initiated a rigorous, scientific restoration program. Detailed structural assessments and crisp technical blueprint overlays (A-101) were generated to map every timber framing member and masonry defect with millimeter precision.\n\nTo correct two centuries of differential foundation settlement, teams positioned a 12-ton hydraulic jack ram under joists and bearing girders. Working in synchronized increments monitored by laser transit realignment, the jacks gently lifted the sagging floors back to true horizontal datum without overstressing historic joints.\n\nDeteriorated sections of the white oak sill plates were surgically excavated and repaired using a pressure injection epoxy resin splice. Liquid epoxy resins infused under pressure bonded period-correct hardwood prosthetics to sound ancient timber, restoring full compressive and tensile strength.",specs:[{label:"Methodology",value:"Structural Conservation"},{label:"Lifting",value:"12-Ton Hydraulic Rams"},{label:"Repair",value:"Epoxy Resin Splice"},{label:"Target Load",value:"Modern Code Compliance"}]},{id:"step-12",year:"1995",title:"Radon Mitigation & Perimeter Drainage",subtitle:"Subterranean Soil Gas Control & French Drains",paragraphs:["Deep subterranean geotechnical assessments conducted during basement restoration revealed natural soil gas permeability across the underlying granite bedrock strata. Radioactive radon gas rising from decaying uranium in the deep ledge required active subterranean soil mechanics and gas interception to ensure healthy indoor air quality.","Engineers excavated a sub-slab radon suction core pit beneath the newly poured basement concrete slab. Perforated collection pipes embedded inside washed drainage gravel intercepted soil gas before it could penetrate the living space, directing it into a sealed vertical riser pipe.","An inline continuous PVC exhaust fan system mounted in the attic created permanent negative pressure beneath the basement floor slab. Soil gases drawn into the suction core pit were safely evacuated and discharged above the roofline high into the atmosphere.","Simultaneously, groundwater infiltration was eliminated by installing a perforated PVC French drain in washed gravel laid at the exterior footing level (-12 Ft. Below Grade). Perforated PVC conduits wrapped in permeable geotextile fabric intercepted subterranean groundwater.","This subterranean drainage network relieved hydrostatic groundwater pressure vectors acting against foundation walls. Water draining into the washed gravel was diverted away by gravity to a daylight discharge basin in the lower meadow, leaving the granite cellar permanently bone-dry."],description:"Deep subterranean geotechnical assessments conducted during basement restoration revealed natural soil gas permeability across the underlying granite bedrock strata. Radioactive radon gas rising from decaying uranium in the deep ledge required active subterranean soil mechanics and gas interception to ensure healthy indoor air quality.\n\nEngineers excavated a sub-slab radon suction core pit beneath the newly poured basement concrete slab. Perforated collection pipes embedded inside washed drainage gravel intercepted soil gas before it could penetrate the living space, directing it into a sealed vertical riser pipe.\n\nSimultaneously, groundwater infiltration was eliminated by installing a perforated PVC French drain in washed gravel laid at the exterior footing level (-12 Ft. Below Grade). Perforated PVC conduits wrapped in permeable geotextile fabric intercepted subterranean groundwater.",specs:[{label:"Soil Gas",value:"Sub-Slab Radon Suction"},{label:"Drainage",value:"Perforated PVC French Drain"},{label:"Substratum",value:"Granite / Washed Gravel"},{label:"Fan System",value:"Continuous Inline Exhaust"}]},{id:"step-13",year:"2010",title:"Ecological Insulation & Glazing",subtitle:"Thermal Envelopes & Triple Glazing",paragraphs:["In response to 21st-century environmental engineering mandates and zero-carbon energy targets, the homestead underwent a comprehensive high-performance thermal retrofitting. The primary focus was creating a continuous, airtight building envelope without altering the historic exterior architectural profile.","Inside the wall cavities between exterior cedar siding and interior framing, contractors sprayed an R-45 closed-cell spray foam wall cavity barrier. This monolithic polyurethane foam insulation expanded to seal every gap, crack, and mortise joint, establishing an impenetrable barrier against air leakage and vapor diffusion.","All perimeter rim joists, attic rafters, and foundation interfaces were meticulously sealed with elastomeric membranes. This zero-draft encapsulation eliminated convective heat loss and transformed the drafty historic structure into a high-efficiency thermal vessel.","Historic window openings were fitted with a triple-pane Low-E argon glazing profile set in insulated hardwood sash frames. The argon gas fill between the three glass panes provided superior thermal resistance while Low-E metallic coatings reflected interior radiant heat back into the living space.","By eradicating thermal bridging across walls and fenestration, the building achieved an exceptional overall U-factor (0.022 BTUs/hr-sq.ft-°F). Coupled with deep ground-source geothermal loops bored 400 feet into bedrock, the home achieved whisper-quiet, ultra-efficient radiant climate control."],description:"In response to 21st-century environmental engineering mandates and zero-carbon energy targets, the homestead underwent a comprehensive high-performance thermal retrofitting. The primary focus was creating a continuous, airtight building envelope without altering the historic exterior architectural profile.\n\nInside the wall cavities between exterior cedar siding and interior framing, contractors sprayed an R-45 closed-cell spray foam wall cavity barrier. This monolithic polyurethane foam insulation expanded to seal every gap, crack, and mortise joint, establishing an impenetrable barrier against air leakage and vapor diffusion.\n\nHistoric window openings were fitted with a triple-pane Low-E argon glazing profile set in insulated hardwood sash frames. The argon gas fill between the three glass panes provided superior thermal resistance while Low-E metallic coatings reflected interior radiant heat back into the living space.",specs:[{label:"Envelope",value:"Closed-Cell Foam (R-45)"},{label:"Glazing",value:"Triple-Pane Low-E Argon"},{label:"Air Sealing",value:"Elastomeric Vapor Barrier"},{label:"HVAC",value:"Geothermal Radiant Loop"}]},{id:"step-14",year:"Present",title:"Present Modern Design",subtitle:"Minimalist Architectural Synthesis & Energy Independence",paragraphs:["Today, the homestead stands as a breathtaking architectural synthesis of centuries-old agrarian craftsmanship and uncompromising minimalist contemporary design. The restored 1780 fieldstone hearth and hand-hewn white oak timbers serve as tactile, historical monuments anchored within an expansive, light-filled modern interior.","Bold structural engineering defines the contemporary addition, featuring a cantilevered steel W12x50 moment connection. These high-strength steel beams support frameless floor-to-ceiling glass curtain walls that dissolve the visual boundary between the interior living space and the surrounding meadow landscape.","The high-contrast juxtaposition between rough historic masonry, textured old-growth oak, and sleek industrial black steel creates a dynamic visual dialogue across 240 years of architectural history. Every structural transition is celebrated with crisp, honest detailing.","Energy independence is achieved through advanced microgrid infrastructure utilizing a bi-directional V2H battery rack in the utility annex. This intelligent storage system balances clean energy generated by discreet rooftop cantilevered solar PV arrays (R-60 Core roof system) with electric vehicle fast-charging.","Operating at net-negative carbon emissions (-15% positive generation), the 3,800-square-foot residence powers daily living and a Level 2 EV charging bay entirely from renewable resources. By honoring every era of its built heritage while pioneering future structural and ecological design, the homestead exemplifies timeless living architecture."],description:"Today, the homestead stands as a breathtaking architectural synthesis of centuries-old agrarian craftsmanship and uncompromising minimalist contemporary design. The restored 1780 fieldstone hearth and hand-hewn white oak timbers serve as tactile, historical monuments anchored within an expansive, light-filled modern interior.\n\nBold structural engineering defines the contemporary addition, featuring a cantilevered steel W12x50 moment connection. These high-strength steel beams support frameless floor-to-ceiling glass curtain walls that dissolve the visual boundary between the interior living space and the surrounding meadow landscape.\n\nEnergy independence is achieved through advanced microgrid infrastructure utilizing a bi-directional V2H battery rack in the utility annex. This intelligent storage system balances clean energy generated by discreet rooftop cantilevered solar PV arrays (R-60 Core roof system) with electric vehicle fast-charging.",specs:[{label:"Footprint / EV",value:"3,800 sq. ft. / Level 2 EV Bay"},{label:"Structure",value:"Cantilevered W12x50 Steel"},{label:"Microgrid",value:"Bi-Directional V2H Battery"},{label:"Net Energy",value:"-15% (Positive Generation)"}]},{id:"step-15",year:"Geotechnical",title:"Geotechnical Engineering Drill-down",subtitle:"Soil Mechanics & Stress Bulb Distribution",paragraphs:["A dedicated geotechnical engineering drill-down reveals the complex subterranean soil mechanics governing foundation stability under the restored 3,800-square-foot residence. Subsurface bore logs and strata profiling map the exact load transfer paths from surface footings down to deep bedrock ledges.","Beneath the dry-laid fieldstone footings, structural loads propagate through glacial till layers where the soil strata stress bulb distribution curve models subterranean bearing contour lines. Charting these contours demonstrates how pressure widens and attenuates as depth increases into the dense substratum.","Laboratory soil mechanics testing confirms a safe soil bearing capacity of 3.5 tons per square foot across the compacted glacial till and clay horizons. This robust bearing threshold ensures zero differential settlement under multi-story dead and live loads.","Lateral subterranean analysis focuses on a hydrostatic groundwater pressure vector acting against the deep basement retaining walls (-12 Ft. Below Grade). During heavy seasonal rainfall, saturated soil attempts to exert immense horizontal shear force against perimeter masonry.","The combination of washed gravel backfill, geotextile filtration fabric, and sub-slab perimeter drainage channels completely intercepts and dissipates these hydrostatic vectors. By reducing lateral water pressure to absolute zero, the geotechnical design guarantees perpetual subterranean structural integrity."],description:"A dedicated geotechnical engineering drill-down reveals the complex subterranean soil mechanics governing foundation stability under the restored 3,800-square-foot residence. Subsurface bore logs and strata profiling map the exact load transfer paths from surface footings down to deep bedrock ledges.\n\nBeneath the dry-laid fieldstone footings, structural loads propagate through glacial till layers where the soil strata stress bulb distribution curve models subterranean bearing contour lines. Charting these contours demonstrates how pressure widens and attenuates as depth increases into the dense substratum.\n\nLateral subterranean analysis focuses on a hydrostatic groundwater pressure vector acting against the deep basement retaining walls (-12 Ft. Below Grade). During heavy seasonal rainfall, saturated soil attempts to exert immense horizontal shear force against perimeter masonry.",specs:[{label:"Discipline",value:"Geotechnical Engineering"},{label:"Bearing Capacity",value:"3.5 Tons / Sq. Ft."},{label:"Stress Model",value:"Boussinesq Stress Bulb"},{label:"Water Table",value:"-12 Ft. Below Grade"}]},{id:"step-16",year:"Thermodynamics",title:"Thermodynamics Drill-down",subtitle:"Heat Flux & Dew-Point Condensation Planes",paragraphs:["A rigorous building physics and thermodynamics drill-down quantifies the thermal envelope performance across the hybrid historic and contemporary wall sections. Advanced finite element thermal modeling verifies heat flux vectors across every material boundary.","Critical to envelope longevity is exact dew-point condensation plane tracking within the multi-layered wall assembly. Temperature and vapor pressure gradients are plotted across exterior cedar siding, air gaps, wood fiber sheathing, and interior closed-cell foam.","Thermal analysis confirms that the dew-point temperature plane occurs exclusively on the exterior face of the vapor-impermeable closed-cell foam insulation. This ensures that any interstitial moisture remains outside the timber framing, preventing winter condensation, mold formation, and structural wood rot.","Continuous exterior wood fiber insulation board overlays achieve complete infrared thermal bridging elimination across structural posts and studs. Thermographic heat flux mapping proves zero cold spotting and uniform surface temperatures across interior walls (U-Factor: 0.022 BTUs/hr-sq.ft-°F).","Multi-layer conductive, convective, and radiative heat transfer calculations demonstrate a staggering 94.2% reduction in total thermal loss compared to the 1780 log cabin baseline. This thermodynamic mastery enables ultra-low-energy climate comfort in any extreme weather conditions."],description:"A rigorous building physics and thermodynamics drill-down quantifies the thermal envelope performance across the hybrid historic and contemporary wall sections. Advanced finite element thermal modeling verifies heat flux vectors across every material boundary.\n\nCritical to envelope longevity is exact dew-point condensation plane tracking within the multi-layered wall assembly. Temperature and vapor pressure gradients are plotted across exterior cedar siding, air gaps, wood fiber sheathing, and interior closed-cell foam.\n\nContinuous exterior wood fiber insulation board overlays achieve complete infrared thermal bridging elimination across structural posts and studs. Thermographic heat flux mapping proves zero cold spotting and uniform surface temperatures across interior walls (U-Factor: 0.022 BTUs/hr-sq.ft-°F).",specs:[{label:"Discipline",value:"Building Thermodynamics"},{label:"U-Factor",value:"0.022 BTUs/hr-sq.ft-°F"},{label:"Dew-Point",value:"Exterior of Vapor Barrier"},{label:"Heat Loss Reduc.",value:"94.2% vs. 1780 Baseline"}]},{id:"step-17",year:"Kinematics",title:"Structural Kinematics Drill-down",subtitle:"Load Vectors & Shear Deflection",paragraphs:["The final engineering analysis presents a comprehensive structural kinematics drill-down examining static equilibrium, dynamic moment resistance, and wind-load deflection across the entire architectural complex. Vector mechanics illustrate the exact equilibrium of forces keeping the structure upright.","A dead vs live load vector diagram maps the continuous downward gravity loads from roof snow accumulation down through timber trusses, girders, and steel columns into the granite footings. Permanent dead loads (32 lbs/sq.ft.) and transient live loads (40 lbs/sq.ft.) are cleanly separated in structural calculations.","Under severe storm simulations featuring 90 mph wind gusts, lateral wind forces exert overturning moments against the exterior facades. Structural moment vector analysis demonstrates how the hybrid steel moment frame and braced timber shear walls work in unison to resist rotational uplift.","Precision optical and strain gauge monitoring verifies wind moment shear-wall deflection limits across top-story spans. Even under maximum peak wind shear, lateral story drift is restricted to less than 0.25 inches (deflection index L/360), well below structural damage thresholds.","This kinematic equilibrium between gravity loads, shear resistance, and elastic deflection limits validates the structural synthesis. By uniting 18th-century timber craftsmanship with 21st-century steel moment engineering, the Homestead Dossier achieves structural perfection that will stand for generations to come."],description:"The final engineering analysis presents a comprehensive structural kinematics drill-down examining static equilibrium, dynamic moment resistance, and wind-load deflection across the entire architectural complex. Vector mechanics illustrate the exact equilibrium of forces keeping the structure upright.\n\nA dead vs live load vector diagram maps the continuous downward gravity loads from roof snow accumulation down through timber trusses, girders, and steel columns into the granite footings. Permanent dead loads (32 lbs/sq.ft.) and transient live loads (40 lbs/sq.ft.) are cleanly separated in structural calculations.\n\nPrecision optical and strain gauge monitoring verifies wind moment shear-wall deflection limits across top-story spans. Even under maximum peak wind shear, lateral story drift is restricted to less than 0.25 inches (deflection index L/360), well below structural damage thresholds.",specs:[{label:"Discipline",value:"Structural Kinematics"},{label:"Dead Load",value:"32 lbs / sq. ft."},{label:"Live Load",value:"40 lbs / sq. ft."},{label:"Shear Deflection",value:"Max L/360 @ 90 mph"}]}],t={grid:"public/planning_paper_dot_grid.webp"},a={},o={},r={};function n(e){if("undefined"==typeof Image)return null;if(!a[e]&&t[e]){const o=new Image;o.src=t[e],a[e]=o}return a[e]||null}function i(e,t,a,r){if("undefined"==typeof document&&"undefined"==typeof OffscreenCanvas)return null;if(!o[e]){let n=null;if("undefined"!=typeof OffscreenCanvas?n=new OffscreenCanvas(t,a):"undefined"!=typeof document&&(n=document.createElement("canvas"),n.width=t,n.height=a),n){r(n.getContext("2d"),t,a),o[e]=n}}return o[e]||null}function s(e,t,a,o,n){if(!r[t]){const s=i(`pattern_${t}`,a,o,n);s&&e&&"function"==typeof e.createPattern&&(r[t]=e.createPattern(s,"repeat"))}return r[t]||null}function l(e){return{assemblyProg:Math.min(1,e/.35),secondaryProg:Math.max(0,Math.min(1,(e-.35)/.35)),holdProg:Math.max(0,Math.min(1,(e-.7)/.3))}}let c=[];function f(t,a=0){const o=Math.max(0,Math.min(e.length-1,t)),r=Math.max(0,Math.min(1,a));for(let e=0;e25){e.strokeStyle=y,e.globalAlpha=.55,e.lineWidth=Math.max(.8,.85*i),e.beginPath();const s=.2+(t+a)%.2,l=.65+(o+r)%.2;e.moveTo(p+(E-p)*s+u*n*.6,m+(T-m)*s+g*n*.6),e.lineTo(p+(E-p)*l+u*n*.4,m+(T-m)*l+g*n*.4),e.stroke()}e.restore()}function u(e,t,a,o,r,n=2.8,i={}){h(e,t,a,t+o,a,i.roughness||.6,e.lineWidth||1,{...i,overshoot:n}),h(e,t+o,a,t+o,a+r,i.roughness||.6,e.lineWidth||1,{...i,overshoot:n}),h(e,t+o,a+r,t,a+r,i.roughness||.6,e.lineWidth||1,{...i,overshoot:n}),h(e,t,a+r,t,a,i.roughness||.6,e.lineWidth||1,{...i,overshoot:n})}function g(e,t,a,o,r,n=!1){if(o<=0||r<=0)return;e.save(),e.beginPath(),e.rect(t,a,o,r),e.clip();const i=s(e,n?"masonry_weathered_tile":"masonry_clean_tile",64,64,(e,t,a)=>{e.strokeStyle="rgba(255, 255, 255, 0.45)",e.lineWidth=1;for(let o=-a;o.3||h(e,o,0,o-a,a,.4,1,{color:"rgba(255, 255, 255, 0.45)",secondaryColor:"rgba(255, 255, 255, 0.2)",overshoot:!1,doubleStroke:!1})});if(i)e.fillStyle=i,e.fillRect(t-20,a-20,o+40,r+40);else{const i=14;for(let s=-r;s.3||h(e,t+s,a,t+s-r,a+r,.4,1,{color:"rgba(255, 255, 255, 0.45)",secondaryColor:"rgba(255, 255, 255, 0.2)",overshoot:!1,doubleStroke:!1})}e.restore()}function p(e,t,a,o,r){if(o<=0||r<=0)return;e.save(),e.beginPath(),e.rect(t,a,o,r),e.clip();const n=s(e,"steel_hatch_tile",64,64,(e,t,a)=>{for(let o=-a;o{for(let o=-a;o{A(e,t/2,a/2,o,r,s)});f?e.drawImage(f,-l/2,-c/2):A(e,0,0,o,r,s),e.restore()}function b(e,t,a,o,r,n,i={}){e.save(),h(e,t+n,a,t+o-n,a,i.roughness||.6,e.lineWidth||1.2,i),h(e,t+o,a+n,t+o,a+r-n,i.roughness||.6,e.lineWidth||1.2,i),h(e,t+o-n,a+r,t+n,a+r,i.roughness||.6,e.lineWidth||1.2,i),h(e,t,a+r-n,t,a+n,i.roughness||.6,e.lineWidth||1.2,i);const s=[[t+o-n,a+n,0],[t+o-n,a+r-n,Math.PI/2],[t+n,a+r-n,Math.PI],[t+n,a+n,1.5*Math.PI]];for(const[t,a,o]of s)for(let r=0;r<2;r++){e.strokeStyle=0===r?"rgba(255, 255, 255, 0.4)":i.color||"#ffffff",e.lineWidth=0===r?1.4*(e.lineWidth||1.2):e.lineWidth||1.2,e.beginPath();for(let r=0;r<=4;r++){const i=o+r/4*(Math.PI/2);let s=t+Math.cos(i)*n,l=a+Math.sin(i)*n;r>0&&r<4&&(s+=.6*Math.sin(r+o),l+=.6*Math.cos(r+o)),0===r?e.moveTo(s,l):e.lineTo(s,l)}e.stroke()}e.restore()}function I(e,t,a,o,r,n,i="#ffffff",s=6,l="#0a0a0a"){e.save(),e.strokeStyle="#D12B3E",e.fillStyle="#D12B3E",e.lineWidth=1.4,h(e,t,a,o,r,.4,1.4,{color:"#D12B3E",secondaryColor:"rgba(209, 43, 62, 0.45)",overshoot:2.2});const c=Math.atan2(r-a,o-t),f=Math.PI/4;for(const[n,i]of[[t,a],[o,r]])h(e,n-Math.cos(c+f)*s,i-Math.sin(c+f)*s,n+Math.cos(c+f)*s,i+Math.sin(c+f)*s,.3,1.5,{color:"#D12B3E",secondaryColor:"rgba(209, 43, 62, 0.45)",overshoot:1.5});const d=(t+o)/2,u=(a+r)/2;e.font="bold 11px system-ui, -apple-system, sans-serif",e.textAlign="center",e.textBaseline="middle";const g=e.measureText(n).width+14,p=18;e.save(),e.translate(d+5,u+4),e.rotate(.031),e.fillStyle="#000000",e.beginPath(),e.rect(-g/2,-9,g,p),e.fill(),e.restore(),e.save(),e.translate(d,u),e.rotate(-.015);const m="#ffffff"===l||"#000000"===l?"#0a0a0a":l;e.fillStyle=m,e.beginPath(),e.rect(-g/2,-9,g,p),e.fill(),e.lineWidth=1.4,b(e,-g/2,-9,g,p,4,{color:"#D12B3E",secondaryColor:"rgba(209, 43, 62, 0.45)"}),e.fillStyle="#ffffff",e.fillText(n,0,1),e.restore(),e.restore()}function R(e,t,a,o,r,n="#ffffff"){e.save(),e.strokeStyle="#D12B3E",e.fillStyle="#D12B3E",e.lineWidth=1.4;for(let r of[1,.35]){const n=o*r;for(let o=0;o<2;o++){e.strokeStyle=0===o?"rgba(209, 43, 62, 0.4)":"#D12B3E",e.lineWidth=0===o?2:1.4,e.beginPath();for(let o=0;o<=16;o++){const r=o*Math.PI*2/16;let i=t+Math.cos(r)*n,s=a+Math.sin(r)*n;o>0&&o<16&&(i+=.6*Math.sin(1.7*o+t),s+=.6*Math.cos(1.3*o+a)),0===o?e.moveTo(i,s):e.lineTo(i,s)}e.stroke()}}h(e,t-o-6,a,t+o+6,a,.3,1.4,{color:"#D12B3E",secondaryColor:"rgba(209, 43, 62, 0.45)"}),h(e,t,a-o-6,t,a+o+6,.3,1.4,{color:"#D12B3E",secondaryColor:"rgba(209, 43, 62, 0.45)"}),e.beginPath(),e.moveTo(t,a),e.arc(t,a,.35*o,0,Math.PI/2),e.closePath(),e.fill(),e.beginPath(),e.moveTo(t,a),e.arc(t,a,.35*o,Math.PI,1.5*Math.PI),e.closePath(),e.fill(),r&&(e.fillStyle="#ffffff",e.font="bold 10px system-ui, -apple-system, sans-serif",e.textAlign="center",e.fillText(r,t,a+o+15)),e.restore()}function S(e,t,a,o,r,n,i="",s="",l=null){const c=d("g",{class:"tech-callout"});s&&c.setAttribute("id",s),null!==l&&c.setAttribute("data-phase",String(l)),c.style.transition="none";const f=d("polyline",{points:`${o},${r} ${t+(o>t?20:-20)},${a} ${t},${a}`,fill:"none",stroke:"#D12B3E","stroke-width":"1.8","stroke-dasharray":"4, 2"});c.appendChild(f);const h=d("circle",{cx:o,cy:r,r:4,fill:"#D12B3E",stroke:"#ffffff","stroke-width":"1.2"});c.appendChild(h);const u=o>t?-1.5:1.5,g=o>t?1.4:-1.7,p=n?6.8*n.length+24:0,m=i?5.9*i.length+24:0,E=Math.max(190,Math.ceil(Math.max(p,m))),T=i?36:26,A=t-(o>t?0:E),y=a-24,b=d("g",{class:"tech-callout-box"});b.style.transition="none";const I=A+E/2,R=y+T/2;b.setAttribute("data-center-x",String(I)),b.setAttribute("data-center-y",String(R));const S=d("rect",{x:A+(o>t?7:-7),y:y+8,width:E,height:T,fill:"#000000",rx:"4",ry:"4",transform:`rotate(${g}, ${I}, ${R})`});b.appendChild(S);const v=d("rect",{x:A,y:y,width:E,height:T,fill:"#0a0a0a",stroke:"#D12B3E","stroke-width":"2",rx:"4",ry:"4",transform:`rotate(${u}, ${I}, ${R})`});b.appendChild(v);const w=d("text",{x:A+8,y:y+16,fill:"#ffffff","font-family":'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',"font-size":"11","font-weight":"bold",transform:`rotate(${u}, ${I}, ${R})`});if(w.textContent=n,b.appendChild(w),i){const e=d("text",{x:A+8,y:y+28,fill:"#cccccc","font-family":'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',"font-size":"10",transform:`rotate(${u}, ${I}, ${R})`});e.textContent=i,b.appendChild(e)}return c.appendChild(b),e.appendChild(c),c}function v(e,t,a,o,r,n="OCT 1954"){const i=d("g",{class:"tech-title-block",transform:"translate(0, -50)"});i.style.transition="none";const s=d("rect",{x:488,y:520,width:300,height:70,fill:"#000000",rx:"6",ry:"6",transform:"rotate(1.7, 638, 555)"});i.appendChild(s);const l=d("g",{transform:"rotate(-1.2, 630, 545)"}),c=d("rect",{x:480,y:510,width:300,height:70,fill:"#0a0a0a",stroke:"#D12B3E","stroke-width":"2.5",rx:"6",ry:"6"});l.appendChild(c);const f=d("line",{x1:480,y1:535,x2:780,y2:535,stroke:"#D12B3E","stroke-width":"1.5"}),h=d("line",{x1:480,y1:558,x2:780,y2:558,stroke:"#ffffff","stroke-width":"1"}),u=d("line",{x1:640,y1:535,x2:640,y2:580,stroke:"#ffffff","stroke-width":"1"});l.append(f,h,u);const g=d("text",{x:490,y:528,fill:"#ffffff","font-family":'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',"font-size":"11","font-weight":"bold"});g.textContent=`PROJ: ${t}`;const p=d("text",{x:490,y:550,fill:"#ffffff","font-family":'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',"font-size":"10","font-weight":"bold"});p.textContent=`DWG: ${a}`;const m=d("text",{x:650,y:550,fill:"#ffffff","font-family":'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',"font-size":"10","font-weight":"bold"});m.textContent=`REV: ${o}`;const E=d("text",{x:490,y:572,fill:"#cccccc","font-family":'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',"font-size":"10"});E.textContent=`SCALE: ${r}`;const T=d("text",{x:650,y:572,fill:"#cccccc","font-family":'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',"font-size":"10"});T.textContent=`DATE: ${n}`,l.append(g,p,m,E,T),i.appendChild(l),e.appendChild(i)}function w(e,t=5){const a=Math.max(0,Math.min(.9999,e)),o=Math.floor(a*t);return{idx:o,localProg:a*t-o}}function L(e,t){const a="object"==typeof t&&null!==t&&"number"==typeof t.idx?t.idx:"number"==typeof t?t:0,o="object"==typeof t&&null!==t&&"number"==typeof t.localProg?t.localProg:1,r=Array.from(e.querySelectorAll(".tech-callout, .tech-arrow, .tech-stamp-badge, .tech-datum")),n={};r.forEach(e=>{const t=e.getAttribute("data-phase"),r=null===t||""===t?0:parseInt(t,10);void 0===n[r]&&(n[r]=0);const i=n[r]++;if(ra){e.style.opacity="0",e.style.pointerEvents="none";const t=e.querySelector(".tech-callout-box");if(t){const e=parseFloat(t.getAttribute("data-center-x"))||0,a=parseFloat(t.getAttribute("data-center-y"))||0;t.setAttribute("transform",`translate(${e}, ${a}) scale(0.35) translate(${-e}, ${-a})`)}}else{const t=(o-i*.18)/.45;if(t<=0){e.style.opacity="0",e.style.pointerEvents="none";const t=e.querySelector(".tech-callout-box");if(t){const e=parseFloat(t.getAttribute("data-center-x"))||0,a=parseFloat(t.getAttribute("data-center-y"))||0;t.setAttribute("transform",`translate(${e}, ${a}) scale(0.35) translate(${-e}, ${-a})`)}}else if(t>=1){e.style.opacity="1",e.style.pointerEvents="auto";const t=e.querySelector(".tech-callout-box");if(t){const e=parseFloat(t.getAttribute("data-center-x"))||0,a=parseFloat(t.getAttribute("data-center-y"))||0;t.setAttribute("transform",`translate(${e}, ${a}) scale(1) translate(${-e}, ${-a})`)}}else{const a=Math.min(1,2*t);e.style.opacity=String(a.toFixed(4)),e.style.pointerEvents=a>.1?"auto":"none";const o=e.querySelector(".tech-callout-box");if(o){const e=.35+.65*function(e){const t=1.70158;return 1+(t+1)*Math.pow(e-1,3)+t*Math.pow(e-1,2)}(t),a=parseFloat(o.getAttribute("data-center-x"))||0,r=parseFloat(o.getAttribute("data-center-y"))||0;o.setAttribute("transform",`translate(${a}, ${r}) scale(${e.toFixed(4)}) translate(${-a}, ${-r})`)}}}})}function O(e,t,a,o,r,n="#ffffff"){e.save(),h(e,t,a,o,r,.3,2.5,{color:n,secondaryColor:"rgba(255, 255, 255, 0.45)",overshoot:3});const i=Math.atan2(r-a,o-t),s=Math.sqrt((o-t)**2+(r-a)**2),l=Math.floor(s/22);for(let o=0;o<=l;o++){const r=t+Math.cos(i)*(22*o),s=a+Math.sin(i)*(22*o);for(let t=0;t<2;t++){e.strokeStyle=0===t?"rgba(255, 255, 255, 0.4)":n,e.lineWidth=0===t?1.8:1.2,e.beginPath();const a=2.5;for(let t=0;t<=8;t++){const n=t*Math.PI*2/8,i=r+Math.cos(n)*a+(t>0&&t<8?.4*Math.sin(t+o):0),l=s+Math.sin(n)*a+(t>0&&t<8?.4*Math.cos(t+o):0);0===t?e.moveTo(i,l):e.lineTo(i,l)}e.stroke()}}e.restore()}function N(e,t,a,o,r,n=18,i="#0a0a0a",s="#ffffff",l=.03){e.save(),e.save(),e.translate(t+12,a+14),e.rotate(l),e.fillStyle="rgba(0, 0, 0, 0.25)",e.fillRect(-12,-14,o+n,r+n),e.restore(),e.fillStyle=i,e.fillRect(t,a,o,r),e.lineWidth=2.2,u(e,t,a,o,r,3.2,{color:s,secondaryColor:"rgba(255, 255, 255, 0.45)"}),e.fillStyle="#e5e5e5",e.beginPath(),e.moveTo(t,a+r),e.lineTo(t+n,a+r+n),e.lineTo(t+o+n,a+r+n),e.lineTo(t+o,a+r),e.closePath(),e.fill(),h(e,t,a+r,t+n,a+r+n,.4,1.8,{color:s,overshoot:2}),h(e,t+n,a+r+n,t+o+n,a+r+n,.4,1.8,{color:s,overshoot:2}),h(e,t+o+n,a+r+n,t+o,a+r,.4,1.8,{color:s,overshoot:2}),e.fillStyle="#cccccc",e.beginPath(),e.moveTo(t+o,a),e.lineTo(t+o+n,a+n),e.lineTo(t+o+n,a+r+n),e.lineTo(t+o,a+r),e.closePath(),e.fill(),h(e,t+o,a,t+o+n,a+n,.4,1.8,{color:s,overshoot:2}),h(e,t+o+n,a+n,t+o+n,a+r+n,.4,1.8,{color:s,overshoot:2}),e.restore()}function C(e,t,a,o){e.save();const r=280,n=104,i=(t%2==0?-1.2:1.4)*Math.PI/180,s=(t%2==0?1.6:-1.5)*Math.PI/180;e.save(),e.translate(406,84),e.rotate(s),e.fillStyle="rgba(0, 0, 0, 0.65)",e.fillRect(-140,-52,r,n),e.restore(),e.save(),e.translate(400,76),e.rotate(i),e.fillStyle="#0a0a0a",e.fillRect(-140,-52,r,n),e.lineWidth=1.8,u(e,-140,-52,r,n,3,{color:"#ffffff",secondaryColor:"rgba(255, 255, 255, 0.45)"}),h(e,-130,-32,130,-32,.3,1.3,{color:"#ffffff",overshoot:2}),e.fillStyle="#ffffff",e.font='bold 11px "Impact", "Arial Black", sans-serif',e.letterSpacing="0.06em";const l='10px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';if(0===t){e.fillText("SPEC: FRONTIER COMPASS & WIND ROSE",-128,-38);const t=-90,o=13;for(let a of[28,18,9])for(let r=0;r<2;r++){e.strokeStyle=0===r?"rgba(204, 204, 204, 0.35)":"#cccccc",e.lineWidth=0===r?1.6:1,e.beginPath();for(let r=0;r<=16;r++){const n=r*Math.PI*2/16;let i=t+Math.cos(n)*a,s=o+Math.sin(n)*a;r>0&&r<16&&(i+=.4*Math.sin(r+a),s+=.4*Math.cos(r+a)),0===r?e.moveTo(i,s):e.lineTo(i,s)}e.stroke()}const r=-Math.PI/4+a*Math.PI*.6,n=t+26*Math.cos(r),i=o+26*Math.sin(r);h(e,t-11*Math.cos(r),o-11*Math.sin(r),n,i,.3,2,{color:"#ffffff"}),e.fillStyle="#ffffff",e.beginPath(),e.arc(n,i,3,0,2*Math.PI),e.fill(),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("ELEVATION : +1,420 FT ASL",-35,-12),e.fillText("MAG DECL : 14°12' W",-35,3),e.fillText(`WIND VEL : ${(25+20*a).toFixed(1)} KTS NW`,-35,18),e.fillText(`CANOPY DEN: ${(98-15*a).toFixed(0)}% VIRGIN`,-35,33)}else if(1===t){e.fillText("SPEC: T-AUGER ROTATION & DRAW-BORE TENSION",-128,-38);const t=-95,o=12;y(e,t,o,24,8,a*Math.PI*4,"#ffffff"),y(e,t+34,o+10,16,6,-a*Math.PI*6,"#cccccc"),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("MORTISE DEPTH: 112.5 MM",-35,-12),e.fillText("TREENAIL PEG : TAPER 1:16",-35,3),e.fillText(`DRAW TENSION : ${(1800+600*a).toFixed(0)} PSI`,-35,18),e.fillText(`JOINT GAP : ${(1.8-1.8*a).toFixed(2)} MM`,-35,33)}else if(2===t){e.fillText("SPEC: GEOLOGICAL BORE LOG & WATER TABLE",-128,-38);const t=-115,o=-15,r=50;u(e,t,o,r,56,2,{color:"#cccccc"}),E(e,t,o+28,r,28,"rgba(255, 255, 255, 0.3)");const n=o+42-24*a;h(e,t,n,t+r,n,.4,2,{color:"#ffffff",overshoot:2.2}),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("STRATUM : METAMORPHIC GRANITE",-50,-12),e.fillText(`WATER HEAD: ${(14.2-8*a).toFixed(1)} FT BELOW SILL`,-50,3),e.fillText(`FROST LINE: ${(48+6*a).toFixed(0)} IN DEPTH`,-50,18),e.fillText(`PERIM GRAD: ${(1.5+1.5*a).toFixed(1)}% AWAY`,-50,33)}else if(3===t){e.fillText("SPEC: OVERSHOT FLUME VELOCITY & CROWN TORQUE",-128,-38);const t=-95,o=12;y(e,t,o,26,16,a*Math.PI*3,"#ffffff");for(let a=0;a<2;a++){e.strokeStyle=0===a?"rgba(255, 255, 255, 0.4)":"#ffffff",e.lineWidth=0===a?2:1.5,e.beginPath();for(let a=0;a<=12;a++){const r=a*Math.PI*2/12,n=t+12*Math.cos(r)+(a>0&&a<12?.4*Math.sin(a):0),i=o+12*Math.sin(r)+(a>0&&a<12?.4*Math.cos(a):0);0===a?e.moveTo(n,i):e.lineTo(n,i)}e.stroke()}e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("FLUME VEL : 42.5 CFS EQUIV",-40,-12),e.fillText("HEAD DROP : 14.0 FT NET",-40,3),e.fillText(`WHEEL RPM : ${(8+12*a).toFixed(1)} RPM`,-40,18),e.fillText(`SHAFT TORQ: ${(420+380*a).toFixed(0)} FT-LBS`,-40,33)}else if(4===t){e.fillText("SPEC: STEAM GEAR RATIO & PUMP KINEMATICS",-128,-38);const t=-95,o=12;y(e,t,o,24,12,a*Math.PI*4,"#ffffff"),y(e,t+36,o+8,16,8,-a*Math.PI*6,"#cccccc"),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("RATIO : 3:2 REDUCTION DRIVE",-35,-12),e.fillText(`PUMP RATE : ${(12+18*a).toFixed(1)} GAL/MIN`,-35,3),e.fillText(`BOILER PSI: ${(45+40*a).toFixed(0)} PSI NET`,-35,18),e.fillText(`STROKE VEL: ${(.8+1.2*a).toFixed(2)} M/SEC`,-35,33)}else if(5===t){e.fillText("SPEC: SPINDLE LATHE RPM & GAS MANIFOLD PSI",-128,-38);const t=-95,o=12;for(let a=0;a<2;a++){e.strokeStyle=0===a?"rgba(204, 204, 204, 0.4)":"#cccccc",e.lineWidth=0===a?1.8:1.2,e.beginPath();for(let a=0;a<=14;a++){const r=a*Math.PI*2/14,n=t+24*Math.cos(r)+(a>0&&a<14?.5*Math.sin(a):0),i=o+24*Math.sin(r)+(a>0&&a<14?.5*Math.cos(a):0);0===a?e.moveTo(n,i):e.lineTo(n,i)}e.stroke()}h(e,t-20,o,t+20,o,.3,1.5,{color:"#ffffff",overshoot:1.6}),h(e,t,o-20,t,o+20,.3,1.5,{color:"#ffffff",overshoot:1.6}),e.fillStyle="#ffffff",e.beginPath(),e.arc(t+14*Math.cos(a*Math.PI*6),o+14*Math.sin(a*Math.PI*6),4,0,2*Math.PI),e.fill(),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("SPINDLE : STEAM LATHE TURNED",-40,-12),e.fillText(`LATHE VEL : ${(850+400*a).toFixed(0)} RPM`,-40,3),e.fillText(`GAS PRESS : ${(3.2+.6*a).toFixed(2)} PSI`,-40,18),e.fillText(`ILLUM LUX : ${(120+80*a).toFixed(0)} LUMENS`,-40,33)}else if(6===t){e.fillText("SPEC: IRRIGATION SLUICE & WEIR REGULATOR",-128,-38);const t=-110,o=-12,r=45,n=52;u(e,t,o,r,n,2,{color:"#cccccc"});const i=o+n-36*a;e.fillStyle="rgba(255, 255, 255, 0.25)",e.fillRect(t,i,r,n-(i-o)),h(e,t,i,t+r,i,.4,2,{color:"#ffffff",overshoot:2.2}),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText(`GATE APERTURE: ${(24*a).toFixed(1)} IN OPEN`,-50,-12),e.fillText(`FLOW VELOCITY: ${(1.2+3.4*a).toFixed(2)} FT/SEC`,-50,3),e.fillText(`HEAD DROP : ${(6.4-2.1*a).toFixed(1)} FT NET`,-50,18),e.fillText(`DISCHARGE : ${(450*a).toFixed(0)} GPM`,-50,33)}else if(7===t){e.fillText("SPEC: AC WATT-HOUR DISK & BOILER BTU OUTPUT",-128,-38);const t=-95,o=12;for(let a=0;a<2;a++){e.strokeStyle=0===a?"rgba(255, 255, 255, 0.4)":"#ffffff",e.lineWidth=0===a?2:1.5,e.beginPath();for(let a=0;a<=14;a++){const r=a*Math.PI*2/14,n=t+22*Math.cos(r)+(a>0&&a<14?.4*Math.sin(a):0),i=o+22*Math.sin(r)+(a>0&&a<14?.4*Math.cos(a):0);0===a?e.moveTo(n,i):e.lineTo(n,i)}e.stroke()}y(e,t,o,14,8,a*Math.PI*8,"#cccccc"),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("VOLTAGE : 110V 60HZ SINGLE PH.",-45,-12),e.fillText(`METER DISK: ${(12+48*a).toFixed(1)} RPM SPIN`,-45,3),e.fillText(`BOILER OUT: ${(65+55*a).toFixed(0)} K-BTU/HR`,-45,18),e.fillText(`WATER TEMP: ${(140+45*a).toFixed(0)} °F RAD`,-45,33)}else if(8===t){e.fillText("SPEC: STRUCTURAL STRAIN & DEFLECTION",-128,-38);const t=-110,o=12;h(e,t-20,o,t+20,o,.3,1,{color:"#cccccc",overshoot:1.5}),e.save(),e.strokeStyle="#ffffff",e.lineWidth=2,e.beginPath();for(let r=0;r<=6;r++){const n=r/6,i=t-20+40*n,s=o+Math.sin(n*Math.PI)*a*18+(r>0&&r<6?.5*Math.sin(2*r):0);0===r?e.moveTo(i,s):e.lineTo(i,s)}e.stroke(),e.restore(),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText(`MAX DEFLECTION: ${(4.8*a).toFixed(2)} IN DOWN`,-50,-12),e.fillText("SPAN LENGTH : 24'-0\" CLEAR",-50,3),e.fillText(`LOAD RATIO : ${(1.2+.8*a).toFixed(2)}x SAFE LIMIT`,-50,18),e.fillText(`STRAIN INDEX : ${(82+65*a).toFixed(0)} MICRO-STR`,-50,33)}else if(9===t){e.fillText("SPEC: SCREW-JACK AXIAL LOAD & SHORING VECTOR",-128,-38);const t=-95,o=12;u(e,t-12,o-22,24,44,2,{color:"#ffffff"}),p(e,t-12,o-22,24,44),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("SHORING : STEEL SCREW-JACK",-50,-12),e.fillText(`AXIAL LOAD: ${(8.2+6*a).toFixed(1)} TONS NET`,-50,3),e.fillText(`TORQUE ADV: ${(12+18*a).toFixed(1)} FT-LBS`,-50,18),e.fillText(`DEFLECT RT: ${(1.8-1.8*a).toFixed(2)} MM/YR`,-50,33)}else if(10===t){e.fillText("SPEC: 12-TON HYDRAULIC LIFT CALIBRATION",-128,-38);R(e,-95,12,24,"DATUM","#ffffff"),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText(`JACK RAM PRESS: ${(3400*a).toFixed(0)} PSI`,-40,-12),e.fillText(`VERTICAL LIFT : ${(3.75*a).toFixed(2)} IN UP`,-40,3),e.fillText(`SILL DATUM ERR: ${(1.2-1.2*a).toFixed(2)} DEGREES`,-40,18),e.fillText(`BEAM LOAD TRSF: ${(100*a).toFixed(0)}% TO STEEL`,-40,33)}else if(11===t){e.fillText("SPEC: SUB-SLAB RADON SUCTION & VACUUM IN. WG",-128,-38);const t=-95,o=12;for(let a=0;a<2;a++){e.strokeStyle=0===a?"rgba(255, 255, 255, 0.4)":"#ffffff",e.lineWidth=0===a?2:1.5,e.beginPath();for(let a=0;a<=14;a++){const r=a*Math.PI*2/14,n=t+22*Math.cos(r)+(a>0&&a<14?.4*Math.sin(a):0),i=o+22*Math.sin(r)+(a>0&&a<14?.4*Math.cos(a):0);0===a?e.moveTo(n,i):e.lineTo(n,i)}e.stroke()}y(e,t,o,15,6,a*Math.PI*6,"#cccccc"),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("SUCTION : SUB-SLAB ACTIVE CORE",-45,-12),e.fillText(`VACUUM WG : ${(-.4-1.4*a).toFixed(2)} IN. WG`,-45,3),e.fillText(`FAN EXHAUST: ${(40+105*a).toFixed(0)} CFM`,-45,18),e.fillText(`RADON LEVEL: ${(18.4-17.6*a).toFixed(1)} PCI/L`,-45,33)}else if(12===t){e.fillText("SPEC: THERMODYNAMIC ENERGY EXCHANGE",-128,-38);const t=-110,o=-15,r=45,n=50;u(e,t,o,r,n,2,{color:"#cccccc"}),m(e,t,o,r,n,a,"rgba(255, 255, 255, 0.45)"),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText(`ENVELOPE R-VAL: R-${(12+33*a).toFixed(0)} FOAM`,-50,-12),e.fillText(`AIR LEAKAGE : ${(12.5-11.8*a).toFixed(2)} ACH50`,-50,3),e.fillText(`GLAZING U-FAC : ${(.35-.22*a).toFixed(3)} BTU`,-50,18),e.fillText(`LOOP COP EFF : ${(2.1+2.4*a).toFixed(1)} COP`,-50,33)}else if(13===t){e.fillText("SPEC: CANTILEVER MOMENT & V2H BATTERY",-128,-38);const t=-110,o=10;h(e,t-15,o+15,t+15,o-15,.4,2,{color:"#ffffff",overshoot:2}),e.fillStyle="#ffffff",e.beginPath(),e.arc(t,o,4,0,2*Math.PI),e.fill(),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText(`MOMENT LOAD : ${(120+180*a).toFixed(0)} KIP-FT`,-50,-12),e.fillText(`PV GENERATION: ${(4.2+11.8*a).toFixed(1)} KW PEAK`,-50,3),e.fillText(`V2H STORAGE : ${(28+72*a).toFixed(0)}% CHARGED`,-50,18),e.fillText(`NET CARBON : ${(-15*a).toFixed(0)}% (POSITIVE)`,-50,33)}else if(14===t){e.fillText("SPEC: BOUSSINESQ STRESS BULB & SOIL STRATA",-128,-38);const t=-95,o=12;for(let a=8;a<=24;a+=8)for(let r=0;r<2;r++){e.strokeStyle=0===r?"rgba(204, 204, 204, 0.35)":"#cccccc",e.lineWidth=0===r?1.6:1,e.beginPath();for(let r=0;r<=10;r++){const n=r*Math.PI/10;let i=t+Math.cos(n)*a,s=o+Math.sin(n)*a;r>0&&r<10&&(i+=.4*Math.sin(r+a),s+=.4*Math.cos(r+a)),0===r?e.moveTo(i,s):e.lineTo(i,s)}e.stroke()}e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("STRATUM : COMPACTED GLACIAL TILL",-50,-12),e.fillText("SAFE BEARING: 3.5 TONS / SQ. FT.",-50,3),e.fillText(`STRESS ATTEN: ${(100-75*a).toFixed(0)}% @ -12 FT`,-50,18),e.fillText(`HYDRO HEAD : ${(4.5-4.5*a).toFixed(1)} PSI NET`,-50,33)}else if(15===t){e.fillText("SPEC: DEW-POINT PLANE & INFRARED FLUX MATRIX",-128,-38);const t=-95,o=12;u(e,t-16,o-16,32,32,2,{color:"#ffffff"}),m(e,t-16,o-16,32,32,a,"rgba(255, 255, 255, 0.4)"),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("THERMAL FLUX: FINITE ELEMENT MODEL",-45,-12),e.fillText(`U-FACTOR NET: ${(.08-.058*a).toFixed(3)} BTU/HR`,-45,3),e.fillText("DEW-POINT : EXTERIOR VAPOR BARRIER",-45,18),e.fillText(`HEAT LOSS : -${(60+34.2*a).toFixed(1)}% REDUCTION`,-45,33)}else if(16===t){e.fillText("SPEC: WIND MOMENT SHEAR DEFLECTION VECTOR",-128,-38);const t=-95,o=12;h(e,t-18,o+18,t+18*a,o-18,.4,2,{color:"#ffffff",overshoot:2}),u(e,t-6,o-6,12,12,1.5,{color:"#ffffff"}),e.font=l,e.letterSpacing="0em",e.fillStyle="#cccccc",e.fillText("MECHANICS : DYNAMIC VECTOR EQUILIB.",-45,-12),e.fillText("WIND SHEAR: 90 MPH GUST DESIGN",-45,3),e.fillText(`STORY DRIFT: ${(.24*a).toFixed(2)} IN. (L/360)`,-45,18),e.fillText(`LIVE RATIO : ${(40+10*a).toFixed(0)} PSF DESIGN`,-45,33)}e.restore(),e.restore()}const x=[{initSVG:(e,t,a)=>{S(e,25,50,240,270,"VIRGIN HEMLOCK CANOPY","98% OLD-GROWTH DENSITY","stage0-callout1",0),S(e,775,50,560,280,"FIELDSTONE DRY-STACK","GLACIAL ERRATIC CLEARING","stage0-callout2",0),S(e,25,235,280,360,"HAND-HEWN TIMBER SILLS","BROADAXE MORTISE JOINING","stage0-callout3",1),S(e,775,250,520,370,"CLAY & STRAW CHINKING","THERMAL MASS INSULATION","stage0-callout4",1),S(e,25,420,160,470,"SUBTERRANEAN DUG WELL","TAPPING SURFACE WATER TABLE","stage0-callout5",2),S(e,775,420,500,470,"PITCH-ROOF SHAKES","HAND-SPLIT WHITE CEDAR","stage0-callout6",2),S(e,25,520,180,520,"SPLIT-RAIL WORM FENCE","NO POST-HOLE DIGGING","stage0-callout7",3),S(e,775,520,560,500,"CENTRAL HEATING CORE","RADIANT THERMAL CHIMNEY","stage0-callout8",4),v(e,"EARLY SETTLEMENT HOMESTEAD","A-101","1780",'SCALE: 3/8" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=1)for(let t=0;t<5;t++){b(e,260+65*t,340-Math.min(s,25),45,20,5,{color:"#cccccc"})}if(n.idx>=2&&I(e,250,350,550,350,"30'-0\" FRONTAGE","#ffffff",12,"#000000"),n.idx>=3){const t=340-s-60*i;h(e,230,340-s,400,t,.4,2,{color:"#ffffff",overshoot:2.8}),h(e,570,340-s,400,t,.4,2,{color:"#ffffff",overshoot:2.8}),h(e,230,340-s,570,340-s,.4,1.8,{color:"#cccccc",overshoot:2.2})}}},{initSVG:(e,t,a)=>{S(e,25,50,240,280,"SEASONED WHITE OAK","HEAVY TIMBER STRUCTURAL GIRTS","stage1-callout1",0),S(e,775,50,560,300,"SCRIBED GIRT CONNECTIONS","GROUND FITTED ALIGNMENT","stage1-callout2",0),S(e,25,235,340,320,"T-HANDLE AUGER BORING","DEEP CIRCULAR MORTISE CAVITY","stage1-callout3",1),S(e,775,250,460,330,"FRAMING CHISEL CLEARING","RECTANGULAR SLOT SHAPING","stage1-callout4",1),S(e,25,420,300,380,"INTERLOCKING TENONS","NO METAL HARDWARE REQ.","stage1-callout5",2),S(e,775,420,500,390,"DRAW-BORE TREENAILS","OFFSET HOLE TENSION PEG","stage1-callout6",3),S(e,25,520,260,450,"COMMUNAL BENT RAISING","GIN POLE & PULLEY TACKLE","stage1-callout7",4),v(e,"MORTISE-AND-TENON FRAMING","A-201","1795",'SCALE: 1" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=1&&(y(e,360,310,28,8,a*Math.PI*6,"#ffffff"),h(e,360,310,360,370+30*i,.4,2,{color:"#ffffff",overshoot:1.6})),n.idx>=2){const t=310+90*i;T(e,t,290,100,40,4,2),u(e,t,290,100,40,2.2,{color:"#ffffff"})}if(n.idx>=3)for(let t=0;t<6;t++){const a=330+25*t,o=260+45*i;e.fillStyle="#0a0a0a",e.fillRect(a,o,6,22),u(e,a,o,6,22,1.2,{color:"#ffffff"})}if(n.idx>=4)for(let t=0;t<5;t++){const a=180+90*t,o=220-40*i;h(e,a,o,a+60,o-30,.4,2,{color:"#cccccc",overshoot:2.2})}}},{initSVG:(e,t,a)=>{S(e,25,50,260,320,"HYDROLOGICAL PROFILE","PERMEABLE GRAVEL VS BEDROCK","stage2-callout1",0),S(e,775,50,540,320,"GLACIAL TILL STRATA","NATURAL FILTER BED","stage2-callout2",0),S(e,25,235,280,380,"SPRING HOUSE STRUCTURE","YEAR-ROUND 48°F WATER","stage2-callout3",1),S(e,775,250,520,390,"FROST LINE DATUM","36-INCH FOOTING DEPTH","stage2-callout4",1),S(e,25,420,200,465,"GRAVITY DRAINAGE TRENCH","PERIMETER HYDROLOGICAL DIVERSION","stage2-callout5",2),S(e,775,420,500,465,"GRANITE LEDGE OUTCROP","NATURAL FOUNDATION FOOTING","stage2-callout6",2),S(e,25,520,220,510,"SUBTERRANEAN STRATA MAP","+420M ASL DATUM","stage2-callout7",3),S(e,775,520,560,510,"STEPPED CELLAR ROOTS","THERMAL STABILITY 50°F","stage2-callout8",4),v(e,"GEOLOGY & SUBTERRANEAN DRAINAGE","A-301","1810",'SCALE: 1/4" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=1){h(e,280,380,520,380,.4,2,{color:"#cccccc",overshoot:2.4});for(let t=0;t<6;t++)b(e,290+35*t,385,30,28,4,{color:"#cccccc"})}n.idx>=2&&I(e,140,350,140,470,"12'-0\" STRATA DEPTH","#ffffff",-16,"#000000")}},{initSVG:(e,t,a)=>{S(e,25,50,220,260,"PERENNIAL STREAM DAM","REGULATED HYDRAULIC MILLRACE","stage3-callout1",0),S(e,775,50,560,270,"ELEVATED WOODEN FLUME","MAXIMIZED GRAVITATIONAL HEAD","stage3-callout2",0),S(e,25,235,260,380,"16-BLADE OVERSHOT WHEEL","KINETIC MOMENTUM OAK AXLE","stage3-callout3",1),S(e,775,250,520,380,"SLUICE WEIR GATE LEVER","RACK-AND-PINION FLOW CONTROL","stage3-callout4",1),S(e,25,420,310,440,"TRUNDLE LANTERN CROWN GEAR","90 DEGREE TORQUE REDIRECTION","stage3-callout5",2),S(e,775,420,500,440,"RECIPROCATING VERTICAL SAW","AUTOMATED TIMBER MILLING","stage3-callout6",3),S(e,25,520,280,510,"TWELVEFOLD PRODUCTIVITY","REGIONAL AGRARIAN HUB","stage3-callout7",4),v(e,"WATERCOURSE & MILLWRIGHT","A-401","1835",'SCALE: 1/2" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t{S(e,25,50,240,280,"BALLOON FRAMING REVOLUTION","LIGHTWEIGHT 2X4 STUDS","stage4-callout1",0),S(e,775,50,560,290,"STANDARDIZED DIMENSION LUMBER","SAWMILL MASS PRODUCTION","stage4-callout2",0),S(e,25,235,280,360,"CAST IRON HEATING STOVE","COAL FUEL TRANSITION","stage4-callout3",1),S(e,775,250,500,370,"MACHINE-CUT STEEL NAILS","INSTANT JOINERY REPLACEMENT","stage4-callout4",1),S(e,25,420,260,420,"STEAM PUMP INFRASTRUCTURE","AQUIFER PLUMBING SANITATION","stage4-callout5",2),S(e,775,420,500,420,"DOUBLE-HUNG GLASS WINDOWS","PULLEY & COUNTERWEIGHT","stage4-callout6",2),S(e,25,520,240,500,"WROUGHT IRON TIE-RODS","ADJUSTABLE TURNBUCKLE","stage4-callout7",3),S(e,775,520,540,500,"MECHANIZED WELL PUMP","INDOOR SANITATION HYGIENE","stage4-callout8",4),v(e,"INDUSTRIAL REVOLUTION FRAMING","A-501","1860",'SCALE: 3/8" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=1&&(p(e,450,280,60,80),u(e,450,280,60,80,2.2,{color:"#ffffff"}))}},{initSVG:(e,t,a)=>{S(e,25,50,230,270,"QUEEN ANNE ORNAMENTATION","MULTI-GABLED ROOF PROFILE","stage5-callout1",0),S(e,775,50,550,280,"SPINDLE LATHE BALUSTERS","HIGH-SPEED TURNED MILLWORK","stage5-callout2",0),S(e,25,235,270,350,"FISH-SCALE SHINGLE SKIN","CEDAR PATTERN SHADOW LINES","stage5-callout3",1),S(e,775,250,520,360,"CHAMFERED PORCH COLUMNS","GINGERBREAD GABLE SCROLLWORK","stage5-callout4",1),S(e,25,420,300,440,"BLACK-IRON GAS MANIFOLD","THREADED CENTRAL FUEL PIPE","stage5-callout5",2),S(e,775,420,500,440,"BRASS PARLOR SCONCES","EVENING ILLUMINATION NETWORK","stage5-callout6",3),S(e,25,520,280,510,"EMBOSSED WALLCOVERINGS","PLASTER CEILING MEDALLIONS","stage5-callout7",4),v(e,"VICTORIAN ORNAMENTAL MILLWORK","A-601","1885",'SCALE: 3/4" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t{S(e,25,50,240,270,"TIMBER BANK BARN EXPANSION","GRAVITY-FED HAY LOFT","stage6-callout1",0),S(e,775,50,540,280,"GALVANIZED STEEL WINDMILL","DEEP AQUIFER PUMPING","stage6-callout2",0),S(e,25,235,260,360,"SILO FERMENTATION TOWER","ENSILAGE WINTER STORAGE","stage6-callout3",1),S(e,775,250,500,370,"SOIL DRAINAGE TILES","TERRACOTTA PERIMETER LOOP","stage6-callout4",1),S(e,25,420,220,470,"IRRIGATION SLUICE CHANNELS","PERENNIAL STREAM HARNESSING","stage6-callout5",2),S(e,775,420,500,470,"CONCRETE FLOORING SLABS","SANITARY DAIRY PRODUCTION","stage6-callout6",2),S(e,25,520,200,510,"POLLINATOR CAVITIES","STONE WALL WILDLIFE HABITAT","stage6-callout7",3),S(e,775,520,560,510,"HEIRLOOM ORCHARD FURROWS","45 CULTIVATED ACRES","stage6-callout8",4),v(e,"GRADUAL FARM IMPROVEMENTS","A-701","1900",'SCALE: 1/4" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=1&&(y(e,540,260,45,12,a*Math.PI*4,"#ffffff"),h(e,540,260,540,350,.4,2,{color:"#ffffff",overshoot:2}))}},{initSVG:(e,t,a)=>{S(e,25,50,240,270,"PORCELAIN KNOB-AND-TUBE","CERAMIC JOIST INSULATORS","stage7-callout1",0),S(e,775,50,540,280,"COPPER CONDUCTOR WIRING","SPARK ARC PREVENTATIVE CLEAT","stage7-callout2",0),S(e,25,235,280,360,"CAST-IRON HOT WATER BOILER","COAL-FIRED BASEMENT FURNACE","stage7-callout3",1),S(e,775,250,500,370,"HYDRONIC RADIATOR FINS","FLUTED CONVECTIVE SURFACE","stage7-callout4",1),S(e,25,420,320,440,"WATT-HOUR INDUCTION DISK","SPINNING ELECTROMECHANICAL METER","stage7-callout5",2),S(e,775,420,480,440,"CLOSED-LOOP IRON PIPING","UNIFORM DRAFT-FREE WARMTH","stage7-callout6",3),S(e,25,520,280,510,"110V AC SINGLE-PHASE","MODERN ENERGY ECONOMY","stage7-callout7",4),v(e,"ELECTRIFICATION & HYDRONIC HEATING","A-801","1925",'SCALE: 1/2" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=1&&h(e,o+9,274,o+55+9,274+6*Math.sin(20*a+t),.6,2,{color:"#ffffff",overshoot:1.6})}for(let t=0;t<7;t++){const o=200+16*t,r=310-40*a;h(e,o,320,o+12*Math.sin(10*a+t),r,.4,1.5,{color:"#cccccc",overshoot:1.5})}}},{initSVG:(e,t,a)=>{S(e,25,50,240,270,"ROOF RIDGE SAG & COLLAPSE","STRUCTURAL RIDGE DEFLECTION","stage8-callout1",0),S(e,775,50,540,280,"WATER INFILTRATION ROT","FUNGAL DECAY OF HEMLOCK GIRTS","stage8-callout2",0),S(e,25,235,260,360,"COLLAPSED FLUME TRESTLE","DISRUPTED HYDRAULIC POWER","stage8-callout3",1),S(e,775,250,500,370,"FROST-HEAVED DRY-STACK","LATERAL FOUNDATION BULGING","stage8-callout4",1),S(e,25,420,200,460,"FOUNDATION TREE ROOTS","BIOLOGICAL MASONRY DISRUPTION","stage8-callout5",2),S(e,775,420,500,460,"RUSTED IRON TIE-RODS","TENSILE FAILURE & SEPARATION","stage8-callout6",2),S(e,25,520,220,510,"SEVERE MORTAR LEACHING","ACIDIC RAINFALL EROSION","stage8-callout7",3),S(e,775,520,540,510,"BOTANICAL CANOPY OVERGROWTH","PIONEER SAPLING WEDGING","stage8-callout8",4),v(e,"DECLINE & STRUCTURAL WEATHERING","A-901","1950",'SCALE: 3/8" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=1&&(E(e,180,360,120,60,"rgba(255, 255, 255, 0.4)"),u(e,180,360,120,60,2,{color:"#cccccc"}))}},{initSVG:(e,t,a)=>{S(e,25,50,240,270,"IMMINENT COLLAPSE ARREST","PRESERVATIONIST INTERVENTION","stage9-callout1",0),S(e,775,50,540,280,"SCREW-JACK STEEL LALLY","ADJUSTABLE THREADED COLLAR","stage9-callout2",0),S(e,25,235,280,360,"SAGGING SUMMER BEAMS","UPWARD JACKING FORCE HALT","stage9-callout3",1),S(e,775,250,500,370,"TEMPORARY CONCRETE PADS","DIRT FLOOR LOAD TRANSFER","stage9-callout4",1),S(e,25,420,320,440,"STRUCTURAL STRAPPING","BULGING MASONRY CONTAINMENT","stage9-callout5",2),S(e,775,420,480,440,"EXTERNAL WATT-HOUR METER","WEATHERPROOF TEMPORARY POWER","stage9-callout6",3),S(e,25,520,280,510,"HEAVY TAR CANVAS SHEETING","RAPID ROOF WEATHERPROOFING","stage9-callout7",4),v(e,"EMERGENCY SHORING & STABILIZATION","A-1001","1965",'SCALE: 3/8" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=3&&(g(e,180,200,440,35,!0),u(e,180,200,440,35,2.2,{color:"#cccccc"}))}},{initSVG:(e,t,a)=>{S(e,25,50,240,270,"GEOTECHNICAL CORE SAMPLING","LOAD-BEARING STRATA VERIFIED","stage10-callout1",0),S(e,775,50,540,280,"STRUCTURAL FINITE ELEMENT","COMPUTERIZED DEFLECTION MODEL","stage10-callout2",0),S(e,25,235,260,360,"HYDRAULIC LIFTING RE-LEVEL","PRECISION 12-TON JACKING","stage10-callout3",1),S(e,775,250,500,370,"REINFORCED CONCRETE UNDERPIN","4,000 PSI REBAR FOUNDATION","stage10-callout4",1),S(e,25,420,200,465,"EPOXY RESIN SPLICING","SILL REPAIR & STRUCTURAL GRAFT","stage10-callout5",2),S(e,775,420,500,465,"PERMEABLE MEMBRANE ENVELOPE","BREATHABLE AIR SEALING","stage10-callout6",2),S(e,25,520,220,510,"CONCEALED STEEL LINTEL","C-CHANNEL REINFORCEMENT","stage10-callout7",3),S(e,775,520,540,510,"LASER TRANSIT REALIGNMENT","CODE COMPLIANCE DATUM","stage10-callout8",4),v(e,"MODERNIZATION & STABILIZATION","A-1101","1980",'SCALE: 3/8" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=1&&R(e,400,280-s,20,"DATUM +0.00","#ffffff")}},{initSVG:(e,t,a)=>{S(e,25,50,240,270,"SUB-SLAB RADON SUCTION PIT","INTERCEPTING BEDROCK SOIL GAS","stage11-callout1",0),S(e,775,50,540,280,"PERFORATED COLLECTION PIPES","WASHED DRAINAGE GRAVEL BED","stage11-callout2",0),S(e,25,235,280,360,"CONTINUOUS PVC EXHAUST FAN","PERMANENT NEGATIVE PRESSURE","stage11-callout3",1),S(e,775,250,500,370,"PERFORATED PVC FRENCH DRAIN","GEOTEXTILE FABRIC WRAPPED","stage11-callout4",1),S(e,25,420,320,440,"-12 FT BELOW GRADE FOOTING","HYDROSTATIC VECTOR RELIEF","stage11-callout5",2),S(e,775,420,480,440,"DAYLIGHT DISCHARGE BASIN","GRAVITY MEADOW DIVERSION","stage11-callout6",3),S(e,25,520,280,510,"BONE-DRY GRANITE CELLAR","INDOOR AIR QUALITY SAFE","stage11-callout7",4),v(e,"RADON MITIGATION & FRENCH DRAINAGE","A-1201","1995",'SCALE: 1/2" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t{S(e,25,50,240,270,"CLOSED-CELL SPRAY FOAM","R-45 CONTINUOUS CAVITY SEAL","stage12-callout1",0),S(e,775,50,540,280,"MINERAL WOOL ACOUSTIC BATT","FIRE-RETARDANT & SOUNDPROOF","stage12-callout2",0),S(e,25,235,260,360,"TRIPLE-PANE LOW-E GLAZING","ARGON GAS THERMAL BREAK","stage12-callout3",1),S(e,775,250,500,370,"HEAT RECOVERY VENTILATION","92% THERMAL EXCHANGE EFFICIENCY","stage12-callout4",1),S(e,25,420,220,480,"GEOTHERMAL HEAT LOOP","400 FT BEDROCK BOREHOLE","stage12-callout5",2),S(e,775,420,500,480,"SOLAR THERMAL MANIFOLD","HYDRONIC RADIANT FLOOR SUPPLY","stage12-callout6",2),S(e,25,520,200,510,"ELASTOMERIC VAPOR BARRIER","ZERO-DRAFT ENCAPSULATION","stage12-callout7",3),S(e,775,520,540,510,"RADIANT CLIMATE CONTROL","U-FACTOR 0.022 NET","stage12-callout8",4),v(e,"ECOLOGICAL & THERMAL RETROFIT","A-1301","2010",'SCALE: 1/2" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=1&&y(e,500,280,35,12,a*Math.PI*4,"#ffffff")}},{initSVG:(e,t,a)=>{S(e,25,50,240,270,"HISTORIC POST-AND-BEAM EXPOSED","PRESERVED 1780 HEWN TIMBERS","stage13-callout1",0),S(e,775,50,540,280,"STRUCTURAL GLASS CURTAIN WALL","FRAMELESS STEEL SPIDER JOINTS","stage13-callout2",0),S(e,25,235,260,360,"CANTILEVERED STEEL MEZZANINE","FLOATING LOFT ARCHITECTURE","stage13-callout3",1),S(e,775,250,500,370,"INTEGRATED SMART-HOME DATUM","AUTOMATED SHADING & CLIMATE","stage13-callout4",1),S(e,25,420,260,460,"GEOTHERMAL & HRV SYSTEMS","HYDRONIC RADIANT HEATING","stage13-callout5",2),S(e,775,420,500,460,"RECLAIMED OAK PLANK FLOORING","SUSTAINABLE MATERIAL REUSE","stage13-callout6",2),S(e,25,520,240,510,"BI-DIRECTIONAL V2H BATTERY","MICROGRID ENERGY STORAGE","stage13-callout7",3),S(e,775,520,540,510,"NET-NEGATIVE CARBON OP.","POSITIVE ENERGY GENERATION","stage13-callout8",4),v(e,"PRESENT MODERN DESIGN","A-1401","2026",'SCALE: 3/8" = 1\'-0"',"AUG 1952")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t=1&&I(e,200,360,600,360,"MODERN HOMESTEAD","#ffffff",-20,"#000000")}},{initSVG:(e,t,a)=>{S(e,25,50,240,270,"BOUSSINESQ STRESS BULB","SUBTERRANEAN LOAD PROPAGATION","stage14-callout1",0),S(e,775,50,540,280,"3.5 TONS/SQ. FT. BEARING","COMPACTED GLACIAL TILL STRATA","stage14-callout2",0),S(e,25,235,280,360,"HYDROSTATIC HEAD VECTOR","-12 FT BELOW GRADE PRESSURE","stage14-callout3",1),S(e,775,250,500,370,"WASHED GRAVEL BACKFILL","LATERAL SHEAR DISSIPATION","stage14-callout4",1),S(e,25,420,320,440,"ZERO DIFFERENTIAL SETTLE.","MULTI-STORY LOAD STABILITY","stage14-callout5",2),S(e,775,420,480,440,"BORE LOG DIAGNOSTICS","METAMORPHIC GRANITE SHELF","stage14-callout6",3),S(e,25,520,280,510,"PERPETUAL SOIL INTEGRITY","GEOTECHNICAL DRILL-DOWN","stage14-callout7",4),v(e,"GEOTECHNICAL SOIL STRATA PROFILE","A-1501","2026",'SCALE: 1/4" = 1\'-0"',"JUL 2026")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t0&&a<16&&(n+=.5*Math.sin(a+t),i+=.5*Math.cos(a+t)),0===a?e.moveTo(n,i):e.lineTo(n,i)}e.stroke()}for(let t=0;t<6;t++){const o=320+30*t;I(e,140,o,140+60*a,o,`HYDRO VECTOR ${t+1}`,"#ffffff",4,"#000000")}}},{initSVG:(e,t,a)=>{S(e,25,50,240,270,"DEW-POINT CONDENSATION TRACK","EXTERIOR VAPOR BARRIER FACE","stage15-callout1",0),S(e,775,50,540,280,"INFRARED FLUX MATRIX","ZERO THERMAL BRIDGING OVERLAY","stage15-callout2",0),S(e,25,235,280,360,"R-45 CLOSED-CELL FOAM","INTERSTITIAL ROT PREVENTION","stage15-callout3",1),S(e,775,250,500,370,"FINITE ELEMENT MODELING","U-FACTOR 0.022 NET RATING","stage15-callout4",1),S(e,25,420,320,440,"MULTI-LAYER VAPOR GRADIENT","WOOD FIBER SHEATHING BOARD","stage15-callout5",2),S(e,775,420,480,440,"94.2% HEAT LOSS REDUCTION","ADVANCED BUILDING PHYSICS","stage15-callout6",3),S(e,25,520,280,510,"THERMODYNAMIC MASTERY","ULTRA-LOW-ENERGY CLIMATE","stage15-callout7",4),v(e,"THERMODYNAMIC FLUX & DEW-POINT PLANE","A-1601","2026",'SCALE: 3/4" = 1\'-0"',"JUL 2026")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t{S(e,25,50,240,270,"DEAD VS LIVE LOAD VECTORS","32 PSF PERMANENT / 40 PSF LIVE","stage16-callout1",0),S(e,775,50,540,280,"WIND MOMENT SHEAR DEFLECTION","90 MPH GUST OVERTURNING MOMENT","stage16-callout2",0),S(e,25,235,280,360,"HYBRID STEEL MOMENT FRAME","BRACED TIMBER SHEAR WALLS","stage16-callout3",1),S(e,775,250,500,370,"OPTICAL STRAIN GAUGE DATUM","STORY DRIFT INDEX L/360","stage16-callout4",1),S(e,25,420,320,440,"MAX DEFLECTION < 0.25 INCHES","ELASTIC TOLERANCE COMPLIANT","stage16-callout5",2),S(e,775,420,480,440,"GRAVITY LOAD EQUILIBRIUM","POST DOWN TO GRANITE FOOTING","stage16-callout6",3),S(e,25,520,280,510,"STRUCTURAL KINEMATICS MATRIX","18TH CENTURY MEETS 21ST CENTURY","stage16-callout7",4),v(e,"STRUCTURAL KINEMATICS & SHEAR DRIFT","A-1701","2026",'SCALE: 3/8" = 1\'-0"',"JUL 2026")},updateSVG:(e,t,a,o,r)=>{},renderCanvas:(e,t,a,o,r,n)=>{for(let t=0;t0){const t=+e.replace("px","");return isNaN(t)?(err("offset value must be in 'px' format. Fallback to 0.5."),{format:"percent",value:.5}):{format:"pixels",value:t}}return"number"!=typeof e&&isNaN(+e)?null:(e>1&&err("offset value is greater than 1. Fallback to 1."),e<0&&err("offset value is lower than 0. Fallback to 0."),{format:"percent",value:Math.min(Math.max(0,e),1)})}function U(e){const{top:t}=e.getBoundingClientRect();return t+window.pageYOffset-(document.body.clientTop||0)}let F,B,H;function k(e){const t=e?e.scrollTop:window.pageYOffset;F!==t&&(F=t,F>B?H="down":F{},stepExit:()=>{},stepProgress:()=>{}},d=[]}function u(e){e&&!s&&R(),!e&&s&&A(),s=e}function g(e,t){const a=P(e),r=n[a];void 0!==t&&(r.progress=t);const i={element:e,index:a,progress:t,direction:H};"enter"===r.state&&o.stepProgress(i)}function p([e]){const t=P(e.target),a=n[t],o=e.target.offsetHeight;o!==a.height&&(a.height=o,T(a),b(a),y(a))}function m([e]){k(t);const{isIntersecting:a,target:r}=e;a?function(e){const t=P(e),a=n[t],r={element:e,index:t,direction:H};a.direction=H,a.state="enter",d[t]||o.stepEnter(r),f&&(d[t]=!0)}(r):function(e){const t=P(e),a=n[t];if(!a.state)return!1;const r={element:e,index:t,direction:H};l&&("down"===H&&a.progress<1?g(e,1):"up"===H&&a.progress>0&&g(e,0)),a.direction=H,a.state="exit",o.stepExit(r)}(r)}function E([e]){const t=P(e.target),a=n[t],{isIntersecting:o,intersectionRatio:r,target:i}=e;o&&"enter"===a.state&&g(i,r)}function T({observers:e}){Object.keys(e).map(t=>{e[t].disconnect()})}function A(){n.forEach(T)}function y(e){const t=new ResizeObserver(p);t.observe(e.node),e.observers.resize=t}function b(t){const o=window.innerHeight,n=t.offset||e,i="pixels"===n.format?1:o,s=n.value*i,l=t.height/2-s,f=t.height/2-(o-s),d=new IntersectionObserver(m,{rootMargin:`${l}px 0px ${f}px 0px`,threshold:.5,root:a});d.observe(t.node),t.observers.step=d,c&&M({id:r,step:t,marginTop:l})}function I(t){const a=window.innerHeight,o=t.offset||e,r="pixels"===o.format?1:a,n=o.value*r,s=`${-n+t.height}px 0px ${n-a}px 0px`,l=function(e,t){const a=Math.ceil(e/t),o=[],r=1/a;for(let e=0;e(function(e){F=0,B=0,document.addEventListener("scroll",()=>k(e))}(E),n=function(e,t=document){return"string"==typeof e?Array.from(t.querySelectorAll(e)):e instanceof Element?[e]:e instanceof NodeList?Array.from(e):e instanceof Array?e:[]}(o,r).map((e,t)=>({index:t,direction:void 0,height:e.offsetHeight,node:e,observers:{},offset:G(e.dataset.offset),top:U(e),progress:0,state:void 0})),n.length?(l=g,f=p,c=m,i=Math.max(1,+d),e=G(s),t=E,a=T,h(),function(e){e.forEach(e=>e.node.setAttribute("data-scrollama-index",e.index))}(n),u(!0),S):(D("no step elements"),S)),S.enable=()=>(u(!0),S),S.disable=()=>(u(!1),S),S.destroy=()=>(u(!1),h(),S),S.resize=()=>(R(),S),S.offset=t=>null==t?e.value:(e=G(t),R(),S),S.onStepEnter=e=>("function"==typeof e?o.stepEnter=e:D("onStepEnter requires a function"),S),S.onStepExit=e=>("function"==typeof e?o.stepExit=e:D("onStepExit requires a function"),S),S.onStepProgress=e=>("function"==typeof e?o.stepProgress=e:D("onStepProgress requires a function"),S),S}const W=Object.create(null),$={"step-1":["DETAIL A-1.1: DOVETAIL LOG CORNER NOTCH","SECTION B-1.2: DRY-LAID FIELDSTONE HEARTH SECTION"],"step-2":["DETAIL A-2.1: DRAW-BORE TREENAIL OFFSET JOINT","SECTION B-2.2: TIMBER BENT ASSEMBLY RIGGING"],"step-3":["DETAIL A-3.1: SUBTERRANEAN GRAVITY DRAIN TRENCH","SECTION B-3.2: SPRING HOUSE THERMAL ENVELOPE"],"step-4":["DETAIL A-4.1: OVERSHOT WATERWHEEL BUCKET PROFILE","SECTION B-4.2: TRUNDLE LANTERN CROWN GEAR MESH"],"step-5":["DETAIL A-5.1: BALLOON FRAMING STUD-TO-SILL JOINT","SECTION B-5.2: WROUGHT IRON TURNBUCKLE TIE-ROD"],"step-6":["DETAIL A-6.1: SPINDLE LATHE TURNED BALUSTER PROFILE","SECTION B-6.2: BLACK-IRON GAS MANIFOLD DISTRIBUTOR"],"step-7":["DETAIL A-7.1: TIMBER BANK BARN HAY LOFT HOIST","SECTION B-7.2: TERRACOTTA PERIMETER DRAINAGE TILE"],"step-8":["DETAIL A-8.1: PORCELAIN KNOB-AND-TUBE CERAMIC INSULATOR","SECTION B-8.2: CAST-IRON HYDRONIC RADIATOR FIN SECTION"],"step-9":["DETAIL A-9.1: STRUCTURAL RIDGE DEFLECTION & SAG","SECTION B-9.2: SEVERE MORTAR LEACHING & TREE ROOT INVASION"],"step-10":["DETAIL A-10.1: SCREW-JACK STEEL LALLY COLUMN THREAD","SECTION B-10.2: TEMPORARY CONCRETE FOOTING BEARING PAD"],"step-11":["DETAIL A-11.1: HYDRAULIC JACKING & RE-LEVELING DATUM","SECTION B-11.2: CONCEALED C-CHANNEL LINTEL REINFORCEMENT"],"step-12":["DETAIL A-12.1: SUB-SLAB RADON SUCTION PIT CORE","SECTION B-12.2: PERFORATED PVC FRENCH DRAINAGE LOOP"],"step-13":["DETAIL A-13.1: CLOSED-CELL SPRAY FOAM CAVITY SEAL","SECTION B-13.2: GEOTHERMAL BOREHOLE HEAT EXCHANGE LOOP"],"step-14":["DETAIL A-14.1: HISTORIC POST-AND-BEAM EXPOSED INTERIOR","SECTION B-14.2: BI-DIRECTIONAL V2H BATTERY STORAGE"],"step-15":["DETAIL A-15.1: BOUSSINESQ STRESS BULB SOIL ATTENUATION","SECTION B-15.2: HYDROSTATIC GROUNDWATER PRESSURE VECTOR"],"step-16":["DETAIL A-16.1: DEW-POINT CONDENSATION PLANE INTERSECTION","SECTION B-16.2: INFRARED FLUX MATRIX THERMAL BRIDGING ELIMINATION"],"step-17":["DETAIL A-17.1: DEAD VS LIVE LOAD VECTOR RESOLUTION","SECTION B-17.2: WIND MOMENT SHEAR DEFLECTION DRIFT (L/360)"]};function Y(e,t,a,o,r,n="#ffffff",i=1.8,s={}){const l=o-t,c=r-a,f=Math.sqrt(l*l+c*c);if(f<.1)return;let d=t,h=a,u=o,g=r;if(!1!==s.overrun){const e=l/f,n=c/f,i=1.5+1.5*Math.abs(Math.sin(.1*(11.3*t+29.1*a))),s=1.5+1.5*Math.abs(Math.cos(.1*(19.7*o+37.3*r)));d-=e*i,h-=n*i,u+=e*s,g+=n*s}const p=Math.max(3,Math.floor(f/12)),m=[{x:d,y:h}];for(let e=1;e0||it+2&&de.canvas.width-15&&i>8.5;)i-=.5,e.font=`bold ${i}px "Courier New", Courier, monospace`,s=e.measureText(t).width;e.fillStyle=r,e.shadowColor="rgba(255, 255, 255, 0.45)",e.shadowBlur=1.5,e.fillText(t,a,o),e.restore()}(n,a||"",15,24,"#ffffff",13),function(e,t,a){Y(e,40,.5*a,t-40,.5*a,"rgba(255, 255, 255, 0.16)",1,{dashed:!0,singlePass:!0,overrun:!1}),Y(e,.5*t,30,.5*t,a-25,"rgba(255, 255, 255, 0.16)",1,{dashed:!0,singlePass:!0,overrun:!1})}(n,r.width,r.height),"step-1"===e&&1===t?(z(n,60,50,180,45),J(n,60,50,180,45),z(n,60,105,180,45),J(n,60,105,180,45),K(n,[{x:60,y:50},{x:95,y:72.5},{x:60,y:95}],"#ffffff",2),Y(n,70,55,230,55,"rgba(255,255,255,0.3)",1,{overrun:!1}),Y(n,70,110,230,110,"rgba(255,255,255,0.3)",1,{overrun:!1}),X(n,260,72.5,380,72.5,"SLOPE TAPER 1:4 (DRAINAGE)",270,66,!0),X(n,260,127.5,380,127.5,"BROADAXE HEWN OAK SILL",270,122,!1)):"step-1"===e&&3===t?(z(n,50,140,400,40),Q(n,50,140,400,40),ee(n,50,140,400,40,16),z(n,180,45,140,95),_(n,180,45,140,95),Y(n,190,60,310,60,"#ffffff",1.5,{overrun:!1}),Y(n,190,85,310,85,"#ffffff",1.5,{overrun:!1}),Y(n,190,110,310,110,"#ffffff",1.5,{overrun:!1}),X(n,340,92,440,92,"CENTRAL MASS CHIMNEY",355,86,!1)):"step-2"===e&&1===t?(z(n,100,60,140,90),J(n,100,60,140,90),z(n,240,80,120,50),J(n,240,80,120,50),q(n,210,105,10,"#ffffff",1.8),q(n,213,105,8,"#D12B3E",1.5,0,2*Math.PI,{dashed:!0}),X(n,213,160,213,50,"3.1 MM PIN OFFSET TENSION",250,58,!1)):"step-2"===e&&3===t?(Y(n,80,160,420,160,"#ffffff",2),Y(n,150,160,150,60,"#ffffff",2.2),Y(n,150,60,330,60,"#ffffff",2.2),Y(n,330,60,330,160,"#ffffff",2.2),Y(n,150,110,190,60,"#ffffff",1.8),Y(n,330,110,290,60,"#ffffff",1.8),J(n,145,60,190,100),X(n,350,60,440,40,"PULLEY RIGGING POINT",360,34,!1)):"step-3"===e&&1===t?(K(n,[{x:120,y:60},{x:380,y:60},{x:330,y:160},{x:170,y:160}],"#ffffff",2,!0),Q(n,140,70,220,80),q(n,250,135,18,"#ffffff",2),ee(n,235,120,30,30,6),X(n,268,135,420,135,"PERFORATED CLAY TILE DATUM",280,128,!1)):"step-3"===e&&3===t?(z(n,130,70,240,90),_(n,130,70,240,90),z(n,150,130,200,30,"rgba(255,255,255,0.7)",1.5),K(n,[{x:130,y:70},{x:250,y:35},{x:370,y:70}],"#ffffff",2.2),X(n,380,145,460,145,"CONSTANT 48°F WATER",385,138,!1)):"step-4"===e&&1===t?(q(n,160,115,65,"#ffffff",2),q(n,160,115,50,"#ffffff",1.5),q(n,160,115,14,"#ffffff",2),Y(n,160,50,175,65,"#ffffff",2),Y(n,225,115,210,130,"#ffffff",2),Y(n,160,180,145,165,"#ffffff",2),Y(n,95,115,110,100,"#ffffff",2),J(n,110,65,100,100),X(n,225,115,390,115,"16 OAK BUCKET CAVITIES",240,108,!1)):"step-4"===e&&3===t?(z(n,120,85,110,24),J(n,120,85,110,24),z(n,226,55,24,84),ee(n,226,55,24,84,8),X(n,230,97,380,97,"90° TORQUE TRANSLATION",245,90,!1)):"step-5"===e&&1===t?(z(n,180,130,140,30),J(n,180,130,140,30),z(n,210,45,22,85),J(n,210,45,22,85),z(n,268,45,22,85),J(n,268,45,22,85),X(n,232,85,370,85,"CONTINUOUS 2X4 DIMENSION STUD",245,78,!1)):"step-5"===e&&3===t?(Y(n,80,110,210,110,"#ffffff",3),z(n,210,98,80,24),ee(n,210,98,80,24,7),Y(n,290,110,420,110,"#ffffff",3),X(n,250,122,380,160,"REVERSE THREAD TENSION COLLAR",280,172,!1)):"step-6"===e&&1===t?(K(n,[{x:230,y:45},{x:215,y:65},{x:245,y:85},{x:230,y:105},{x:215,y:125},{x:245,y:145},{x:230,y:165}],"#ffffff",2.2),K(n,[{x:270,y:45},{x:285,y:65},{x:255,y:85},{x:270,y:105},{x:285,y:125},{x:255,y:145},{x:270,y:165}],"#ffffff",2.2),J(n,225,50,50,110),Y(n,250,40,250,170,"rgba(255,255,255,0.3)",1,{dashed:!0}),X(n,275,105,410,105,"HIGH-SPEED TURNED CHISEL CUT",285,98,!1)):"step-6"===e&&3===t?(Y(n,100,115,400,115,"#ffffff",4),q(n,200,115,8,"#ffffff",1.8),q(n,300,115,8,"#ffffff",1.8),Y(n,200,115,200,60,"#ffffff",2.5),Y(n,300,115,300,60,"#ffffff",2.5),ee(n,190,65,20,50,8),ee(n,290,65,20,50,8),X(n,315,85,440,85,'THREADED 3/4" PIPE TEE',320,78,!1)):"step-7"===e&&1===t?(z(n,120,60,260,110),J(n,120,60,260,110),K(n,[{x:120,y:60},{x:250,y:30},{x:380,y:60}],"#ffffff",2),q(n,250,50,8,"#ffffff",2),Y(n,250,58,250,130,"#ffffff",1.5),X(n,260,80,420,80,"GRAVITY PULLEY TRACK DATUM",270,73,!1)):"step-7"===e&&3===t?(q(n,210,115,35,"#ffffff",2.2),q(n,210,115,25,"#ffffff",1.5),ee(n,175,80,70,70,8),Y(n,150,160,270,160,"rgba(255,255,255,0.3)",1),Y(n,160,170,260,170,"rgba(255,255,255,0.3)",1),Q(n,150,150,120,25),X(n,248,115,410,115,"POROUS CLAY INFILTRATION JOINT",258,108,!1)):"step-8"===e&&1===t?(z(n,140,90,220,30),J(n,140,90,220,30),z(n,220,65,28,25),ee(n,220,65,28,25,6),Y(n,80,77,420,77,"#ffffff",2.2),X(n,250,77,390,50,"CERAMIC JOIST STANDOFF CLEAT",260,44,!1)):"step-8"===e&&3===t?(z(n,150,65,26,95),ee(n,150,65,26,95,8),z(n,190,65,26,95),ee(n,190,65,26,95,8),z(n,230,65,26,95),ee(n,230,65,26,95,8),z(n,270,65,26,95),ee(n,270,65,26,95,8),Y(n,145,145,305,145,"#ffffff",4),X(n,300,110,430,110,"HIGH THERMAL MASS COLUMN",310,103,!1)):"step-9"===e&&1===t?(K(n,[{x:100,y:130},{x:250,y:165},{x:400,y:130}],"#ffffff",2.5),Y(n,100,130,400,130,"rgba(255,255,255,0.3)",1,{dashed:!0}),J(n,140,130,220,30),X(n,250,130,250,147,"4.8 INCH RIDGE SAG DEFLECTION",265,142,!1)):"step-9"===e&&3===t?(z(n,120,60,220,100),_(n,120,60,220,100),K(n,[{x:340,y:80},{x:310,y:110},{x:325,y:155}],"#ffffff",2.5),Q(n,310,90,40,60),X(n,330,115,440,115,"BIOLOGICAL WEDGING FAILURE",335,108,!1)):"step-10"===e&&1===t?(z(n,225,45,50,120),Y(n,225,75,275,85,"#ffffff",2),Y(n,225,95,275,105,"#ffffff",2),Y(n,225,115,275,125,"#ffffff",2),z(n,210,85,80,16),ee(n,225,45,50,120,10),X(n,290,93,420,93,"ADJUSTABLE ACME THREAD COLLAR",295,86,!1)):"step-10"===e&&3===t?(K(n,[{x:180,y:140},{x:320,y:140},{x:350,y:175},{x:150,y:175}],"#ffffff",2,!0),Q(n,160,145,180,25),z(n,235,65,30,75),ee(n,235,65,30,75,8),X(n,335,158,450,158,"POURED LOAD SPREADER DATUM",340,151,!1)):"step-11"===e&&1===t?(z(n,160,110,180,28),J(n,160,110,180,28),z(n,220,145,60,35),ee(n,220,145,60,35,8),Y(n,250,145,250,138,"#ffffff",3),X(n,340,124,450,124,"PRECISION 12-TON LIFT POINT",345,117,!1)):"step-11"===e&&3===t?(z(n,180,80,140,44),J(n,180,80,140,44),K(n,[{x:180,y:80},{x:210,y:80},{x:210,y:114},{x:180,y:114}],"#ffffff",2.5),ee(n,180,80,30,34,6),K(n,[{x:320,y:80},{x:290,y:80},{x:290,y:114},{x:320,y:114}],"#ffffff",2.5),ee(n,290,80,30,34,6),X(n,320,102,440,102,"STRUCTURAL STEEL C-CHANNEL",325,95,!1)):"step-12"===e&&1===t?(z(n,140,120,220,25),Q(n,140,120,220,25),z(n,210,145,80,35),Q(n,210,145,80,35,20),z(n,240,60,20,85),ee(n,240,60,20,85,7),X(n,260,90,410,90,'4" PVC ACTIVE SUCTION RISER',265,83,!1)):"step-12"===e&&3===t?(q(n,210,120,30,"#ffffff",2.2),ee(n,180,90,60,60,8),q(n,210,142,4,"#ffffff",1.2),q(n,192,135,4,"#ffffff",1.2),q(n,228,135,4,"#ffffff",1.2),z(n,160,75,100,90,"rgba(255,255,255,0.4)",1.2),Q(n,160,75,100,90,45),X(n,240,120,410,120,"GEOTEXTILE FABRIC GRAVEL ENCASEMENT",248,113,!1)):"step-13"===e&&1===t?(z(n,160,60,180,100),Z(n,160,60,180,100),K(n,[{x:160,y:75},{x:220,y:75},{x:280,y:75},{x:340,y:75}],"rgba(255,255,255,0.4)",2),K(n,[{x:160,y:115},{x:220,y:115},{x:280,y:115},{x:340,y:115}],"rgba(255,255,255,0.4)",2),X(n,340,105,450,105,"R-45 CONTINUOUS FOAM MATRIX",345,98,!1)):"step-13"===e&&3===t?(z(n,220,50,60,130),Q(n,220,50,60,130,25),Y(n,242,50,242,165,"#ffffff",2),Y(n,258,50,258,165,"#ffffff",2),K(n,[{x:242,y:165},{x:250,y:175},{x:258,y:165}],"#ffffff",2),X(n,280,110,430,110,"400 FT VERTICAL CLOSED LOOP",285,103,!1)):"step-14"===e&&1===t?(z(n,120,60,40,110),J(n,120,60,40,110),z(n,160,60,200,36),J(n,160,60,200,36),X(n,360,78,460,78,"PRESERVED 1780 HEWN TIMBER",365,71,!1)):"step-14"===e&&3===t?(z(n,170,70,160,95),ee(n,170,70,160,95,12),z(n,220,55,60,15),K(n,[{x:250,y:90},{x:240,y:115},{x:255,y:115},{x:245,y:140}],"#ffffff",2.5),X(n,330,115,450,115,"MICROGRID SOLID-STATE DATUM",335,108,!1)):"step-15"===e&&1===t?(z(n,200,55,100,30),Q(n,200,55,100,30,15),K(n,[{x:170,y:85},{x:250,y:120},{x:330,y:85}],"#ffffff",1.5),K(n,[{x:140,y:85},{x:250,y:155},{x:360,y:85}],"#ffffff",1.5),K(n,[{x:110,y:85},{x:250,y:190},{x:390,y:85}],"#ffffff",1.5),Q(n,120,85,260,100,40),X(n,330,130,440,130,"ISOBAR STRESS DISSIPATION",335,123,!1)):"step-15"===e&&3===t?(z(n,220,60,35,110),_(n,220,60,35,110),Y(n,140,85,220,85,"#ffffff",2),K(n,[{x:220,y:85},{x:210,y:80},{x:210,y:90}],"#ffffff",1.5,!0),Y(n,160,120,220,120,"#ffffff",2),K(n,[{x:220,y:120},{x:210,y:115},{x:210,y:125}],"#ffffff",1.5,!0),Q(n,130,70,85,70,30),X(n,255,100,410,100,"LATERAL HYDROSTATIC THRUST",260,93,!1)):"step-16"===e&&1===t?(z(n,140,60,90,110),Z(n,140,60,90,110),z(n,230,60,130,110),ee(n,230,60,130,110,10),Y(n,230,50,230,180,"#D12B3E",2.5,{dashed:!0}),j(n,"DEW-POINT ISOTHERM DATUM",245,165,"#D12B3E",11)):"step-16"===e&&3===t?(z(n,150,70,200,85),ee(n,150,70,200,85,7),Y(n,120,95,150,95,"#ffffff",1.8),Y(n,120,130,150,130,"#ffffff",1.8),X(n,350,112,460,112,"ZERO THERMAL BRIDGING PROFILE",355,105,!1)):"step-17"===e&&1===t?(z(n,220,80,60,90),J(n,220,80,60,90),Y(n,250,35,250,80,"#ffffff",2.5),K(n,[{x:250,y:80},{x:244,y:70},{x:256,y:70}],"#ffffff",1.5,!0),X(n,280,120,430,120,"32 PSF DEAD + 40 PSF LIVE LOAD",285,113,!1)):"step-17"===e&&3===t&&(K(n,[{x:170,y:165},{x:290,y:165},{x:310,y:65},{x:190,y:65}],"#ffffff",2,!0),ee(n,170,65,140,100,12),Y(n,120,85,190,85,"#ffffff",2.5),K(n,[{x:190,y:85},{x:180,y:79},{x:180,y:91}],"#ffffff",1.5,!0),X(n,310,115,440,115,"MAX STORY DRIFT INDEX L/360",315,108,!1));try{const e=r.toDataURL("image/png");return W[o]=e,e}catch(e){return""}}window.forceScrollytellingUpdate=function(){const t=document.getElementById("floating-toc");if(t){const e=window.scrollY||window.pageYOffset||document.documentElement.scrollTop||0,a=document.querySelector(".page-header");e>(a?a.offsetHeight-50:300)?t.classList.contains("is-hidden")&&(t.classList.remove("is-hidden"),t.classList.add("is-visible")):t.classList.contains("is-hidden")||(t.classList.add("is-hidden"),t.classList.remove("is-visible"))}const a=Array.from(document.querySelectorAll(".stage-section"));if(0===a.length)return;const o=.5*window.innerHeight;let r=-1,n=0;for(let e=0;eo){if(r=e,t.height>0)if(t.top>=0)n=0;else{const e=-t.top/Math.max(1,t.height-.5*window.innerHeight);n=Math.max(0,Math.min(1,e))}break}}if(-1===r){let e=1/0;for(let t=0;t{t===r?e.classList.contains("is-active")||e.classList.add("is-active"):e.classList.contains("is-active")&&e.classList.remove("is-active")});const i=document.getElementById("active-stage-label");i&&e[r]&&(i.textContent=`STAGE ${r+1}: ${e[r].title.toUpperCase()}`);const s=document.querySelectorAll(".toc-item"),l=document.getElementById("toc-active-indicator");s.forEach((e,t)=>{if(t===r){if(e.classList.contains("is-active")||e.classList.add("is-active"),l){const t=e.closest(".toc-item-wrapper")||e.parentElement||e,a=t.offsetTop,o=t.offsetHeight;l.style.transform=`translateY(${a}px)`,l.style.height=`${o}px`}}else e.classList.contains("is-active")&&e.classList.remove("is-active")});const c=document.getElementById("bottom-timeline-overlay");if(c){c.classList.contains("is-hidden")&&(c.classList.remove("is-hidden"),c.classList.add("is-visible"));c.querySelectorAll(".timeline-point").forEach((e,t)=>{t===r?e.classList.contains("is-active")||e.classList.add("is-active"):e.classList.contains("is-active")&&e.classList.remove("is-active")})}f(r,n)},window.scrollToStep=function(e){const t=document.querySelectorAll(".stage-section");if(e>=0&&e{const o=document.createElement("section"),r=a%2==0;o.setAttribute("class","stage-section "+(r?"layout-text-left":"layout-text-right")),o.setAttribute("id",`stage-section-${a}`),o.setAttribute("data-stage-index",String(a));const n=e.specs.map(e=>`\n
\n ${e.label}\n ${e.value}\n
`).join(""),i=Array.isArray(e.paragraphs)?e.paragraphs.map((t,a)=>{const o=$[e.id]||[];let r="";if(1===a&&o[0]){const t=te(e.id,1,o[0]);t&&(r=`
${o[0]}
`)}else if(3===a&&o[1]){const t=te(e.id,3,o[1]);t&&(r=`
${o[1]}
`)}return`${r}\n

${t}

`}).join(""):`

${e.description}

`,s=`\n
\n
\n Stage ${a+1} // ${e.year}\n

${e.title}: ${e.subtitle}

\n
${i}
\n
${n}
\n
\n
\n `,l=`\n
\n
\n \n \n
Stage ${a+1}: ${e.title}
\n
\n
\n `;o.innerHTML=s+l,t.appendChild(o),o.querySelectorAll(".step-paragraph-item").forEach(e=>{e.addEventListener("click",()=>{e.scrollIntoView({behavior:"smooth",block:"center"})})})}))}(),function(){const t=document.getElementById("toc-list");if(!t)return;t.innerHTML=`
${e.map((e,t)=>`\n
  • \n \n
  • \n `).join("")}`,t.querySelectorAll(".toc-item").forEach((e,t)=>{e.addEventListener("click",()=>{"function"==typeof window.stepTo&&window.stepTo(t)})});const a=document.getElementById("toc-toggle-btn"),o=document.getElementById("floating-toc"),r=document.getElementById("toc-toggle-symbol");a&&o&&!a.dataset.listenerAttached&&(a.dataset.listenerAttached="true",a.addEventListener("click",()=>{const e=o.classList.toggle("is-collapsed");a.setAttribute("aria-expanded",String(!e)),r&&(r.textContent=e?"[ + ]":"[ - ]"),e||"function"!=typeof window.forceScrollytellingUpdate||window.forceScrollytellingUpdate()}))}(),function(){const t=document.getElementById("timeline-points");t&&(t.innerHTML="",e.forEach((e,a)=>{const o=document.createElement("li"),r=document.createElement("button");r.className="timeline-point",r.type="button",r.textContent=`${e.year}`,r.setAttribute("aria-label",`Jump to Stage ${a+1}: ${e.year}`),r.addEventListener("click",()=>{const e=document.getElementById(`stage-section-${a}`);e&&e.scrollIntoView({behavior:"smooth"})}),o.appendChild(r),t.appendChild(o)}))}(),function(){c=[];for(const e of Object.keys(t))n(e);for(let t=0;t{e(t.index,0),"function"==typeof window.forceScrollytellingUpdate&&window.forceScrollytellingUpdate()}).onStepProgress(t=>{e(t.index,t.progress),"function"==typeof window.forceScrollytellingUpdate&&window.forceScrollytellingUpdate()}),window.addEventListener("resize",()=>{t.resize()},{passive:!0})}(f);const a=()=>{"function"==typeof window.forceScrollytellingUpdate&&window.forceScrollytellingUpdate()};window.addEventListener("scroll",a,{passive:!0}),window.addEventListener("resize",a,{passive:!0}),"function"==typeof window.forceScrollytellingUpdate&&window.forceScrollytellingUpdate()}window.stepTo=function(e){window.scrollToStep(e)},window._scrubProgress=0,window.resetScrub=function(){window._scrubProgress=0,window.scrollTo({top:0,behavior:"instant"}),"function"==typeof window.forceScrollytellingUpdate&&window.forceScrollytellingUpdate()},window.scrubNext=function(){window._scrubProgress=Math.min(1,window._scrubProgress+1/30);const e=Math.max(0,document.documentElement.scrollHeight-window.innerHeight);window.scrollTo({top:e*window._scrubProgress,behavior:"instant"}),"function"==typeof window.forceScrollytellingUpdate&&window.forceScrollytellingUpdate()},window.scrubPrev=function(){window._scrubProgress=Math.max(0,window._scrubProgress-1/30);const e=Math.max(0,document.documentElement.scrollHeight-window.innerHeight);window.scrollTo({top:e*window._scrubProgress,behavior:"instant"}),"function"==typeof window.forceScrollytellingUpdate&&window.forceScrollytellingUpdate()},window.scrubForward=window.scrubNext,window.scrubBackward=window.scrubPrev,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",ae):ae(); diff --git a/suites-experimental/scrollytelling/dist/index.html b/suites-experimental/scrollytelling/dist/index.html new file mode 100644 index 000000000..eedd69258 --- /dev/null +++ b/suites-experimental/scrollytelling/dist/index.html @@ -0,0 +1,104 @@ + + + + + + + Project A-101: The Homestead Dossier - Speedometer Scrollytelling Workload + + + + +
    + + +
    + +
    +
    +

    Speedometer Experimental Scrollytelling Benchmark Workload

    +
    +
    + + + + diff --git a/suites-experimental/scrollytelling/dist/public/planning_paper_dot_grid.webp b/suites-experimental/scrollytelling/dist/public/planning_paper_dot_grid.webp new file mode 100644 index 000000000..3eccdc1cc Binary files /dev/null and b/suites-experimental/scrollytelling/dist/public/planning_paper_dot_grid.webp differ diff --git a/suites-experimental/scrollytelling/dist/styles.css b/suites-experimental/scrollytelling/dist/styles.css new file mode 100644 index 000000000..6b85a55d5 --- /dev/null +++ b/suites-experimental/scrollytelling/dist/styles.css @@ -0,0 +1,965 @@ +/* + * Copyright (C) 2024-2026 Speedometer Contributors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + */ + +/** + * 1950s Black & White Blueprint / Planning Design + * Reference: STYLE_CONFIG in src/graphics.js + * + * Aesthetic Rules: + * - Pure black drafting background (#000000) with crisp B&W geometry. + * - Negative blueprint drafting theme (light typography & wireframes on dark void). + * - Slanted container borders (-2.0deg to +2.0deg) and independent drop shadow angles (Shadow Angle != Box Angle). + * - Gradual organic watercolor animation reveals for accent coloring during scroll. + */ + +*, +*::before, +*::after { + transition: none !important; + animation: none !important; +} + +:root { + --bg-color: #000000; + --paper-light: rgba(0, 0, 0, 0.65); + --paper-card: rgba(0, 0, 0, 0.85); + --ink-primary: #ffffff; + --ink-secondary: #cccccc; + --ink-muted: #777777; + --accent-crimson: #d12b3e; + --accent-ruby: #c72436; + --accent-dark: #ffffff; + --accent-light: #cccccc; + --border-color: #ffffff; + --shadow-solid: rgba(0, 0, 0, 0.75); + --text-color: var(--ink-primary); + --text-muted: var(--ink-muted); + --card-bg: var(--paper-light); + --card-active-bg: var(--paper-card); + --accent-color: var(--accent-crimson); + --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); + --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: var(--bg-color); + color: var(--ink-primary); + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 1rem; + line-height: 1.6; + overflow-x: hidden; + position: relative; + z-index: 0; +} + +body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background-color: var(--bg-color, #000000); + background-image: url("public/planning_paper_dot_grid.webp"); + background-repeat: repeat; + background-size: 256px 256px; + pointer-events: none; +} + +#app { + max-width: 1400px; + margin: 0 auto; + padding: 0 2rem; + position: relative; +} + +/* 1950s Construction Paper Header Box with Slanted Geometry & Independent Shadow */ +.page-header { + position: relative; + z-index: 1; + padding: 4.5rem 2rem 2.5rem; + background-color: var(--paper-card); + border: 4px solid var(--accent-crimson, #d12b3e); + border-radius: 10px; + margin-top: 2.5rem; + margin-bottom: 1rem; + text-align: center; + transform: rotate(-0.6deg); + overflow: visible; +} + +.page-header::before { + content: ""; + position: absolute; + z-index: -1; + top: 14px; + left: 10px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 10px; + transform: rotate(1.3deg); +} + +.page-header:hover { + transform: rotate(0deg); +} + +.page-header:hover::before { + top: 16px; + left: 14px; + transform: rotate(-0.9deg); +} + +.page-header h1 { + font-family: "Impact", "Arial Black", "Futura", "Helvetica Neue", system-ui, -apple-system, sans-serif; + font-size: 3.5rem; + font-weight: 900; + letter-spacing: -0.02em; + text-transform: uppercase; + margin-bottom: 0.75rem; + color: var(--ink-primary); +} + +.subtitle { + position: relative; + z-index: 1; + font-size: 1rem; + color: var(--ink-primary, #ffffff); + background-color: var(--paper-card, rgba(0, 0, 0, 0.85)); + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + margin-bottom: 1.25rem; + display: inline-block; + padding: 0.4rem 1.2rem; + border: 3px solid var(--border-color, #ffffff); + border-radius: 6px; + transform: rotate(1deg); + overflow: visible; +} + +.subtitle::after { + content: ""; + position: absolute; + z-index: -1; + top: 6px; + left: -6px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 6px; + transform: rotate(-1.7deg); +} + +.header-status { + display: flex; + justify-content: center; + gap: 1.5rem; + flex-wrap: wrap; + margin-top: 1.25rem; +} + +.status-tag { + position: relative; + z-index: 1; + font-family: "Impact", "Arial Black", "Futura", "Helvetica Neue", system-ui, -apple-system, sans-serif; + font-size: 1rem; + font-weight: 900; + padding: 0.5rem 1.4rem; + border: 2px solid #ffffff; + background-color: var(--accent-crimson, #d12b3e); + color: #ffffff; + border-radius: 6px; + letter-spacing: 0.1em; + text-transform: uppercase; + transform: rotate(-1.5deg); + display: inline-block; + overflow: visible; +} + +.status-tag::after { + content: ""; + position: absolute; + z-index: -1; + top: 8px; + left: 10px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid, rgba(0, 0, 0, 0.75)); + border-radius: 6px; + transform: rotate(2.3deg); +} + +/* Floating Table of Contents (1950s Slanted Index Banner with Independent Shadow) */ +.floating-toc { + position: fixed; + top: 2.5rem; + right: 2.5rem; + z-index: 1000; + background-color: var(--paper-card); + border: 3px solid var(--border-color); + border-radius: 8px; + padding: 1rem; + width: 260px; + transform: rotate(1.2deg); + overflow: visible; +} + +.floating-toc::after { + content: ""; + position: absolute; + z-index: -1; + top: 10px; + left: 6px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 8px; + transform: rotate(-1.1deg); +} + +.floating-toc:hover { + transform: rotate(0deg) scale(1.02); +} + +.floating-toc:hover::after { + top: 12px; + left: 10px; + transform: rotate(-1.6deg); +} + +/* Visibility Toggling for Floating TOC (Zero Transitions / Instant Synchronous Toggle) */ +.floating-toc.is-hidden { + display: none !important; +} +.floating-toc.is-visible { + display: block !important; +} + +/* Table of Contents Interactive Toggle & Collapsed State (1950s Blueprint Style, 0 Transitions) */ +.toc-toggle-btn { + background: transparent; + border: none; + color: inherit; + width: 100%; + cursor: pointer; + padding: 0; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + gap: 0.4rem; +} +.toc-toggle-symbol { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 1rem; + font-weight: 700; +} + +.floating-toc.is-collapsed { + width: 160px; + padding: 0; +} +.floating-toc.is-collapsed .toc-header { + margin: 0; + border-bottom: none; + padding: 0.65rem 1rem; + border-radius: 8px; +} +.floating-toc.is-collapsed .toc-list, +.floating-toc.is-collapsed .toc-active-indicator { + display: none !important; +} + +/* Independent Drop Shadow Angle for Collapsed State (Rule 3: Collage/Diorama Rule) */ +.floating-toc.is-collapsed::after { + top: 6px; + left: 5px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 8px; + transform: rotate(-1.5deg); +} +.floating-toc.is-collapsed:hover::after { + top: 8px; + left: 7px; + transform: rotate(-1.8deg); +} + +.toc-header { + border-bottom: 3px solid var(--accent-crimson, #d12b3e); + text-align: center; + background-color: var(--paper-card, rgba(0, 0, 0, 0.85)); + color: var(--ink-primary, #ffffff); + margin: -1rem -1rem 0.75rem -1rem; + padding: 0.65rem 1rem 0.55rem; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.toc-title { + font-family: "Impact", "Arial Black", "Futura", "Helvetica Neue", system-ui, -apple-system, sans-serif; + font-size: 1rem; + font-weight: 900; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.toc-list { + list-style: none; + position: relative; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.3rem; + z-index: 10; +} + +.toc-item-wrapper { + position: relative; + z-index: 10; +} + +.toc-item { + width: 100%; + background: transparent; + border: 2px solid transparent; + border-radius: 4px; + color: var(--ink-secondary); + text-align: left; + padding: 0.5rem 0.6rem; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + opacity: 1 !important; +} + +.toc-item.is-active { + background: var(--accent-crimson, #d12b3e) !important; + color: #ffffff !important; + border-color: #ffffff !important; +} + +.toc-num { + color: var(--accent-crimson, #d12b3e); + font-weight: 900; + letter-spacing: 0.05em; + opacity: 1 !important; +} + +.toc-item.is-active .toc-num { + color: #ffffff !important; +} + +.toc-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-transform: uppercase; +} + +.toc-active-indicator { + display: none !important; +} + +.scrolly-container { + display: flex; + flex-direction: column; + gap: 0; + position: relative; +} + +/* Stage Layout & Asymmetric Composition */ +.stage-section { + display: flex; + position: relative; + gap: 4.5rem; + align-items: flex-start; + justify-content: space-between; + min-height: min(650px, 80vh); + padding-top: 3rem; + padding-bottom: 3rem; +} + +.stage-section.layout-text-left { + flex-direction: row; +} + +.stage-section.layout-text-right { + flex-direction: row-reverse; +} + +.stage-graphic-column { + flex: 1 1 55%; + position: sticky; + top: 0; + max-height: 100vh; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + z-index: 10; +} + +.stage-narrative-column { + flex: 1 1 45%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; + padding-top: 0; + padding-bottom: 0; + z-index: 5; +} + +/* Irregular Slanted Tilt Angles & Independent Drop Shadow Offsets/Angles per Stage */ +#stage-section-0 { + --step-tilt: -1.4deg; + --step-shadow-tilt: 0.9deg; + --step-shadow-x: 12px; + --step-shadow-y: 8px; + --graphic-tilt: 1.2deg; + --graphic-shadow-tilt: -1.6deg; + --graphic-shadow-x: 16px; + --graphic-shadow-y: 10px; +} +#stage-section-1 { + --step-tilt: 1.6deg; + --step-shadow-tilt: -1.1deg; + --step-shadow-x: -10px; + --step-shadow-y: 14px; + --graphic-tilt: -1.4deg; + --graphic-shadow-tilt: 1.3deg; + --graphic-shadow-x: -12px; + --graphic-shadow-y: 16px; +} +#stage-section-2 { + --step-tilt: -1.1deg; + --step-shadow-tilt: 1.5deg; + --step-shadow-x: 14px; + --step-shadow-y: -6px; + --graphic-tilt: 1.5deg; + --graphic-shadow-tilt: -0.9deg; + --graphic-shadow-x: 10px; + --graphic-shadow-y: 18px; +} +#stage-section-3 { + --step-tilt: 1.3deg; + --step-shadow-tilt: -1.4deg; + --step-shadow-x: 8px; + --step-shadow-y: 16px; + --graphic-tilt: -1.1deg; + --graphic-shadow-tilt: 1.7deg; + --graphic-shadow-x: -14px; + --graphic-shadow-y: 12px; +} +#stage-section-4 { + --step-tilt: -1.5deg; + --step-shadow-tilt: 1.2deg; + --step-shadow-x: -12px; + --step-shadow-y: 10px; + --graphic-tilt: 1.3deg; + --graphic-shadow-tilt: -1.8deg; + --graphic-shadow-x: 18px; + --graphic-shadow-y: 8px; +} +#stage-section-5 { + --step-tilt: 1.2deg; + --step-shadow-tilt: -1.5deg; + --step-shadow-x: 16px; + --step-shadow-y: 14px; + --graphic-tilt: -1.5deg; + --graphic-shadow-tilt: 1.1deg; + --graphic-shadow-x: -10px; + --graphic-shadow-y: -8px; +} +#stage-section-6 { + --step-tilt: -1.3deg; + --step-shadow-tilt: 1.6deg; + --step-shadow-x: 10px; + --step-shadow-y: 18px; + --graphic-tilt: 1.4deg; + --graphic-shadow-tilt: -1.2deg; + --graphic-shadow-x: 14px; + --graphic-shadow-y: 14px; +} +#stage-section-7 { + --step-tilt: 1.5deg; + --step-shadow-tilt: -1.1deg; + --step-shadow-x: -14px; + --step-shadow-y: 12px; + --graphic-tilt: -1.2deg; + --graphic-shadow-tilt: 1.5deg; + --graphic-shadow-x: -16px; + --graphic-shadow-y: 16px; +} + +/* Graphic Sticky Wrapper (1950s Architectural Drawing Frame with Independent Shadow) */ +.graphic-sticky-wrapper { + position: relative; + z-index: 1; + width: 100%; + aspect-ratio: 4 / 3; + border: 4px solid var(--border-color); + border-radius: 10px; + background-color: transparent; + overflow: visible; + display: flex; + align-items: center; + justify-content: center; + transform: rotateZ(var(--graphic-tilt, 0deg)); +} + +.graphic-sticky-wrapper::after { + content: ""; + position: absolute; + z-index: -1; + top: var(--graphic-shadow-y, 14px); + left: var(--graphic-shadow-x, 14px); + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 10px; + transform: rotateZ(var(--graphic-shadow-tilt, -1.5deg)); +} + +.graphic-canvas, +.graphic-svg, +#graphic-canvas, +#graphic-svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + clip-path: inset(0px round 5px); +} + +.graphic-canvas, +#graphic-canvas { + z-index: 1; +} + +.graphic-svg, +#graphic-svg { + z-index: 2; + pointer-events: none; +} + +/* Clean Blueprint Dot Grid / Watercolor Wash Overlay */ +.graphic-texture-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: radial-gradient(circle at center, rgba(255, 255, 255, 0.12), transparent 70%); + background-size: 100% 100%; + background-position: center; + mix-blend-mode: screen; + opacity: 0.35; + border-radius: 5px; + z-index: 2; + pointer-events: none; + clip-path: inset(0px round 5px); +} + +/* 1950s Graphic Callout Badge Caption with Independent Shadow */ +.caption { + position: absolute; + z-index: 5; + bottom: 1.25rem; + left: 1.25rem; + right: 1.25rem; + background-color: var(--paper-card, rgba(0, 0, 0, 0.85)); + color: var(--ink-primary, #ffffff); + border: 3px solid var(--accent-crimson, #d12b3e); + border-radius: 6px; + padding: 0.65rem 1.25rem; + font-family: "Impact", "Arial Black", "Futura", "Helvetica Neue", system-ui, -apple-system, sans-serif; + font-size: 0.95rem; + font-weight: 900; + text-align: center; + letter-spacing: 0.08em; + text-transform: uppercase; + transform: rotate(-0.8deg); + overflow: visible; +} + +.caption::after { + content: ""; + position: absolute; + z-index: -1; + top: 5px; + left: 6px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 6px; + transform: rotate(1.6deg); +} + +.caption-title { + color: var(--ink-secondary, #cccccc); + font-weight: 700; +} + +.caption-text { + color: var(--ink-primary, #ffffff); + font-weight: 900; +} + +/* Narrative Cards (.step) on Construction Paper with Independent Shadow - Upright & Transparent */ +article, +.step { + position: relative; + z-index: 1; + background-color: transparent !important; + border: 3px solid var(--ink-secondary, #cccccc); + border-radius: 8px; + padding: 1.35rem 1.75rem; + opacity: 1 !important; + transform: none !important; + overflow: visible; +} + +article.is-active, +.step.is-active { + background-color: transparent !important; + transform: none !important; +} + +.step::after { + content: ""; + position: absolute; + z-index: -1; + top: var(--step-shadow-y, 6px); + left: var(--step-shadow-x, 6px); + width: 100%; + height: 100%; + background-color: var(--shadow-solid, rgba(0, 0, 0, 0.75)); + border-radius: 8px; + opacity: 1; + transform: rotateZ(var(--step-shadow-tilt, 1deg)); +} + +.step-meta { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 900; + color: var(--accent-crimson, #d12b3e); + text-transform: uppercase; + letter-spacing: 0.12em; + display: block; + margin-top: 0.2rem; + margin-bottom: 0.5rem; + border-bottom: 2px solid var(--accent-ruby, #c72436); + padding-bottom: 0.4rem; + opacity: 1 !important; +} + +.step-title { + font-family: "Impact", "Arial Black", "Futura", "Helvetica Neue", system-ui, -apple-system, sans-serif; + font-size: 2rem; + font-weight: 900; + letter-spacing: -0.02em; + text-transform: uppercase; + margin-bottom: 0.5rem; + color: var(--ink-primary); + line-height: 1.1; +} + +/* Standard Vertical Paragraph Text (0 Layout Animations, 0 Transforms, No Separate Boxes) */ +.step-paragraph-item { + position: relative; + z-index: 1; + margin-bottom: 1.2rem; + opacity: 1 !important; + cursor: pointer; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 1rem; + color: var(--ink-primary); + line-height: 1.65; + font-weight: 400; + transform: rotate(0deg); +} + +.step-paragraph-item:last-child { + margin-bottom: 0; +} + +/* Inline Transparent Illustrations Between Paragraphs (Strict Defense-in-Depth Guardrails) */ +.step-inline-illustration { + opacity: 1 !important; + transition: none !important; + animation: none !important; + background: transparent !important; + border: 2px solid var(--ink-secondary); + border-radius: 6px; + margin: 16px 0; + padding: 12px; + text-align: center; +} + +.step-inline-illustration-img { + opacity: 1 !important; + transition: none !important; + animation: none !important; + background: transparent !important; + max-width: 100%; + height: auto; + display: inline-block; +} + +/* Step Specs (1950s Slanted Print Data Box with Independent Shadow) */ +.step-specs { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + background: var(--bg-color); + padding: 0.75rem 1rem; + border: 3px solid var(--ink-primary); + border-radius: 6px; + margin-top: 0.6rem; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + transform: rotate(0.5deg); + overflow: visible; +} + +.step-specs::after { + content: ""; + position: absolute; + z-index: -1; + top: 6px; + left: -6px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 6px; + transform: rotate(-1.3deg); +} + +.spec-item { + display: flex; + flex-direction: column; + border-left: 3px solid var(--accent-crimson, #d12b3e); + padding-left: 0.65rem; +} + +.spec-label { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 700; + color: var(--ink-secondary, #cccccc); + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 1 !important; +} + +.spec-value { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 900; + color: var(--ink-primary); +} + +.page-footer { + position: relative; + z-index: 1; + padding: 4.5rem 2rem 3.5rem; + background-color: var(--paper-card); + border: 4px solid var(--border-color); + border-radius: 10px; + margin-top: 1rem; + margin-bottom: 4rem; + text-align: center; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 900; + color: var(--ink-primary); + text-transform: uppercase; + letter-spacing: 0.08em; + transform: rotate(0.5deg); + overflow: visible; +} + +.page-footer::after { + content: ""; + position: absolute; + z-index: -1; + top: 12px; + left: 10px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 10px; + transform: rotate(-0.9deg); +} + +@media (max-width: 1100px) { + .floating-toc { + top: auto; + bottom: 1.5rem; + right: 1.5rem; + width: 230px; + padding: 0.75rem; + transform: rotate(0deg); + } + .floating-toc.is-collapsed { + width: 145px; + padding: 0; + } + .toc-title { + font-size: 1rem; + } + .toc-item { + font-size: 0.85rem; + padding: 0.35rem 0.45rem; + } + .step-title { + font-size: 1.5rem; + } +} + +@media (max-width: 900px) { + .scrolly-container { + gap: 2.5rem; + } + .stage-section, + .stage-section.layout-text-left, + .stage-section.layout-text-right { + flex-direction: column; + gap: 2.5rem; + min-height: auto; + padding-top: 1.5rem; + padding-bottom: 2rem; + } + .stage-graphic-column { + position: relative; + top: 0; + width: 100%; + height: auto; + } + .stage-narrative-column { + width: 100%; + padding-top: 0; + padding-bottom: 0; + } + .step-specs { + grid-template-columns: 1fr; + } +} + +/* Bottom Timeline Navigation Overlay (Canon Autozoom Style, Zero Transitions) */ +.bottom-timeline-overlay, +#bottom-timeline-overlay { + position: fixed; + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%) rotate(-0.5deg); + z-index: 1000; + background-color: var(--paper-card, rgba(0, 0, 0, 0.85)); + border: 3px solid var(--border-color, #ffffff); + border-radius: 8px; + padding: 0.75rem 1.5rem; + width: max-content; + max-width: 90vw; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + opacity: 1 !important; + overflow: visible; + transition: none !important; + animation: none !important; +} +.bottom-timeline-overlay::after, +#bottom-timeline-overlay::after { + content: ""; + position: absolute; + z-index: -1; + top: 6px; + left: 8px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid, rgba(0, 0, 0, 0.75)); + border-radius: 8px; + transform: rotate(1.1deg); + transition: none !important; + animation: none !important; +} +.bottom-timeline-overlay.is-hidden, +#bottom-timeline-overlay.is-hidden { + display: none !important; +} +.bottom-timeline-overlay.is-visible, +#bottom-timeline-overlay.is-visible { + display: flex !important; +} +.timeline-track { + position: absolute; + top: 50%; + left: 1.5rem; + right: 1.5rem; + height: 2px; + background-color: var(--ink-secondary, #cccccc); + z-index: 1; +} +.timeline-points { + list-style: none; + display: flex; + gap: 1.25rem; + margin: 0; + padding: 0; + position: relative; + z-index: 2; +} +.timeline-point { + background-color: var(--paper-light, rgba(0, 0, 0, 0.65)); + border: 2px solid var(--border-color, #ffffff); + border-radius: 6px; + color: var(--ink-secondary, #cccccc); + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 700; + padding: 0.35rem 0.65rem; + cursor: pointer; + opacity: 1 !important; + transition: none !important; + animation: none !important; + transform: rotate(0deg); + z-index: 2; +} +.timeline-point.is-active { + background: var(--accent-crimson, #d12b3e) !important; + color: #ffffff !important; + border-color: #ffffff !important; +} diff --git a/suites-experimental/scrollytelling/index.html b/suites-experimental/scrollytelling/index.html new file mode 100644 index 000000000..eedd69258 --- /dev/null +++ b/suites-experimental/scrollytelling/index.html @@ -0,0 +1,104 @@ + + + + + + + Project A-101: The Homestead Dossier - Speedometer Scrollytelling Workload + + + + +
    + + +
    + +
    +
    +

    Speedometer Experimental Scrollytelling Benchmark Workload

    +
    +
    + + + + diff --git a/suites-experimental/scrollytelling/package-lock.json b/suites-experimental/scrollytelling/package-lock.json new file mode 100644 index 000000000..be3a4d2eb --- /dev/null +++ b/suites-experimental/scrollytelling/package-lock.json @@ -0,0 +1,1368 @@ +{ + "name": "scrollytelling", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scrollytelling", + "version": "1.0.0", + "dependencies": { + "scrollama": "^3.2.0" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", + "rollup": "^4.41.1", + "rollup-plugin-copy": "^3.5.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-copy": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz", + "integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scrollama": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/scrollama/-/scrollama-3.2.0.tgz", + "integrity": "sha512-PIPwB1kYBnbw/ezvPBJa5dCN5qEwokfpAkI3BmpZWAwcVID4nDf1qH6WV16A2fQaJmsKx0un5S/zhxN+PQeKDQ==", + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/suites-experimental/scrollytelling/package.json b/suites-experimental/scrollytelling/package.json new file mode 100644 index 000000000..1002ab32b --- /dev/null +++ b/suites-experimental/scrollytelling/package.json @@ -0,0 +1,19 @@ +{ + "name": "scrollytelling", + "version": "1.0.0", + "description": "Scrollytelling benchmark suite featuring Project A-101: The Homestead Dossier.", + "private": true, + "type": "module", + "scripts": { + "build": "rollup --config rollup.config.js" + }, + "dependencies": { + "scrollama": "^3.2.0" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", + "rollup": "^4.41.1", + "rollup-plugin-copy": "^3.5.0" + } +} diff --git a/suites-experimental/scrollytelling/public/planning_paper_dot_grid.webp b/suites-experimental/scrollytelling/public/planning_paper_dot_grid.webp new file mode 100644 index 000000000..3eccdc1cc Binary files /dev/null and b/suites-experimental/scrollytelling/public/planning_paper_dot_grid.webp differ diff --git a/suites-experimental/scrollytelling/rollup.config.js b/suites-experimental/scrollytelling/rollup.config.js new file mode 100644 index 000000000..c3ac1fe54 --- /dev/null +++ b/suites-experimental/scrollytelling/rollup.config.js @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * Copyright (C) 2024 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +import resolve from "@rollup/plugin-node-resolve"; +import terser from "@rollup/plugin-terser"; +import copy from "rollup-plugin-copy"; +import fs from "fs"; +import process from "process"; + +const DEV_OUTPUT_DIR = "build-dev"; +const PROD_OUTPUT_DIR = "dist"; + +const outputDir = process.env.ROLLUP_WATCH ? DEV_OUTPUT_DIR : PROD_OUTPUT_DIR; + +if (!process.env.ROLLUP_WATCH && fs.existsSync(outputDir)) { + // Clean previous build output + fs.rmSync(outputDir, { recursive: true, force: true }); +} + +export default { + input: "src/main.js", + output: [ + { + file: `${outputDir}/app.js`, + format: "es", + name: "app", + plugins: process.env.ROLLUP_WATCH ? [] : [terser()], + }, + ], + plugins: [ + resolve(), + copy({ + targets: [ + { src: "index.html", dest: `${outputDir}/` }, + { src: "src/styles.css", dest: `${outputDir}/` }, + { src: "public/*", dest: `${outputDir}/public/`, noErrorOnMissing: true }, + ], + }), + ], +}; diff --git a/suites-experimental/scrollytelling/src/content.js b/suites-experimental/scrollytelling/src/content.js new file mode 100644 index 000000000..8d1cae6b9 --- /dev/null +++ b/suites-experimental/scrollytelling/src/content.js @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * Copyright (C) 2024 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * 1950s Black & White Blueprint / Planning Design Content & Specification Data + * Style reference: STYLE_CONFIG in src/graphics.js + * All narrative steps, badges, and technical specs conform to the monochrome blueprint drafting theme + * on a pure black background (#000000), enhanced by gradual organic watercolor animation reveals. + */ +export const STAGES = [ + { + id: "step-1", + year: "1780", + title: "Early Settlement", + subtitle: "Timber Clearing & Rudimentary Shelter", + paragraphs: [ + "The rugged New England frontier in 1780 presented a formidable wilderness of virgin old-growth hemlock, white oak, and sugar maple. Early homesteaders arrived with rudimentary hand tools, clearing dense canopy and wrestling with glacial till to carve out a small, survivable clearing in the forest. Every square foot of cleared land required immense physical labor, transforming the untamed woodland into the beginnings of a permanent agrarian settlement.", + "Construction began immediately by felling massive timber trees to fashion hand-hewn logs. Broadaxes and adzes were used to square the timbers, which were carefully notched and interlocked at the corners with a dovetail log corner notch without iron nails. Mud, clay, and dried woodland moss were painstakingly rammed into the chinks between logs to create a windbreak against the punishing sub-zero winters and gale-force winds.", + "Survival during these formative decades relied entirely on localized natural resources. A shallow dug well tapped the immediate surface water table, while a massive dry-laid fieldstone hearth section served as the sole source of thermal heating, illumination, and daily cooking. This initial 320-square-foot cabin represented a fragile human foothold in an otherwise untamed ecological landscape.", + "The perimeter of the clearing was delineated by split-rail worm fences crafted from durable cedar and chestnut logs. These interlocking zigzag barriers kept wild grazing animals away from early subsistence vegetable patches without requiring post-hole digging in the rocky, frost-heaved soil.", + "Throughout the harsh winter months, the central stone hearth acted as the structural and social nucleus of the homestead. Heat radiated from the massive fieldstone chimney stack, warming the sleeping loft above and preserving dried herbs, smoked provisions, and root vegetables stored in the crawlspace below.", + ], + description: + "The rugged New England frontier in 1780 presented a formidable wilderness of virgin old-growth hemlock, white oak, and sugar maple. Early homesteaders arrived with rudimentary hand tools, clearing dense canopy and wrestling with glacial till to carve out a small, survivable clearing in the forest. Every square foot of cleared land required immense physical labor, transforming the untamed woodland into the beginnings of a permanent agrarian settlement.\n\nConstruction began immediately by felling massive timber trees to fashion hand-hewn logs. Broadaxes and adzes were used to square the timbers, which were carefully notched and interlocked at the corners with a dovetail log corner notch without iron nails. Mud, clay, and dried woodland moss were painstakingly rammed into the chinks between logs to create a windbreak against the punishing sub-zero winters and gale-force winds.\n\nSurvival during these formative decades relied entirely on localized natural resources. A shallow dug well tapped the immediate surface water table, while a massive dry-laid fieldstone hearth section served as the sole source of thermal heating, illumination, and daily cooking. This initial 320-square-foot cabin represented a fragile human foothold in an otherwise untamed ecological landscape.", + specs: [ + { label: "Material", value: "Old-Growth Timber" }, + { label: "Foundation", value: "Direct Soil / Log Sills" }, + { label: "Tools", value: "Hand Axe, Broadaxe, Adze" }, + { label: "Footprint", value: "320 sq. ft." }, + ], + }, + { + id: "step-2", + year: "1795", + title: "Mortise-and-Tenon Framing", + subtitle: "Heavy Timber Joinery & Structural Rigidity", + paragraphs: [ + "As the initial settlement matured, builders sought greater structural longevity and architectural volume by transitioning from rough log construction to sophisticated heavy timber framing. Seasoned white oak and eastern white pine were selected for their exceptional structural rigidity and resistance to decay across multi-story spans.", + "Master housewrights employed precision hand tools including T-handled auger bits for mortise boring deep circular cavities, followed by heavy framing chisels to clear rectangular mortise slots. Post and girt connections were painstakingly scribed and fitted on the ground to ensure exact geometric alignment before erection.", + "Each structural joint relied on precision joinery rather than metal fasteners. Tenons carved at the ends of connecting girts and summer beams slid snugly into corresponding mortise pockets, creating a robust framework capable of supporting heavy second-story live loads and resisting lateral wind shear.", + "To lock the joinery permanently in place, master craftsmen utilized draw-boring techniques with tapered wooden treenail pegs. Offset holes were drilled through the mortise and tenon, and seasoned hickory pegs were driven home, pulling the joint tightly together under tremendous mechanical tension.", + "The raising of the timber bents was a communal undertaking requiring block and tackle pulleys, wooden pikes, and coordinated manpower. Once raised and pegged, the self-supporting timber cage stood braced against New England storm gales, forming an enduring skeleton that would endure centuries.", + ], + description: + "As the initial settlement matured, builders sought greater structural longevity and architectural volume by transitioning from rough log construction to sophisticated heavy timber framing. Seasoned white oak and eastern white pine were selected for their exceptional structural rigidity and resistance to decay across multi-story spans.\n\nMaster housewrights employed precision hand tools including T-handled auger bits for mortise boring deep circular cavities, followed by heavy framing chisels to clear rectangular mortise slots. Post and girt connections were painstakingly scribed and fitted on the ground to ensure exact geometric alignment before erection.\n\nEach structural joint relied on precision joinery rather than metal fasteners. Tenons carved at the ends of connecting girts and summer beams slid snugly into corresponding mortise pockets, creating a robust framework capable of supporting heavy second-story live loads and resisting lateral wind shear.", + specs: [ + { label: "Joinery", value: "Mortise-and-Tenon" }, + { label: "Fasteners", value: "Draw-Bored Treenails" }, + { label: "Tooling", value: "T-Handle Auger Bit" }, + { label: "Timber", value: "Seasoned White Oak" }, + ], + }, + { + id: "step-3", + year: "1810", + title: "Geology & Landscape", + subtitle: "Bedrock Adaptation & Natural Drainage", + paragraphs: [ + "As agricultural operations expanded, builders sought permanent subterranean storage by engaging directly with the site's underlying geology. Deep excavations for enlarged root cellars exposed dense veins of metamorphic granite bedrock and impervious clay subsoils, requiring structural adaptation rather than resistance.", + "Dry-laid fieldstone footings were laboriously assembled right atop the granite glacial erratic footing ledge cleared from surrounding arable meadows. Master stonemasons keyed these massive granite foundations directly onto the bedrock shelf without mortar, relying on gravity and precise geometric friction to resist heave from seasonal freeze-thaw cycles and heavy frost penetration.", + "Subterranean cutaway profiling revealed the complex stratification of topsoil, glacial till, and bedrock shelves. By understanding these natural geological horizons, builders anchored the bearing walls directly over structural ledges capable of distributing multi-ton loads without differential settlement.", + "To protect timber sills from rot and groundwater inundation, homesteaders engineered gravity perimeter trench drainage around foundation walls. By mapping the natural topography and subterranean hydrological flow, they diverted seasonal runoff away from the cellar walls, establishing an enduring relationship between the domestic footprint and the natural contour of the land.", + "The cellar floor itself was stepped and leveled directly into the granite shelf, creating cool, stable thermal zones (+420m ASL elevation) where root crops and preserved provisions remained at a steady 50°F year-round, shielded from surface frost.", + ], + description: + "As agricultural operations expanded, builders sought permanent subterranean storage by engaging directly with the site's underlying geology. Deep excavations for enlarged root cellars exposed dense veins of metamorphic granite bedrock and impervious clay subsoils, requiring structural adaptation rather than resistance.\n\nDry-laid fieldstone footings were laboriously assembled right atop the granite glacial erratic footing ledge cleared from surrounding arable meadows. Master stonemasons keyed these massive granite foundations directly onto the bedrock shelf without mortar, relying on gravity and precise geometric friction to resist heave from seasonal freeze-thaw cycles and heavy frost penetration.\n\nTo protect timber sills from rot and groundwater inundation, homesteaders engineered gravity perimeter trench drainage around foundation walls. By mapping the natural topography and subterranean hydrological flow, they diverted seasonal runoff away from the cellar walls, establishing an enduring relationship between the domestic footprint and the natural contour of the land.", + specs: [ + { label: "Substratum", value: "Granite Bedrock" }, + { label: "Footing", value: "Dry-Laid Fieldstone" }, + { label: "Drainage", value: "Gravity Trenching" }, + { label: "Elevation", value: "+420m ASL" }, + ], + }, + { + id: "step-4", + year: "1835", + title: "Watercourse & Millwright", + subtitle: "Hydraulic Power & Mechanical Transmission", + paragraphs: [ + "The presence of a perennial stream running along the valley contour prompted the construction of a mechanized watercourse and timber milling facility. By damming the brook and excavating a regulated millrace, homesteaders harnessed hydraulic potential to replace exhaustive manual sawing with automated kinetic power.", + "At the heart of the hydraulic works stood a wooden overshot waterwheel bucket profile engineered by specialized millwrights. Water delivered via an elevated wooden flume poured into carefully angled buckets, maximizing gravitational torque and rotational momentum across the central oak axle.", + "Flume flow and water velocity were controlled precisely through vertical iron-bound sluice gates. By raising or lowering the weir gate with mechanical rack-and-pinion levers, the miller adjusted hydraulic output in real-time to match seasonal water volume and sawing load variations.", + "Rotational kinetic energy from the waterwheel was transmitted inland through an intricate train featuring a trundle lantern crown gear and heavy wooden drive shafts. Masterfully carved applewood cog teeth meshed smoothly to step up rotational speed and redirect torque 90 degrees upward into the mill floor.", + "This mechanical gearing powered vertical reciprocating saw blades and heavy rotating granite millstones. The automated output transformed local forestry and grain harvesting, multiplying productivity twelvefold and turning the homestead into a regional agricultural hub.", + ], + description: + "The presence of a perennial stream running along the valley contour prompted the construction of a mechanized watercourse and timber milling facility. By damming the brook and excavating a regulated millrace, homesteaders harnessed hydraulic potential to replace exhaustive manual sawing with automated kinetic power.\n\nAt the heart of the hydraulic works stood a wooden overshot waterwheel bucket profile engineered by specialized millwrights. Water delivered via an elevated wooden flume poured into carefully angled buckets, maximizing gravitational torque and rotational momentum across the central oak axle.\n\nRotational kinetic energy from the waterwheel was transmitted inland through an intricate train featuring a trundle lantern crown gear and heavy wooden drive shafts. Masterfully carved applewood cog teeth meshed smoothly to step up rotational speed and redirect torque 90 degrees upward into the mill floor.", + specs: [ + { label: "Power Source", value: "16-Blade Overshot Wheel" }, + { label: "Transmission", value: "Trundle Crown Gears" }, + { label: "Head Height", value: "14-Foot Drop" }, + { label: "Output", value: "12 Horsepower Equiv." }, + ], + }, + { + id: "step-5", + year: "1860", + title: "Industrial Revolution", + subtitle: "Steam-Milled Lumber & Iron Reinforcement", + paragraphs: [ + "By the mid-nineteenth century, the arrival of steam-powered sawmills and regional rail networks revolutionized rural building practices. Heavy post-and-beam timber framing gave way to balloon framing, utilizing standardized, steam-milled dimensioned lumber attached with mass-produced cut wire nails. This industrial shift accelerated construction speed and allowed for more flexible interior layouts.", + "Standardized 2x4 framing members arrived by rail, where each steam-milled 2x4 balloon framing stud on sill plate rose continuously from the foundation all the way to the roof rafters. This continuous framing method allowed for rapid assembly by smaller work crews using machine-cut nails, transforming residential construction from a specialized craft into an industrial assembly process.", + "Industrial metallurgy provided significant structural reinforcement and spatial expansion. Wrought iron tie-rods, cast-iron lintels, and masonry reinforcements enabled taller, multi-story additions with expansive window openings that flooded interior parlors with natural light and improved ventilation across the entire homestead.", + "Across long floor spans, a wrought iron tie-rod with turnbuckle was anchored through exterior plates. By tightening these metal rods, builders counteracted outward structural spreading and locked the framing rigidly under heavy second-story loads.", + "Technological modernization extended deep into homestead infrastructure with the introduction of mechanized water pumping systems. Hand-driven and steam-powered mechanical pumps drew pristine water from deep aquifers directly into indoor copper plumbing, fundamentally transforming domestic labor, sanitation hygiene, and the daily rhythm of agrarian life.", + ], + description: + "By the mid-nineteenth century, the arrival of steam-powered sawmills and regional rail networks revolutionized rural building practices. Heavy post-and-beam timber framing gave way to balloon framing, utilizing standardized, steam-milled dimensioned lumber attached with mass-produced cut wire nails. This industrial shift accelerated construction speed and allowed for more flexible interior layouts.\n\nIndustrial metallurgy provided significant structural reinforcement and spatial expansion. Wrought iron tie-rods, cast-iron lintels, and masonry reinforcements enabled taller, multi-story additions with expansive window openings that flooded interior parlors with natural light and improved ventilation across the entire homestead.\n\nTechnological modernization extended deep into homestead infrastructure with the introduction of mechanized water pumping systems. Hand-driven and steam-powered mechanical pumps drew pristine water from deep aquifers directly into indoor copper plumbing, fundamentally transforming domestic labor, sanitation hygiene, and the daily rhythm of agrarian life.", + specs: [ + { label: "Framing", value: "Balloon Framing (2x4)" }, + { label: "Reinforcement", value: "Wrought Iron Tie-Rods" }, + { label: "Utilities", value: "Steam Mechanized Pump" }, + { label: "Footprint", value: "1,200 sq. ft." }, + ], + }, + { + id: "step-6", + year: "1885", + title: "Victorian Expansion", + subtitle: "Ornamental Millwork & Gas Lighting Infrastructure", + paragraphs: [ + "As agricultural wealth reached its zenith in the late nineteenth century, the homestead underwent a decorative Victorian expansion. Aesthetic tastes shifted toward ornate Queen Anne detailing, prominent wraparound verandas, and complex multi-gabled roof profiles that expressed domestic pride and craftsmanship.", + "High-speed steam-powered spindle lathes in regional millwork shops produced intricate decorative components, such as a spindle lathe turned porch baluster profile. Elaborate turned balusters, gingerbread gable scrollwork, and chamfered porch columns were shipped by rail and assembled to transform the exterior facade with rich architectural shadow lines.", + "The exterior skin was enhanced with patterned fish-scale cedar shingles, decorative frieze boards, and projected bay windows. These multi-faceted fenestrations captured morning and afternoon sunlight while creating elegant interior alcoves for parlor seating and botanical conservatories.", + "Interior modernization took a giant leap forward with the installation of a centralized gas illumination network fed by a threaded black-iron gas pipe manifold. Heavy threaded black-iron gas pipes were plumbed through basement walls and floor cavities, distributing fuel safely to wall-mounted brass sconces and overhead chandeliers.", + "The soft glow of gas lighting extended active domestic life far beyond twilight hours. Combined with ornamental plaster ceiling medallions and embossed wallcoverings, the homestead achieved an unprecedented level of interior refinement and technological comfort.", + ], + description: + "As agricultural wealth reached its zenith in the late nineteenth century, the homestead underwent a decorative Victorian expansion. Aesthetic tastes shifted toward ornate Queen Anne detailing, prominent wraparound verandas, and complex multi-gabled roof profiles that expressed domestic pride and craftsmanship.\n\nHigh-speed steam-powered spindle lathes in regional millwork shops produced intricate decorative components, such as a spindle lathe turned porch baluster profile. Elaborate turned balusters, gingerbread gable scrollwork, and chamfered porch columns were shipped by rail and assembled to transform the exterior facade with rich architectural shadow lines.\n\nInterior modernization took a giant leap forward with the installation of a centralized gas illumination network fed by a threaded black-iron gas pipe manifold. Heavy threaded black-iron gas pipes were plumbed through basement walls and floor cavities, distributing fuel safely to wall-mounted brass sconces and overhead chandeliers.", + specs: [ + { label: "Millwork", value: "Spindle Lathe Balusters" }, + { label: "Lighting", value: "Black-Iron Gas Manifold" }, + { label: "Siding", value: "Fish-Scale Shingles" }, + { label: "Style", value: "Victorian / Queen Anne" }, + ], + }, + { + id: "step-7", + year: "1900", + title: "Gradual Farm Improvements", + subtitle: "Agrarian Complex & Stone Boundary Walls", + paragraphs: [ + "At the turn of the twentieth century, decades of agricultural prosperity culminated in the transformation of the isolated homestead into a sophisticated, multi-building agrarian complex. The domestic dwelling was flanked by towering timber frame barns, specialized granaries, and livestock outbuildings designed for maximum operational efficiency.", + "Miles of dry-stone boundary walls, built from generations of frost-heaved fieldstones, crisscrossed the rolling landscape where a dry-stone boundary wall with pollinator cavity demarcated pasture lands. Master stonemasons assembled these walls without mortar, deliberately incorporating internal gaps for wildlife.", + "These enduring stone enclosures not only organized the agrarian layout and contained grazing herds, but also created vital microhabitats for native small mammals, songbirds, and beneficial solitary bees that pollinated the adjacent fruit orchards.", + "Water management reached a new level of sophistication through structured irrigation channels regulated by an adjustable irrigation sluice weir gate. Precision wooden weir gates allowed farmers to divert exact volumes of stream water into gravity-fed orchard furrows.", + "By harnessing and directing nearby perennial streams through engineered granite culverts, farmers established thriving heirloom orchards and expansive grain fields, sustaining a resilient, self-sufficient rural economy across 45 cultivated acres.", + ], + description: + "At the turn of the twentieth century, decades of agricultural prosperity culminated in the transformation of the isolated homestead into a sophisticated, multi-building agrarian complex. The domestic dwelling was flanked by towering timber frame barns, specialized granaries, and livestock outbuildings designed for maximum operational efficiency.\n\nMiles of dry-stone boundary walls, built from generations of frost-heaved fieldstones, crisscrossed the rolling landscape where a dry-stone boundary wall with pollinator cavity demarcated pasture lands. Master stonemasons assembled these walls without mortar, deliberately incorporating internal gaps for wildlife.\n\nWater management reached a new level of sophistication through structured irrigation channels regulated by an adjustable irrigation sluice weir gate. Precision wooden weir gates allowed farmers to divert exact volumes of stream water into gravity-fed orchard furrows.", + specs: [ + { label: "Outbuildings", value: "Timber Barn & Granary" }, + { label: "Enclosure", value: "Dry-Stone Boundary Walls" }, + { label: "Irrigation", value: "Sluice Weir Gates" }, + { label: "Acreage", value: "45 Acres Cultivated" }, + ], + }, + { + id: "step-8", + year: "1925", + title: "Electrification & Heating", + subtitle: "Knob-and-Tube Wiring & Hydronic Radiators", + paragraphs: [ + "The arrival of rural electrification in 1925 marked one of the most profound technological milestones in the homestead's history. Power lines strung from regional hydroelectric generators brought clean, continuous electrical current right to the farmhouse service entrance.", + "Electricians routed copper conductors through ceiling and floor joists supported by each porcelain knob-and-tube insulator. These glazed ceramic tubes and cleats maintained safe physical separation between live wires and dry wooden framing, preventing electrical spark arcs while powering overhead lighting circuits.", + "In tandem with electrical illumination, central heating replaced scattered wood-burning parlor stoves. A high-capacity coal-fired boiler installed in the cellar circulated heated water through closed-loop iron piping to cast-iron hot water radiators positioned under windows throughout the house.", + "The fluted, multi-column shape of each cast-iron hot water radiator fin profile maximized surface area for convective air currents and gentle thermal radiation. This hydronic system delivered draft-free, uniform warmth across every room, stabilizing interior temperatures during harsh winter freezes.", + "On the exterior clapboard siding near the service entry, an external watt-hour induction meter disk spun continuously inside its glass enclosure. This spinning electromechanical disc tracked cumulative power usage, connecting the rural dwelling directly to the modern energy economy.", + ], + description: + "The arrival of rural electrification in 1925 marked one of the most profound technological milestones in the homestead's history. Power lines strung from regional hydroelectric generators brought clean, continuous electrical current right to the farmhouse service entrance.\n\nElectricians routed copper conductors through ceiling and floor joists supported by each porcelain knob-and-tube insulator. These glazed ceramic tubes and cleats maintained safe physical separation between live wires and dry wooden framing, preventing electrical spark arcs while powering overhead lighting circuits.\n\nThe fluted, multi-column shape of each cast-iron hot water radiator fin profile maximized surface area for convective air currents and gentle thermal radiation. This hydronic system delivered draft-free, uniform warmth across every room, stabilizing interior temperatures during harsh winter freezes.", + specs: [ + { label: "Electrical", value: "Knob-and-Tube Wiring" }, + { label: "Heating", value: "Hydronic Cast Radiators" }, + { label: "Metering", value: "Spinning Induction Disk" }, + { label: "Voltage", value: "110V AC Single-Phase" }, + ], + }, + { + id: "step-9", + year: "1950", + title: "Decline & Abandonment", + subtitle: "Weathering, Roof Sag & Botanical Reclamation", + paragraphs: [ + "Following post-war economic shifts and the rapid industrialization of agriculture, the historic homestead experienced a profound transition as cultivation ceased and human occupants departed. Left without regular maintenance, the structure entered a prolonged period of architectural weathering and environmental exposure.", + "Winter freeze-thaw cycles and heavy snow accumulation initiated structural compromise across the roofline, visible in weathered timber siding erosion & rafter sag curve. Untreated timber rafters sagged downward under persistent moisture loads, creating a pronounced deflection that dislodged slate shingles from their battens.", + "On the exterior walls, decades of wind-driven rain caused severe grain erosion across the cedar siding. Lime mortar joints between fieldstones leached away under acidic rainfall, leaving exterior foundation walls vulnerable to moisture intrusion and differential frost settlement.", + "In the absence of human intervention, ecological succession rapidly reclaimed the built environment as a pioneer tree root masonry fracture formed along the footing. Pioneer saplings and sugar maples took root in the accumulated leaf litter along the foundation, driving sturdy roots into masonry joints.", + "Wild grapevines, poison ivy, and Virginia creeper climbed the siding, enveloping cracked windowpanes and framing bents in a dense botanical canopy. The decaying architecture slowly merged back into the surrounding forest ecosystem, entering a state of romantic structural repose.", + ], + description: + "Following post-war economic shifts and the rapid industrialization of agriculture, the historic homestead experienced a profound transition as cultivation ceased and human occupants departed. Left without regular maintenance, the structure entered a prolonged period of architectural weathering and environmental exposure.\n\nWinter freeze-thaw cycles and heavy snow accumulation initiated structural compromise across the roofline, visible in weathered timber siding erosion & rafter sag curve. Untreated timber rafters sagged downward under persistent moisture loads, creating a pronounced deflection that dislodged slate shingles from their battens.\n\nIn the absence of human intervention, ecological succession rapidly reclaimed the built environment as a pioneer tree root masonry fracture formed along the footing. Pioneer saplings and sugar maples took root in the accumulated leaf litter along the foundation, driving sturdy roots into masonry joints.", + specs: [ + { label: "Structural State", value: "Compromised / Rafter Sag" }, + { label: "Weathering", value: "Severe Mortar Leaching" }, + { label: "Overgrowth", value: "Pioneer Root Fractures" }, + { label: "Human Occupancy", value: "0 (Abandoned)" }, + ], + }, + { + id: "step-10", + year: "1965", + title: "Emergency Shoring & Stabilization", + subtitle: "Screw-Jack Columns & Structural Halts", + paragraphs: [ + "Alarmed by the imminent threat of catastrophic collapse, local historical preservationists and structural engineers intervened in 1965 to execute emergency stabilization measures. Their primary goal was to arrest ongoing structural deflection and weatherproof the vulnerable building envelope before the harsh winter season.", + "In the damp root cellar, engineers inserted an adjustable screw-jack steel lally column beneath the sagging main summer beams and girders. By turning the threaded collars with heavy wrenches, teams applied upward jacking force to halt downward deflection and stabilize the compromised timber structure.", + "Where historic oak sill plates had rotted entirely from ground contact, temporary reinforced concrete pads were poured onto the basement dirt floor. The screw-jack columns transferred multi-ton dead loads directly onto these new concrete footings, bypassing the deteriorated perimeter masonry.", + "On the exterior, structural stabilization strapping was applied across bulging masonry sections, while utility service was tracked by an external watt-hour induction meter disk. Modern weatherproof service panels were installed to provide safe temporary site power for restoration machinery and diagnostic illumination.", + "Compromised sections of the sagging roof structure were covered with heavy tar canvas sheeting and timber battens. This rapid weatherproofing intervention successfully sealed out rain and snowmelt, preserving the historic timber skeleton while comprehensive architectural restoration plans were prepared.", + ], + description: + "Alarmed by the imminent threat of catastrophic collapse, local historical preservationists and structural engineers intervened in 1965 to execute emergency stabilization measures. Their primary goal was to arrest ongoing structural deflection and weatherproof the vulnerable building envelope before the harsh winter season.\n\nIn the damp root cellar, engineers inserted an adjustable screw-jack steel lally column beneath the sagging main summer beams and girders. By turning the threaded collars with heavy wrenches, teams applied upward jacking force to halt downward deflection and stabilize the compromised timber structure.\n\nOn the exterior, structural stabilization strapping was applied across bulging masonry sections, while utility service was tracked by an external watt-hour induction meter disk. Modern weatherproof service panels were installed to provide safe temporary site power for restoration machinery and diagnostic illumination.", + specs: [ + { label: "Shoring", value: "Screw-Jack Lally Columns" }, + { label: "Load Transfer", value: "Temporary Concrete Pads" }, + { label: "Weatherproofing", value: "Heavy Tar Canvas Sheeting" }, + { label: "Status", value: "Emergency Stabilization" }, + ], + }, + { + id: "step-11", + year: "1980", + title: "Modernization Planning", + subtitle: "Blueprint Overlays & Hydraulic Jacking", + paragraphs: [ + "A new chapter of formal preservation commenced in 1980 as conservation engineers and historic architects initiated a rigorous, scientific restoration program. Detailed structural assessments and crisp technical blueprint overlays (A-101) were generated to map every timber framing member and masonry defect with millimeter precision.", + "To correct two centuries of differential foundation settlement, teams positioned a 12-ton hydraulic jack ram under joists and bearing girders. Working in synchronized increments monitored by laser transit realignment, the jacks gently lifted the sagging floors back to true horizontal datum without overstressing historic joints.", + "Where historic timber girders exhibited excessive deflection under modern design criteria, concealed structural steel C-channel grids and steel lintels were inserted parallel to the original oak beams. This hidden structural reinforcement carried all heavy floor live loads while preserving the authentic visual aesthetic of the exposed 18th-century timbers.", + "Deteriorated sections of the white oak sill plates were surgically excavated and repaired using a pressure injection epoxy resin splice. Liquid epoxy resins infused under pressure bonded period-correct hardwood prosthetics to sound ancient timber, restoring full compressive and tensile strength.", + "This meticulous conservation methodology successfully bridged historical preservation integrity with modern residential building codes. The stabilized and reinforced shell now stood fully capable of supporting contemporary domestic living standards while celebrating its authentic material history.", + ], + description: + "A new chapter of formal preservation commenced in 1980 as conservation engineers and historic architects initiated a rigorous, scientific restoration program. Detailed structural assessments and crisp technical blueprint overlays (A-101) were generated to map every timber framing member and masonry defect with millimeter precision.\n\nTo correct two centuries of differential foundation settlement, teams positioned a 12-ton hydraulic jack ram under joists and bearing girders. Working in synchronized increments monitored by laser transit realignment, the jacks gently lifted the sagging floors back to true horizontal datum without overstressing historic joints.\n\nDeteriorated sections of the white oak sill plates were surgically excavated and repaired using a pressure injection epoxy resin splice. Liquid epoxy resins infused under pressure bonded period-correct hardwood prosthetics to sound ancient timber, restoring full compressive and tensile strength.", + specs: [ + { label: "Methodology", value: "Structural Conservation" }, + { label: "Lifting", value: "12-Ton Hydraulic Rams" }, + { label: "Repair", value: "Epoxy Resin Splice" }, + { label: "Target Load", value: "Modern Code Compliance" }, + ], + }, + { + id: "step-12", + year: "1995", + title: "Radon Mitigation & Perimeter Drainage", + subtitle: "Subterranean Soil Gas Control & French Drains", + paragraphs: [ + "Deep subterranean geotechnical assessments conducted during basement restoration revealed natural soil gas permeability across the underlying granite bedrock strata. Radioactive radon gas rising from decaying uranium in the deep ledge required active subterranean soil mechanics and gas interception to ensure healthy indoor air quality.", + "Engineers excavated a sub-slab radon suction core pit beneath the newly poured basement concrete slab. Perforated collection pipes embedded inside washed drainage gravel intercepted soil gas before it could penetrate the living space, directing it into a sealed vertical riser pipe.", + "An inline continuous PVC exhaust fan system mounted in the attic created permanent negative pressure beneath the basement floor slab. Soil gases drawn into the suction core pit were safely evacuated and discharged above the roofline high into the atmosphere.", + "Simultaneously, groundwater infiltration was eliminated by installing a perforated PVC French drain in washed gravel laid at the exterior footing level (-12 Ft. Below Grade). Perforated PVC conduits wrapped in permeable geotextile fabric intercepted subterranean groundwater.", + "This subterranean drainage network relieved hydrostatic groundwater pressure vectors acting against foundation walls. Water draining into the washed gravel was diverted away by gravity to a daylight discharge basin in the lower meadow, leaving the granite cellar permanently bone-dry.", + ], + description: + "Deep subterranean geotechnical assessments conducted during basement restoration revealed natural soil gas permeability across the underlying granite bedrock strata. Radioactive radon gas rising from decaying uranium in the deep ledge required active subterranean soil mechanics and gas interception to ensure healthy indoor air quality.\n\nEngineers excavated a sub-slab radon suction core pit beneath the newly poured basement concrete slab. Perforated collection pipes embedded inside washed drainage gravel intercepted soil gas before it could penetrate the living space, directing it into a sealed vertical riser pipe.\n\nSimultaneously, groundwater infiltration was eliminated by installing a perforated PVC French drain in washed gravel laid at the exterior footing level (-12 Ft. Below Grade). Perforated PVC conduits wrapped in permeable geotextile fabric intercepted subterranean groundwater.", + specs: [ + { label: "Soil Gas", value: "Sub-Slab Radon Suction" }, + { label: "Drainage", value: "Perforated PVC French Drain" }, + { label: "Substratum", value: "Granite / Washed Gravel" }, + { label: "Fan System", value: "Continuous Inline Exhaust" }, + ], + }, + { + id: "step-13", + year: "2010", + title: "Ecological Insulation & Glazing", + subtitle: "Thermal Envelopes & Triple Glazing", + paragraphs: [ + "In response to 21st-century environmental engineering mandates and zero-carbon energy targets, the homestead underwent a comprehensive high-performance thermal retrofitting. The primary focus was creating a continuous, airtight building envelope without altering the historic exterior architectural profile.", + "Inside the wall cavities between exterior cedar siding and interior framing, contractors sprayed an R-45 closed-cell spray foam wall cavity barrier. This monolithic polyurethane foam insulation expanded to seal every gap, crack, and mortise joint, establishing an impenetrable barrier against air leakage and vapor diffusion.", + "All perimeter rim joists, attic rafters, and foundation interfaces were meticulously sealed with elastomeric membranes. This zero-draft encapsulation eliminated convective heat loss and transformed the drafty historic structure into a high-efficiency thermal vessel.", + "Historic window openings were fitted with a triple-pane Low-E argon glazing profile set in insulated hardwood sash frames. The argon gas fill between the three glass panes provided superior thermal resistance while Low-E metallic coatings reflected interior radiant heat back into the living space.", + "By eradicating thermal bridging across walls and fenestration, the building achieved an exceptional overall U-factor (0.022 BTUs/hr-sq.ft-°F). Coupled with deep ground-source geothermal loops bored 400 feet into bedrock, the home achieved whisper-quiet, ultra-efficient radiant climate control.", + ], + description: + "In response to 21st-century environmental engineering mandates and zero-carbon energy targets, the homestead underwent a comprehensive high-performance thermal retrofitting. The primary focus was creating a continuous, airtight building envelope without altering the historic exterior architectural profile.\n\nInside the wall cavities between exterior cedar siding and interior framing, contractors sprayed an R-45 closed-cell spray foam wall cavity barrier. This monolithic polyurethane foam insulation expanded to seal every gap, crack, and mortise joint, establishing an impenetrable barrier against air leakage and vapor diffusion.\n\nHistoric window openings were fitted with a triple-pane Low-E argon glazing profile set in insulated hardwood sash frames. The argon gas fill between the three glass panes provided superior thermal resistance while Low-E metallic coatings reflected interior radiant heat back into the living space.", + specs: [ + { label: "Envelope", value: "Closed-Cell Foam (R-45)" }, + { label: "Glazing", value: "Triple-Pane Low-E Argon" }, + { label: "Air Sealing", value: "Elastomeric Vapor Barrier" }, + { label: "HVAC", value: "Geothermal Radiant Loop" }, + ], + }, + { + id: "step-14", + year: "Present", + title: "Present Modern Design", + subtitle: "Minimalist Architectural Synthesis & Energy Independence", + paragraphs: [ + "Today, the homestead stands as a breathtaking architectural synthesis of centuries-old agrarian craftsmanship and uncompromising minimalist contemporary design. The restored 1780 fieldstone hearth and hand-hewn white oak timbers serve as tactile, historical monuments anchored within an expansive, light-filled modern interior.", + "Bold structural engineering defines the contemporary addition, featuring a cantilevered steel W12x50 moment connection. These high-strength steel beams support frameless floor-to-ceiling glass curtain walls that dissolve the visual boundary between the interior living space and the surrounding meadow landscape.", + "The high-contrast juxtaposition between rough historic masonry, textured old-growth oak, and sleek industrial black steel creates a dynamic visual dialogue across 240 years of architectural history. Every structural transition is celebrated with crisp, honest detailing.", + "Energy independence is achieved through advanced microgrid infrastructure utilizing a bi-directional V2H battery rack in the utility annex. This intelligent storage system balances clean energy generated by discreet rooftop cantilevered solar PV arrays (R-60 Core roof system) with electric vehicle fast-charging.", + "Operating at net-negative carbon emissions (-15% positive generation), the 3,800-square-foot residence powers daily living and a Level 2 EV charging bay entirely from renewable resources. By honoring every era of its built heritage while pioneering future structural and ecological design, the homestead exemplifies timeless living architecture.", + ], + description: + "Today, the homestead stands as a breathtaking architectural synthesis of centuries-old agrarian craftsmanship and uncompromising minimalist contemporary design. The restored 1780 fieldstone hearth and hand-hewn white oak timbers serve as tactile, historical monuments anchored within an expansive, light-filled modern interior.\n\nBold structural engineering defines the contemporary addition, featuring a cantilevered steel W12x50 moment connection. These high-strength steel beams support frameless floor-to-ceiling glass curtain walls that dissolve the visual boundary between the interior living space and the surrounding meadow landscape.\n\nEnergy independence is achieved through advanced microgrid infrastructure utilizing a bi-directional V2H battery rack in the utility annex. This intelligent storage system balances clean energy generated by discreet rooftop cantilevered solar PV arrays (R-60 Core roof system) with electric vehicle fast-charging.", + specs: [ + { label: "Footprint / EV", value: "3,800 sq. ft. / Level 2 EV Bay" }, + { label: "Structure", value: "Cantilevered W12x50 Steel" }, + { label: "Microgrid", value: "Bi-Directional V2H Battery" }, + { label: "Net Energy", value: "-15% (Positive Generation)" }, + ], + }, + { + id: "step-15", + year: "Geotechnical", + title: "Geotechnical Engineering Drill-down", + subtitle: "Soil Mechanics & Stress Bulb Distribution", + paragraphs: [ + "A dedicated geotechnical engineering drill-down reveals the complex subterranean soil mechanics governing foundation stability under the restored 3,800-square-foot residence. Subsurface bore logs and strata profiling map the exact load transfer paths from surface footings down to deep bedrock ledges.", + "Beneath the dry-laid fieldstone footings, structural loads propagate through glacial till layers where the soil strata stress bulb distribution curve models subterranean bearing contour lines. Charting these contours demonstrates how pressure widens and attenuates as depth increases into the dense substratum.", + "Laboratory soil mechanics testing confirms a safe soil bearing capacity of 3.5 tons per square foot across the compacted glacial till and clay horizons. This robust bearing threshold ensures zero differential settlement under multi-story dead and live loads.", + "Lateral subterranean analysis focuses on a hydrostatic groundwater pressure vector acting against the deep basement retaining walls (-12 Ft. Below Grade). During heavy seasonal rainfall, saturated soil attempts to exert immense horizontal shear force against perimeter masonry.", + "The combination of washed gravel backfill, geotextile filtration fabric, and sub-slab perimeter drainage channels completely intercepts and dissipates these hydrostatic vectors. By reducing lateral water pressure to absolute zero, the geotechnical design guarantees perpetual subterranean structural integrity.", + ], + description: + "A dedicated geotechnical engineering drill-down reveals the complex subterranean soil mechanics governing foundation stability under the restored 3,800-square-foot residence. Subsurface bore logs and strata profiling map the exact load transfer paths from surface footings down to deep bedrock ledges.\n\nBeneath the dry-laid fieldstone footings, structural loads propagate through glacial till layers where the soil strata stress bulb distribution curve models subterranean bearing contour lines. Charting these contours demonstrates how pressure widens and attenuates as depth increases into the dense substratum.\n\nLateral subterranean analysis focuses on a hydrostatic groundwater pressure vector acting against the deep basement retaining walls (-12 Ft. Below Grade). During heavy seasonal rainfall, saturated soil attempts to exert immense horizontal shear force against perimeter masonry.", + specs: [ + { label: "Discipline", value: "Geotechnical Engineering" }, + { label: "Bearing Capacity", value: "3.5 Tons / Sq. Ft." }, + { label: "Stress Model", value: "Boussinesq Stress Bulb" }, + { label: "Water Table", value: "-12 Ft. Below Grade" }, + ], + }, + { + id: "step-16", + year: "Thermodynamics", + title: "Thermodynamics Drill-down", + subtitle: "Heat Flux & Dew-Point Condensation Planes", + paragraphs: [ + "A rigorous building physics and thermodynamics drill-down quantifies the thermal envelope performance across the hybrid historic and contemporary wall sections. Advanced finite element thermal modeling verifies heat flux vectors across every material boundary.", + "Critical to envelope longevity is exact dew-point condensation plane tracking within the multi-layered wall assembly. Temperature and vapor pressure gradients are plotted across exterior cedar siding, air gaps, wood fiber sheathing, and interior closed-cell foam.", + "Thermal analysis confirms that the dew-point temperature plane occurs exclusively on the exterior face of the vapor-impermeable closed-cell foam insulation. This ensures that any interstitial moisture remains outside the timber framing, preventing winter condensation, mold formation, and structural wood rot.", + "Continuous exterior wood fiber insulation board overlays achieve complete infrared thermal bridging elimination across structural posts and studs. Thermographic heat flux mapping proves zero cold spotting and uniform surface temperatures across interior walls (U-Factor: 0.022 BTUs/hr-sq.ft-°F).", + "Multi-layer conductive, convective, and radiative heat transfer calculations demonstrate a staggering 94.2% reduction in total thermal loss compared to the 1780 log cabin baseline. This thermodynamic mastery enables ultra-low-energy climate comfort in any extreme weather conditions.", + ], + description: + "A rigorous building physics and thermodynamics drill-down quantifies the thermal envelope performance across the hybrid historic and contemporary wall sections. Advanced finite element thermal modeling verifies heat flux vectors across every material boundary.\n\nCritical to envelope longevity is exact dew-point condensation plane tracking within the multi-layered wall assembly. Temperature and vapor pressure gradients are plotted across exterior cedar siding, air gaps, wood fiber sheathing, and interior closed-cell foam.\n\nContinuous exterior wood fiber insulation board overlays achieve complete infrared thermal bridging elimination across structural posts and studs. Thermographic heat flux mapping proves zero cold spotting and uniform surface temperatures across interior walls (U-Factor: 0.022 BTUs/hr-sq.ft-°F).", + specs: [ + { label: "Discipline", value: "Building Thermodynamics" }, + { label: "U-Factor", value: "0.022 BTUs/hr-sq.ft-°F" }, + { label: "Dew-Point", value: "Exterior of Vapor Barrier" }, + { label: "Heat Loss Reduc.", value: "94.2% vs. 1780 Baseline" }, + ], + }, + { + id: "step-17", + year: "Kinematics", + title: "Structural Kinematics Drill-down", + subtitle: "Load Vectors & Shear Deflection", + paragraphs: [ + "The final engineering analysis presents a comprehensive structural kinematics drill-down examining static equilibrium, dynamic moment resistance, and wind-load deflection across the entire architectural complex. Vector mechanics illustrate the exact equilibrium of forces keeping the structure upright.", + "A dead vs live load vector diagram maps the continuous downward gravity loads from roof snow accumulation down through timber trusses, girders, and steel columns into the granite footings. Permanent dead loads (32 lbs/sq.ft.) and transient live loads (40 lbs/sq.ft.) are cleanly separated in structural calculations.", + "Under severe storm simulations featuring 90 mph wind gusts, lateral wind forces exert overturning moments against the exterior facades. Structural moment vector analysis demonstrates how the hybrid steel moment frame and braced timber shear walls work in unison to resist rotational uplift.", + "Precision optical and strain gauge monitoring verifies wind moment shear-wall deflection limits across top-story spans. Even under maximum peak wind shear, lateral story drift is restricted to less than 0.25 inches (deflection index L/360), well below structural damage thresholds.", + "This kinematic equilibrium between gravity loads, shear resistance, and elastic deflection limits validates the structural synthesis. By uniting 18th-century timber craftsmanship with 21st-century steel moment engineering, the Homestead Dossier achieves structural perfection that will stand for generations to come.", + ], + description: + "The final engineering analysis presents a comprehensive structural kinematics drill-down examining static equilibrium, dynamic moment resistance, and wind-load deflection across the entire architectural complex. Vector mechanics illustrate the exact equilibrium of forces keeping the structure upright.\n\nA dead vs live load vector diagram maps the continuous downward gravity loads from roof snow accumulation down through timber trusses, girders, and steel columns into the granite footings. Permanent dead loads (32 lbs/sq.ft.) and transient live loads (40 lbs/sq.ft.) are cleanly separated in structural calculations.\n\nPrecision optical and strain gauge monitoring verifies wind moment shear-wall deflection limits across top-story spans. Even under maximum peak wind shear, lateral story drift is restricted to less than 0.25 inches (deflection index L/360), well below structural damage thresholds.", + specs: [ + { label: "Discipline", value: "Structural Kinematics" }, + { label: "Dead Load", value: "32 lbs / sq. ft." }, + { label: "Live Load", value: "40 lbs / sq. ft." }, + { label: "Shear Deflection", value: "Max L/360 @ 90 mph" }, + ], + }, +]; diff --git a/suites-experimental/scrollytelling/src/engine-scrollama.js b/suites-experimental/scrollytelling/src/engine-scrollama.js new file mode 100644 index 000000000..4d25dd908 --- /dev/null +++ b/suites-experimental/scrollytelling/src/engine-scrollama.js @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * Copyright (C) 2024 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +import scrollama from "scrollama"; + +export function initScrollamaEngine(onUpdateCallback) { + const scroller = scrollama(); + + scroller + .setup({ + step: ".stage-section", + offset: 0.5, + progress: true, + }) + .onStepEnter((res) => { + onUpdateCallback(res.index, 0.0); + if (typeof window.forceScrollytellingUpdate === "function") + window.forceScrollytellingUpdate(); + }) + .onStepProgress((res) => { + onUpdateCallback(res.index, res.progress); + if (typeof window.forceScrollytellingUpdate === "function") + window.forceScrollytellingUpdate(); + }); + + window.addEventListener( + "resize", + () => { + scroller.resize(); + }, + { passive: true } + ); + + return scroller; +} diff --git a/suites-experimental/scrollytelling/src/graphics.js b/suites-experimental/scrollytelling/src/graphics.js new file mode 100644 index 000000000..a5669cda8 --- /dev/null +++ b/suites-experimental/scrollytelling/src/graphics.js @@ -0,0 +1,2396 @@ +/* + * Copyright (C) 2014 Apple Inc. All rights reserved. + * Copyright (C) 2024 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * 1950s Black & White Blueprint / Planning Design with Gradual Organic Watercolor Animation Reveals + * Enhanced with Organic Hand-Drafted Fuzzy Pencil Linework, Offscreen Sprite Caching, & High-Contrast Pure Black + * + * Core Style & Aesthetic Rules: + * - Theme: Negative blueprint drafting / technical planning print on a pure black background (#000000). + * - Palette: Strictly high-contrast B&W for all structural UI, wireframes, typography, and procedural line art. + * Primary drafting ink is pure white (#ffffff), secondary lines/specifications are light gray (#cccccc), and hatching/grids are muted gray (#777777). + * - Background Assets: Strictly planning paper dot grid (no static watercolor or grayscale bitmap background textures). + * - Linework: All vector paths and geometric primitives use dual-pass, irregular, wobbly, and fuzzy pencil strokes with corner overruns. + * - Performance: Repeating hatchings and complex geometric elements are cached to offscreen sprites and procedural patterns. + */ +import { STAGES } from "./content.js"; + +export const STYLE_CONFIG = { + theme: "1950s Black & White Blueprint / Planning Design with Organic Watercolor Accents", + background: "#000000", + canvasFill: "#000000", + palette: { + inkPrimary: "#ffffff", + inkSecondary: "#cccccc", + inkMuted: "#777777", + paperLight: "#111111", + paperCard: "#0a0a0a", + accents: { + blueprintBlue: "#00a8ff", + amberGold: "#fbc531", + terracotta: "#e84118", + emerald: "#4cd137", + }, + }, + revealModes: ["circular", "slide", "fade"], +}; + +const IMAGE_SOURCES = { + grid: "public/planning_paper_dot_grid.webp", +}; + +const imageCache = {}; +const spriteCache = {}; +const patternCache = {}; + +function getOrLoadImage(key) { + if (typeof Image === "undefined") + return null; + if (!imageCache[key] && IMAGE_SOURCES[key]) { + const img = new Image(); + img.src = IMAGE_SOURCES[key]; + imageCache[key] = img; + } + return imageCache[key] || null; +} + +/* eslint-disable-next-line no-unused-vars */ +function drawImageIfLoaded(ctx, img, x, y, w, h, alpha = 0.35, composite = "screen", filter = "invert(1)") { + if (img && img.complete && img.naturalWidth > 0) { + ctx.save(); + ctx.globalAlpha = alpha; + ctx.globalCompositeOperation = composite; + if (filter && typeof ctx.filter !== "undefined") + ctx.filter = filter; + + ctx.drawImage(img, x, y, w, h); + ctx.restore(); + } +} + +/* eslint-disable-next-line no-unused-vars */ +let offscreenCanvas = null; +/* eslint-disable-next-line no-unused-vars */ +function getOffscreenCanvas(w, h) { + if (typeof document === "undefined") + return null; + if (!offscreenCanvas) + offscreenCanvas = document.createElement("canvas"); + + if (offscreenCanvas.width !== w || offscreenCanvas.height !== h) { + offscreenCanvas.width = w; + offscreenCanvas.height = h; + } + return offscreenCanvas; +} + +function getCachedSprite(key, width, height, renderFunc) { + if (typeof document === "undefined" && typeof OffscreenCanvas === "undefined") + return null; + if (!spriteCache[key]) { + let canvas = null; + if (typeof OffscreenCanvas !== "undefined") { + canvas = new OffscreenCanvas(width, height); + } else if (typeof document !== "undefined") { + canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + } + if (canvas) { + const ctx = canvas.getContext("2d"); + renderFunc(ctx, width, height); + spriteCache[key] = canvas; + } + } + return spriteCache[key] || null; +} + +function getCachedPattern(ctx, key, width, height, renderFunc) { + if (!patternCache[key]) { + const sprite = getCachedSprite(`pattern_${key}`, width, height, renderFunc); + if (sprite && ctx && typeof ctx.createPattern === "function") + patternCache[key] = ctx.createPattern(sprite, "repeat"); + } + return patternCache[key] || null; +} + +/* eslint-disable-next-line no-unused-vars */ +function applyCircularFocusClip(ctx, x, y, maxRadius, prog) { + const r = maxRadius * Math.max(0, Math.min(1, prog)); + ctx.beginPath(); + ctx.arc(x, y, r, 0, Math.PI * 2); + ctx.clip(); +} + +/* eslint-disable-next-line no-unused-vars */ +function applySlideClip(ctx, x, y, width, height, prog, direction = "left-to-right") { + const p = Math.max(0, Math.min(1, prog)); + ctx.beginPath(); + if (direction === "left-to-right") { + ctx.rect(x, y, width * p, height); + } else if (direction === "top-to-bottom") { + ctx.rect(x, y, width, height * p); + } else if (direction === "center-out") { + const w = width * p; + ctx.rect(x + (width - w) / 2, y, w, height); + } else { + ctx.rect(x, y, width * p, height); + } + ctx.clip(); +} + +/* eslint-disable-next-line no-unused-vars */ +export function drawTintedBrush(ctx, imgKey, x, y, w, h, color, alpha = 0.5, blendMode = "screen", prog = 1.0, revealMode = "fade", revealArgs = {}) { + if (prog <= 0) + return; + ctx.save(); + ctx.globalAlpha = alpha * Math.max(0, Math.min(1, prog)); + ctx.fillStyle = color; + ctx.fillRect(x, y, w, h); + ctx.restore(); +} + +export function getLandscapeRevealTiming(localProg) { + const p = Math.max(0.0, Math.min(1.0, localProg)); + const colorProg = Math.max(0.0, Math.min(1.0, p / 0.6)); + const labelProg = Math.max(0.0, Math.min(1.0, (p - 0.6) / 0.4)); + return { colorProg, labelProg }; +} + +function getPhaseTiming(localProg) { + const assemblyProg = Math.min(1.0, localProg / 0.35); + const secondaryProg = Math.max(0.0, Math.min(1.0, (localProg - 0.35) / 0.35)); + const holdProg = Math.max(0.0, Math.min(1.0, (localProg - 0.7) / 0.3)); + return { assemblyProg, secondaryProg, holdProg }; +} + +/* eslint-disable-next-line no-unused-vars */ +export function drawColorizedBackground(ctx, bgKey, x, y, w, h, color, wcKey = "wc1", prog = 1.0, revealMode = "fade", revealArgs = {}) { + if (prog <= 0) + return; + ctx.save(); + ctx.globalAlpha = 0.75 * Math.max(0, Math.min(1, prog)); + ctx.fillStyle = color; + ctx.fillRect(x, y, w, h); + ctx.restore(); +} + +let stageGraphics = []; + +export function initGraphics() { + stageGraphics = []; + + for (const key of Object.keys(IMAGE_SOURCES)) + getOrLoadImage(key); + + for (let i = 0; i < STAGES.length; i++) { + const canvas = document.getElementById(`graphic-canvas-${i}`); + const svg = document.getElementById(`graphic-svg-${i}`); + const caption = document.getElementById(`graphic-caption-${i}`); + + let ctx = null; + if (canvas) { + canvas.width = 800; + canvas.height = 600; + ctx = canvas.getContext("2d"); + } + + let g = null; + if (svg) { + svg.innerHTML = ""; + g = document.createElementNS("http://www.w3.org/2000/svg", "g"); + g.setAttribute("id", `svg-stage-${i}`); + g.setAttribute("opacity", "1"); + g.setAttribute("transform", "scale(1)"); + g.style.transition = "none"; + + const handler = STAGE_HANDLERS[i]; + if (handler && typeof handler.initSVG === "function") + handler.initSVG(g, 800, 600); + + svg.appendChild(g); + } + + if (caption && STAGES[i]) + caption.textContent = `Stage ${i + 1}: ${STAGES[i].title}`; + + stageGraphics.push({ + canvas, + ctx, + svg, + g, + caption, + lastProg: -1, + }); + } +} + +export function updateGraphics(stageIndex, progress = 0) { + const idx = Math.max(0, Math.min(STAGES.length - 1, stageIndex)); + const prog = Math.max(0, Math.min(1, progress)); + + for (let i = 0; i < stageGraphics.length; i++) { + const item = stageGraphics[i]; + if (!item || !item.g || !item.ctx || !item.canvas) + continue; + + const stageProg = i === idx ? prog : i < idx ? 1.0 : 0.0; + + if (i === idx || item.lastProg !== stageProg) { + item.g.setAttribute("transform", "translate(0, 50) scale(1)"); + + const phase = getParagraphPhase(stageProg, 5); + setCalloutPhaseVisibility(item.g, phase); + + const handler = STAGE_HANDLERS[i]; + if (handler) { + if (typeof handler.updateSVG === "function") + handler.updateSVG(item.g, stageProg, item.canvas.width, item.canvas.height, phase); + + if (typeof handler.renderCanvas === "function") { + item.ctx.clearRect(0, 0, item.canvas.width, item.canvas.height); + item.ctx.save(); + item.ctx.translate(0, 50); + handler.renderCanvas(item.ctx, item.g, stageProg, item.canvas.width, item.canvas.height, phase); + item.ctx.restore(); + drawTopMiddleWidget(item.ctx, i, stageProg, phase); + } + } + + item.lastProg = stageProg; + } + } +} + +/* ========================================================================= + 1950s STRICT GRAYSCALE & B&W ORGANIC FUZZY PENCIL DRAFTING UTILITIES + ========================================================================= */ + +function createSVGElement(tag, attrs = {}) { + const el = document.createElementNS("http://www.w3.org/2000/svg", tag); + for (const [k, v] of Object.entries(attrs)) + el.setAttribute(k, String(v)); + + return el; +} + +/* eslint-disable-next-line no-unused-vars */ +function addSVGImage(g, src, x, y, width, height, opacity = 0.35, blendMode = "screen", filterStyle = "invert(1)") { + const ns = "http://www.w3.org/2000/svg"; + const img = document.createElementNS(ns, "image"); + img.setAttribute("href", src); + if (typeof img.setAttributeNS === "function") + img.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", src); + + img.setAttribute("x", String(x)); + img.setAttribute("y", String(y)); + img.setAttribute("width", String(width)); + img.setAttribute("height", String(height)); + img.setAttribute("opacity", String(opacity)); + if (blendMode) + img.style.mixBlendMode = blendMode; + if (filterStyle) + img.style.filter = filterStyle; + g.appendChild(img); + return img; +} + +/** + * Core Organic Fuzzy Pencil Line Primitive + * Draws dual-pass, high-resolution scanned pencil strokes with natural human drafting wobble, + * uneven line pressure, and quirky drafting errors (overshooting corners by ±1.5..3.0px). + */ +function drawFuzzyPencilLine(ctx, x1, y1, x2, y2, roughness = 0.8, width = 1.2, options = {}) { + const dx = x2 - x1; + const dy = y2 - y1; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < 0.1) + return; + + const ux = dx / dist; + const uy = dy / dist; + const nx = -uy; + const ny = ux; + + let sx = x1, + sy = y1, + ex = x2, + ey = y2; + + if (options.overshoot !== false && options.overshoot !== 0) { + const seedOvershoot = ((x1 * 3.1 + y1 * 7.7 + x2 * 5.3 + y2 * 11.1) % 1.5) + 1.5; + const os = typeof options.overshoot === "number" ? options.overshoot : seedOvershoot; + sx = x1 - ux * os; + sy = y1 - uy * os; + ex = x2 + ux * os; + ey = y2 + uy * os; + } + + const steps = Math.max(3, Math.ceil(dist / 14)); + const color = options.color || ctx.strokeStyle || "#ffffff"; + const secondaryColor = options.secondaryColor || "rgba(255, 255, 255, 0.45)"; + + // Pass 1: Fuzzy secondary underlying stroke (soft pencil lead scatter) + ctx.save(); + ctx.strokeStyle = secondaryColor; + ctx.lineWidth = width * 1.4; + ctx.shadowBlur = options.shadowBlur || 2.2; + ctx.shadowColor = options.shadowColor || "rgba(255, 255, 255, 0.65)"; + ctx.beginPath(); + ctx.moveTo(sx + nx * 0.5, sy + ny * 0.5); + for (let i = 1; i <= steps; i++) { + const t = i / steps; + let px = sx + (ex - sx) * t; + let py = sy + (ey - sy) * t; + if (i < steps) { + const seed = (x1 * 13.1 + y1 * 71.7 + x2 * 19.3 + y2 * 41.9 + i * 17.3) * 0.1; + px += Math.sin(seed) * roughness * 1.2; + py += Math.cos(seed * 1.3) * roughness * 1.2; + } + ctx.lineTo(px, py); + } + ctx.stroke(); + ctx.restore(); + + // Pass 2: Primary sharp/wobbly core pencil line + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = width; + ctx.beginPath(); + ctx.moveTo(sx, sy); + for (let i = 1; i <= steps; i++) { + const t = i / steps; + let px = sx + (ex - sx) * t; + let py = sy + (ey - sy) * t; + if (i < steps) { + const seed = (x1 * 17.3 + y1 * 53.1 + x2 * 23.9 + y2 * 31.7 + i * 29.3) * 0.12; + px += Math.sin(seed * 1.1) * roughness; + py += Math.cos(seed * 0.9) * roughness; + } + ctx.lineTo(px, py); + } + ctx.stroke(); + + // Optional double-stroked overlapping middle segment for human uneven pressure + if (options.doubleStroke !== false && dist > 25) { + ctx.strokeStyle = color; + ctx.globalAlpha = 0.55; + ctx.lineWidth = Math.max(0.8, width * 0.85); + ctx.beginPath(); + const midT1 = 0.2 + ((x1 + y1) % 0.2); + const midT2 = 0.65 + ((x2 + y2) % 0.2); + ctx.moveTo(sx + (ex - sx) * midT1 + nx * roughness * 0.6, sy + (ey - sy) * midT1 + ny * roughness * 0.6); + ctx.lineTo(sx + (ex - sx) * midT2 + nx * roughness * 0.4, sy + (ey - sy) * midT2 + ny * roughness * 0.4); + ctx.stroke(); + } + ctx.restore(); +} + +/* eslint-disable-next-line no-unused-vars */ +function drawHandLine(ctx, x1, y1, x2, y2, roughness = 0.6, steps = 4) { + drawFuzzyPencilLine(ctx, x1, y1, x2, y2, roughness, ctx.lineWidth || 1, { + overshoot: 2.2, + doubleStroke: typeof steps === "number" && steps > 3, + }); +} + +function drawSketchRect(ctx, x, y, w, h, overshoot = 2.8, options = {}) { + drawFuzzyPencilLine(ctx, x, y, x + w, y, options.roughness || 0.6, ctx.lineWidth || 1, { ...options, overshoot }); + drawFuzzyPencilLine(ctx, x + w, y, x + w, y + h, options.roughness || 0.6, ctx.lineWidth || 1, { ...options, overshoot }); + drawFuzzyPencilLine(ctx, x + w, y + h, x, y + h, options.roughness || 0.6, ctx.lineWidth || 1, { ...options, overshoot }); + drawFuzzyPencilLine(ctx, x, y + h, x, y, options.roughness || 0.6, ctx.lineWidth || 1, { ...options, overshoot }); +} + +/* eslint-disable-next-line no-unused-vars */ +function drawCrossSectionHatching(ctx, x, y, w, h, spacing = 8, angle = -Math.PI / 4, color = "rgba(255, 255, 255, 0.35)") { + if (w <= 0 || h <= 0) + return; + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + + const diag = Math.sqrt(w * w + h * h) * 1.5; + const cx = x + w / 2; + const cy = y + h / 2; + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + for (let offset = -diag; offset <= diag; offset += spacing) { + const x1 = cx + offset * cos - diag * sin; + const y1 = cy + offset * sin + diag * cos; + const x2 = cx + offset * cos + diag * sin; + const y2 = cy + offset * sin - diag * cos; + drawFuzzyPencilLine(ctx, x1, y1, x2, y2, 0.4, 1, { + color, + secondaryColor: "rgba(255, 255, 255, 0.18)", + overshoot: false, + doubleStroke: false, + }); + } + ctx.restore(); +} + +function drawMasonryHatch(ctx, x, y, w, h, weathered = false) { + if (w <= 0 || h <= 0) + return; + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + + const tileKey = weathered ? "masonry_weathered_tile" : "masonry_clean_tile"; + const pattern = getCachedPattern(ctx, tileKey, 64, 64, (sCtx, sw, sh) => { + sCtx.strokeStyle = "rgba(255, 255, 255, 0.45)"; + sCtx.lineWidth = 1; + const spacing = 14; + for (let d = -sh; d < sw + sh; d += spacing) { + if (weathered && Math.sin(d * 0.5) > 0.3) + continue; + drawFuzzyPencilLine(sCtx, d, 0, d - sh, sh, 0.4, 1, { + color: "rgba(255, 255, 255, 0.45)", + secondaryColor: "rgba(255, 255, 255, 0.2)", + overshoot: false, + doubleStroke: false, + }); + } + }); + + if (pattern) { + ctx.fillStyle = pattern; + ctx.fillRect(x - 20, y - 20, w + 40, h + 40); + } else { + const spacing = 14; + for (let d = -h; d < w + h; d += spacing) { + if (weathered && Math.sin(d * 0.5) > 0.3) + continue; + drawFuzzyPencilLine(ctx, x + d, y, x + d - h, y + h, 0.4, 1, { + color: "rgba(255, 255, 255, 0.45)", + secondaryColor: "rgba(255, 255, 255, 0.2)", + overshoot: false, + doubleStroke: false, + }); + } + } + ctx.restore(); +} + +function drawSteelHatch(ctx, x, y, w, h) { + if (w <= 0 || h <= 0) + return; + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + + const pattern = getCachedPattern(ctx, "steel_hatch_tile", 64, 64, (sCtx, sw, sh) => { + for (let d = -sh; d < sw + sh; d += 16) { + drawFuzzyPencilLine(sCtx, d, 0, d - sh, sh, 0.3, 1, { + color: "rgba(255, 255, 255, 0.55)", + secondaryColor: "rgba(255, 255, 255, 0.25)", + overshoot: false, + doubleStroke: false, + }); + drawFuzzyPencilLine(sCtx, d + 4, 0, d + 4 - sh, sh, 0.3, 1, { + color: "rgba(255, 255, 255, 0.55)", + secondaryColor: "rgba(255, 255, 255, 0.25)", + overshoot: false, + doubleStroke: false, + }); + } + }); + + if (pattern) { + ctx.fillStyle = pattern; + ctx.fillRect(x - 20, y - 20, w + 40, h + 40); + } else { + for (let d = -h; d < w + h; d += 16) { + drawFuzzyPencilLine(ctx, x + d, y, x + d - h, y + h, 0.3, 1, { + color: "rgba(255, 255, 255, 0.55)", + secondaryColor: "rgba(255, 255, 255, 0.25)", + overshoot: false, + doubleStroke: false, + }); + drawFuzzyPencilLine(ctx, x + d + 4, y, x + d + 4 - h, y + h, 0.3, 1, { + color: "rgba(255, 255, 255, 0.55)", + secondaryColor: "rgba(255, 255, 255, 0.25)", + overshoot: false, + doubleStroke: false, + }); + } + } + ctx.restore(); +} + +function drawInsulationHatch(ctx, x, y, w, h, prog = 1.0, color = "rgba(204, 204, 204, 0.7)") { + if (w <= 0 || h <= 0) + return; + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + + const loopW = 12; + const loops = Math.ceil(w / loopW); + const midY = y + h / 2; + const amp = (h / 2 - 3) * Math.min(1.0, prog * 1.2); + + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(255, 255, 255, 0.3)" : color; + ctx.lineWidth = pass === 0 ? 2.2 : 1.5; + ctx.beginPath(); + ctx.moveTo(x, midY); + for (let i = 0; i < loops; i++) { + const lx = x + i * loopW; + const jx = (i % 2 === 0 ? 0.6 : -0.6) * (pass === 0 ? 1 : 0); + ctx.bezierCurveTo(lx + loopW * 0.25 + jx, midY - amp, lx + loopW * 0.75 + jx, midY - amp, lx + loopW, midY); + ctx.bezierCurveTo(lx + loopW * 0.75 - jx, midY + amp, lx + loopW * 0.25 - jx, midY + amp, lx, midY); + } + ctx.stroke(); + } + ctx.restore(); +} + +function drawEarthHatch(ctx, x, y, w, h, color = "rgba(255, 255, 255, 0.35)") { + if (w <= 0 || h <= 0) + return; + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + + const pattern = getCachedPattern(ctx, "earth_hatch_tile", 80, 80, (sCtx, sw, sh) => { + for (let d = -sh; d < sw + sh; d += 20) { + drawFuzzyPencilLine(sCtx, d, 0, d - sh, sh, 0.4, 1, { + color, + secondaryColor: "rgba(255, 255, 255, 0.15)", + overshoot: false, + doubleStroke: false, + }); + } + for (let ty = 15; ty < sh; ty += 25) { + for (let tx = 10; tx < sw; tx += 40) { + drawFuzzyPencilLine(sCtx, tx, ty, tx + 14, ty, 0.5, 1.2, { + color, + secondaryColor: "rgba(255, 255, 255, 0.15)", + overshoot: false, + doubleStroke: false, + }); + } + } + }); + + if (pattern) { + ctx.fillStyle = pattern; + ctx.fillRect(x - 20, y - 20, w + 40, h + 40); + } else { + for (let d = -h; d < w + h; d += 20) { + drawFuzzyPencilLine(ctx, x + d, y, x + d - h, y + h, 0.4, 1, { + color, + secondaryColor: "rgba(255, 255, 255, 0.15)", + overshoot: false, + doubleStroke: false, + }); + } + for (let ty = y + 15; ty < y + h; ty += 25) { + for (let tx = x + 10; tx < x + w; tx += 40) { + drawFuzzyPencilLine(ctx, tx, ty, tx + 14, ty, 0.5, 1.2, { + color, + secondaryColor: "rgba(255, 255, 255, 0.15)", + overshoot: false, + doubleStroke: false, + }); + } + } + } + ctx.restore(); +} + +function drawWoodGrainHatch(ctx, x, y, w, h, density = 4, count = 3) { + if (w <= 0 || h <= 0) + return; + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + + const spacing = Math.max(4, h / (density + 1)); + for (let i = 1; i <= density; i++) { + const py = y + i * spacing; + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(255, 255, 255, 0.14)" : "rgba(255, 255, 255, 0.28)"; + ctx.lineWidth = pass === 0 ? 1.6 : 1.1; + ctx.beginPath(); + ctx.moveTo(x, py); + for (let c = 0; c < count; c++) { + const j = pass === 0 ? 0.8 : 0; + const cx1 = x + (w / count) * (c + 0.3); + const cy1 = py + (i % 2 === 0 ? -3 : 3) * (c + 1) + j; + const cx2 = x + (w / count) * (c + 0.7); + const cy2 = py + (i % 2 === 0 ? 3 : -3) * (c + 1) - j; + const ex = x + (w / count) * (c + 1); + ctx.bezierCurveTo(cx1, cy1, cx2, cy2, ex, py); + } + ctx.stroke(); + } + } + ctx.restore(); +} + +function drawFuzzyGearGeometry(ctx, cx, cy, radius, teeth, color = "#ffffff") { + ctx.save(); + const pts = []; + for (let i = 0; i < teeth * 2; i++) { + const r = i % 2 === 0 ? radius : radius * 0.75; + const a = (i * Math.PI) / teeth; + pts.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]); + } + for (let i = 0; i < pts.length; i++) { + const p1 = pts[i]; + const p2 = pts[(i + 1) % pts.length]; + drawFuzzyPencilLine(ctx, p1[0], p1[1], p2[0], p2[1], 0.5, 1.6, { + color, + secondaryColor: "rgba(255, 255, 255, 0.4)", + overshoot: 1.6, + doubleStroke: true, + }); + } + + const innerPts = 12; + for (let i = 0; i < innerPts; i++) { + const a1 = (i * Math.PI * 2) / innerPts; + const a2 = ((i + 1) * Math.PI * 2) / innerPts; + drawFuzzyPencilLine(ctx, cx + Math.cos(a1) * (radius * 0.32), cy + Math.sin(a1) * (radius * 0.32), cx + Math.cos(a2) * (radius * 0.32), cy + Math.sin(a2) * (radius * 0.32), 0.4, 1.4, { + color, + secondaryColor: "rgba(255, 255, 255, 0.4)", + overshoot: 0.8, + }); + } + ctx.restore(); +} + +function drawGear(ctx, cx, cy, radius, teeth, angle, color = "#ffffff") { + ctx.save(); + ctx.translate(cx, cy); + ctx.rotate(angle); + + const spriteW = Math.ceil(radius * 2.8); + const spriteH = Math.ceil(radius * 2.8); + const spriteKey = `gear_${teeth}_${Math.round(radius)}_${color}`; + + const sprite = getCachedSprite(spriteKey, spriteW, spriteH, (sCtx, sw, sh) => { + drawFuzzyGearGeometry(sCtx, sw / 2, sh / 2, radius, teeth, color); + }); + + if (sprite) + ctx.drawImage(sprite, -spriteW / 2, -spriteH / 2); + else + drawFuzzyGearGeometry(ctx, 0, 0, radius, teeth, color); + + ctx.restore(); +} + +function drawRoundRect(ctx, x, y, w, h, r, options = {}) { + ctx.save(); + drawFuzzyPencilLine(ctx, x + r, y, x + w - r, y, options.roughness || 0.6, ctx.lineWidth || 1.2, options); + drawFuzzyPencilLine(ctx, x + w, y + r, x + w, y + h - r, options.roughness || 0.6, ctx.lineWidth || 1.2, options); + drawFuzzyPencilLine(ctx, x + w - r, y + h, x + r, y + h, options.roughness || 0.6, ctx.lineWidth || 1.2, options); + drawFuzzyPencilLine(ctx, x, y + h - r, x, y + r, options.roughness || 0.6, ctx.lineWidth || 1.2, options); + + const corners = [ + [x + w - r, y + r, 0], + [x + w - r, y + h - r, Math.PI / 2], + [x + r, y + h - r, Math.PI], + [x + r, y + r, Math.PI * 1.5], + ]; + for (const [cx, cy, startA] of corners) { + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(255, 255, 255, 0.4)" : options.color || "#ffffff"; + ctx.lineWidth = pass === 0 ? (ctx.lineWidth || 1.2) * 1.4 : ctx.lineWidth || 1.2; + ctx.beginPath(); + for (let s = 0; s <= 4; s++) { + const a = startA + (s / 4) * (Math.PI / 2); + let px = cx + Math.cos(a) * r; + let py = cy + Math.sin(a) * r; + if (s > 0 && s < 4) { + px += Math.sin(s + startA) * 0.6; + py += Math.cos(s + startA) * 0.6; + } + if (s === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + } + } + ctx.restore(); +} + +/* Dimension Line with Independent & Irregular Shadow Angle */ +function drawDimensionLine(ctx, x1, y1, x2, y2, label, color = "#ffffff", tickSize = 6, bgColor = "#0a0a0a") { + ctx.save(); + ctx.strokeStyle = "#D12B3E"; + ctx.fillStyle = "#D12B3E"; + ctx.lineWidth = 1.4; + + drawFuzzyPencilLine(ctx, x1, y1, x2, y2, 0.4, 1.4, { + color: "#D12B3E", + secondaryColor: "rgba(209, 43, 62, 0.45)", + overshoot: 2.2, + }); + + const angle = Math.atan2(y2 - y1, x2 - x1); + const tickAngle = Math.PI / 4; + for (const [ptX, ptY] of [ + [x1, y1], + [x2, y2], + ]) { + drawFuzzyPencilLine(ctx, ptX - Math.cos(angle + tickAngle) * tickSize, ptY - Math.sin(angle + tickAngle) * tickSize, ptX + Math.cos(angle + tickAngle) * tickSize, ptY + Math.sin(angle + tickAngle) * tickSize, 0.3, 1.5, { + color: "#D12B3E", + secondaryColor: "rgba(209, 43, 62, 0.45)", + overshoot: 1.5, + }); + } + + const midX = (x1 + x2) / 2; + const midY = (y1 + y2) / 2; + ctx.font = "bold 11px system-ui, -apple-system, sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + + const textW = ctx.measureText(label).width; + const boxW = textW + 14; + const boxH = 18; + + ctx.save(); + ctx.translate(midX + 5, midY + 4); + ctx.rotate(0.031); + ctx.fillStyle = "#000000"; + ctx.beginPath(); + ctx.rect(-boxW / 2, -boxH / 2, boxW, boxH); + ctx.fill(); + ctx.restore(); + + ctx.save(); + ctx.translate(midX, midY); + ctx.rotate(-0.015); + const badgeBgColor = bgColor === "#ffffff" || bgColor === "#000000" ? "#0a0a0a" : bgColor; + ctx.fillStyle = badgeBgColor; + ctx.beginPath(); + ctx.rect(-boxW / 2, -boxH / 2, boxW, boxH); + ctx.fill(); + ctx.lineWidth = 1.4; + drawRoundRect(ctx, -boxW / 2, -boxH / 2, boxW, boxH, 4, { color: "#D12B3E", secondaryColor: "rgba(209, 43, 62, 0.45)" }); + ctx.fillStyle = "#ffffff"; + ctx.fillText(label, 0, 1); + ctx.restore(); + + ctx.restore(); +} + +function drawSurveyReticle(ctx, x, y, radius, label, color = "#ffffff") { + ctx.save(); + ctx.strokeStyle = "#D12B3E"; + ctx.fillStyle = "#D12B3E"; + ctx.lineWidth = 1.4; + + const ringPts = 16; + for (let rFactor of [1.0, 0.35]) { + const r = radius * rFactor; + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(209, 43, 62, 0.4)" : "#D12B3E"; + ctx.lineWidth = pass === 0 ? 2.0 : 1.4; + ctx.beginPath(); + for (let i = 0; i <= ringPts; i++) { + const a = (i * Math.PI * 2) / ringPts; + let px = x + Math.cos(a) * r; + let py = y + Math.sin(a) * r; + if (i > 0 && i < ringPts) { + px += Math.sin(i * 1.7 + x) * 0.6; + py += Math.cos(i * 1.3 + y) * 0.6; + } + if (i === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + } + } + + drawFuzzyPencilLine(ctx, x - radius - 6, y, x + radius + 6, y, 0.3, 1.4, { color: "#D12B3E", secondaryColor: "rgba(209, 43, 62, 0.45)" }); + drawFuzzyPencilLine(ctx, x, y - radius - 6, x, y + radius + 6, 0.3, 1.4, { color: "#D12B3E", secondaryColor: "rgba(209, 43, 62, 0.45)" }); + + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.arc(x, y, radius * 0.35, 0, Math.PI / 2); + ctx.closePath(); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.arc(x, y, radius * 0.35, Math.PI, Math.PI * 1.5); + ctx.closePath(); + ctx.fill(); + + if (label) { + ctx.fillStyle = "#ffffff"; + ctx.font = "bold 10px system-ui, -apple-system, sans-serif"; + ctx.textAlign = "center"; + ctx.fillText(label, x, y + radius + 15); + } + ctx.restore(); +} + +/* Title Block with Independent & Irregular Shadow Angle */ +// eslint-disable-next-line no-unused-vars +function drawTitleBlock(ctx, width, height, title, dwgNo, scale, date = "OCT 1954", color = "#ffffff", bgColor = "#0a0a0a") { + ctx.save(); + const boxW = 280; + const boxH = 58; + const bx = width - boxW - 20; + const by = height - boxH - 20; + + ctx.save(); + ctx.translate(bx + boxW / 2 + 10, by + boxH / 2 + 12); + ctx.rotate(0.026); + ctx.fillStyle = "#000000"; + ctx.beginPath(); + ctx.rect(-boxW / 2, -boxH / 2, boxW, boxH); + ctx.fill(); + ctx.restore(); + + ctx.save(); + ctx.translate(bx + boxW / 2, by + boxH / 2); + ctx.rotate(-0.014); + ctx.fillStyle = bgColor; + ctx.beginPath(); + ctx.rect(-boxW / 2, -boxH / 2, boxW, boxH); + ctx.fill(); + + ctx.lineWidth = 2.2; + drawRoundRect(ctx, -boxW / 2, -boxH / 2, boxW, boxH, 6, { color: "#D12B3E", secondaryColor: "rgba(209, 43, 62, 0.45)" }); + + drawFuzzyPencilLine(ctx, -boxW / 2, -boxH / 2 + 24, -boxW / 2 + boxW, -boxH / 2 + 24, 0.3, 1.5, { color: "#D12B3E", secondaryColor: "rgba(209, 43, 62, 0.45)", overshoot: 1.8 }); + drawFuzzyPencilLine(ctx, -boxW / 2 + 195, -boxH / 2 + 24, -boxW / 2 + 195, -boxH / 2 + boxH, 0.3, 1, { color: "#ffffff", secondaryColor: "rgba(255, 255, 255, 0.4)", overshoot: 1.8 }); + + ctx.fillStyle = "#ffffff"; + ctx.font = "bold 11px system-ui, -apple-system, sans-serif"; + ctx.textAlign = "left"; + ctx.fillText("PROJECT A-101: THE HOMESTEAD DOSSIER", -boxW / 2 + 8, -boxH / 2 + 16); + + ctx.font = "bold 11px system-ui, -apple-system, sans-serif"; + ctx.fillText(title, -boxW / 2 + 8, -boxH / 2 + 40); + ctx.fillStyle = "#cccccc"; + ctx.font = "10px system-ui, -apple-system, sans-serif"; + ctx.fillText(`SCALE: ${scale}`, -boxW / 2 + 8, -boxH / 2 + 51); + + ctx.fillStyle = "#ffffff"; + ctx.font = "bold 11px system-ui, -apple-system, sans-serif"; + ctx.fillText(`DWG ${dwgNo}`, -boxW / 2 + 203, -boxH / 2 + 40); + ctx.fillStyle = "#cccccc"; + ctx.font = "10px system-ui, -apple-system, sans-serif"; + ctx.fillText(`DATE: ${date}`, -boxW / 2 + 203, -boxH / 2 + 51); + ctx.restore(); + + ctx.restore(); +} + +/* 1950s Slanted Geometric SVG Callouts & Badges with Independent Shadow Angles */ +function addSVGCallout(g, x, y, targetX, targetY, text, subtext = "", id = "", phase = null) { + const group = createSVGElement("g", { class: "tech-callout" }); + if (id) + group.setAttribute("id", id); + if (phase !== null) + group.setAttribute("data-phase", String(phase)); + group.style.transition = "none"; + + const leader = createSVGElement("polyline", { + points: `${targetX},${targetY} ${x + (targetX > x ? 20 : -20)},${y} ${x},${y}`, + fill: "none", + stroke: "#D12B3E", + "stroke-width": "1.8", + "stroke-dasharray": "4, 2", + }); + group.appendChild(leader); + + const targetDot = createSVGElement("circle", { + cx: targetX, + cy: targetY, + r: 4, + fill: "#D12B3E", + stroke: "#ffffff", + "stroke-width": "1.2", + }); + group.appendChild(targetDot); + + const tilt = targetX > x ? -1.5 : 1.5; + const shadowTilt = targetX > x ? 1.4 : -1.7; + const estTextW = text ? text.length * 6.8 + 24 : 0; + const estSubW = subtext ? subtext.length * 5.9 + 24 : 0; + const boxW = Math.max(190, Math.ceil(Math.max(estTextW, estSubW))); + const boxH = subtext ? 36 : 26; + const bx = x - (targetX > x ? 0 : boxW); + const by = y - 24; + + const calloutBox = createSVGElement("g", { class: "tech-callout-box" }); + calloutBox.style.transition = "none"; + const boxCenterX = bx + boxW / 2; + const boxCenterY = by + boxH / 2; + calloutBox.setAttribute("data-center-x", String(boxCenterX)); + calloutBox.setAttribute("data-center-y", String(boxCenterY)); + + const shadowBg = createSVGElement("rect", { + x: bx + (targetX > x ? 7 : -7), + y: by + 8, + width: boxW, + height: boxH, + fill: "#000000", + rx: "4", + ry: "4", + transform: `rotate(${shadowTilt}, ${boxCenterX}, ${boxCenterY})`, + }); + calloutBox.appendChild(shadowBg); + + const labelBg = createSVGElement("rect", { + x: bx, + y: by, + width: boxW, + height: boxH, + fill: "#0a0a0a", + stroke: "#D12B3E", + "stroke-width": "2", + rx: "4", + ry: "4", + transform: `rotate(${tilt}, ${boxCenterX}, ${boxCenterY})`, + }); + calloutBox.appendChild(labelBg); + + const textEl = createSVGElement("text", { + x: bx + 8, + y: by + 16, + fill: "#ffffff", + "font-family": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', + "font-size": "11", + "font-weight": "bold", + transform: `rotate(${tilt}, ${boxCenterX}, ${boxCenterY})`, + }); + textEl.textContent = text; + calloutBox.appendChild(textEl); + + if (subtext) { + const subEl = createSVGElement("text", { + x: bx + 8, + y: by + 28, + fill: "#cccccc", + "font-family": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', + "font-size": "10", + transform: `rotate(${tilt}, ${boxCenterX}, ${boxCenterY})`, + }); + subEl.textContent = subtext; + calloutBox.appendChild(subEl); + } + + group.appendChild(calloutBox); + g.appendChild(group); + return group; +} + +// eslint-disable-next-line no-unused-vars +function addSVGStressArrow(g, x1, y1, x2, y2, label, id = "", phase = null) { + const group = createSVGElement("g", { class: "tech-arrow" }); + if (id) + group.setAttribute("id", id); + if (phase !== null) + group.setAttribute("data-phase", String(phase)); + group.style.transition = "none"; + group.setAttribute("data-base-x1", String(x1)); + group.setAttribute("data-base-y1", String(y1)); + group.setAttribute("data-base-x2", String(x2)); + group.setAttribute("data-base-y2", String(y2)); + + const line = createSVGElement("line", { + x1, + y1, + x2, + y2, + stroke: "#D12B3E", + "stroke-width": "2.5", + class: "tech-arrow-line", + }); + group.appendChild(line); + + const angle = Math.atan2(y2 - y1, x2 - x1); + const headLen = 12; + const hx1 = x2 - headLen * Math.cos(angle - Math.PI / 6); + const hy1 = y2 - headLen * Math.sin(angle - Math.PI / 6); + const hx2 = x2 - headLen * Math.cos(angle + Math.PI / 6); + const hy2 = y2 - headLen * Math.sin(angle + Math.PI / 6); + + const head = createSVGElement("polygon", { + points: `${x2},${y2} ${hx1},${hy1} ${hx2},${hy2}`, + fill: "#D12B3E", + class: "tech-arrow-head", + }); + group.appendChild(head); + + const tx = (x1 + x2) / 2 + 10; + const ty = (y1 + y2) / 2; + const textEl = createSVGElement("text", { + x: tx, + y: ty, + fill: "#ffffff", + "font-family": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', + "font-size": "11", + "font-weight": "bold", + class: "tech-arrow-label", + }); + textEl.textContent = label; + group.appendChild(textEl); + + g.appendChild(group); + return group; +} + +function addSVGTitleBlock(g, project, dwgNo, rev, scale, date = "OCT 1954") { + const box = createSVGElement("g", { class: "tech-title-block", transform: "translate(0, -50)" }); + box.style.transition = "none"; + + const shadow = createSVGElement("rect", { + x: 488, + y: 520, + width: 300, + height: 70, + fill: "#000000", + rx: "6", + ry: "6", + transform: "rotate(1.7, 638, 555)", + }); + box.appendChild(shadow); + + const frameGroup = createSVGElement("g", { transform: "rotate(-1.2, 630, 545)" }); + const frame = createSVGElement("rect", { + x: 480, + y: 510, + width: 300, + height: 70, + fill: "#0a0a0a", + stroke: "#D12B3E", + "stroke-width": "2.5", + rx: "6", + ry: "6", + }); + frameGroup.appendChild(frame); + + const div1 = createSVGElement("line", { x1: 480, y1: 535, x2: 780, y2: 535, stroke: "#D12B3E", "stroke-width": "1.5" }); + const div2 = createSVGElement("line", { x1: 480, y1: 558, x2: 780, y2: 558, stroke: "#ffffff", "stroke-width": "1" }); + const div3 = createSVGElement("line", { x1: 640, y1: 535, x2: 640, y2: 580, stroke: "#ffffff", "stroke-width": "1" }); + frameGroup.append(div1, div2, div3); + + const tProj = createSVGElement("text", { x: 490, y: 528, fill: "#ffffff", "font-family": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', "font-size": "11", "font-weight": "bold" }); + tProj.textContent = `PROJ: ${project}`; + + const tDwg = createSVGElement("text", { x: 490, y: 550, fill: "#ffffff", "font-family": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', "font-size": "10", "font-weight": "bold" }); + tDwg.textContent = `DWG: ${dwgNo}`; + + const tRev = createSVGElement("text", { x: 650, y: 550, fill: "#ffffff", "font-family": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', "font-size": "10", "font-weight": "bold" }); + tRev.textContent = `REV: ${rev}`; + + const tScale = createSVGElement("text", { x: 490, y: 572, fill: "#cccccc", "font-family": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', "font-size": "10" }); + tScale.textContent = `SCALE: ${scale}`; + + const tDate = createSVGElement("text", { x: 650, y: 572, fill: "#cccccc", "font-family": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', "font-size": "10" }); + tDate.textContent = `DATE: ${date}`; + + frameGroup.append(tProj, tDwg, tRev, tScale, tDate); + box.appendChild(frameGroup); + g.appendChild(box); +} + +function getParagraphPhase(progress, numParagraphs = 5) { + const p = Math.max(0, Math.min(0.9999, progress)); + const idx = Math.floor(p * numParagraphs); + const localProg = p * numParagraphs - idx; + return { idx, localProg }; +} + +function setCalloutPhaseVisibility(g, phaseInput) { + const phaseIdx = typeof phaseInput === "object" && phaseInput !== null && typeof phaseInput.idx === "number" ? phaseInput.idx : typeof phaseInput === "number" ? phaseInput : 0; + const localProg = typeof phaseInput === "object" && phaseInput !== null && typeof phaseInput.localProg === "number" ? phaseInput.localProg : 1.0; + + const callouts = Array.from(g.querySelectorAll(".tech-callout, .tech-arrow, .tech-stamp-badge, .tech-datum")); + const phaseCounters = {}; + + callouts.forEach((el) => { + const targetPhaseAttr = el.getAttribute("data-phase"); + const targetPhase = targetPhaseAttr === null || targetPhaseAttr === "" ? 0 : parseInt(targetPhaseAttr, 10); + + if (phaseCounters[targetPhase] === undefined) + phaseCounters[targetPhase] = 0; + + const itemIndex = phaseCounters[targetPhase]++; + + if (targetPhase < phaseIdx) { + el.style.opacity = "1"; + el.style.pointerEvents = "auto"; + const box = el.querySelector(".tech-callout-box"); + if (box) { + const cx = parseFloat(box.getAttribute("data-center-x")) || 0; + const cy = parseFloat(box.getAttribute("data-center-y")) || 0; + box.setAttribute("transform", `translate(${cx}, ${cy}) scale(1) translate(${-cx}, ${-cy})`); + } + } else if (targetPhase > phaseIdx) { + el.style.opacity = "0"; + el.style.pointerEvents = "none"; + const box = el.querySelector(".tech-callout-box"); + if (box) { + const cx = parseFloat(box.getAttribute("data-center-x")) || 0; + const cy = parseFloat(box.getAttribute("data-center-y")) || 0; + box.setAttribute("transform", `translate(${cx}, ${cy}) scale(0.35) translate(${-cx}, ${-cy})`); + } + } else { + const stagger = 0.18; + const duration = 0.45; + const start = itemIndex * stagger; + const t = (localProg - start) / duration; + + if (t <= 0) { + el.style.opacity = "0"; + el.style.pointerEvents = "none"; + const box = el.querySelector(".tech-callout-box"); + if (box) { + const cx = parseFloat(box.getAttribute("data-center-x")) || 0; + const cy = parseFloat(box.getAttribute("data-center-y")) || 0; + box.setAttribute("transform", `translate(${cx}, ${cy}) scale(0.35) translate(${-cx}, ${-cy})`); + } + } else if (t >= 1) { + el.style.opacity = "1"; + el.style.pointerEvents = "auto"; + const box = el.querySelector(".tech-callout-box"); + if (box) { + const cx = parseFloat(box.getAttribute("data-center-x")) || 0; + const cy = parseFloat(box.getAttribute("data-center-y")) || 0; + box.setAttribute("transform", `translate(${cx}, ${cy}) scale(1) translate(${-cx}, ${-cy})`); + } + } else { + const opacity = Math.min(1, t * 2.0); + el.style.opacity = String(opacity.toFixed(4)); + el.style.pointerEvents = opacity > 0.1 ? "auto" : "none"; + const box = el.querySelector(".tech-callout-box"); + if (box) { + const popScale = 0.35 + 0.65 * easeOutBack(t); + const cx = parseFloat(box.getAttribute("data-center-x")) || 0; + const cy = parseFloat(box.getAttribute("data-center-y")) || 0; + box.setAttribute("transform", `translate(${cx}, ${cy}) scale(${popScale.toFixed(4)}) translate(${-cx}, ${-cy})`); + } + } + } + }); +} + +// eslint-disable-next-line no-unused-vars +function easeOutQuart(t) { + return 1 - Math.pow(1 - t, 4); +} + +function easeOutBack(t) { + const c1 = 1.70158; + const c3 = c1 + 1; + return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); +} + +function drawMechanicalTrack(ctx, x1, y1, x2, y2, color = "#ffffff") { + ctx.save(); + drawFuzzyPencilLine(ctx, x1, y1, x2, y2, 0.3, 2.5, { color, secondaryColor: "rgba(255, 255, 255, 0.45)", overshoot: 3.0 }); + const angle = Math.atan2(y2 - y1, x2 - x1); + const dist = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); + const steps = Math.floor(dist / 22); + for (let i = 0; i <= steps; i++) { + const tx = x1 + Math.cos(angle) * (i * 22); + const ty = y1 + Math.sin(angle) * (i * 22); + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(255, 255, 255, 0.4)" : color; + ctx.lineWidth = pass === 0 ? 1.8 : 1.2; + ctx.beginPath(); + const r = 2.5; + for (let s = 0; s <= 8; s++) { + const a = (s * Math.PI * 2) / 8; + const px = tx + Math.cos(a) * r + (s > 0 && s < 8 ? Math.sin(s + i) * 0.4 : 0); + const py = ty + Math.sin(a) * r + (s > 0 && s < 8 ? Math.cos(s + i) * 0.4 : 0); + if (s === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + } + } + ctx.restore(); +} + +function drawDioramaPlatform(ctx, x, y, w, h, depth = 18, color = "#0a0a0a", borderColor = "#ffffff", shadowTilt = 0.03) { + ctx.save(); + ctx.save(); + ctx.translate(x + 12, y + 14); + ctx.rotate(shadowTilt); + ctx.fillStyle = "rgba(0, 0, 0, 0.25)"; + ctx.fillRect(-12, -14, w + depth, h + depth); + ctx.restore(); + + ctx.fillStyle = color; + ctx.fillRect(x, y, w, h); + ctx.lineWidth = 2.2; + drawSketchRect(ctx, x, y, w, h, 3.2, { color: borderColor, secondaryColor: "rgba(255, 255, 255, 0.45)" }); + + ctx.fillStyle = "#e5e5e5"; + ctx.beginPath(); + ctx.moveTo(x, y + h); + ctx.lineTo(x + depth, y + h + depth); + ctx.lineTo(x + w + depth, y + h + depth); + ctx.lineTo(x + w, y + h); + ctx.closePath(); + ctx.fill(); + + drawFuzzyPencilLine(ctx, x, y + h, x + depth, y + h + depth, 0.4, 1.8, { color: borderColor, overshoot: 2.0 }); + drawFuzzyPencilLine(ctx, x + depth, y + h + depth, x + w + depth, y + h + depth, 0.4, 1.8, { color: borderColor, overshoot: 2.0 }); + drawFuzzyPencilLine(ctx, x + w + depth, y + h + depth, x + w, y + h, 0.4, 1.8, { color: borderColor, overshoot: 2.0 }); + + ctx.fillStyle = "#cccccc"; + ctx.beginPath(); + ctx.moveTo(x + w, y); + ctx.lineTo(x + w + depth, y + depth); + ctx.lineTo(x + w + depth, y + h + depth); + ctx.lineTo(x + w, y + h); + ctx.closePath(); + ctx.fill(); + + drawFuzzyPencilLine(ctx, x + w, y, x + w + depth, y + depth, 0.4, 1.8, { color: borderColor, overshoot: 2.0 }); + drawFuzzyPencilLine(ctx, x + w + depth, y + depth, x + w + depth, y + h + depth, 0.4, 1.8, { color: borderColor, overshoot: 2.0 }); + ctx.restore(); +} + +// eslint-disable-next-line no-unused-vars +function drawPopUpProp(ctx, x, y, w, h, prog, drawContentCallback) { + ctx.save(); + const eased = easeOutBack(Math.min(1.0, prog * 1.2)); + const currH = h * eased; + const currY = y + (h - currH); + ctx.translate(x, currY); + if (currH > 1) { + ctx.save(); + ctx.beginPath(); + ctx.rect(0, 0, w, currH); + ctx.clip(); + drawContentCallback(ctx, w, currH, eased); + ctx.restore(); + } + ctx.restore(); +} + +function drawTopMiddleWidget(ctx, stageIndex, prog, phase) { + ctx.save(); + const cx = 400, + cy = 76, + w = 280, + h = 104; + const boxTilt = ((stageIndex % 2 === 0 ? -1.2 : 1.4) * Math.PI) / 180; + const shadowTilt = ((stageIndex % 2 === 0 ? 1.6 : -1.5) * Math.PI) / 180; + + ctx.save(); + ctx.translate(cx + 6, cy + 8); + ctx.rotate(shadowTilt); + ctx.fillStyle = "rgba(0, 0, 0, 0.65)"; + ctx.fillRect(-w / 2, -h / 2, w, h); + ctx.restore(); + + ctx.save(); + ctx.translate(cx, cy); + ctx.rotate(boxTilt); + ctx.fillStyle = "#0a0a0a"; + ctx.fillRect(-w / 2, -h / 2, w, h); + + ctx.lineWidth = 1.8; + drawSketchRect(ctx, -w / 2, -h / 2, w, h, 3.0, { color: "#ffffff", secondaryColor: "rgba(255, 255, 255, 0.45)" }); + + drawFuzzyPencilLine(ctx, -w / 2 + 10, -h / 2 + 20, w / 2 - 10, -h / 2 + 20, 0.3, 1.3, { color: "#ffffff", overshoot: 2.0 }); + + ctx.fillStyle = "#ffffff"; + ctx.font = 'bold 11px "Impact", "Arial Black", sans-serif'; + ctx.letterSpacing = "0.06em"; + + const fontStyle = '10px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'; + if (stageIndex === 0) { + ctx.fillText("SPEC: FRONTIER COMPASS & WIND ROSE", -w / 2 + 12, -h / 2 + 14); + + const rx = -90, + ry = 13; + for (let r of [28, 18, 9]) { + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(204, 204, 204, 0.35)" : "#cccccc"; + ctx.lineWidth = pass === 0 ? 1.6 : 1.0; + ctx.beginPath(); + for (let s = 0; s <= 16; s++) { + const a = (s * Math.PI * 2) / 16; + let px = rx + Math.cos(a) * r; + let py = ry + Math.sin(a) * r; + if (s > 0 && s < 16) { + px += Math.sin(s + r) * 0.4; + py += Math.cos(s + r) * 0.4; + } + if (s === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + } + } + + const angle = -Math.PI / 4 + prog * Math.PI * 0.6; + const nx = rx + Math.cos(angle) * 26; + const ny = ry + Math.sin(angle) * 26; + drawFuzzyPencilLine(ctx, rx - Math.cos(angle) * 11, ry - Math.sin(angle) * 11, nx, ny, 0.3, 2, { color: "#ffffff" }); + ctx.fillStyle = "#ffffff"; + ctx.beginPath(); + ctx.arc(nx, ny, 3, 0, Math.PI * 2); + ctx.fill(); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("ELEVATION : +1,420 FT ASL", -35, -12); + ctx.fillText("MAG DECL : 14°12' W", -35, 3); + ctx.fillText(`WIND VEL : ${(25 + prog * 20).toFixed(1)} KTS NW`, -35, 18); + ctx.fillText(`CANOPY DEN: ${(98 - prog * 15).toFixed(0)}% VIRGIN`, -35, 33); + } else if (stageIndex === 1) { + ctx.fillText("SPEC: T-AUGER ROTATION & DRAW-BORE TENSION", -w / 2 + 12, -h / 2 + 14); + + const gx = -95, + gy = 12; + drawGear(ctx, gx, gy, 24, 8, prog * Math.PI * 4, "#ffffff"); + drawGear(ctx, gx + 34, gy + 10, 16, 6, -prog * Math.PI * 6, "#cccccc"); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("MORTISE DEPTH: 112.5 MM", -35, -12); + ctx.fillText("TREENAIL PEG : TAPER 1:16", -35, 3); + ctx.fillText(`DRAW TENSION : ${(1800 + prog * 600).toFixed(0)} PSI`, -35, 18); + ctx.fillText(`JOINT GAP : ${(1.8 - prog * 1.8).toFixed(2)} MM`, -35, 33); + } else if (stageIndex === 2) { + ctx.fillText("SPEC: GEOLOGICAL BORE LOG & WATER TABLE", -w / 2 + 12, -h / 2 + 14); + + const bx = -115, + by = -15, + bw = 50, + bh = 56; + drawSketchRect(ctx, bx, by, bw, bh, 2.0, { color: "#cccccc" }); + drawEarthHatch(ctx, bx, by + 28, bw, 28, "rgba(255, 255, 255, 0.3)"); + + const waterY = by + 42 - prog * 24; + drawFuzzyPencilLine(ctx, bx, waterY, bx + bw, waterY, 0.4, 2, { color: "#ffffff", overshoot: 2.2 }); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("STRATUM : METAMORPHIC GRANITE", -50, -12); + ctx.fillText(`WATER HEAD: ${(14.2 - prog * 8).toFixed(1)} FT BELOW SILL`, -50, 3); + ctx.fillText(`FROST LINE: ${(48 + prog * 6).toFixed(0)} IN DEPTH`, -50, 18); + ctx.fillText(`PERIM GRAD: ${(1.5 + prog * 1.5).toFixed(1)}% AWAY`, -50, 33); + } else if (stageIndex === 3) { + ctx.fillText("SPEC: OVERSHOT FLUME VELOCITY & CROWN TORQUE", -w / 2 + 12, -h / 2 + 14); + + const wx = -95, + wy = 12; + drawGear(ctx, wx, wy, 26, 16, prog * Math.PI * 3, "#ffffff"); + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(255, 255, 255, 0.4)" : "#ffffff"; + ctx.lineWidth = pass === 0 ? 2.0 : 1.5; + ctx.beginPath(); + for (let s = 0; s <= 12; s++) { + const a = (s * Math.PI * 2) / 12; + const px = wx + Math.cos(a) * 12 + (s > 0 && s < 12 ? Math.sin(s) * 0.4 : 0); + const py = wy + Math.sin(a) * 12 + (s > 0 && s < 12 ? Math.cos(s) * 0.4 : 0); + if (s === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + } + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("FLUME VEL : 42.5 CFS EQUIV", -40, -12); + ctx.fillText("HEAD DROP : 14.0 FT NET", -40, 3); + ctx.fillText(`WHEEL RPM : ${(8 + prog * 12).toFixed(1)} RPM`, -40, 18); + ctx.fillText(`SHAFT TORQ: ${(420 + prog * 380).toFixed(0)} FT-LBS`, -40, 33); + } else if (stageIndex === 4) { + ctx.fillText("SPEC: STEAM GEAR RATIO & PUMP KINEMATICS", -w / 2 + 12, -h / 2 + 14); + + const gx = -95, + gy = 12; + drawGear(ctx, gx, gy, 24, 12, prog * Math.PI * 4, "#ffffff"); + drawGear(ctx, gx + 36, gy + 8, 16, 8, -prog * Math.PI * 6, "#cccccc"); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("RATIO : 3:2 REDUCTION DRIVE", -35, -12); + ctx.fillText(`PUMP RATE : ${(12 + prog * 18).toFixed(1)} GAL/MIN`, -35, 3); + ctx.fillText(`BOILER PSI: ${(45 + prog * 40).toFixed(0)} PSI NET`, -35, 18); + ctx.fillText(`STROKE VEL: ${(0.8 + prog * 1.2).toFixed(2)} M/SEC`, -35, 33); + } else if (stageIndex === 5) { + ctx.fillText("SPEC: SPINDLE LATHE RPM & GAS MANIFOLD PSI", -w / 2 + 12, -h / 2 + 14); + + const lx = -95, + ly = 12; + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(204, 204, 204, 0.4)" : "#cccccc"; + ctx.lineWidth = pass === 0 ? 1.8 : 1.2; + ctx.beginPath(); + for (let s = 0; s <= 14; s++) { + const a = (s * Math.PI * 2) / 14; + const px = lx + Math.cos(a) * 24 + (s > 0 && s < 14 ? Math.sin(s) * 0.5 : 0); + const py = ly + Math.sin(a) * 24 + (s > 0 && s < 14 ? Math.cos(s) * 0.5 : 0); + if (s === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + } + drawFuzzyPencilLine(ctx, lx - 20, ly, lx + 20, ly, 0.3, 1.5, { color: "#ffffff", overshoot: 1.6 }); + drawFuzzyPencilLine(ctx, lx, ly - 20, lx, ly + 20, 0.3, 1.5, { color: "#ffffff", overshoot: 1.6 }); + ctx.fillStyle = "#ffffff"; + ctx.beginPath(); + ctx.arc(lx + Math.cos(prog * Math.PI * 6) * 14, ly + Math.sin(prog * Math.PI * 6) * 14, 4, 0, Math.PI * 2); + ctx.fill(); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("SPINDLE : STEAM LATHE TURNED", -40, -12); + ctx.fillText(`LATHE VEL : ${(850 + prog * 400).toFixed(0)} RPM`, -40, 3); + ctx.fillText(`GAS PRESS : ${(3.2 + prog * 0.6).toFixed(2)} PSI`, -40, 18); + ctx.fillText(`ILLUM LUX : ${(120 + prog * 80).toFixed(0)} LUMENS`, -40, 33); + } else if (stageIndex === 6) { + ctx.fillText("SPEC: IRRIGATION SLUICE & WEIR REGULATOR", -w / 2 + 12, -h / 2 + 14); + + const sx = -110, + sy = -12, + sw = 45, + sh = 52; + drawSketchRect(ctx, sx, sy, sw, sh, 2.0, { color: "#cccccc" }); + const gateY = sy + sh - prog * 36; + ctx.fillStyle = "rgba(255, 255, 255, 0.25)"; + ctx.fillRect(sx, gateY, sw, sh - (gateY - sy)); + drawFuzzyPencilLine(ctx, sx, gateY, sx + sw, gateY, 0.4, 2, { color: "#ffffff", overshoot: 2.2 }); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText(`GATE APERTURE: ${(prog * 24).toFixed(1)} IN OPEN`, -50, -12); + ctx.fillText(`FLOW VELOCITY: ${(1.2 + prog * 3.4).toFixed(2)} FT/SEC`, -50, 3); + ctx.fillText(`HEAD DROP : ${(6.4 - prog * 2.1).toFixed(1)} FT NET`, -50, 18); + ctx.fillText(`DISCHARGE : ${(450 * prog).toFixed(0)} GPM`, -50, 33); + } else if (stageIndex === 7) { + ctx.fillText("SPEC: AC WATT-HOUR DISK & BOILER BTU OUTPUT", -w / 2 + 12, -h / 2 + 14); + + const mx = -95, + my = 12; + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(255, 255, 255, 0.4)" : "#ffffff"; + ctx.lineWidth = pass === 0 ? 2.0 : 1.5; + ctx.beginPath(); + for (let s = 0; s <= 14; s++) { + const a = (s * Math.PI * 2) / 14; + const px = mx + Math.cos(a) * 22 + (s > 0 && s < 14 ? Math.sin(s) * 0.4 : 0); + const py = my + Math.sin(a) * 22 + (s > 0 && s < 14 ? Math.cos(s) * 0.4 : 0); + if (s === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + } + drawGear(ctx, mx, my, 14, 8, prog * Math.PI * 8, "#cccccc"); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("VOLTAGE : 110V 60HZ SINGLE PH.", -45, -12); + ctx.fillText(`METER DISK: ${(12 + prog * 48).toFixed(1)} RPM SPIN`, -45, 3); + ctx.fillText(`BOILER OUT: ${(65 + prog * 55).toFixed(0)} K-BTU/HR`, -45, 18); + ctx.fillText(`WATER TEMP: ${(140 + prog * 45).toFixed(0)} °F RAD`, -45, 33); + } else if (stageIndex === 8) { + ctx.fillText("SPEC: STRUCTURAL STRAIN & DEFLECTION", -w / 2 + 12, -h / 2 + 14); + + const rx = -110, + ry = 12; + drawFuzzyPencilLine(ctx, rx - 20, ry, rx + 20, ry, 0.3, 1, { color: "#cccccc", overshoot: 1.5 }); + ctx.save(); + ctx.strokeStyle = "#ffffff"; + ctx.lineWidth = 2; + ctx.beginPath(); + for (let s = 0; s <= 6; s++) { + const t = s / 6; + const px = rx - 20 + 40 * t; + const py = ry + Math.sin(t * Math.PI) * prog * 18 + (s > 0 && s < 6 ? Math.sin(s * 2) * 0.5 : 0); + if (s === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + ctx.restore(); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText(`MAX DEFLECTION: ${(prog * 4.8).toFixed(2)} IN DOWN`, -50, -12); + ctx.fillText("SPAN LENGTH : 24'-0\" CLEAR", -50, 3); + ctx.fillText(`LOAD RATIO : ${(1.2 + prog * 0.8).toFixed(2)}x SAFE LIMIT`, -50, 18); + ctx.fillText(`STRAIN INDEX : ${(82 + prog * 65).toFixed(0)} MICRO-STR`, -50, 33); + } else if (stageIndex === 9) { + ctx.fillText("SPEC: SCREW-JACK AXIAL LOAD & SHORING VECTOR", -w / 2 + 12, -h / 2 + 14); + + const jx = -95, + jy = 12; + drawSketchRect(ctx, jx - 12, jy - 22, 24, 44, 2.0, { color: "#ffffff" }); + drawSteelHatch(ctx, jx - 12, jy - 22, 24, 44); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("SHORING : STEEL SCREW-JACK", -50, -12); + ctx.fillText(`AXIAL LOAD: ${(8.2 + prog * 6.0).toFixed(1)} TONS NET`, -50, 3); + ctx.fillText(`TORQUE ADV: ${(12 + prog * 18).toFixed(1)} FT-LBS`, -50, 18); + ctx.fillText(`DEFLECT RT: ${(1.8 - prog * 1.8).toFixed(2)} MM/YR`, -50, 33); + } else if (stageIndex === 10) { + ctx.fillText("SPEC: 12-TON HYDRAULIC LIFT CALIBRATION", -w / 2 + 12, -h / 2 + 14); + + const cx2 = -95, + cy2 = 12; + drawSurveyReticle(ctx, cx2, cy2, 24, "DATUM", "#ffffff"); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText(`JACK RAM PRESS: ${(prog * 3400).toFixed(0)} PSI`, -40, -12); + ctx.fillText(`VERTICAL LIFT : ${(prog * 3.75).toFixed(2)} IN UP`, -40, 3); + ctx.fillText(`SILL DATUM ERR: ${(1.2 - prog * 1.2).toFixed(2)} DEGREES`, -40, 18); + ctx.fillText(`BEAM LOAD TRSF: ${(prog * 100).toFixed(0)}% TO STEEL`, -40, 33); + } else if (stageIndex === 11) { + ctx.fillText("SPEC: SUB-SLAB RADON SUCTION & VACUUM IN. WG", -w / 2 + 12, -h / 2 + 14); + + const vx = -95, + vy = 12; + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(255, 255, 255, 0.4)" : "#ffffff"; + ctx.lineWidth = pass === 0 ? 2.0 : 1.5; + ctx.beginPath(); + for (let s = 0; s <= 14; s++) { + const a = (s * Math.PI * 2) / 14; + const px = vx + Math.cos(a) * 22 + (s > 0 && s < 14 ? Math.sin(s) * 0.4 : 0); + const py = vy + Math.sin(a) * 22 + (s > 0 && s < 14 ? Math.cos(s) * 0.4 : 0); + if (s === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + } + drawGear(ctx, vx, vy, 15, 6, prog * Math.PI * 6, "#cccccc"); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("SUCTION : SUB-SLAB ACTIVE CORE", -45, -12); + ctx.fillText(`VACUUM WG : ${(-0.4 - prog * 1.4).toFixed(2)} IN. WG`, -45, 3); + ctx.fillText(`FAN EXHAUST: ${(40 + prog * 105).toFixed(0)} CFM`, -45, 18); + ctx.fillText(`RADON LEVEL: ${(18.4 - prog * 17.6).toFixed(1)} PCI/L`, -45, 33); + } else if (stageIndex === 12) { + ctx.fillText("SPEC: THERMODYNAMIC ENERGY EXCHANGE", -w / 2 + 12, -h / 2 + 14); + + const tx = -110, + ty = -15, + tw = 45, + th = 50; + drawSketchRect(ctx, tx, ty, tw, th, 2.0, { color: "#cccccc" }); + drawInsulationHatch(ctx, tx, ty, tw, th, prog, "rgba(255, 255, 255, 0.45)"); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText(`ENVELOPE R-VAL: R-${(12 + prog * 33).toFixed(0)} FOAM`, -50, -12); + ctx.fillText(`AIR LEAKAGE : ${(12.5 - prog * 11.8).toFixed(2)} ACH50`, -50, 3); + ctx.fillText(`GLAZING U-FAC : ${(0.35 - prog * 0.22).toFixed(3)} BTU`, -50, 18); + ctx.fillText(`LOOP COP EFF : ${(2.1 + prog * 2.4).toFixed(1)} COP`, -50, 33); + } else if (stageIndex === 13) { + ctx.fillText("SPEC: CANTILEVER MOMENT & V2H BATTERY", -w / 2 + 12, -h / 2 + 14); + + const mx = -110, + my = 10; + drawFuzzyPencilLine(ctx, mx - 15, my + 15, mx + 15, my - 15, 0.4, 2, { color: "#ffffff", overshoot: 2.0 }); + ctx.fillStyle = "#ffffff"; + ctx.beginPath(); + ctx.arc(mx, my, 4, 0, Math.PI * 2); + ctx.fill(); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText(`MOMENT LOAD : ${(120 + prog * 180).toFixed(0)} KIP-FT`, -50, -12); + ctx.fillText(`PV GENERATION: ${(4.2 + prog * 11.8).toFixed(1)} KW PEAK`, -50, 3); + ctx.fillText(`V2H STORAGE : ${(28 + prog * 72).toFixed(0)}% CHARGED`, -50, 18); + ctx.fillText(`NET CARBON : ${(-15 * prog).toFixed(0)}% (POSITIVE)`, -50, 33); + } else if (stageIndex === 14) { + ctx.fillText("SPEC: BOUSSINESQ STRESS BULB & SOIL STRATA", -w / 2 + 12, -h / 2 + 14); + + const sx = -95, + sy = 12; + for (let r = 8; r <= 24; r += 8) { + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(204, 204, 204, 0.35)" : "#cccccc"; + ctx.lineWidth = pass === 0 ? 1.6 : 1.0; + ctx.beginPath(); + for (let s = 0; s <= 10; s++) { + const a = (s * Math.PI) / 10; + let px = sx + Math.cos(a) * r; + let py = sy + Math.sin(a) * r; + if (s > 0 && s < 10) { + px += Math.sin(s + r) * 0.4; + py += Math.cos(s + r) * 0.4; + } + if (s === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + } + } + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("STRATUM : COMPACTED GLACIAL TILL", -50, -12); + ctx.fillText("SAFE BEARING: 3.5 TONS / SQ. FT.", -50, 3); + ctx.fillText(`STRESS ATTEN: ${(100 - prog * 75).toFixed(0)}% @ -12 FT`, -50, 18); + ctx.fillText(`HYDRO HEAD : ${(4.5 - prog * 4.5).toFixed(1)} PSI NET`, -50, 33); + } else if (stageIndex === 15) { + ctx.fillText("SPEC: DEW-POINT PLANE & INFRARED FLUX MATRIX", -w / 2 + 12, -h / 2 + 14); + + const dx = -95, + dy = 12; + drawSketchRect(ctx, dx - 16, dy - 16, 32, 32, 2.0, { color: "#ffffff" }); + drawInsulationHatch(ctx, dx - 16, dy - 16, 32, 32, prog, "rgba(255, 255, 255, 0.4)"); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("THERMAL FLUX: FINITE ELEMENT MODEL", -45, -12); + ctx.fillText(`U-FACTOR NET: ${(0.08 - prog * 0.058).toFixed(3)} BTU/HR`, -45, 3); + ctx.fillText("DEW-POINT : EXTERIOR VAPOR BARRIER", -45, 18); + ctx.fillText(`HEAT LOSS : -${(60 + prog * 34.2).toFixed(1)}% REDUCTION`, -45, 33); + } else if (stageIndex === 16) { + ctx.fillText("SPEC: WIND MOMENT SHEAR DEFLECTION VECTOR", -w / 2 + 12, -h / 2 + 14); + + const kx = -95, + ky = 12; + drawFuzzyPencilLine(ctx, kx - 18, ky + 18, kx + prog * 18, ky - 18, 0.4, 2, { color: "#ffffff", overshoot: 2.0 }); + drawSketchRect(ctx, kx - 6, ky - 6, 12, 12, 1.5, { color: "#ffffff" }); + + ctx.font = fontStyle; + ctx.letterSpacing = "0em"; + ctx.fillStyle = "#cccccc"; + ctx.fillText("MECHANICS : DYNAMIC VECTOR EQUILIB.", -45, -12); + ctx.fillText("WIND SHEAR: 90 MPH GUST DESIGN", -45, 3); + ctx.fillText(`STORY DRIFT: ${(prog * 0.24).toFixed(2)} IN. (L/360)`, -45, 18); + ctx.fillText(`LIVE RATIO : ${(40 + prog * 10).toFixed(0)} PSF DESIGN`, -45, 33); + } + + ctx.restore(); + ctx.restore(); +} + +/* ========================================================================= + ALL 17 STAGE HANDLERS (STAGE 0 .. STAGE 16) + ========================================================================= */ + +const STAGE_HANDLERS = [ + // Stage 0: 1780 - Early Settlement + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "VIRGIN HEMLOCK CANOPY", "98% OLD-GROWTH DENSITY", "stage0-callout1", 0); + addSVGCallout(g, 775, 50, 560, 280, "FIELDSTONE DRY-STACK", "GLACIAL ERRATIC CLEARING", "stage0-callout2", 0); + addSVGCallout(g, 25, 235, 280, 360, "HAND-HEWN TIMBER SILLS", "BROADAXE MORTISE JOINING", "stage0-callout3", 1); + addSVGCallout(g, 775, 250, 520, 370, "CLAY & STRAW CHINKING", "THERMAL MASS INSULATION", "stage0-callout4", 1); + addSVGCallout(g, 25, 420, 160, 470, "SUBTERRANEAN DUG WELL", "TAPPING SURFACE WATER TABLE", "stage0-callout5", 2); + addSVGCallout(g, 775, 420, 500, 470, "PITCH-ROOF SHAKES", "HAND-SPLIT WHITE CEDAR", "stage0-callout6", 2); + addSVGCallout(g, 25, 520, 180, 520, "SPLIT-RAIL WORM FENCE", "NO POST-HOLE DIGGING", "stage0-callout7", 3); + addSVGCallout(g, 775, 520, 560, 500, "CENTRAL HEATING CORE", "RADIANT THERMAL CHIMNEY", "stage0-callout8", 4); + addSVGTitleBlock(g, "EARLY SETTLEMENT HOMESTEAD", "A-101", "1780", 'SCALE: 3/8" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 100, 360, 600, 140, 24, "#0a0a0a", "#ffffff", -0.02); + drawEarthHatch(ctx, 100, 360, 600, 140, "rgba(255, 255, 255, 0.25)"); + + const { assemblyProg } = getPhaseTiming(phase.localProg); + const wallH = 100 * assemblyProg; + drawMasonryHatch(ctx, 250, 340 - wallH, 300, wallH, false); + drawSketchRect(ctx, 250, 340 - wallH, 300, wallH, 2.5, { color: "#ffffff" }); + + if (phase.idx >= 1) { + for (let k = 0; k < 5; k++) { + const bx = 260 + k * 65; + drawRoundRect(ctx, bx, 340 - Math.min(wallH, 25), 45, 20, 5, { color: "#cccccc" }); + } + } + + if (phase.idx >= 2) + drawDimensionLine(ctx, 250, 350, 550, 350, "30'-0\" FRONTAGE", "#ffffff", 12, "#000000"); + + if (phase.idx >= 3) { + const roofY = 340 - wallH - 60 * assemblyProg; + drawFuzzyPencilLine(ctx, 230, 340 - wallH, 400, roofY, 0.4, 2, { color: "#ffffff", overshoot: 2.8 }); + drawFuzzyPencilLine(ctx, 570, 340 - wallH, 400, roofY, 0.4, 2, { color: "#ffffff", overshoot: 2.8 }); + drawFuzzyPencilLine(ctx, 230, 340 - wallH, 570, 340 - wallH, 0.4, 1.8, { color: "#cccccc", overshoot: 2.2 }); + } + }, + }, + // Stage 1: 1795 - Mortise-and-Tenon Framing + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 280, "SEASONED WHITE OAK", "HEAVY TIMBER STRUCTURAL GIRTS", "stage1-callout1", 0); + addSVGCallout(g, 775, 50, 560, 300, "SCRIBED GIRT CONNECTIONS", "GROUND FITTED ALIGNMENT", "stage1-callout2", 0); + addSVGCallout(g, 25, 235, 340, 320, "T-HANDLE AUGER BORING", "DEEP CIRCULAR MORTISE CAVITY", "stage1-callout3", 1); + addSVGCallout(g, 775, 250, 460, 330, "FRAMING CHISEL CLEARING", "RECTANGULAR SLOT SHAPING", "stage1-callout4", 1); + addSVGCallout(g, 25, 420, 300, 380, "INTERLOCKING TENONS", "NO METAL HARDWARE REQ.", "stage1-callout5", 2); + addSVGCallout(g, 775, 420, 500, 390, "DRAW-BORE TREENAILS", "OFFSET HOLE TENSION PEG", "stage1-callout6", 3); + addSVGCallout(g, 25, 520, 260, 450, "COMMUNAL BENT RAISING", "GIN POLE & PULLEY TACKLE", "stage1-callout7", 4); + addSVGTitleBlock(g, "MORTISE-AND-TENON FRAMING", "A-201", "1795", 'SCALE: 1" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 140, 320, 520, 160, 28, "#111111", "#ffffff", 0.025); + drawMechanicalTrack(ctx, 160, 360, 640, 360, "rgba(255, 255, 255, 0.35)"); + + const { assemblyProg } = getPhaseTiming(phase.localProg); + drawWoodGrainHatch(ctx, 220, 250, 90, 200, 6, 3); + drawSketchRect(ctx, 220, 250, 90, 200, 2.5, { color: "#ffffff" }); + + drawWoodGrainHatch(ctx, 490, 250, 90, 200, 6, 3); + drawSketchRect(ctx, 490, 250, 90, 200, 2.5, { color: "#ffffff" }); + + if (phase.idx >= 1) { + drawGear(ctx, 360, 310, 28, 8, prog * Math.PI * 6, "#ffffff"); + drawFuzzyPencilLine(ctx, 360, 310, 360, 370 + assemblyProg * 30, 0.4, 2, { color: "#ffffff", overshoot: 1.6 }); + } + if (phase.idx >= 2) { + const tenonX = 310 + assemblyProg * 90; + drawWoodGrainHatch(ctx, tenonX, 290, 100, 40, 4, 2); + drawSketchRect(ctx, tenonX, 290, 100, 40, 2.2, { color: "#ffffff" }); + } + if (phase.idx >= 3) { + for (let p = 0; p < 6; p++) { + const px = 330 + p * 25; + const py = 260 + assemblyProg * 45; + ctx.fillStyle = "#0a0a0a"; + ctx.fillRect(px, py, 6, 22); + drawSketchRect(ctx, px, py, 6, 22, 1.2, { color: "#ffffff" }); + } + } + if (phase.idx >= 4) { + for (let b = 0; b < 5; b++) { + const bx = 180 + b * 90; + const by = 220 - assemblyProg * 40; + drawFuzzyPencilLine(ctx, bx, by, bx + 60, by - 30, 0.4, 2, { color: "#cccccc", overshoot: 2.2 }); + } + } + }, + }, + // Stage 2: 1810 - Geology & Landscape + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 260, 320, "HYDROLOGICAL PROFILE", "PERMEABLE GRAVEL VS BEDROCK", "stage2-callout1", 0); + addSVGCallout(g, 775, 50, 540, 320, "GLACIAL TILL STRATA", "NATURAL FILTER BED", "stage2-callout2", 0); + addSVGCallout(g, 25, 235, 280, 380, "SPRING HOUSE STRUCTURE", "YEAR-ROUND 48°F WATER", "stage2-callout3", 1); + addSVGCallout(g, 775, 250, 520, 390, "FROST LINE DATUM", "36-INCH FOOTING DEPTH", "stage2-callout4", 1); + addSVGCallout(g, 25, 420, 200, 465, "GRAVITY DRAINAGE TRENCH", "PERIMETER HYDROLOGICAL DIVERSION", "stage2-callout5", 2); + addSVGCallout(g, 775, 420, 500, 465, "GRANITE LEDGE OUTCROP", "NATURAL FOUNDATION FOOTING", "stage2-callout6", 2); + addSVGCallout(g, 25, 520, 220, 510, "SUBTERRANEAN STRATA MAP", "+420M ASL DATUM", "stage2-callout7", 3); + addSVGCallout(g, 775, 520, 560, 510, "STEPPED CELLAR ROOTS", "THERMAL STABILITY 50°F", "stage2-callout8", 4); + addSVGTitleBlock(g, "GEOLOGY & SUBTERRANEAN DRAINAGE", "A-301", "1810", 'SCALE: 1/4" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 120, 280, 560, 220, 32, "#080808", "#ffffff", 0.03); + drawEarthHatch(ctx, 120, 350, 560, 150, "rgba(255, 255, 255, 0.35)"); + + const { assemblyProg } = getPhaseTiming(phase.localProg); + const strataH = 120 * assemblyProg; + drawMasonryHatch(ctx, 300, 420 - strataH, 200, strataH, true); + drawSketchRect(ctx, 300, 420 - strataH, 200, strataH, 2.5, { color: "#ffffff" }); + + if (phase.idx >= 1) { + drawFuzzyPencilLine(ctx, 280, 380, 520, 380, 0.4, 2, { color: "#cccccc", overshoot: 2.4 }); + for (let k = 0; k < 6; k++) + drawRoundRect(ctx, 290 + k * 35, 385, 30, 28, 4, { color: "#cccccc" }); + } + + if (phase.idx >= 2) + drawDimensionLine(ctx, 140, 350, 140, 470, "12'-0\" STRATA DEPTH", "#ffffff", -16, "#000000"); + }, + }, + // Stage 3: 1835 - Watercourse & Millwright + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 220, 260, "PERENNIAL STREAM DAM", "REGULATED HYDRAULIC MILLRACE", "stage3-callout1", 0); + addSVGCallout(g, 775, 50, 560, 270, "ELEVATED WOODEN FLUME", "MAXIMIZED GRAVITATIONAL HEAD", "stage3-callout2", 0); + addSVGCallout(g, 25, 235, 260, 380, "16-BLADE OVERSHOT WHEEL", "KINETIC MOMENTUM OAK AXLE", "stage3-callout3", 1); + addSVGCallout(g, 775, 250, 520, 380, "SLUICE WEIR GATE LEVER", "RACK-AND-PINION FLOW CONTROL", "stage3-callout4", 1); + addSVGCallout(g, 25, 420, 310, 440, "TRUNDLE LANTERN CROWN GEAR", "90 DEGREE TORQUE REDIRECTION", "stage3-callout5", 2); + addSVGCallout(g, 775, 420, 500, 440, "RECIPROCATING VERTICAL SAW", "AUTOMATED TIMBER MILLING", "stage3-callout6", 3); + addSVGCallout(g, 25, 520, 280, 510, "TWELVEFOLD PRODUCTIVITY", "REGIONAL AGRARIAN HUB", "stage3-callout7", 4); + addSVGTitleBlock(g, "WATERCOURSE & MILLWRIGHT", "A-401", "1835", 'SCALE: 1/2" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 100, 360, 600, 150, 26, "#0a0a0a", "#ffffff", -0.025); + drawWoodGrainHatch(ctx, 120, 230, 220, 40, 4, 3); + drawSketchRect(ctx, 120, 230, 220, 40, 2.2, { color: "#ffffff" }); + + drawGear(ctx, 260, 380, 85, 16, prog * Math.PI * 4, "#ffffff"); + drawGear(ctx, 260, 380, 28, 8, prog * Math.PI * 4, "#cccccc"); + drawGear(ctx, 480, 380, 55, 14, -prog * Math.PI * 6, "#ffffff"); + drawGear(ctx, 560, 340, 38, 10, prog * Math.PI * 8, "#cccccc"); + + for (let w = 0; w < 8; w++) { + const wy = 270 + w * 14; + const wx = 240 + Math.sin(prog * 15 + w) * 10; + drawFuzzyPencilLine(ctx, 220, 250, wx, wy, 0.4, 1.5, { color: "#ffffff", overshoot: 1.5 }); + } + + const sawY = 280 + Math.sin(prog * 20) * 35; + drawSketchRect(ctx, 600, sawY - 40, 16, 80, 2.5, { color: "#ffffff" }); + drawSteelHatch(ctx, 600, sawY - 40, 16, 80); + }, + }, + // Stage 4: 1860 - Industrial Revolution + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 280, "BALLOON FRAMING REVOLUTION", "LIGHTWEIGHT 2X4 STUDS", "stage4-callout1", 0); + addSVGCallout(g, 775, 50, 560, 290, "STANDARDIZED DIMENSION LUMBER", "SAWMILL MASS PRODUCTION", "stage4-callout2", 0); + addSVGCallout(g, 25, 235, 280, 360, "CAST IRON HEATING STOVE", "COAL FUEL TRANSITION", "stage4-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "MACHINE-CUT STEEL NAILS", "INSTANT JOINERY REPLACEMENT", "stage4-callout4", 1); + addSVGCallout(g, 25, 420, 260, 420, "STEAM PUMP INFRASTRUCTURE", "AQUIFER PLUMBING SANITATION", "stage4-callout5", 2); + addSVGCallout(g, 775, 420, 500, 420, "DOUBLE-HUNG GLASS WINDOWS", "PULLEY & COUNTERWEIGHT", "stage4-callout6", 2); + addSVGCallout(g, 25, 520, 240, 500, "WROUGHT IRON TIE-RODS", "ADJUSTABLE TURNBUCKLE", "stage4-callout7", 3); + addSVGCallout(g, 775, 520, 540, 500, "MECHANIZED WELL PUMP", "INDOOR SANITATION HYGIENE", "stage4-callout8", 4); + addSVGTitleBlock(g, "INDUSTRIAL REVOLUTION FRAMING", "A-501", "1860", 'SCALE: 3/8" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 140, 340, 520, 160, 26, "#0a0a0a", "#ffffff", 0.025); + drawMechanicalTrack(ctx, 160, 380, 640, 380, "rgba(255, 255, 255, 0.35)"); + + const { assemblyProg } = getPhaseTiming(phase.localProg); + const numStuds = Math.floor(12 * assemblyProg); + for (let i = 0; i < numStuds; i++) { + const sx = 200 + i * 35; + drawWoodGrainHatch(ctx, sx, 220, 12, 120, 2, 2); + drawSketchRect(ctx, sx, 220, 12, 120, 1.8, { color: "#ffffff" }); + } + if (phase.idx >= 1) { + drawSteelHatch(ctx, 450, 280, 60, 80); + drawSketchRect(ctx, 450, 280, 60, 80, 2.2, { color: "#ffffff" }); + } + }, + }, + // Stage 5: 1885 - Victorian Expansion + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 230, 270, "QUEEN ANNE ORNAMENTATION", "MULTI-GABLED ROOF PROFILE", "stage5-callout1", 0); + addSVGCallout(g, 775, 50, 550, 280, "SPINDLE LATHE BALUSTERS", "HIGH-SPEED TURNED MILLWORK", "stage5-callout2", 0); + addSVGCallout(g, 25, 235, 270, 350, "FISH-SCALE SHINGLE SKIN", "CEDAR PATTERN SHADOW LINES", "stage5-callout3", 1); + addSVGCallout(g, 775, 250, 520, 360, "CHAMFERED PORCH COLUMNS", "GINGERBREAD GABLE SCROLLWORK", "stage5-callout4", 1); + addSVGCallout(g, 25, 420, 300, 440, "BLACK-IRON GAS MANIFOLD", "THREADED CENTRAL FUEL PIPE", "stage5-callout5", 2); + addSVGCallout(g, 775, 420, 500, 440, "BRASS PARLOR SCONCES", "EVENING ILLUMINATION NETWORK", "stage5-callout6", 3); + addSVGCallout(g, 25, 520, 280, 510, "EMBOSSED WALLCOVERINGS", "PLASTER CEILING MEDALLIONS", "stage5-callout7", 4); + addSVGTitleBlock(g, "VICTORIAN ORNAMENTAL MILLWORK", "A-601", "1885", 'SCALE: 3/4" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 120, 350, 560, 150, 24, "#111111", "#ffffff", 0.03); + drawMasonryHatch(ctx, 160, 250, 180, 130, false); + drawSketchRect(ctx, 160, 250, 180, 130, 2.5, { color: "#ffffff" }); + + drawWoodGrainHatch(ctx, 380, 240, 50, 180, 6, 2); + drawSketchRect(ctx, 380, 240, 50, 180, 2.5, { color: "#ffffff" }); + + for (let b = 0; b < 6; b++) { + const bx = 460 + b * 28; + const by = 310 + Math.sin(prog * 12 + b) * 4; + drawRoundRect(ctx, bx, by, 14, 65, 4, { color: "#ffffff" }); + } + drawSteelHatch(ctx, 220, 390, 360, 25); + drawSketchRect(ctx, 220, 390, 360, 25, 2.0, { color: "#ffffff" }); + + for (let g = 0; g < 5; g++) { + const gx = 250 + g * 75; + drawSurveyReticle(ctx, gx, 330, 16 + Math.sin(prog * 8 + g) * 4, "GAS LUX", "#ffffff"); + } + }, + }, + // Stage 6: 1900 - Gradual Farm Improvements + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "TIMBER BANK BARN EXPANSION", "GRAVITY-FED HAY LOFT", "stage6-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "GALVANIZED STEEL WINDMILL", "DEEP AQUIFER PUMPING", "stage6-callout2", 0); + addSVGCallout(g, 25, 235, 260, 360, "SILO FERMENTATION TOWER", "ENSILAGE WINTER STORAGE", "stage6-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "SOIL DRAINAGE TILES", "TERRACOTTA PERIMETER LOOP", "stage6-callout4", 1); + addSVGCallout(g, 25, 420, 220, 470, "IRRIGATION SLUICE CHANNELS", "PERENNIAL STREAM HARNESSING", "stage6-callout5", 2); + addSVGCallout(g, 775, 420, 500, 470, "CONCRETE FLOORING SLABS", "SANITARY DAIRY PRODUCTION", "stage6-callout6", 2); + addSVGCallout(g, 25, 520, 200, 510, "POLLINATOR CAVITIES", "STONE WALL WILDLIFE HABITAT", "stage6-callout7", 3); + addSVGCallout(g, 775, 520, 560, 510, "HEIRLOOM ORCHARD FURROWS", "45 CULTIVATED ACRES", "stage6-callout8", 4); + addSVGTitleBlock(g, "GRADUAL FARM IMPROVEMENTS", "A-701", "1900", 'SCALE: 1/4" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 110, 350, 580, 150, 25, "#0a0a0a", "#ffffff", -0.025); + + const { assemblyProg } = getPhaseTiming(phase.localProg); + const barnW = 240 * assemblyProg; + drawWoodGrainHatch(ctx, 200, 230, barnW, 120, 4, 3); + drawSketchRect(ctx, 200, 230, barnW, 120, 2.5, { color: "#ffffff" }); + + if (phase.idx >= 1) { + drawGear(ctx, 540, 260, 45, 12, prog * Math.PI * 4, "#ffffff"); + drawFuzzyPencilLine(ctx, 540, 260, 540, 350, 0.4, 2, { color: "#ffffff", overshoot: 2.0 }); + } + }, + }, + // Stage 7: 1925 - Electrification & Heating + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "PORCELAIN KNOB-AND-TUBE", "CERAMIC JOIST INSULATORS", "stage7-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "COPPER CONDUCTOR WIRING", "SPARK ARC PREVENTATIVE CLEAT", "stage7-callout2", 0); + addSVGCallout(g, 25, 235, 280, 360, "CAST-IRON HOT WATER BOILER", "COAL-FIRED BASEMENT FURNACE", "stage7-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "HYDRONIC RADIATOR FINS", "FLUTED CONVECTIVE SURFACE", "stage7-callout4", 1); + addSVGCallout(g, 25, 420, 320, 440, "WATT-HOUR INDUCTION DISK", "SPINNING ELECTROMECHANICAL METER", "stage7-callout5", 2); + addSVGCallout(g, 775, 420, 480, 440, "CLOSED-LOOP IRON PIPING", "UNIFORM DRAFT-FREE WARMTH", "stage7-callout6", 3); + addSVGCallout(g, 25, 520, 280, 510, "110V AC SINGLE-PHASE", "MODERN ENERGY ECONOMY", "stage7-callout7", 4); + addSVGTitleBlock(g, "ELECTRIFICATION & HYDRONIC HEATING", "A-801", "1925", 'SCALE: 1/2" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 110, 360, 580, 145, 25, "#0a0a0a", "#ffffff", -0.028); + drawSteelHatch(ctx, 180, 320, 140, 90); + drawSketchRect(ctx, 180, 320, 140, 90, 2.5, { color: "#ffffff" }); + + drawGear(ctx, 520, 320, 38, 12, prog * Math.PI * 10, "#ffffff"); + for (let k = 0; k < 6; k++) { + const kx = 220 + k * 55; + drawRoundRect(ctx, kx, 260, 18, 28, 6, { color: "#ffffff" }); + if (phase.idx >= 1) + drawFuzzyPencilLine(ctx, kx + 9, 274, kx + 55 + 9, 274 + Math.sin(prog * 20 + k) * 6, 0.6, 2, { color: "#ffffff", overshoot: 1.6 }); + } + for (let r = 0; r < 7; r++) { + const rx = 200 + r * 16; + const ry = 310 - prog * 40; + drawFuzzyPencilLine(ctx, rx, 320, rx + Math.sin(prog * 10 + r) * 12, ry, 0.4, 1.5, { color: "#cccccc", overshoot: 1.5 }); + } + }, + }, + // Stage 8: 1950 - Decline & Abandonment + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "ROOF RIDGE SAG & COLLAPSE", "STRUCTURAL RIDGE DEFLECTION", "stage8-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "WATER INFILTRATION ROT", "FUNGAL DECAY OF HEMLOCK GIRTS", "stage8-callout2", 0); + addSVGCallout(g, 25, 235, 260, 360, "COLLAPSED FLUME TRESTLE", "DISRUPTED HYDRAULIC POWER", "stage8-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "FROST-HEAVED DRY-STACK", "LATERAL FOUNDATION BULGING", "stage8-callout4", 1); + addSVGCallout(g, 25, 420, 200, 460, "FOUNDATION TREE ROOTS", "BIOLOGICAL MASONRY DISRUPTION", "stage8-callout5", 2); + addSVGCallout(g, 775, 420, 500, 460, "RUSTED IRON TIE-RODS", "TENSILE FAILURE & SEPARATION", "stage8-callout6", 2); + addSVGCallout(g, 25, 520, 220, 510, "SEVERE MORTAR LEACHING", "ACIDIC RAINFALL EROSION", "stage8-callout7", 3); + addSVGCallout(g, 775, 520, 540, 510, "BOTANICAL CANOPY OVERGROWTH", "PIONEER SAPLING WEDGING", "stage8-callout8", 4); + addSVGTitleBlock(g, "DECLINE & STRUCTURAL WEATHERING", "A-901", "1950", 'SCALE: 3/8" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 130, 360, 540, 150, 26, "#0a0a0a", "#ffffff", 0.02); + + const { assemblyProg } = getPhaseTiming(phase.localProg); + const sag = 40 * assemblyProg; + drawWoodGrainHatch(ctx, 200, 250 + sag, 400, 100 - sag * 0.5, 5, 2); + drawSketchRect(ctx, 200, 250 + sag, 400, 100 - sag * 0.5, 2.5, { color: "#ffffff" }); + + if (phase.idx >= 1) { + drawEarthHatch(ctx, 180, 360, 120, 60, "rgba(255, 255, 255, 0.4)"); + drawSketchRect(ctx, 180, 360, 120, 60, 2.0, { color: "#cccccc" }); + } + }, + }, + // Stage 9: 1965 - Emergency Shoring & Stabilization + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "IMMINENT COLLAPSE ARREST", "PRESERVATIONIST INTERVENTION", "stage9-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "SCREW-JACK STEEL LALLY", "ADJUSTABLE THREADED COLLAR", "stage9-callout2", 0); + addSVGCallout(g, 25, 235, 280, 360, "SAGGING SUMMER BEAMS", "UPWARD JACKING FORCE HALT", "stage9-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "TEMPORARY CONCRETE PADS", "DIRT FLOOR LOAD TRANSFER", "stage9-callout4", 1); + addSVGCallout(g, 25, 420, 320, 440, "STRUCTURAL STRAPPING", "BULGING MASONRY CONTAINMENT", "stage9-callout5", 2); + addSVGCallout(g, 775, 420, 480, 440, "EXTERNAL WATT-HOUR METER", "WEATHERPROOF TEMPORARY POWER", "stage9-callout6", 3); + addSVGCallout(g, 25, 520, 280, 510, "HEAVY TAR CANVAS SHEETING", "RAPID ROOF WEATHERPROOFING", "stage9-callout7", 4); + addSVGTitleBlock(g, "EMERGENCY SHORING & STABILIZATION", "A-1001", "1965", 'SCALE: 3/8" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 130, 370, 540, 140, 22, "#111111", "#ffffff", 0.028); + drawWoodGrainHatch(ctx, 180, 250, 440, 45, 5, 3); + drawSketchRect(ctx, 180, 250, 440, 45, 2.5, { color: "#ffffff" }); + + drawEarthHatch(ctx, 280, 460, 240, 35); + drawSketchRect(ctx, 280, 460, 240, 35, 2.0, { color: "#cccccc" }); + + for (let j = 0; j < 4; j++) { + const jx = 290 + j * 75; + const colH = 155 + prog * 15; + drawSteelHatch(ctx, jx, 460 - colH, 26, colH); + drawSketchRect(ctx, jx, 460 - colH, 26, colH, 2.0, { color: "#ffffff" }); + drawGear(ctx, jx + 13, 460 - colH + 20, 22, 8, prog * Math.PI * 6, "#ffffff"); + } + if (phase.idx >= 3) { + drawMasonryHatch(ctx, 180, 200, 440, 35, true); + drawSketchRect(ctx, 180, 200, 440, 35, 2.2, { color: "#cccccc" }); + } + }, + }, + // Stage 10: 1980 - Modernization Planning + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "GEOTECHNICAL CORE SAMPLING", "LOAD-BEARING STRATA VERIFIED", "stage10-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "STRUCTURAL FINITE ELEMENT", "COMPUTERIZED DEFLECTION MODEL", "stage10-callout2", 0); + addSVGCallout(g, 25, 235, 260, 360, "HYDRAULIC LIFTING RE-LEVEL", "PRECISION 12-TON JACKING", "stage10-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "REINFORCED CONCRETE UNDERPIN", "4,000 PSI REBAR FOUNDATION", "stage10-callout4", 1); + addSVGCallout(g, 25, 420, 200, 465, "EPOXY RESIN SPLICING", "SILL REPAIR & STRUCTURAL GRAFT", "stage10-callout5", 2); + addSVGCallout(g, 775, 420, 500, 465, "PERMEABLE MEMBRANE ENVELOPE", "BREATHABLE AIR SEALING", "stage10-callout6", 2); + addSVGCallout(g, 25, 520, 220, 510, "CONCEALED STEEL LINTEL", "C-CHANNEL REINFORCEMENT", "stage10-callout7", 3); + addSVGCallout(g, 775, 520, 540, 510, "LASER TRANSIT REALIGNMENT", "CODE COMPLIANCE DATUM", "stage10-callout8", 4); + addSVGTitleBlock(g, "MODERNIZATION & STABILIZATION", "A-1101", "1980", 'SCALE: 3/8" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 120, 350, 560, 150, 26, "#0a0a0a", "#ffffff", -0.025); + + const { assemblyProg } = getPhaseTiming(phase.localProg); + const liftH = 30 * assemblyProg; + drawSteelHatch(ctx, 220, 330 - liftH, 360, 20); + drawSketchRect(ctx, 220, 330 - liftH, 360, 20, 2.5, { color: "#ffffff" }); + + if (phase.idx >= 1) + drawSurveyReticle(ctx, 400, 280 - liftH, 20, "DATUM +0.00", "#ffffff"); + }, + }, + // Stage 11: 1995 - Radon Mitigation & Perimeter Drainage + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "SUB-SLAB RADON SUCTION PIT", "INTERCEPTING BEDROCK SOIL GAS", "stage11-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "PERFORATED COLLECTION PIPES", "WASHED DRAINAGE GRAVEL BED", "stage11-callout2", 0); + addSVGCallout(g, 25, 235, 280, 360, "CONTINUOUS PVC EXHAUST FAN", "PERMANENT NEGATIVE PRESSURE", "stage11-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "PERFORATED PVC FRENCH DRAIN", "GEOTEXTILE FABRIC WRAPPED", "stage11-callout4", 1); + addSVGCallout(g, 25, 420, 320, 440, "-12 FT BELOW GRADE FOOTING", "HYDROSTATIC VECTOR RELIEF", "stage11-callout5", 2); + addSVGCallout(g, 775, 420, 480, 440, "DAYLIGHT DISCHARGE BASIN", "GRAVITY MEADOW DIVERSION", "stage11-callout6", 3); + addSVGCallout(g, 25, 520, 280, 510, "BONE-DRY GRANITE CELLAR", "INDOOR AIR QUALITY SAFE", "stage11-callout7", 4); + addSVGTitleBlock(g, "RADON MITIGATION & FRENCH DRAINAGE", "A-1201", "1995", 'SCALE: 1/2" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 100, 260, 600, 250, 30, "#0a0a0a", "#ffffff", -0.025); + drawEarthHatch(ctx, 120, 420, 560, 90); + drawMasonryHatch(ctx, 360, 420, 80, 60, false); + drawSketchRect(ctx, 360, 420, 80, 60, 2.2, { color: "#ffffff" }); + + drawGear(ctx, 400, 290, 32, 12, prog * Math.PI * 12, "#ffffff"); + for (let a = 0; a < 8; a++) { + const ay = 410 - a * 15; + const ax = 400 + Math.sin(prog * 12 + a) * 8; + drawFuzzyPencilLine(ctx, ax, ay + 10, ax, ay - 10, 0.4, 1.5, { color: "#ffffff", overshoot: 1.5 }); + } + drawEarthHatch(ctx, 140, 390, 90, 45); + drawEarthHatch(ctx, 570, 390, 90, 45); + for (let p = 0; p < 5; p++) + drawFuzzyPencilLine(ctx, 150 + p * 15, 410, 150 + p * 15 + prog * 20, 410, 0.3, 2, { color: "#ffffff", overshoot: 1.5 }); + }, + }, + // Stage 12: 2010 - Ecological Insulation + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "CLOSED-CELL SPRAY FOAM", "R-45 CONTINUOUS CAVITY SEAL", "stage12-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "MINERAL WOOL ACOUSTIC BATT", "FIRE-RETARDANT & SOUNDPROOF", "stage12-callout2", 0); + addSVGCallout(g, 25, 235, 260, 360, "TRIPLE-PANE LOW-E GLAZING", "ARGON GAS THERMAL BREAK", "stage12-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "HEAT RECOVERY VENTILATION", "92% THERMAL EXCHANGE EFFICIENCY", "stage12-callout4", 1); + addSVGCallout(g, 25, 420, 220, 480, "GEOTHERMAL HEAT LOOP", "400 FT BEDROCK BOREHOLE", "stage12-callout5", 2); + addSVGCallout(g, 775, 420, 500, 480, "SOLAR THERMAL MANIFOLD", "HYDRONIC RADIANT FLOOR SUPPLY", "stage12-callout6", 2); + addSVGCallout(g, 25, 520, 200, 510, "ELASTOMERIC VAPOR BARRIER", "ZERO-DRAFT ENCAPSULATION", "stage12-callout7", 3); + addSVGCallout(g, 775, 520, 540, 510, "RADIANT CLIMATE CONTROL", "U-FACTOR 0.022 NET", "stage12-callout8", 4); + addSVGTitleBlock(g, "ECOLOGICAL & THERMAL RETROFIT", "A-1301", "2010", 'SCALE: 1/2" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 110, 340, 580, 150, 25, "#0a0a0a", "#ffffff", 0.02); + + const { assemblyProg } = getPhaseTiming(phase.localProg); + const foamW = 200 * assemblyProg; + drawWoodGrainHatch(ctx, 180, 240, foamW, 100, 4, 2); + drawSketchRect(ctx, 180, 240, foamW, 100, 2.5, { color: "#ffffff" }); + + if (phase.idx >= 1) + drawGear(ctx, 500, 280, 35, 12, prog * Math.PI * 4, "#ffffff"); + }, + }, + // Stage 13: Present Modern Design + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "HISTORIC POST-AND-BEAM EXPOSED", "PRESERVED 1780 HEWN TIMBERS", "stage13-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "STRUCTURAL GLASS CURTAIN WALL", "FRAMELESS STEEL SPIDER JOINTS", "stage13-callout2", 0); + addSVGCallout(g, 25, 235, 260, 360, "CANTILEVERED STEEL MEZZANINE", "FLOATING LOFT ARCHITECTURE", "stage13-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "INTEGRATED SMART-HOME DATUM", "AUTOMATED SHADING & CLIMATE", "stage13-callout4", 1); + addSVGCallout(g, 25, 420, 260, 460, "GEOTHERMAL & HRV SYSTEMS", "HYDRONIC RADIANT HEATING", "stage13-callout5", 2); + addSVGCallout(g, 775, 420, 500, 460, "RECLAIMED OAK PLANK FLOORING", "SUSTAINABLE MATERIAL REUSE", "stage13-callout6", 2); + addSVGCallout(g, 25, 520, 240, 510, "BI-DIRECTIONAL V2H BATTERY", "MICROGRID ENERGY STORAGE", "stage13-callout7", 3); + addSVGCallout(g, 775, 520, 540, 510, "NET-NEGATIVE CARBON OP.", "POSITIVE ENERGY GENERATION", "stage13-callout8", 4); + addSVGTitleBlock(g, "PRESENT MODERN DESIGN", "A-1401", "2026", 'SCALE: 3/8" = 1\'-0"', "AUG 1952"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 120, 350, 560, 150, 24, "#0a0a0a", "#ffffff", -0.02); + + const { assemblyProg } = getPhaseTiming(phase.localProg); + drawSteelHatch(ctx, 200, 260, 400 * assemblyProg, 80); + drawSketchRect(ctx, 200, 260, 400 * assemblyProg, 80, 2.5, { color: "#ffffff" }); + + if (phase.idx >= 1) + drawDimensionLine(ctx, 200, 360, 600, 360, "MODERN HOMESTEAD", "#ffffff", -20, "#000000"); + }, + }, + // Stage 14: 2026 - Geotechnical Engineering Drill-down + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "BOUSSINESQ STRESS BULB", "SUBTERRANEAN LOAD PROPAGATION", "stage14-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "3.5 TONS/SQ. FT. BEARING", "COMPACTED GLACIAL TILL STRATA", "stage14-callout2", 0); + addSVGCallout(g, 25, 235, 280, 360, "HYDROSTATIC HEAD VECTOR", "-12 FT BELOW GRADE PRESSURE", "stage14-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "WASHED GRAVEL BACKFILL", "LATERAL SHEAR DISSIPATION", "stage14-callout4", 1); + addSVGCallout(g, 25, 420, 320, 440, "ZERO DIFFERENTIAL SETTLE.", "MULTI-STORY LOAD STABILITY", "stage14-callout5", 2); + addSVGCallout(g, 775, 420, 480, 440, "BORE LOG DIAGNOSTICS", "METAMORPHIC GRANITE SHELF", "stage14-callout6", 3); + addSVGCallout(g, 25, 520, 280, 510, "PERPETUAL SOIL INTEGRITY", "GEOTECHNICAL DRILL-DOWN", "stage14-callout7", 4); + addSVGTitleBlock(g, "GEOTECHNICAL SOIL STRATA PROFILE", "A-1501", "2026", 'SCALE: 1/4" = 1\'-0"', "JUL 2026"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 110, 250, 580, 260, 32, "#111111", "#ffffff", 0.035); + drawEarthHatch(ctx, 130, 270, 540, 240); + drawMasonryHatch(ctx, 350, 250, 100, 40, false); + drawSketchRect(ctx, 350, 250, 100, 40, 2.2, { color: "#ffffff" }); + + for (let r = 1; r <= 6; r++) { + for (let pass = 0; pass < 2; pass++) { + ctx.strokeStyle = pass === 0 ? "rgba(255, 255, 255, 0.35)" : "#ffffff"; + ctx.lineWidth = pass === 0 ? 2.2 : 1.6; + ctx.beginPath(); + const rad = r * 30 * prog; + for (let s = 0; s <= 16; s++) { + const a = (s * Math.PI) / 16; + let px = 400 + Math.cos(a) * rad; + let py = 290 + Math.sin(a) * rad; + if (s > 0 && s < 16) { + px += Math.sin(s + r) * 0.5; + py += Math.cos(s + r) * 0.5; + } + if (s === 0) + ctx.moveTo(px, py); + else + ctx.lineTo(px, py); + } + ctx.stroke(); + } + } + for (let h = 0; h < 6; h++) { + const hy = 320 + h * 30; + drawDimensionLine(ctx, 140, hy, 140 + prog * 60, hy, `HYDRO VECTOR ${h + 1}`, "#ffffff", 4, "#000000"); + } + }, + }, + // Stage 15: 2026 - Thermodynamics Drill-down + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "DEW-POINT CONDENSATION TRACK", "EXTERIOR VAPOR BARRIER FACE", "stage15-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "INFRARED FLUX MATRIX", "ZERO THERMAL BRIDGING OVERLAY", "stage15-callout2", 0); + addSVGCallout(g, 25, 235, 280, 360, "R-45 CLOSED-CELL FOAM", "INTERSTITIAL ROT PREVENTION", "stage15-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "FINITE ELEMENT MODELING", "U-FACTOR 0.022 NET RATING", "stage15-callout4", 1); + addSVGCallout(g, 25, 420, 320, 440, "MULTI-LAYER VAPOR GRADIENT", "WOOD FIBER SHEATHING BOARD", "stage15-callout5", 2); + addSVGCallout(g, 775, 420, 480, 440, "94.2% HEAT LOSS REDUCTION", "ADVANCED BUILDING PHYSICS", "stage15-callout6", 3); + addSVGCallout(g, 25, 520, 280, 510, "THERMODYNAMIC MASTERY", "ULTRA-LOW-ENERGY CLIMATE", "stage15-callout7", 4); + addSVGTitleBlock(g, "THERMODYNAMIC FLUX & DEW-POINT PLANE", "A-1601", "2026", 'SCALE: 3/4" = 1\'-0"', "JUL 2026"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 120, 260, 560, 240, 28, "#0a0a0a", "#ffffff", -0.025); + drawWoodGrainHatch(ctx, 140, 280, 140, 200, 5, 2); + drawSketchRect(ctx, 140, 280, 140, 200, 2.5, { color: "#ffffff" }); + + drawInsulationHatch(ctx, 420, 280, 240, 200, prog, "rgba(255, 255, 255, 0.45)"); + drawSketchRect(ctx, 420, 280, 240, 200, 2.5, { color: "#ffffff" }); + + drawFuzzyPencilLine(ctx, 400, 270, 400, 490, 0.3, 3, { color: "#ffffff", overshoot: 3.0 }); + for (let f = 0; f < 8; f++) { + const fy = 290 + f * 24; + const fx = 420 - prog * 60; + drawFuzzyPencilLine(ctx, 560, fy, fx, fy, 0.4, 1.5, { color: "#ffffff", overshoot: 1.8 }); + ctx.fillStyle = "#ffffff"; + drawSketchRect(ctx, fx - 4, fy - 3, 6, 6, 1.2, { color: "#ffffff" }); + } + }, + }, + // Stage 16: 2026 - Structural Kinematics Drill-down + { + initSVG: (g, width, height) => { + addSVGCallout(g, 25, 50, 240, 270, "DEAD VS LIVE LOAD VECTORS", "32 PSF PERMANENT / 40 PSF LIVE", "stage16-callout1", 0); + addSVGCallout(g, 775, 50, 540, 280, "WIND MOMENT SHEAR DEFLECTION", "90 MPH GUST OVERTURNING MOMENT", "stage16-callout2", 0); + addSVGCallout(g, 25, 235, 280, 360, "HYBRID STEEL MOMENT FRAME", "BRACED TIMBER SHEAR WALLS", "stage16-callout3", 1); + addSVGCallout(g, 775, 250, 500, 370, "OPTICAL STRAIN GAUGE DATUM", "STORY DRIFT INDEX L/360", "stage16-callout4", 1); + addSVGCallout(g, 25, 420, 320, 440, "MAX DEFLECTION < 0.25 INCHES", "ELASTIC TOLERANCE COMPLIANT", "stage16-callout5", 2); + addSVGCallout(g, 775, 420, 480, 440, "GRAVITY LOAD EQUILIBRIUM", "POST DOWN TO GRANITE FOOTING", "stage16-callout6", 3); + addSVGCallout(g, 25, 520, 280, 510, "STRUCTURAL KINEMATICS MATRIX", "18TH CENTURY MEETS 21ST CENTURY", "stage16-callout7", 4); + addSVGTitleBlock(g, "STRUCTURAL KINEMATICS & SHEAR DRIFT", "A-1701", "2026", 'SCALE: 3/8" = 1\'-0"', "JUL 2026"); + }, + updateSVG: (g, prog, width, height, phase) => {}, + renderCanvas: (ctx, g, prog, width, height, phase) => { + for (let x = 0; x < width; x += 40) + drawFuzzyPencilLine(ctx, x, 0, x, height, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + for (let y = 0; y < height; y += 40) + drawFuzzyPencilLine(ctx, 0, y, width, y, 0.3, 1, { color: "rgba(255, 255, 255, 0.12)", secondaryColor: "rgba(255, 255, 255, 0.05)", overshoot: false }); + + drawDioramaPlatform(ctx, 100, 350, 600, 160, 28, "#0a0a0a", "#ffffff", -0.03); + const driftX = prog * 20; + drawSteelHatch(ctx, 220 + driftX, 220, 360, 130); + drawSketchRect(ctx, 220 + driftX, 220, 360, 130, 2.5, { color: "#ffffff" }); + + for (let a = 0; a < 6; a++) { + const ax = 250 + a * 60 + driftX; + drawDimensionLine(ctx, ax, 190, ax, 220 + prog * 10, `LOAD ${a + 1}`, "#ffffff", 5, "#000000"); + } + for (let w = 0; w < 5; w++) { + const wy = 240 + w * 22; + drawDimensionLine(ctx, 120, wy, 210 + driftX, wy, "90 MPH SHEAR", "#ffffff", 5, "#000000"); + } + drawSurveyReticle(ctx, 400 + driftX, 280, 24, "L/360 DRIFT", "#ffffff"); + }, + }, +]; diff --git a/suites-experimental/scrollytelling/src/main.js b/suites-experimental/scrollytelling/src/main.js new file mode 100644 index 000000000..91a34d379 --- /dev/null +++ b/suites-experimental/scrollytelling/src/main.js @@ -0,0 +1,1185 @@ +/* + * Copyright (C) 2024-2026 Speedometer Contributors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + */ + +/** + * 1950s Black & White Blueprint / Planning Design + * Style reference: STYLE_CONFIG in src/graphics.js + * Mandates pure black background (#000000), crisp B&W geometry, slanted borders, + * independent drop shadow angles, and gradual organic watercolor animation reveals. + */ +import { STAGES } from "./content.js"; +import { initGraphics, updateGraphics } from "./graphics.js"; +import { initScrollamaEngine } from "./engine-scrollama.js"; + +const INLINE_ILLUSTRATION_CACHE = Object.create(null); + +const INLINE_ILLUSTRATION_TITLES = { + "step-1": ["DETAIL A-1.1: DOVETAIL LOG CORNER NOTCH", "SECTION B-1.2: DRY-LAID FIELDSTONE HEARTH SECTION"], + "step-2": ["DETAIL A-2.1: DRAW-BORE TREENAIL OFFSET JOINT", "SECTION B-2.2: TIMBER BENT ASSEMBLY RIGGING"], + "step-3": ["DETAIL A-3.1: SUBTERRANEAN GRAVITY DRAIN TRENCH", "SECTION B-3.2: SPRING HOUSE THERMAL ENVELOPE"], + "step-4": ["DETAIL A-4.1: OVERSHOT WATERWHEEL BUCKET PROFILE", "SECTION B-4.2: TRUNDLE LANTERN CROWN GEAR MESH"], + "step-5": ["DETAIL A-5.1: BALLOON FRAMING STUD-TO-SILL JOINT", "SECTION B-5.2: WROUGHT IRON TURNBUCKLE TIE-ROD"], + "step-6": ["DETAIL A-6.1: SPINDLE LATHE TURNED BALUSTER PROFILE", "SECTION B-6.2: BLACK-IRON GAS MANIFOLD DISTRIBUTOR"], + "step-7": ["DETAIL A-7.1: TIMBER BANK BARN HAY LOFT HOIST", "SECTION B-7.2: TERRACOTTA PERIMETER DRAINAGE TILE"], + "step-8": ["DETAIL A-8.1: PORCELAIN KNOB-AND-TUBE CERAMIC INSULATOR", "SECTION B-8.2: CAST-IRON HYDRONIC RADIATOR FIN SECTION"], + "step-9": ["DETAIL A-9.1: STRUCTURAL RIDGE DEFLECTION & SAG", "SECTION B-9.2: SEVERE MORTAR LEACHING & TREE ROOT INVASION"], + "step-10": ["DETAIL A-10.1: SCREW-JACK STEEL LALLY COLUMN THREAD", "SECTION B-10.2: TEMPORARY CONCRETE FOOTING BEARING PAD"], + "step-11": ["DETAIL A-11.1: HYDRAULIC JACKING & RE-LEVELING DATUM", "SECTION B-11.2: CONCEALED C-CHANNEL LINTEL REINFORCEMENT"], + "step-12": ["DETAIL A-12.1: SUB-SLAB RADON SUCTION PIT CORE", "SECTION B-12.2: PERFORATED PVC FRENCH DRAINAGE LOOP"], + "step-13": ["DETAIL A-13.1: CLOSED-CELL SPRAY FOAM CAVITY SEAL", "SECTION B-13.2: GEOTHERMAL BOREHOLE HEAT EXCHANGE LOOP"], + "step-14": ["DETAIL A-14.1: HISTORIC POST-AND-BEAM EXPOSED INTERIOR", "SECTION B-14.2: BI-DIRECTIONAL V2H BATTERY STORAGE"], + "step-15": ["DETAIL A-15.1: BOUSSINESQ STRESS BULB SOIL ATTENUATION", "SECTION B-15.2: HYDROSTATIC GROUNDWATER PRESSURE VECTOR"], + "step-16": ["DETAIL A-16.1: DEW-POINT CONDENSATION PLANE INTERSECTION", "SECTION B-16.2: INFRARED FLUX MATRIX THERMAL BRIDGING ELIMINATION"], + "step-17": ["DETAIL A-17.1: DEAD VS LIVE LOAD VECTOR RESOLUTION", "SECTION B-17.2: WIND MOMENT SHEAR DEFLECTION DRIFT (L/360)"], +}; + +function drawFuzzyLine(ctx, x1, y1, x2, y2, strokeColor = "#ffffff", lineWidth = 1.8, options = {}) { + const dx = x2 - x1; + const dy = y2 - y1; + const len = Math.sqrt(dx * dx + dy * dy); + if (len < 0.1) + return; + + let sx = x1; + let sy = y1; + let ex = x2; + let ey = y2; + + if (options.overrun !== false) { + const ux = dx / len; + const uy = dy / len; + const overrunStart = 1.5 + Math.abs(Math.sin((x1 * 11.3 + y1 * 29.1) * 0.1)) * 1.5; + const overrunEnd = 1.5 + Math.abs(Math.cos((x2 * 19.7 + y2 * 37.3) * 0.1)) * 1.5; + sx -= ux * overrunStart; + sy -= uy * overrunStart; + ex += ux * overrunEnd; + ey += uy * overrunEnd; + } + + const steps = Math.max(3, Math.floor(len / 12)); + const points = [{ x: sx, y: sy }]; + for (let i = 1; i < steps; i++) { + const t = i / steps; + const seed = (sx * 17.1 + sy * 31.7 + ex * 23.3 + ey * 47.9 + i * 19.3) * 0.1; + const jx = Math.sin(seed) * 1.6; + const jy = Math.cos(seed * 1.3) * 1.6; + points.push({ + x: sx + (ex - sx) * t + jx, + y: sy + (ey - sy) * t + jy, + }); + } + points.push({ x: ex, y: ey }); + + ctx.save(); + ctx.strokeStyle = strokeColor; + ctx.lineWidth = lineWidth; + ctx.shadowColor = strokeColor === "#D12B3E" ? "rgba(209, 43, 62, 0.4)" : "rgba(255, 255, 255, 0.45)"; + ctx.shadowBlur = 2.0 + Math.abs(Math.sin((sx + sy) * 0.2)) * 1.0; + if (options.dashed) + ctx.setLineDash([4, 2]); + else if (options.dashArray) + ctx.setLineDash(options.dashArray); + + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + for (let i = 1; i < points.length; i++) + ctx.lineTo(points[i].x, points[i].y); + + ctx.stroke(); + ctx.restore(); + + if (options.singlePass !== true) { + ctx.save(); + ctx.strokeStyle = strokeColor === "#D12B3E" ? "rgba(209, 43, 62, 0.6)" : "rgba(204, 204, 204, 0.7)"; + ctx.lineWidth = Math.max(1, lineWidth * 0.75); + ctx.shadowBlur = 1.5; + if (options.dashed) + ctx.setLineDash([4, 2]); + else if (options.dashArray) + ctx.setLineDash(options.dashArray); + + ctx.beginPath(); + const offSeed = (sx * 13.1 + sy * 71.7) * 0.1; + const baseOx = Math.sin(offSeed) * 0.8; + const baseOy = Math.cos(offSeed) * 0.8; + for (let i = 0; i < points.length; i++) { + const seed2 = (sx * 29.3 + sy * 41.9 + i * 23.7) * 0.1; + const ox = baseOx + Math.sin(seed2 * 1.1) * 0.6; + const oy = baseOy + Math.cos(seed2 * 1.7) * 0.6; + if (i === 0) + ctx.moveTo(points[i].x + ox, points[i].y + oy); + else + ctx.lineTo(points[i].x + ox, points[i].y + oy); + } + ctx.stroke(); + ctx.restore(); + } +} + +function drawFuzzyRect(ctx, x, y, w, h, strokeColor = "#ffffff", lineWidth = 1.8) { + drawFuzzyLine(ctx, x, y, x + w, y, strokeColor, lineWidth, { overrun: true }); + drawFuzzyLine(ctx, x + w, y, x + w, y + h, strokeColor, lineWidth, { overrun: true }); + drawFuzzyLine(ctx, x + w, y + h, x, y + h, strokeColor, lineWidth, { overrun: true }); + drawFuzzyLine(ctx, x, y + h, x, y, strokeColor, lineWidth, { overrun: true }); +} + +function drawFuzzyCircle(ctx, cx, cy, r, strokeColor = "#ffffff", lineWidth = 1.8, startAngle = 0, endAngle = Math.PI * 2, options = {}) { + const segments = Math.max(12, Math.floor(r * 0.8)); + const angleStep = (endAngle - startAngle) / segments; + const points = []; + for (let i = 0; i <= segments; i++) { + const theta = startAngle + i * angleStep; + const seed = (cx * 11.3 + cy * 29.1 + i * 13.7) * 0.1; + const jitterR = r + Math.sin(seed) * 1.5; + points.push({ + x: cx + Math.cos(theta) * jitterR, + y: cy + Math.sin(theta) * jitterR, + }); + } + + ctx.save(); + ctx.strokeStyle = strokeColor; + ctx.lineWidth = lineWidth; + ctx.shadowColor = strokeColor === "#D12B3E" ? "rgba(209, 43, 62, 0.4)" : "rgba(255, 255, 255, 0.45)"; + ctx.shadowBlur = 2.0; + if (options.dashed) + ctx.setLineDash([4, 2]); + + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + for (let i = 1; i < points.length; i++) + ctx.lineTo(points[i].x, points[i].y); + + ctx.stroke(); + ctx.restore(); + + if (options.singlePass !== true) { + ctx.save(); + ctx.strokeStyle = strokeColor === "#D12B3E" ? "rgba(209, 43, 62, 0.6)" : "rgba(204, 204, 204, 0.7)"; + ctx.lineWidth = Math.max(1, lineWidth * 0.75); + ctx.shadowBlur = 1.5; + if (options.dashed) + ctx.setLineDash([4, 2]); + + ctx.beginPath(); + for (let i = 0; i < points.length; i++) { + const seed2 = (cx * 19.3 + cy * 37.1 + i * 17.7) * 0.1; + const ox = Math.sin(seed2) * 1.0; + const oy = Math.cos(seed2 * 1.3) * 1.0; + if (i === 0) + ctx.moveTo(points[i].x + ox, points[i].y + oy); + else + ctx.lineTo(points[i].x + ox, points[i].y + oy); + } + ctx.stroke(); + ctx.restore(); + } +} + +function drawFuzzyPoly(ctx, points, strokeColor = "#ffffff", lineWidth = 1.8, closed = false, options = {}) { + if (!points || points.length < 2) + return; + + for (let i = 0; i < points.length - 1; i++) { + drawFuzzyLine(ctx, points[i].x, points[i].y, points[i + 1].x, points[i + 1].y, strokeColor, lineWidth, { + overrun: closed || i > 0 || i < points.length - 2, + ...options, + }); + } + if (closed) { + drawFuzzyLine(ctx, points[points.length - 1].x, points[points.length - 1].y, points[0].x, points[0].y, strokeColor, lineWidth, { + overrun: true, + ...options, + }); + } +} + +function drawDraftingTitle(ctx, title, x, y, color = "#ffffff", maxFontSize = 13) { + ctx.save(); + let fontSize = maxFontSize; + ctx.font = `bold ${fontSize}px "Courier New", Courier, monospace`; + let measuredWidth = ctx.measureText(title).width; + while (x + measuredWidth > ctx.canvas.width - 15 && fontSize > 8.5) { + fontSize -= 0.5; + ctx.font = `bold ${fontSize}px "Courier New", Courier, monospace`; + measuredWidth = ctx.measureText(title).width; + } + ctx.fillStyle = color; + ctx.shadowColor = "rgba(255, 255, 255, 0.45)"; + ctx.shadowBlur = 1.5; + ctx.fillText(title, x, y); + ctx.restore(); +} + +function drawDraftingText(ctx, text, x, y, color = "#ffffff", fontSize = 11, bold = false) { + ctx.save(); + ctx.font = `${bold ? "bold " : ""}${fontSize}px "Courier New", Courier, monospace`; + ctx.fillStyle = color; + ctx.shadowColor = color === "#D12B3E" ? "rgba(209, 43, 62, 0.4)" : "rgba(255, 255, 255, 0.5)"; + ctx.shadowBlur = 1.5; + ctx.fillText(text, x, y); + ctx.restore(); +} + +function drawLeaderArrow(ctx, x1, y1, x2, y2, text, textX, textY, isDashed = false) { + drawFuzzyLine(ctx, x1, y1, x2, y2, "#D12B3E", 1.5, { dashed: isDashed, overrun: false }); + drawFuzzyCircle(ctx, x1, y1, 3, "#D12B3E", 1.5, 0, Math.PI * 2, { singlePass: true }); + if (text) + drawDraftingText(ctx, text, textX, textY, "#D12B3E", 11); +} + +function drawWoodGrainHatch(ctx, x, y, w, h) { + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + const count = Math.max(3, Math.floor(h / 9)); + for (let i = 0; i < count; i++) { + const seed = (x * 7.1 + y * 13.3 + i * 31.1) * 0.1; + const py = y + ((i + 0.5) / count) * h + Math.sin(seed) * 3; + drawFuzzyLine(ctx, x + 2, py, x + w - 2, py + Math.cos(seed * 1.2) * 4, "rgba(255,255,255,0.22)", 1, { overrun: false, singlePass: true }); + } + ctx.restore(); +} + +function drawMasonryHatch(ctx, x, y, w, h) { + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + const rows = Math.max(3, Math.floor(h / 14)); + const rowH = h / rows; + for (let r = 1; r < rows; r++) { + const seed = (x * 11.7 + y * 19.3 + r * 23.9) * 0.1; + const ry = y + r * rowH + Math.sin(seed) * 2; + drawFuzzyLine(ctx, x, ry, x + w, ry, "rgba(255,255,255,0.28)", 1.2, { overrun: false, singlePass: true }); + } + for (let r = 0; r < rows; r++) { + const ry1 = y + r * rowH; + const ry2 = y + (r + 1) * rowH; + const cols = Math.max(2, Math.floor(w / 24)); + const colW = w / cols; + const offset = r % 2 === 0 ? 0 : colW * 0.5; + for (let c = 0; c <= cols; c++) { + const seed2 = (x * 17.3 + y * 29.7 + r * 13.1 + c * 41.3) * 0.1; + const cx = x + ((c * colW + offset) % w) + Math.sin(seed2) * 3; + if (cx > x + 2 && cx < x + w - 2) + drawFuzzyLine(ctx, cx, ry1, cx, ry2, "rgba(255,255,255,0.25)", 1, { overrun: false, singlePass: true }); + } + } + ctx.restore(); +} + +function drawStipplingHatch(ctx, x, y, w, h, count = 35) { + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + for (let i = 0; i < count; i++) { + const seed = (x * 13.9 + y * 47.1 + i * 83.3) * 0.05; + const px = x + Math.abs(Math.sin(seed)) * w; + const py = y + Math.abs(Math.cos(seed * 1.4)) * h; + if (Math.sin(seed * 2.3) < 0.2) { + drawFuzzyCircle(ctx, px, py, 1.2 + Math.abs(Math.sin(seed * 3.1)) * 1.5, "rgba(255,255,255,0.28)", 1, 0, Math.PI * 2, { singlePass: true }); + } else { + drawFuzzyPoly( + ctx, + [ + { x: px, y: py }, + { x: px + 2.5, y: py + 1.5 }, + { x: px + 1, y: py + 3 }, + ], + "rgba(255,255,255,0.25)", + 1, + true, + { singlePass: true } + ); + } + } + ctx.restore(); +} + +function drawFoamSquiggles(ctx, x, y, w, h) { + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + const count = Math.max(8, Math.floor((w * h) / 350)); + for (let i = 0; i < count; i++) { + const seed = (x * 23.1 + y * 59.3 + i * 71.7) * 0.05; + const cx = x + 8 + Math.abs(Math.sin(seed)) * Math.max(1, w - 16); + const cy = y + 8 + Math.abs(Math.cos(seed * 1.3)) * Math.max(1, h - 16); + const r = 3 + Math.abs(Math.sin(seed * 2.1)) * 4; + drawFuzzyCircle(ctx, cx, cy, r, "rgba(255,255,255,0.32)", 1.2, 0, Math.PI * 1.5, { singlePass: true }); + } + ctx.restore(); +} + +function drawCrossSectionHatch(ctx, x, y, w, h, spacing = 9) { + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + const maxDim = w + h; + for (let d = -h; d < maxDim; d += spacing) + drawFuzzyLine(ctx, x + d, y, x + d - h, y + h, "rgba(255,255,255,0.24)", 1, { overrun: false, singlePass: true }); + + ctx.restore(); +} + +function drawConstructionReticles(ctx, width, height) { + drawFuzzyLine(ctx, 40, height * 0.5, width - 40, height * 0.5, "rgba(255, 255, 255, 0.16)", 1, { dashed: true, singlePass: true, overrun: false }); + drawFuzzyLine(ctx, width * 0.5, 30, width * 0.5, height - 25, "rgba(255, 255, 255, 0.16)", 1, { dashed: true, singlePass: true, overrun: false }); +} + +function getOrGenerateInlineIllustrationDataURL(stageId, pIdx, title) { + const cacheKey = `${stageId}-${pIdx}`; + if (INLINE_ILLUSTRATION_CACHE[cacheKey]) + return INLINE_ILLUSTRATION_CACHE[cacheKey]; + + if (typeof document === "undefined" || !document.createElement) + return ""; + + const canvas = document.createElement("canvas"); + canvas.width = 500; + canvas.height = 200; + const ctx = canvas.getContext("2d"); + if (!ctx) + return ""; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawDraftingTitle(ctx, title || "", 15, 24, "#ffffff", 13); + drawConstructionReticles(ctx, canvas.width, canvas.height); + + if (stageId === "step-1" && pIdx === 1) { + drawFuzzyRect(ctx, 60, 50, 180, 45); + drawWoodGrainHatch(ctx, 60, 50, 180, 45); + drawFuzzyRect(ctx, 60, 105, 180, 45); + drawWoodGrainHatch(ctx, 60, 105, 180, 45); + drawFuzzyPoly( + ctx, + [ + { x: 60, y: 50 }, + { x: 95, y: 72.5 }, + { x: 60, y: 95 }, + ], + "#ffffff", + 2 + ); + drawFuzzyLine(ctx, 70, 55, 230, 55, "rgba(255,255,255,0.3)", 1, { overrun: false }); + drawFuzzyLine(ctx, 70, 110, 230, 110, "rgba(255,255,255,0.3)", 1, { overrun: false }); + drawLeaderArrow(ctx, 260, 72.5, 380, 72.5, "SLOPE TAPER 1:4 (DRAINAGE)", 270, 66, true); + drawLeaderArrow(ctx, 260, 127.5, 380, 127.5, "BROADAXE HEWN OAK SILL", 270, 122, false); + } else if (stageId === "step-1" && pIdx === 3) { + drawFuzzyRect(ctx, 50, 140, 400, 40); + drawStipplingHatch(ctx, 50, 140, 400, 40); + drawCrossSectionHatch(ctx, 50, 140, 400, 40, 16); + drawFuzzyRect(ctx, 180, 45, 140, 95); + drawMasonryHatch(ctx, 180, 45, 140, 95); + drawFuzzyLine(ctx, 190, 60, 310, 60, "#ffffff", 1.5, { overrun: false }); + drawFuzzyLine(ctx, 190, 85, 310, 85, "#ffffff", 1.5, { overrun: false }); + drawFuzzyLine(ctx, 190, 110, 310, 110, "#ffffff", 1.5, { overrun: false }); + drawLeaderArrow(ctx, 340, 92, 440, 92, "CENTRAL MASS CHIMNEY", 355, 86, false); + } else if (stageId === "step-2" && pIdx === 1) { + drawFuzzyRect(ctx, 100, 60, 140, 90); + drawWoodGrainHatch(ctx, 100, 60, 140, 90); + drawFuzzyRect(ctx, 240, 80, 120, 50); + drawWoodGrainHatch(ctx, 240, 80, 120, 50); + drawFuzzyCircle(ctx, 210, 105, 10, "#ffffff", 1.8); + drawFuzzyCircle(ctx, 213, 105, 8, "#D12B3E", 1.5, 0, Math.PI * 2, { dashed: true }); + drawLeaderArrow(ctx, 213, 160, 213, 50, "3.1 MM PIN OFFSET TENSION", 250, 58, false); + } else if (stageId === "step-2" && pIdx === 3) { + drawFuzzyLine(ctx, 80, 160, 420, 160, "#ffffff", 2); + drawFuzzyLine(ctx, 150, 160, 150, 60, "#ffffff", 2.2); + drawFuzzyLine(ctx, 150, 60, 330, 60, "#ffffff", 2.2); + drawFuzzyLine(ctx, 330, 60, 330, 160, "#ffffff", 2.2); + drawFuzzyLine(ctx, 150, 110, 190, 60, "#ffffff", 1.8); + drawFuzzyLine(ctx, 330, 110, 290, 60, "#ffffff", 1.8); + drawWoodGrainHatch(ctx, 145, 60, 190, 100); + drawLeaderArrow(ctx, 350, 60, 440, 40, "PULLEY RIGGING POINT", 360, 34, false); + } else if (stageId === "step-3" && pIdx === 1) { + drawFuzzyPoly( + ctx, + [ + { x: 120, y: 60 }, + { x: 380, y: 60 }, + { x: 330, y: 160 }, + { x: 170, y: 160 }, + ], + "#ffffff", + 2, + true + ); + drawStipplingHatch(ctx, 140, 70, 220, 80); + drawFuzzyCircle(ctx, 250, 135, 18, "#ffffff", 2); + drawCrossSectionHatch(ctx, 235, 120, 30, 30, 6); + drawLeaderArrow(ctx, 268, 135, 420, 135, "PERFORATED CLAY TILE DATUM", 280, 128, false); + } else if (stageId === "step-3" && pIdx === 3) { + drawFuzzyRect(ctx, 130, 70, 240, 90); + drawMasonryHatch(ctx, 130, 70, 240, 90); + drawFuzzyRect(ctx, 150, 130, 200, 30, "rgba(255,255,255,0.7)", 1.5); + drawFuzzyPoly( + ctx, + [ + { x: 130, y: 70 }, + { x: 250, y: 35 }, + { x: 370, y: 70 }, + ], + "#ffffff", + 2.2 + ); + drawLeaderArrow(ctx, 380, 145, 460, 145, "CONSTANT 48°F WATER", 385, 138, false); + } else if (stageId === "step-4" && pIdx === 1) { + drawFuzzyCircle(ctx, 160, 115, 65, "#ffffff", 2); + drawFuzzyCircle(ctx, 160, 115, 50, "#ffffff", 1.5); + drawFuzzyCircle(ctx, 160, 115, 14, "#ffffff", 2); + drawFuzzyLine(ctx, 160, 50, 175, 65, "#ffffff", 2); + drawFuzzyLine(ctx, 225, 115, 210, 130, "#ffffff", 2); + drawFuzzyLine(ctx, 160, 180, 145, 165, "#ffffff", 2); + drawFuzzyLine(ctx, 95, 115, 110, 100, "#ffffff", 2); + drawWoodGrainHatch(ctx, 110, 65, 100, 100); + drawLeaderArrow(ctx, 225, 115, 390, 115, "16 OAK BUCKET CAVITIES", 240, 108, false); + } else if (stageId === "step-4" && pIdx === 3) { + drawFuzzyRect(ctx, 120, 85, 110, 24); + drawWoodGrainHatch(ctx, 120, 85, 110, 24); + drawFuzzyRect(ctx, 226, 55, 24, 84); + drawCrossSectionHatch(ctx, 226, 55, 24, 84, 8); + drawLeaderArrow(ctx, 230, 97, 380, 97, "90° TORQUE TRANSLATION", 245, 90, false); + } else if (stageId === "step-5" && pIdx === 1) { + drawFuzzyRect(ctx, 180, 130, 140, 30); + drawWoodGrainHatch(ctx, 180, 130, 140, 30); + drawFuzzyRect(ctx, 210, 45, 22, 85); + drawWoodGrainHatch(ctx, 210, 45, 22, 85); + drawFuzzyRect(ctx, 268, 45, 22, 85); + drawWoodGrainHatch(ctx, 268, 45, 22, 85); + drawLeaderArrow(ctx, 232, 85, 370, 85, "CONTINUOUS 2X4 DIMENSION STUD", 245, 78, false); + } else if (stageId === "step-5" && pIdx === 3) { + drawFuzzyLine(ctx, 80, 110, 210, 110, "#ffffff", 3); + drawFuzzyRect(ctx, 210, 98, 80, 24); + drawCrossSectionHatch(ctx, 210, 98, 80, 24, 7); + drawFuzzyLine(ctx, 290, 110, 420, 110, "#ffffff", 3); + drawLeaderArrow(ctx, 250, 122, 380, 160, "REVERSE THREAD TENSION COLLAR", 280, 172, false); + } else if (stageId === "step-6" && pIdx === 1) { + drawFuzzyPoly( + ctx, + [ + { x: 230, y: 45 }, + { x: 215, y: 65 }, + { x: 245, y: 85 }, + { x: 230, y: 105 }, + { x: 215, y: 125 }, + { x: 245, y: 145 }, + { x: 230, y: 165 }, + ], + "#ffffff", + 2.2 + ); + drawFuzzyPoly( + ctx, + [ + { x: 270, y: 45 }, + { x: 285, y: 65 }, + { x: 255, y: 85 }, + { x: 270, y: 105 }, + { x: 285, y: 125 }, + { x: 255, y: 145 }, + { x: 270, y: 165 }, + ], + "#ffffff", + 2.2 + ); + drawWoodGrainHatch(ctx, 225, 50, 50, 110); + drawFuzzyLine(ctx, 250, 40, 250, 170, "rgba(255,255,255,0.3)", 1, { dashed: true }); + drawLeaderArrow(ctx, 275, 105, 410, 105, "HIGH-SPEED TURNED CHISEL CUT", 285, 98, false); + } else if (stageId === "step-6" && pIdx === 3) { + drawFuzzyLine(ctx, 100, 115, 400, 115, "#ffffff", 4); + drawFuzzyCircle(ctx, 200, 115, 8, "#ffffff", 1.8); + drawFuzzyCircle(ctx, 300, 115, 8, "#ffffff", 1.8); + drawFuzzyLine(ctx, 200, 115, 200, 60, "#ffffff", 2.5); + drawFuzzyLine(ctx, 300, 115, 300, 60, "#ffffff", 2.5); + drawCrossSectionHatch(ctx, 190, 65, 20, 50, 8); + drawCrossSectionHatch(ctx, 290, 65, 20, 50, 8); + drawLeaderArrow(ctx, 315, 85, 440, 85, 'THREADED 3/4" PIPE TEE', 320, 78, false); + } else if (stageId === "step-7" && pIdx === 1) { + drawFuzzyRect(ctx, 120, 60, 260, 110); + drawWoodGrainHatch(ctx, 120, 60, 260, 110); + drawFuzzyPoly( + ctx, + [ + { x: 120, y: 60 }, + { x: 250, y: 30 }, + { x: 380, y: 60 }, + ], + "#ffffff", + 2 + ); + drawFuzzyCircle(ctx, 250, 50, 8, "#ffffff", 2); + drawFuzzyLine(ctx, 250, 58, 250, 130, "#ffffff", 1.5); + drawLeaderArrow(ctx, 260, 80, 420, 80, "GRAVITY PULLEY TRACK DATUM", 270, 73, false); + } else if (stageId === "step-7" && pIdx === 3) { + drawFuzzyCircle(ctx, 210, 115, 35, "#ffffff", 2.2); + drawFuzzyCircle(ctx, 210, 115, 25, "#ffffff", 1.5); + drawCrossSectionHatch(ctx, 175, 80, 70, 70, 8); + drawFuzzyLine(ctx, 150, 160, 270, 160, "rgba(255,255,255,0.3)", 1); + drawFuzzyLine(ctx, 160, 170, 260, 170, "rgba(255,255,255,0.3)", 1); + drawStipplingHatch(ctx, 150, 150, 120, 25); + drawLeaderArrow(ctx, 248, 115, 410, 115, "POROUS CLAY INFILTRATION JOINT", 258, 108, false); + } else if (stageId === "step-8" && pIdx === 1) { + drawFuzzyRect(ctx, 140, 90, 220, 30); + drawWoodGrainHatch(ctx, 140, 90, 220, 30); + drawFuzzyRect(ctx, 220, 65, 28, 25); + drawCrossSectionHatch(ctx, 220, 65, 28, 25, 6); + drawFuzzyLine(ctx, 80, 77, 420, 77, "#ffffff", 2.2); + drawLeaderArrow(ctx, 250, 77, 390, 50, "CERAMIC JOIST STANDOFF CLEAT", 260, 44, false); + } else if (stageId === "step-8" && pIdx === 3) { + drawFuzzyRect(ctx, 150, 65, 26, 95); + drawCrossSectionHatch(ctx, 150, 65, 26, 95, 8); + drawFuzzyRect(ctx, 190, 65, 26, 95); + drawCrossSectionHatch(ctx, 190, 65, 26, 95, 8); + drawFuzzyRect(ctx, 230, 65, 26, 95); + drawCrossSectionHatch(ctx, 230, 65, 26, 95, 8); + drawFuzzyRect(ctx, 270, 65, 26, 95); + drawCrossSectionHatch(ctx, 270, 65, 26, 95, 8); + drawFuzzyLine(ctx, 145, 145, 305, 145, "#ffffff", 4); + drawLeaderArrow(ctx, 300, 110, 430, 110, "HIGH THERMAL MASS COLUMN", 310, 103, false); + } else if (stageId === "step-9" && pIdx === 1) { + drawFuzzyPoly( + ctx, + [ + { x: 100, y: 130 }, + { x: 250, y: 165 }, + { x: 400, y: 130 }, + ], + "#ffffff", + 2.5 + ); + drawFuzzyLine(ctx, 100, 130, 400, 130, "rgba(255,255,255,0.3)", 1, { dashed: true }); + drawWoodGrainHatch(ctx, 140, 130, 220, 30); + drawLeaderArrow(ctx, 250, 130, 250, 147, "4.8 INCH RIDGE SAG DEFLECTION", 265, 142, false); + } else if (stageId === "step-9" && pIdx === 3) { + drawFuzzyRect(ctx, 120, 60, 220, 100); + drawMasonryHatch(ctx, 120, 60, 220, 100); + drawFuzzyPoly( + ctx, + [ + { x: 340, y: 80 }, + { x: 310, y: 110 }, + { x: 325, y: 155 }, + ], + "#ffffff", + 2.5 + ); + drawStipplingHatch(ctx, 310, 90, 40, 60); + drawLeaderArrow(ctx, 330, 115, 440, 115, "BIOLOGICAL WEDGING FAILURE", 335, 108, false); + } else if (stageId === "step-10" && pIdx === 1) { + drawFuzzyRect(ctx, 225, 45, 50, 120); + drawFuzzyLine(ctx, 225, 75, 275, 85, "#ffffff", 2); + drawFuzzyLine(ctx, 225, 95, 275, 105, "#ffffff", 2); + drawFuzzyLine(ctx, 225, 115, 275, 125, "#ffffff", 2); + drawFuzzyRect(ctx, 210, 85, 80, 16); + drawCrossSectionHatch(ctx, 225, 45, 50, 120, 10); + drawLeaderArrow(ctx, 290, 93, 420, 93, "ADJUSTABLE ACME THREAD COLLAR", 295, 86, false); + } else if (stageId === "step-10" && pIdx === 3) { + drawFuzzyPoly( + ctx, + [ + { x: 180, y: 140 }, + { x: 320, y: 140 }, + { x: 350, y: 175 }, + { x: 150, y: 175 }, + ], + "#ffffff", + 2, + true + ); + drawStipplingHatch(ctx, 160, 145, 180, 25); + drawFuzzyRect(ctx, 235, 65, 30, 75); + drawCrossSectionHatch(ctx, 235, 65, 30, 75, 8); + drawLeaderArrow(ctx, 335, 158, 450, 158, "POURED LOAD SPREADER DATUM", 340, 151, false); + } else if (stageId === "step-11" && pIdx === 1) { + drawFuzzyRect(ctx, 160, 110, 180, 28); + drawWoodGrainHatch(ctx, 160, 110, 180, 28); + drawFuzzyRect(ctx, 220, 145, 60, 35); + drawCrossSectionHatch(ctx, 220, 145, 60, 35, 8); + drawFuzzyLine(ctx, 250, 145, 250, 138, "#ffffff", 3); + drawLeaderArrow(ctx, 340, 124, 450, 124, "PRECISION 12-TON LIFT POINT", 345, 117, false); + } else if (stageId === "step-11" && pIdx === 3) { + drawFuzzyRect(ctx, 180, 80, 140, 44); + drawWoodGrainHatch(ctx, 180, 80, 140, 44); + drawFuzzyPoly( + ctx, + [ + { x: 180, y: 80 }, + { x: 210, y: 80 }, + { x: 210, y: 114 }, + { x: 180, y: 114 }, + ], + "#ffffff", + 2.5 + ); + drawCrossSectionHatch(ctx, 180, 80, 30, 34, 6); + drawFuzzyPoly( + ctx, + [ + { x: 320, y: 80 }, + { x: 290, y: 80 }, + { x: 290, y: 114 }, + { x: 320, y: 114 }, + ], + "#ffffff", + 2.5 + ); + drawCrossSectionHatch(ctx, 290, 80, 30, 34, 6); + drawLeaderArrow(ctx, 320, 102, 440, 102, "STRUCTURAL STEEL C-CHANNEL", 325, 95, false); + } else if (stageId === "step-12" && pIdx === 1) { + drawFuzzyRect(ctx, 140, 120, 220, 25); + drawStipplingHatch(ctx, 140, 120, 220, 25); + drawFuzzyRect(ctx, 210, 145, 80, 35); + drawStipplingHatch(ctx, 210, 145, 80, 35, 20); + drawFuzzyRect(ctx, 240, 60, 20, 85); + drawCrossSectionHatch(ctx, 240, 60, 20, 85, 7); + drawLeaderArrow(ctx, 260, 90, 410, 90, '4" PVC ACTIVE SUCTION RISER', 265, 83, false); + } else if (stageId === "step-12" && pIdx === 3) { + drawFuzzyCircle(ctx, 210, 120, 30, "#ffffff", 2.2); + drawCrossSectionHatch(ctx, 180, 90, 60, 60, 8); + drawFuzzyCircle(ctx, 210, 142, 4, "#ffffff", 1.2); + drawFuzzyCircle(ctx, 192, 135, 4, "#ffffff", 1.2); + drawFuzzyCircle(ctx, 228, 135, 4, "#ffffff", 1.2); + drawFuzzyRect(ctx, 160, 75, 100, 90, "rgba(255,255,255,0.4)", 1.2); + drawStipplingHatch(ctx, 160, 75, 100, 90, 45); + drawLeaderArrow(ctx, 240, 120, 410, 120, "GEOTEXTILE FABRIC GRAVEL ENCASEMENT", 248, 113, false); + } else if (stageId === "step-13" && pIdx === 1) { + drawFuzzyRect(ctx, 160, 60, 180, 100); + drawFoamSquiggles(ctx, 160, 60, 180, 100); + drawFuzzyPoly( + ctx, + [ + { x: 160, y: 75 }, + { x: 220, y: 75 }, + { x: 280, y: 75 }, + { x: 340, y: 75 }, + ], + "rgba(255,255,255,0.4)", + 2 + ); + drawFuzzyPoly( + ctx, + [ + { x: 160, y: 115 }, + { x: 220, y: 115 }, + { x: 280, y: 115 }, + { x: 340, y: 115 }, + ], + "rgba(255,255,255,0.4)", + 2 + ); + drawLeaderArrow(ctx, 340, 105, 450, 105, "R-45 CONTINUOUS FOAM MATRIX", 345, 98, false); + } else if (stageId === "step-13" && pIdx === 3) { + drawFuzzyRect(ctx, 220, 50, 60, 130); + drawStipplingHatch(ctx, 220, 50, 60, 130, 25); + drawFuzzyLine(ctx, 242, 50, 242, 165, "#ffffff", 2); + drawFuzzyLine(ctx, 258, 50, 258, 165, "#ffffff", 2); + drawFuzzyPoly( + ctx, + [ + { x: 242, y: 165 }, + { x: 250, y: 175 }, + { x: 258, y: 165 }, + ], + "#ffffff", + 2 + ); + drawLeaderArrow(ctx, 280, 110, 430, 110, "400 FT VERTICAL CLOSED LOOP", 285, 103, false); + } else if (stageId === "step-14" && pIdx === 1) { + drawFuzzyRect(ctx, 120, 60, 40, 110); + drawWoodGrainHatch(ctx, 120, 60, 40, 110); + drawFuzzyRect(ctx, 160, 60, 200, 36); + drawWoodGrainHatch(ctx, 160, 60, 200, 36); + drawLeaderArrow(ctx, 360, 78, 460, 78, "PRESERVED 1780 HEWN TIMBER", 365, 71, false); + } else if (stageId === "step-14" && pIdx === 3) { + drawFuzzyRect(ctx, 170, 70, 160, 95); + drawCrossSectionHatch(ctx, 170, 70, 160, 95, 12); + drawFuzzyRect(ctx, 220, 55, 60, 15); + drawFuzzyPoly( + ctx, + [ + { x: 250, y: 90 }, + { x: 240, y: 115 }, + { x: 255, y: 115 }, + { x: 245, y: 140 }, + ], + "#ffffff", + 2.5 + ); + drawLeaderArrow(ctx, 330, 115, 450, 115, "MICROGRID SOLID-STATE DATUM", 335, 108, false); + } else if (stageId === "step-15" && pIdx === 1) { + drawFuzzyRect(ctx, 200, 55, 100, 30); + drawStipplingHatch(ctx, 200, 55, 100, 30, 15); + drawFuzzyPoly( + ctx, + [ + { x: 170, y: 85 }, + { x: 250, y: 120 }, + { x: 330, y: 85 }, + ], + "#ffffff", + 1.5 + ); + drawFuzzyPoly( + ctx, + [ + { x: 140, y: 85 }, + { x: 250, y: 155 }, + { x: 360, y: 85 }, + ], + "#ffffff", + 1.5 + ); + drawFuzzyPoly( + ctx, + [ + { x: 110, y: 85 }, + { x: 250, y: 190 }, + { x: 390, y: 85 }, + ], + "#ffffff", + 1.5 + ); + drawStipplingHatch(ctx, 120, 85, 260, 100, 40); + drawLeaderArrow(ctx, 330, 130, 440, 130, "ISOBAR STRESS DISSIPATION", 335, 123, false); + } else if (stageId === "step-15" && pIdx === 3) { + drawFuzzyRect(ctx, 220, 60, 35, 110); + drawMasonryHatch(ctx, 220, 60, 35, 110); + drawFuzzyLine(ctx, 140, 85, 220, 85, "#ffffff", 2); + drawFuzzyPoly( + ctx, + [ + { x: 220, y: 85 }, + { x: 210, y: 80 }, + { x: 210, y: 90 }, + ], + "#ffffff", + 1.5, + true + ); + drawFuzzyLine(ctx, 160, 120, 220, 120, "#ffffff", 2); + drawFuzzyPoly( + ctx, + [ + { x: 220, y: 120 }, + { x: 210, y: 115 }, + { x: 210, y: 125 }, + ], + "#ffffff", + 1.5, + true + ); + drawStipplingHatch(ctx, 130, 70, 85, 70, 30); + drawLeaderArrow(ctx, 255, 100, 410, 100, "LATERAL HYDROSTATIC THRUST", 260, 93, false); + } else if (stageId === "step-16" && pIdx === 1) { + drawFuzzyRect(ctx, 140, 60, 90, 110); + drawFoamSquiggles(ctx, 140, 60, 90, 110); + drawFuzzyRect(ctx, 230, 60, 130, 110); + drawCrossSectionHatch(ctx, 230, 60, 130, 110, 10); + drawFuzzyLine(ctx, 230, 50, 230, 180, "#D12B3E", 2.5, { dashed: true }); + drawDraftingText(ctx, "DEW-POINT ISOTHERM DATUM", 245, 165, "#D12B3E", 11); + } else if (stageId === "step-16" && pIdx === 3) { + drawFuzzyRect(ctx, 150, 70, 200, 85); + drawCrossSectionHatch(ctx, 150, 70, 200, 85, 7); + drawFuzzyLine(ctx, 120, 95, 150, 95, "#ffffff", 1.8); + drawFuzzyLine(ctx, 120, 130, 150, 130, "#ffffff", 1.8); + drawLeaderArrow(ctx, 350, 112, 460, 112, "ZERO THERMAL BRIDGING PROFILE", 355, 105, false); + } else if (stageId === "step-17" && pIdx === 1) { + drawFuzzyRect(ctx, 220, 80, 60, 90); + drawWoodGrainHatch(ctx, 220, 80, 60, 90); + drawFuzzyLine(ctx, 250, 35, 250, 80, "#ffffff", 2.5); + drawFuzzyPoly( + ctx, + [ + { x: 250, y: 80 }, + { x: 244, y: 70 }, + { x: 256, y: 70 }, + ], + "#ffffff", + 1.5, + true + ); + drawLeaderArrow(ctx, 280, 120, 430, 120, "32 PSF DEAD + 40 PSF LIVE LOAD", 285, 113, false); + } else if (stageId === "step-17" && pIdx === 3) { + drawFuzzyPoly( + ctx, + [ + { x: 170, y: 165 }, + { x: 290, y: 165 }, + { x: 310, y: 65 }, + { x: 190, y: 65 }, + ], + "#ffffff", + 2, + true + ); + drawCrossSectionHatch(ctx, 170, 65, 140, 100, 12); + drawFuzzyLine(ctx, 120, 85, 190, 85, "#ffffff", 2.5); + drawFuzzyPoly( + ctx, + [ + { x: 190, y: 85 }, + { x: 180, y: 79 }, + { x: 180, y: 91 }, + ], + "#ffffff", + 1.5, + true + ); + drawLeaderArrow(ctx, 310, 115, 440, 115, "MAX STORY DRIFT INDEX L/360", 315, 108, false); + } + + try { + const dataUrl = canvas.toDataURL("image/png"); + INLINE_ILLUSTRATION_CACHE[cacheKey] = dataUrl; + return dataUrl; + } catch (e) { + return ""; + } +} +function renderStageSections() { + const container = document.getElementById("scrolly-container"); + if (!container) + return; + + container.innerHTML = ""; + STAGES.forEach((stage, idx) => { + const stageEl = document.createElement("section"); + const isOdd = idx % 2 === 0; // 0-indexed: 0 is Stage 1 (odd -> text left), 1 is Stage 2 (even -> text right) + stageEl.setAttribute("class", `stage-section ${isOdd ? "layout-text-left" : "layout-text-right"}`); + stageEl.setAttribute("id", `stage-section-${idx}`); + stageEl.setAttribute("data-stage-index", String(idx)); + + const specsHtml = stage.specs + .map( + (spec) => ` +
    + ${spec.label} + ${spec.value} +
    ` + ) + .join(""); + + const descHtml = Array.isArray(stage.paragraphs) + ? stage.paragraphs + .map((p, pIdx) => { + const illTitles = INLINE_ILLUSTRATION_TITLES[stage.id] || []; + let illPrefix = ""; + if (pIdx === 1 && illTitles[0]) { + const dataUrl = getOrGenerateInlineIllustrationDataURL(stage.id, 1, illTitles[0]); + if (dataUrl) + illPrefix = `
    ${illTitles[0]}
    `; + } else if (pIdx === 3 && illTitles[1]) { + const dataUrl = getOrGenerateInlineIllustrationDataURL(stage.id, 3, illTitles[1]); + if (dataUrl) + illPrefix = `
    ${illTitles[1]}
    `; + } + + return `${illPrefix} +

    ${p}

    `; + }) + .join("") + : `

    ${stage.description}

    `; + + const narrativeHtml = ` +
    +
    + Stage ${idx + 1} // ${stage.year} +

    ${stage.title}: ${stage.subtitle}

    +
    ${descHtml}
    +
    ${specsHtml}
    +
    +
    + `; + + const graphicHtml = ` +
    +
    + + +
    Stage ${idx + 1}: ${stage.title}
    +
    +
    + `; + + stageEl.innerHTML = narrativeHtml + graphicHtml; + container.appendChild(stageEl); + + const pItems = stageEl.querySelectorAll(".step-paragraph-item"); + pItems.forEach((item) => { + item.addEventListener("click", () => { + item.scrollIntoView({ behavior: "smooth", block: "center" }); + }); + }); + }); +} + +function renderFloatingTOC() { + const tocList = document.getElementById("toc-list"); + if (!tocList) + return; + + tocList.innerHTML = `
    ${STAGES.map( + (stage, idx) => ` +
  • + +
  • + ` + ).join("")}`; + + const buttons = tocList.querySelectorAll(".toc-item"); + buttons.forEach((btn, idx) => { + btn.addEventListener("click", () => { + if (typeof window.stepTo === "function") + window.stepTo(idx); + }); + }); + + const toggleBtn = document.getElementById("toc-toggle-btn"); + const floatingToc = document.getElementById("floating-toc"); + const toggleSymbol = document.getElementById("toc-toggle-symbol"); + if (toggleBtn && floatingToc && !toggleBtn.dataset.listenerAttached) { + toggleBtn.dataset.listenerAttached = "true"; + toggleBtn.addEventListener("click", () => { + const isCollapsed = floatingToc.classList.toggle("is-collapsed"); + toggleBtn.setAttribute("aria-expanded", String(!isCollapsed)); + if (toggleSymbol) + toggleSymbol.textContent = isCollapsed ? "[ + ]" : "[ - ]"; + + if (!isCollapsed && typeof window.forceScrollytellingUpdate === "function") + window.forceScrollytellingUpdate(); + }); + } +} + +function renderBottomTimeline() { + const timelinePoints = document.getElementById("timeline-points"); + if (timelinePoints) { + timelinePoints.innerHTML = ""; + STAGES.forEach((stage, idx) => { + const li = document.createElement("li"); + const btn = document.createElement("button"); + btn.className = "timeline-point"; + btn.type = "button"; + btn.textContent = `${stage.year}`; + btn.setAttribute("aria-label", `Jump to Stage ${idx + 1}: ${stage.year}`); + btn.addEventListener("click", () => { + const targetEl = document.getElementById(`stage-section-${idx}`); + if (targetEl) + targetEl.scrollIntoView({ behavior: "smooth" }); + }); + li.appendChild(btn); + timelinePoints.appendChild(li); + }); + } +} + +window.forceScrollytellingUpdate = function () { + const floatingToc = document.getElementById("floating-toc"); + if (floatingToc) { + const scrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop || 0; + const header = document.querySelector(".page-header"); + const threshold = header ? header.offsetHeight - 50 : 300; + const shouldShowTOC = scrollY > threshold; + if (shouldShowTOC) { + if (floatingToc.classList.contains("is-hidden")) { + floatingToc.classList.remove("is-hidden"); + floatingToc.classList.add("is-visible"); + } + } else { + if (!floatingToc.classList.contains("is-hidden")) { + floatingToc.classList.add("is-hidden"); + floatingToc.classList.remove("is-visible"); + } + } + } + + const sections = Array.from(document.querySelectorAll(".stage-section")); + if (sections.length === 0) + return; + + const viewportCenter = window.innerHeight * 0.5; + let activeIndex = -1; + let activeProgress = 0.0; + + // Phase 1: Check if any section straddles the viewport center + for (let idx = 0; idx < sections.length; idx++) { + const secEl = sections[idx]; + const rect = secEl.getBoundingClientRect(); + if (rect.top <= viewportCenter && rect.bottom > viewportCenter) { + activeIndex = idx; + if (rect.height > 0) { + if (rect.top >= 0) { + activeProgress = 0.0; + } else { + const prog = -rect.top / Math.max(1, rect.height - window.innerHeight * 0.5); + activeProgress = Math.max(0.0, Math.min(1.0, prog)); + } + } + break; + } + } + + // Phase 2: If in a gap or out of bounds, find the section with the closest center + if (activeIndex === -1) { + let minDistance = Infinity; + for (let idx = 0; idx < sections.length; idx++) { + const secEl = sections[idx]; + const rect = secEl.getBoundingClientRect(); + const secCenter = rect.top + rect.height * 0.5; + const dist = Math.abs(secCenter - viewportCenter); + if (dist < minDistance) { + minDistance = dist; + activeIndex = idx; + if (secCenter < viewportCenter) + activeProgress = 1.0; + else + activeProgress = 0.0; + } + } + } + + const steps = Array.from(document.querySelectorAll(".step")); + steps.forEach((stepEl, idx) => { + if (idx === activeIndex) { + if (!stepEl.classList.contains("is-active")) + stepEl.classList.add("is-active"); + } else { + if (stepEl.classList.contains("is-active")) + stepEl.classList.remove("is-active"); + } + }); + + const stageLabel = document.getElementById("active-stage-label"); + if (stageLabel && STAGES[activeIndex]) + stageLabel.textContent = `STAGE ${activeIndex + 1}: ${STAGES[activeIndex].title.toUpperCase()}`; + + const tocItems = document.querySelectorAll(".toc-item"); + const indicator = document.getElementById("toc-active-indicator"); + tocItems.forEach((item, idx) => { + if (idx === activeIndex) { + if (!item.classList.contains("is-active")) + item.classList.add("is-active"); + if (indicator) { + const wrapper = item.closest(".toc-item-wrapper") || item.parentElement || item; + const offsetTop = wrapper.offsetTop; + const height = wrapper.offsetHeight; + indicator.style.transform = `translateY(${offsetTop}px)`; + indicator.style.height = `${height}px`; + } + } else { + if (item.classList.contains("is-active")) + item.classList.remove("is-active"); + } + }); + + const bottomTimeline = document.getElementById("bottom-timeline-overlay"); + if (bottomTimeline) { + if (bottomTimeline.classList.contains("is-hidden")) { + bottomTimeline.classList.remove("is-hidden"); + bottomTimeline.classList.add("is-visible"); + } + const points = bottomTimeline.querySelectorAll(".timeline-point"); + points.forEach((pt, idx) => { + if (idx === activeIndex) { + if (!pt.classList.contains("is-active")) + pt.classList.add("is-active"); + } else { + if (pt.classList.contains("is-active")) + pt.classList.remove("is-active"); + } + }); + } + + updateGraphics(activeIndex, activeProgress); +}; + +window.scrollToStep = function (index) { + const sections = document.querySelectorAll(".stage-section"); + if (index >= 0 && index < sections.length) { + const secEl = sections[index]; + secEl.scrollIntoView({ behavior: "instant", block: "start" }); + if (typeof window.forceScrollytellingUpdate === "function") + window.forceScrollytellingUpdate(); + } +}; + +for (let i = 0; i < STAGES.length; i++) { + window[`stepTo${i}`] = function () { + window.scrollToStep(i); + }; +} + +window.stepTo = function (index) { + window.scrollToStep(index); +}; + +window._scrubProgress = 0.0; + +window.resetScrub = function () { + window._scrubProgress = 0.0; + window.scrollTo({ top: 0, behavior: "instant" }); + if (typeof window.forceScrollytellingUpdate === "function") + window.forceScrollytellingUpdate(); +}; + +window.scrubNext = function () { + window._scrubProgress = Math.min(1.0, window._scrubProgress + 1.0 / 30.0); + const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); + window.scrollTo({ top: maxScroll * window._scrubProgress, behavior: "instant" }); + if (typeof window.forceScrollytellingUpdate === "function") + window.forceScrollytellingUpdate(); +}; + +window.scrubPrev = function () { + window._scrubProgress = Math.max(0.0, window._scrubProgress - 1.0 / 30.0); + const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); + window.scrollTo({ top: maxScroll * window._scrubProgress, behavior: "instant" }); + if (typeof window.forceScrollytellingUpdate === "function") + window.forceScrollytellingUpdate(); +}; + +window.scrubForward = window.scrubNext; +window.scrubBackward = window.scrubPrev; + +function initApp() { + renderStageSections(); + renderFloatingTOC(); + renderBottomTimeline(); + initGraphics(); + + initScrollamaEngine(updateGraphics); + + const handleScroll = () => { + if (typeof window.forceScrollytellingUpdate === "function") + window.forceScrollytellingUpdate(); + }; + window.addEventListener("scroll", handleScroll, { passive: true }); + window.addEventListener("resize", handleScroll, { passive: true }); + + if (typeof window.forceScrollytellingUpdate === "function") + window.forceScrollytellingUpdate(); +} + +if (document.readyState === "loading") + document.addEventListener("DOMContentLoaded", initApp); +else + initApp(); diff --git a/suites-experimental/scrollytelling/src/styles.css b/suites-experimental/scrollytelling/src/styles.css new file mode 100644 index 000000000..6b85a55d5 --- /dev/null +++ b/suites-experimental/scrollytelling/src/styles.css @@ -0,0 +1,965 @@ +/* + * Copyright (C) 2024-2026 Speedometer Contributors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + */ + +/** + * 1950s Black & White Blueprint / Planning Design + * Reference: STYLE_CONFIG in src/graphics.js + * + * Aesthetic Rules: + * - Pure black drafting background (#000000) with crisp B&W geometry. + * - Negative blueprint drafting theme (light typography & wireframes on dark void). + * - Slanted container borders (-2.0deg to +2.0deg) and independent drop shadow angles (Shadow Angle != Box Angle). + * - Gradual organic watercolor animation reveals for accent coloring during scroll. + */ + +*, +*::before, +*::after { + transition: none !important; + animation: none !important; +} + +:root { + --bg-color: #000000; + --paper-light: rgba(0, 0, 0, 0.65); + --paper-card: rgba(0, 0, 0, 0.85); + --ink-primary: #ffffff; + --ink-secondary: #cccccc; + --ink-muted: #777777; + --accent-crimson: #d12b3e; + --accent-ruby: #c72436; + --accent-dark: #ffffff; + --accent-light: #cccccc; + --border-color: #ffffff; + --shadow-solid: rgba(0, 0, 0, 0.75); + --text-color: var(--ink-primary); + --text-muted: var(--ink-muted); + --card-bg: var(--paper-light); + --card-active-bg: var(--paper-card); + --accent-color: var(--accent-crimson); + --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); + --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: var(--bg-color); + color: var(--ink-primary); + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 1rem; + line-height: 1.6; + overflow-x: hidden; + position: relative; + z-index: 0; +} + +body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background-color: var(--bg-color, #000000); + background-image: url("public/planning_paper_dot_grid.webp"); + background-repeat: repeat; + background-size: 256px 256px; + pointer-events: none; +} + +#app { + max-width: 1400px; + margin: 0 auto; + padding: 0 2rem; + position: relative; +} + +/* 1950s Construction Paper Header Box with Slanted Geometry & Independent Shadow */ +.page-header { + position: relative; + z-index: 1; + padding: 4.5rem 2rem 2.5rem; + background-color: var(--paper-card); + border: 4px solid var(--accent-crimson, #d12b3e); + border-radius: 10px; + margin-top: 2.5rem; + margin-bottom: 1rem; + text-align: center; + transform: rotate(-0.6deg); + overflow: visible; +} + +.page-header::before { + content: ""; + position: absolute; + z-index: -1; + top: 14px; + left: 10px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 10px; + transform: rotate(1.3deg); +} + +.page-header:hover { + transform: rotate(0deg); +} + +.page-header:hover::before { + top: 16px; + left: 14px; + transform: rotate(-0.9deg); +} + +.page-header h1 { + font-family: "Impact", "Arial Black", "Futura", "Helvetica Neue", system-ui, -apple-system, sans-serif; + font-size: 3.5rem; + font-weight: 900; + letter-spacing: -0.02em; + text-transform: uppercase; + margin-bottom: 0.75rem; + color: var(--ink-primary); +} + +.subtitle { + position: relative; + z-index: 1; + font-size: 1rem; + color: var(--ink-primary, #ffffff); + background-color: var(--paper-card, rgba(0, 0, 0, 0.85)); + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + margin-bottom: 1.25rem; + display: inline-block; + padding: 0.4rem 1.2rem; + border: 3px solid var(--border-color, #ffffff); + border-radius: 6px; + transform: rotate(1deg); + overflow: visible; +} + +.subtitle::after { + content: ""; + position: absolute; + z-index: -1; + top: 6px; + left: -6px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 6px; + transform: rotate(-1.7deg); +} + +.header-status { + display: flex; + justify-content: center; + gap: 1.5rem; + flex-wrap: wrap; + margin-top: 1.25rem; +} + +.status-tag { + position: relative; + z-index: 1; + font-family: "Impact", "Arial Black", "Futura", "Helvetica Neue", system-ui, -apple-system, sans-serif; + font-size: 1rem; + font-weight: 900; + padding: 0.5rem 1.4rem; + border: 2px solid #ffffff; + background-color: var(--accent-crimson, #d12b3e); + color: #ffffff; + border-radius: 6px; + letter-spacing: 0.1em; + text-transform: uppercase; + transform: rotate(-1.5deg); + display: inline-block; + overflow: visible; +} + +.status-tag::after { + content: ""; + position: absolute; + z-index: -1; + top: 8px; + left: 10px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid, rgba(0, 0, 0, 0.75)); + border-radius: 6px; + transform: rotate(2.3deg); +} + +/* Floating Table of Contents (1950s Slanted Index Banner with Independent Shadow) */ +.floating-toc { + position: fixed; + top: 2.5rem; + right: 2.5rem; + z-index: 1000; + background-color: var(--paper-card); + border: 3px solid var(--border-color); + border-radius: 8px; + padding: 1rem; + width: 260px; + transform: rotate(1.2deg); + overflow: visible; +} + +.floating-toc::after { + content: ""; + position: absolute; + z-index: -1; + top: 10px; + left: 6px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 8px; + transform: rotate(-1.1deg); +} + +.floating-toc:hover { + transform: rotate(0deg) scale(1.02); +} + +.floating-toc:hover::after { + top: 12px; + left: 10px; + transform: rotate(-1.6deg); +} + +/* Visibility Toggling for Floating TOC (Zero Transitions / Instant Synchronous Toggle) */ +.floating-toc.is-hidden { + display: none !important; +} +.floating-toc.is-visible { + display: block !important; +} + +/* Table of Contents Interactive Toggle & Collapsed State (1950s Blueprint Style, 0 Transitions) */ +.toc-toggle-btn { + background: transparent; + border: none; + color: inherit; + width: 100%; + cursor: pointer; + padding: 0; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + gap: 0.4rem; +} +.toc-toggle-symbol { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 1rem; + font-weight: 700; +} + +.floating-toc.is-collapsed { + width: 160px; + padding: 0; +} +.floating-toc.is-collapsed .toc-header { + margin: 0; + border-bottom: none; + padding: 0.65rem 1rem; + border-radius: 8px; +} +.floating-toc.is-collapsed .toc-list, +.floating-toc.is-collapsed .toc-active-indicator { + display: none !important; +} + +/* Independent Drop Shadow Angle for Collapsed State (Rule 3: Collage/Diorama Rule) */ +.floating-toc.is-collapsed::after { + top: 6px; + left: 5px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 8px; + transform: rotate(-1.5deg); +} +.floating-toc.is-collapsed:hover::after { + top: 8px; + left: 7px; + transform: rotate(-1.8deg); +} + +.toc-header { + border-bottom: 3px solid var(--accent-crimson, #d12b3e); + text-align: center; + background-color: var(--paper-card, rgba(0, 0, 0, 0.85)); + color: var(--ink-primary, #ffffff); + margin: -1rem -1rem 0.75rem -1rem; + padding: 0.65rem 1rem 0.55rem; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.toc-title { + font-family: "Impact", "Arial Black", "Futura", "Helvetica Neue", system-ui, -apple-system, sans-serif; + font-size: 1rem; + font-weight: 900; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.toc-list { + list-style: none; + position: relative; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.3rem; + z-index: 10; +} + +.toc-item-wrapper { + position: relative; + z-index: 10; +} + +.toc-item { + width: 100%; + background: transparent; + border: 2px solid transparent; + border-radius: 4px; + color: var(--ink-secondary); + text-align: left; + padding: 0.5rem 0.6rem; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + opacity: 1 !important; +} + +.toc-item.is-active { + background: var(--accent-crimson, #d12b3e) !important; + color: #ffffff !important; + border-color: #ffffff !important; +} + +.toc-num { + color: var(--accent-crimson, #d12b3e); + font-weight: 900; + letter-spacing: 0.05em; + opacity: 1 !important; +} + +.toc-item.is-active .toc-num { + color: #ffffff !important; +} + +.toc-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-transform: uppercase; +} + +.toc-active-indicator { + display: none !important; +} + +.scrolly-container { + display: flex; + flex-direction: column; + gap: 0; + position: relative; +} + +/* Stage Layout & Asymmetric Composition */ +.stage-section { + display: flex; + position: relative; + gap: 4.5rem; + align-items: flex-start; + justify-content: space-between; + min-height: min(650px, 80vh); + padding-top: 3rem; + padding-bottom: 3rem; +} + +.stage-section.layout-text-left { + flex-direction: row; +} + +.stage-section.layout-text-right { + flex-direction: row-reverse; +} + +.stage-graphic-column { + flex: 1 1 55%; + position: sticky; + top: 0; + max-height: 100vh; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + z-index: 10; +} + +.stage-narrative-column { + flex: 1 1 45%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; + padding-top: 0; + padding-bottom: 0; + z-index: 5; +} + +/* Irregular Slanted Tilt Angles & Independent Drop Shadow Offsets/Angles per Stage */ +#stage-section-0 { + --step-tilt: -1.4deg; + --step-shadow-tilt: 0.9deg; + --step-shadow-x: 12px; + --step-shadow-y: 8px; + --graphic-tilt: 1.2deg; + --graphic-shadow-tilt: -1.6deg; + --graphic-shadow-x: 16px; + --graphic-shadow-y: 10px; +} +#stage-section-1 { + --step-tilt: 1.6deg; + --step-shadow-tilt: -1.1deg; + --step-shadow-x: -10px; + --step-shadow-y: 14px; + --graphic-tilt: -1.4deg; + --graphic-shadow-tilt: 1.3deg; + --graphic-shadow-x: -12px; + --graphic-shadow-y: 16px; +} +#stage-section-2 { + --step-tilt: -1.1deg; + --step-shadow-tilt: 1.5deg; + --step-shadow-x: 14px; + --step-shadow-y: -6px; + --graphic-tilt: 1.5deg; + --graphic-shadow-tilt: -0.9deg; + --graphic-shadow-x: 10px; + --graphic-shadow-y: 18px; +} +#stage-section-3 { + --step-tilt: 1.3deg; + --step-shadow-tilt: -1.4deg; + --step-shadow-x: 8px; + --step-shadow-y: 16px; + --graphic-tilt: -1.1deg; + --graphic-shadow-tilt: 1.7deg; + --graphic-shadow-x: -14px; + --graphic-shadow-y: 12px; +} +#stage-section-4 { + --step-tilt: -1.5deg; + --step-shadow-tilt: 1.2deg; + --step-shadow-x: -12px; + --step-shadow-y: 10px; + --graphic-tilt: 1.3deg; + --graphic-shadow-tilt: -1.8deg; + --graphic-shadow-x: 18px; + --graphic-shadow-y: 8px; +} +#stage-section-5 { + --step-tilt: 1.2deg; + --step-shadow-tilt: -1.5deg; + --step-shadow-x: 16px; + --step-shadow-y: 14px; + --graphic-tilt: -1.5deg; + --graphic-shadow-tilt: 1.1deg; + --graphic-shadow-x: -10px; + --graphic-shadow-y: -8px; +} +#stage-section-6 { + --step-tilt: -1.3deg; + --step-shadow-tilt: 1.6deg; + --step-shadow-x: 10px; + --step-shadow-y: 18px; + --graphic-tilt: 1.4deg; + --graphic-shadow-tilt: -1.2deg; + --graphic-shadow-x: 14px; + --graphic-shadow-y: 14px; +} +#stage-section-7 { + --step-tilt: 1.5deg; + --step-shadow-tilt: -1.1deg; + --step-shadow-x: -14px; + --step-shadow-y: 12px; + --graphic-tilt: -1.2deg; + --graphic-shadow-tilt: 1.5deg; + --graphic-shadow-x: -16px; + --graphic-shadow-y: 16px; +} + +/* Graphic Sticky Wrapper (1950s Architectural Drawing Frame with Independent Shadow) */ +.graphic-sticky-wrapper { + position: relative; + z-index: 1; + width: 100%; + aspect-ratio: 4 / 3; + border: 4px solid var(--border-color); + border-radius: 10px; + background-color: transparent; + overflow: visible; + display: flex; + align-items: center; + justify-content: center; + transform: rotateZ(var(--graphic-tilt, 0deg)); +} + +.graphic-sticky-wrapper::after { + content: ""; + position: absolute; + z-index: -1; + top: var(--graphic-shadow-y, 14px); + left: var(--graphic-shadow-x, 14px); + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 10px; + transform: rotateZ(var(--graphic-shadow-tilt, -1.5deg)); +} + +.graphic-canvas, +.graphic-svg, +#graphic-canvas, +#graphic-svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + clip-path: inset(0px round 5px); +} + +.graphic-canvas, +#graphic-canvas { + z-index: 1; +} + +.graphic-svg, +#graphic-svg { + z-index: 2; + pointer-events: none; +} + +/* Clean Blueprint Dot Grid / Watercolor Wash Overlay */ +.graphic-texture-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: radial-gradient(circle at center, rgba(255, 255, 255, 0.12), transparent 70%); + background-size: 100% 100%; + background-position: center; + mix-blend-mode: screen; + opacity: 0.35; + border-radius: 5px; + z-index: 2; + pointer-events: none; + clip-path: inset(0px round 5px); +} + +/* 1950s Graphic Callout Badge Caption with Independent Shadow */ +.caption { + position: absolute; + z-index: 5; + bottom: 1.25rem; + left: 1.25rem; + right: 1.25rem; + background-color: var(--paper-card, rgba(0, 0, 0, 0.85)); + color: var(--ink-primary, #ffffff); + border: 3px solid var(--accent-crimson, #d12b3e); + border-radius: 6px; + padding: 0.65rem 1.25rem; + font-family: "Impact", "Arial Black", "Futura", "Helvetica Neue", system-ui, -apple-system, sans-serif; + font-size: 0.95rem; + font-weight: 900; + text-align: center; + letter-spacing: 0.08em; + text-transform: uppercase; + transform: rotate(-0.8deg); + overflow: visible; +} + +.caption::after { + content: ""; + position: absolute; + z-index: -1; + top: 5px; + left: 6px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 6px; + transform: rotate(1.6deg); +} + +.caption-title { + color: var(--ink-secondary, #cccccc); + font-weight: 700; +} + +.caption-text { + color: var(--ink-primary, #ffffff); + font-weight: 900; +} + +/* Narrative Cards (.step) on Construction Paper with Independent Shadow - Upright & Transparent */ +article, +.step { + position: relative; + z-index: 1; + background-color: transparent !important; + border: 3px solid var(--ink-secondary, #cccccc); + border-radius: 8px; + padding: 1.35rem 1.75rem; + opacity: 1 !important; + transform: none !important; + overflow: visible; +} + +article.is-active, +.step.is-active { + background-color: transparent !important; + transform: none !important; +} + +.step::after { + content: ""; + position: absolute; + z-index: -1; + top: var(--step-shadow-y, 6px); + left: var(--step-shadow-x, 6px); + width: 100%; + height: 100%; + background-color: var(--shadow-solid, rgba(0, 0, 0, 0.75)); + border-radius: 8px; + opacity: 1; + transform: rotateZ(var(--step-shadow-tilt, 1deg)); +} + +.step-meta { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 900; + color: var(--accent-crimson, #d12b3e); + text-transform: uppercase; + letter-spacing: 0.12em; + display: block; + margin-top: 0.2rem; + margin-bottom: 0.5rem; + border-bottom: 2px solid var(--accent-ruby, #c72436); + padding-bottom: 0.4rem; + opacity: 1 !important; +} + +.step-title { + font-family: "Impact", "Arial Black", "Futura", "Helvetica Neue", system-ui, -apple-system, sans-serif; + font-size: 2rem; + font-weight: 900; + letter-spacing: -0.02em; + text-transform: uppercase; + margin-bottom: 0.5rem; + color: var(--ink-primary); + line-height: 1.1; +} + +/* Standard Vertical Paragraph Text (0 Layout Animations, 0 Transforms, No Separate Boxes) */ +.step-paragraph-item { + position: relative; + z-index: 1; + margin-bottom: 1.2rem; + opacity: 1 !important; + cursor: pointer; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 1rem; + color: var(--ink-primary); + line-height: 1.65; + font-weight: 400; + transform: rotate(0deg); +} + +.step-paragraph-item:last-child { + margin-bottom: 0; +} + +/* Inline Transparent Illustrations Between Paragraphs (Strict Defense-in-Depth Guardrails) */ +.step-inline-illustration { + opacity: 1 !important; + transition: none !important; + animation: none !important; + background: transparent !important; + border: 2px solid var(--ink-secondary); + border-radius: 6px; + margin: 16px 0; + padding: 12px; + text-align: center; +} + +.step-inline-illustration-img { + opacity: 1 !important; + transition: none !important; + animation: none !important; + background: transparent !important; + max-width: 100%; + height: auto; + display: inline-block; +} + +/* Step Specs (1950s Slanted Print Data Box with Independent Shadow) */ +.step-specs { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + background: var(--bg-color); + padding: 0.75rem 1rem; + border: 3px solid var(--ink-primary); + border-radius: 6px; + margin-top: 0.6rem; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + transform: rotate(0.5deg); + overflow: visible; +} + +.step-specs::after { + content: ""; + position: absolute; + z-index: -1; + top: 6px; + left: -6px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 6px; + transform: rotate(-1.3deg); +} + +.spec-item { + display: flex; + flex-direction: column; + border-left: 3px solid var(--accent-crimson, #d12b3e); + padding-left: 0.65rem; +} + +.spec-label { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 700; + color: var(--ink-secondary, #cccccc); + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 1 !important; +} + +.spec-value { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 900; + color: var(--ink-primary); +} + +.page-footer { + position: relative; + z-index: 1; + padding: 4.5rem 2rem 3.5rem; + background-color: var(--paper-card); + border: 4px solid var(--border-color); + border-radius: 10px; + margin-top: 1rem; + margin-bottom: 4rem; + text-align: center; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 900; + color: var(--ink-primary); + text-transform: uppercase; + letter-spacing: 0.08em; + transform: rotate(0.5deg); + overflow: visible; +} + +.page-footer::after { + content: ""; + position: absolute; + z-index: -1; + top: 12px; + left: 10px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid); + border-radius: 10px; + transform: rotate(-0.9deg); +} + +@media (max-width: 1100px) { + .floating-toc { + top: auto; + bottom: 1.5rem; + right: 1.5rem; + width: 230px; + padding: 0.75rem; + transform: rotate(0deg); + } + .floating-toc.is-collapsed { + width: 145px; + padding: 0; + } + .toc-title { + font-size: 1rem; + } + .toc-item { + font-size: 0.85rem; + padding: 0.35rem 0.45rem; + } + .step-title { + font-size: 1.5rem; + } +} + +@media (max-width: 900px) { + .scrolly-container { + gap: 2.5rem; + } + .stage-section, + .stage-section.layout-text-left, + .stage-section.layout-text-right { + flex-direction: column; + gap: 2.5rem; + min-height: auto; + padding-top: 1.5rem; + padding-bottom: 2rem; + } + .stage-graphic-column { + position: relative; + top: 0; + width: 100%; + height: auto; + } + .stage-narrative-column { + width: 100%; + padding-top: 0; + padding-bottom: 0; + } + .step-specs { + grid-template-columns: 1fr; + } +} + +/* Bottom Timeline Navigation Overlay (Canon Autozoom Style, Zero Transitions) */ +.bottom-timeline-overlay, +#bottom-timeline-overlay { + position: fixed; + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%) rotate(-0.5deg); + z-index: 1000; + background-color: var(--paper-card, rgba(0, 0, 0, 0.85)); + border: 3px solid var(--border-color, #ffffff); + border-radius: 8px; + padding: 0.75rem 1.5rem; + width: max-content; + max-width: 90vw; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + opacity: 1 !important; + overflow: visible; + transition: none !important; + animation: none !important; +} +.bottom-timeline-overlay::after, +#bottom-timeline-overlay::after { + content: ""; + position: absolute; + z-index: -1; + top: 6px; + left: 8px; + width: 100%; + height: 100%; + background-color: var(--shadow-solid, rgba(0, 0, 0, 0.75)); + border-radius: 8px; + transform: rotate(1.1deg); + transition: none !important; + animation: none !important; +} +.bottom-timeline-overlay.is-hidden, +#bottom-timeline-overlay.is-hidden { + display: none !important; +} +.bottom-timeline-overlay.is-visible, +#bottom-timeline-overlay.is-visible { + display: flex !important; +} +.timeline-track { + position: absolute; + top: 50%; + left: 1.5rem; + right: 1.5rem; + height: 2px; + background-color: var(--ink-secondary, #cccccc); + z-index: 1; +} +.timeline-points { + list-style: none; + display: flex; + gap: 1.25rem; + margin: 0; + padding: 0; + position: relative; + z-index: 2; +} +.timeline-point { + background-color: var(--paper-light, rgba(0, 0, 0, 0.65)); + border: 2px solid var(--border-color, #ffffff); + border-radius: 6px; + color: var(--ink-secondary, #cccccc); + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-size: 0.85rem; + font-weight: 700; + padding: 0.35rem 0.65rem; + cursor: pointer; + opacity: 1 !important; + transition: none !important; + animation: none !important; + transform: rotate(0deg); + z-index: 2; +} +.timeline-point.is-active { + background: var(--accent-crimson, #d12b3e) !important; + color: #ffffff !important; + border-color: #ffffff !important; +} diff --git a/suites-experimental/suites.mjs b/suites-experimental/suites.mjs index 3de20b4dc..4bbfb6fad 100644 --- a/suites-experimental/suites.mjs +++ b/suites-experimental/suites.mjs @@ -271,4 +271,50 @@ export const ExperimentalSuites = freezeSuites([ }), ], }, + { + name: "Scrollytelling-Scrollama", + url: "suites-experimental/scrollytelling/dist/index.html?engine=scrollama", + tags: ["experimental", "scrollytelling"], + async prepare(page) { + await page.waitForElement(".scrolly-container"); + page.call("serviceRAF"); + }, + tests: [ + new BenchmarkTestStep("DiscreteStepTransitions", (page) => { + for (let i = 0; i < 8; i++) { + page.call(`stepTo${i}`); + page.call("serviceRAF"); + page.layout(); + } + for (let i = 6; i >= 0; i--) { + page.call(`stepTo${i}`); + page.call("serviceRAF"); + page.layout(); + } + }), + new BenchmarkTestStep("ContinuousScrollScrubbing", (page) => { + page.call("resetScrub"); + page.call("serviceRAF"); + page.layout(); + for (let i = 0; i < 30; i++) { + page.call("scrubForward"); + page.call("serviceRAF"); + page.layout(); + } + for (let i = 0; i < 30; i++) { + page.call("scrubBackward"); + page.call("serviceRAF"); + page.layout(); + } + }), + new BenchmarkTestStep("RapidStepJumps", (page) => { + const jumpTargets = [7, 1, 6, 2, 5, 0, 4, 3]; + for (const target of jumpTargets) { + page.call(`stepTo${target}`); + page.call("serviceRAF"); + page.layout(); + } + }), + ], + }, ]);