# Almanach — SceneryStack Knowledge Base > A file-based knowledge base of SceneryStack knowledge: API guides, software patterns, styling, and accessibility conventions for building interactive simulations. Every page is plain Markdown with structured frontmatter. This file contains the full text of all 423 pages. Each page begins with a "===" separator line. ======================================================================== Page: Deploying a Simulation to GitHub Pages URL: https://veillette.github.io/Almanach/getting-started/deploying-to-github-pages Source: docs/getting-started/deploying-to-github-pages.md Category: Getting Started | Tags: deployment, github-pages, build, ci | Status: verified ======================================================================== # Deploying a Simulation to GitHub Pages A production build of a SceneryStack simulation is exactly what GitHub Pages wants: a folder of static HTML, JS, CSS, and media, with no server-side runtime. This page walks through turning `npm run build`'s output into a live `https://.github.io//` URL, both by hand (pushing to a `gh-pages` branch) and with a GitHub Actions workflow that rebuilds on every push. ::: warning Set the base path before you build, not after GitHub Pages serves a project site from a subpath (`//`), not the domain root. If your bundler config assumes root-relative asset paths (the default for most Vite/Webpack/Esbuild scaffolds), the deployed page loads with every script and asset 404ing, because the browser requests `/main.js` instead of `//main.js`. Set the base path in the bundler config *before* running the production build — see below — not by editing the built output afterward. ::: ## What you're deploying As covered in [Running and Building a Simulation](/getting-started/running-and-building-a-simulation), `npm run build` runs whichever bundler you picked during `npm create scenerystack@latest` in production mode and emits a static bundle — `dist/` for Vite/Esbuild, `build/` for some Webpack configs. That output is entirely self-contained: every `scenerystack/*` import, image, and sound file the bundler could resolve is already inlined or copied alongside the built JS. There is nothing GitHub Pages-specific about the build itself; the only thing you need to get right is *where the built files expect to be served from*. ## Setting the base path Tell the bundler the site will live under `//` rather than at the domain root. For a Vite-scaffolded project, this is the `base` option in `vite.config.ts`: ```ts // vite.config.ts import { defineConfig } from 'vite'; export default defineConfig( { base: '/my-simulation-repo/' } ); ``` For a Webpack-scaffolded project, the equivalent is `output.publicPath`: ```js // webpack.config.js module.exports = { output: { publicPath: '/my-simulation-repo/' } }; ``` If you're deploying to a *user or organization* Pages site (`https://.github.io/`, not a project subpath), the base path is `/` and you can skip this step — that's the one case where the default root-relative config already matches. ## Option 1: publish the built output to a `gh-pages` branch The simplest path for a one-off or infrequent deploy: build locally, then push the output directory to a `gh-pages` branch, which GitHub Pages serves from directly (configure this once in the repo's Settings → Pages → "Deploy from a branch"). ```bash npm run build npx gh-pages -d dist ``` [`gh-pages`](https://www.npmjs.com/package/gh-pages) (add it with `npm install --save-dev gh-pages`) commits the contents of `dist/` to a `gh-pages` branch and force-pushes it, without disturbing your `main` branch's history. Repeat the two commands above whenever you want to publish a new version — nothing about this workflow is SceneryStack-specific, it's the standard static-site-on-Pages pattern applied to your build output. ## Option 2: a GitHub Actions workflow (recommended for anything shared) Committing built artifacts to `gh-pages` by hand works, but it's easy to forget to rebuild before publishing, or to publish a build made with local, uncommitted changes. A workflow that builds from source on every push to `main` avoids both problems: ```yaml # .github/workflows/deploy.yml name: Deploy to GitHub Pages on: push: branches: [main] permissions: contents: read pages: write id-token: write jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm run build - uses: actions/upload-pages-artifact@v3 with: path: dist - id: deployment uses: actions/deploy-pages@v4 ``` This uses GitHub's own `actions/deploy-pages` flow rather than a `gh-pages` branch — enable it once in Settings → Pages → Source → "GitHub Actions". Every push to `main` re-runs `npm ci && npm run build` in a clean environment, so what's published always matches what's committed; there's no local build step to remember, and no chance of publishing a build made against a dirty working tree. | Piece | Why it's there | | --- | --- | | `npm ci` | Installs exactly the versions in `package-lock.json` — reproducible across CI runs, unlike `npm install` | | `node-version: 20` | Match whatever Node version you develop against; see [Troubleshooting Common Setup Errors](/getting-started/troubleshooting-common-setup-errors) for why version drift causes build failures | | `upload-pages-artifact` / `deploy-pages` | GitHub's official Pages-deployment actions — no third-party `gh-pages` dependency needed in the CI path | ## Verifying the deployed build Once the workflow finishes (or the `gh-pages` push lands), visit `https://.github.io//`. Two failure modes are specific to this deployment shape rather than the simulation code itself: - **Blank page, 404s in the console for JS/CSS/image files** — the base path wasn't set (or was set to `/` when the site actually lives at `//`). Fix the bundler's `base`/`publicPath` and rebuild. - **Page loads but sounds/images are missing** — usually a case-sensitivity mismatch between an import path and the actual filename; GitHub Pages' file server is case-sensitive even if your local OS filesystem isn't, so this can build and run locally while 404ing once deployed. Also worth doing before you push: `npm run build`'s output has SceneryStack's runtime assertions compiled away (see [Running and Building a Simulation](/getting-started/running-and-building-a-simulation)), so bugs that assertions would have caught in dev can surface only in the deployed build. Serve `dist/` locally with a static file server (`npx serve dist`, or your bundler's `preview` command) and click through the sim before pushing to Pages, rather than discovering a production-only bug for the first time in front of users. ## Where to go next - [Running and Building a Simulation](/getting-started/running-and-building-a-simulation) — the dev/build workflow this page's build step assumes - [Troubleshooting Common Setup Errors](/getting-started/troubleshooting-common-setup-errors) — Node version and dependency issues that most often break a CI build - [Supported Browsers](/getting-started/supported-browsers) — the platform matrix to verify against once deployed ======================================================================== Page: Installation and Setup URL: https://veillette.github.io/Almanach/getting-started/installation-and-setup Source: docs/getting-started/installation-and-setup.md Category: Getting Started | Tags: setup, installation, tooling, typescript | Status: verified ======================================================================== # Installation and Setup SceneryStack is a TypeScript framework, published as a single npm package. Before writing any code you need Node.js, npm, and (for cloning the underlying libraries) Git — everything else is standard JavaScript tooling. ## Prerequisites | Tool | Why you need it | | --- | --- | | A command line (Terminal, PowerShell, etc.) | Running `npm`/`npx` commands | | [Git](https://git-scm.com/downloads) | Checking out PhET's source repositories if you ever need to patch a library — see [Modifying SceneryStack](/guides/modifying-scenerystack) | | [Node.js and npm](https://nodejs.org/) | Installing dependencies and running the dev server/build | SceneryStack **is a TypeScript library**: it can be consumed from plain JavaScript, but its options objects rely entirely on compile-time types rather than runtime checks, so TypeScript is strongly recommended. Any editor with good TypeScript support works well — [VS Code](https://code.visualstudio.com/) and [WebStorm](https://www.jetbrains.com/webstorm/) are the two most commonly used by SceneryStack maintainers. ## Scaffolding a new project (recommended) The fastest way to start a new simulation or application is the official scaffolding tool: ```bash npm create scenerystack@latest ``` This walks you through: - naming the project (it creates a directory with that name) - choosing a bundler — [Vite](https://vite.dev/), [Webpack](https://webpack.js.org/), [Esbuild](https://esbuild.github.io/), or [Parcel](https://parceljs.org/) - whether to set up [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) ...and prints the exact commands to install dependencies and open the running project in your browser. See [Your First Simulation](/getting-started/your-first-simulation) for what the generated code looks like, and [Running and Building a Simulation](/getting-started/running-and-building-a-simulation) for the dev-server/build workflow it wires up. ## Adding SceneryStack to an existing project If you already have a bundler-based project and just want the library: ```bash npm install scenerystack ``` Then import from the subpath for whichever library you need — `scenerystack` is not meant to be imported from its bare package root: ```ts import { Node, Text } from 'scenerystack/scenery'; import { Property } from 'scenerystack/axon'; ``` ::: tip Import from subpaths, not the package root Every SceneryStack library — `axon`, `dot`, `kite`, `scenery`, `sun`, `scenery-phet`, `joist`, `sim`, `tandem`, `phetcommon`, and others — is its own subpath export (`scenerystack/`). There is no flat `scenerystack` import; see [What is SceneryStack?](/getting-started/what-is-scenerystack) for the full library table. ::: ## Where to go next - [Your First Simulation](/getting-started/your-first-simulation) — build a minimal one-screen `Sim` - [Scenery Application vs. Standalone Library](/getting-started/scenery-application-vs-standalone-library) — decide whether you even need `Sim` - [Project Structure Conventions](/getting-started/project-structure-conventions) — how a SceneryStack repo is usually laid out - [Supported Browsers](/getting-started/supported-browsers) — the platform support matrix to design against ======================================================================== Page: Project Structure Conventions URL: https://veillette.github.io/Almanach/getting-started/project-structure-conventions Source: docs/getting-started/project-structure-conventions.md Category: Getting Started | Tags: conventions, project-structure, repo-layout | Status: verified ======================================================================== # Project Structure Conventions `npm create scenerystack@latest` scaffolds a project, but it doesn't force a folder layout on your simulation-specific code — the conventions below are the ones PhET's own simulations (and most SceneryStack projects) converge on, because they keep [model-view separation](/patterns/model-view-separation) visible in the filesystem, not just in the code. ## A typical layout ``` my-simulation/ ├── js/ │ ├── my-simulation-main.ts # entry point: builds Screens and the Sim │ ├── my-screen/ │ │ ├── model/ │ │ │ └── MyScreenModel.ts │ │ └── view/ │ │ ├── MyScreenView.ts │ │ └── MyThingNode.ts │ └── common/ # code shared by more than one screen │ ├── model/ │ └── view/ ├── images/ # source art (SVG/PNG), one file per asset ├── sounds/ # audio cues, if the sim uses scenery-phet sound ├── my-simulation-strings_en.json # translatable strings, see Translation and Localization ├── package.json ├── tsconfig.json └── index.html ``` The top-level split that matters most: **one folder per screen**, each with its own `model/` and `view/` subfolders, plus a `common/` folder for anything shared across screens. This mirrors the `Screen`/`ScreenView` pairing described in [Your First Simulation](/getting-started/your-first-simulation) — if your sim only has one screen, the per-screen folder is still worth keeping separate from `common/` so a second screen has somewhere obvious to go later. ## Where things live | Path | Contents | | --- | --- | | `js//model/` | Plain TypeScript classes holding `Property`/`Emitter` state — no `scenery` imports | | `js//view/` | `ScreenView` subclass plus any `Node` subclasses specific to that screen | | `js/common/` | Model or view code shared by two or more screens | | `images/` | Source images referenced via generated image modules (PNG, SVG, JPG) | | `sounds/` | Audio files referenced the same way, for `scenery-phet`/`tambo` sound generators | | `-strings_en.json` | The English source-of-truth strings file — see [Translation and Localization](/guides/translation-and-localization) | | `-main.ts` | The only file that imports `Sim`/`Screen` and calls `onReadyToLaunch` | ::: tip Keep `model/` free of scenery imports The fastest way to lose model-view separation over time is a stray `import { Node } from 'scenerystack/scenery'` inside a model file "just this once." If a model file ever needs to import from `scenerystack/scenery`, `sun`, or `scenery-phet`, that's a signal the code belongs in `view/` instead. ::: ## Where to go next - [Running and Building a Simulation](/getting-started/running-and-building-a-simulation) — what the dev server and production build do with this layout - [Model-View Separation](/patterns/model-view-separation) — the architectural rule this layout exists to reinforce - [Translation and Localization](/guides/translation-and-localization) — how the strings file at the repo root becomes runtime `StringProperty` instances ======================================================================== Page: Running and Building a Simulation URL: https://veillette.github.io/Almanach/getting-started/running-and-building-a-simulation Source: docs/getting-started/running-and-building-a-simulation.md Category: Getting Started | Tags: build, tooling, dev-server | Status: verified ======================================================================== # Running and Building a Simulation A SceneryStack project is a normal bundler-based TypeScript web app: there's no SceneryStack-specific dev server or build tool, just the bundler you picked when scaffolding — [Vite](https://vite.dev/), [Webpack](https://webpack.js.org/), [Esbuild](https://esbuild.github.io/), or [Parcel](https://parceljs.org/) — configured to serve and bundle a page that imports `scenerystack/sim`. ## Development `npm create scenerystack@latest` wires an npm script (conventionally `npm run dev` or `npm start`, depending on the bundler chosen) that starts the bundler's dev server: ```bash npm run dev ``` This serves the sim with live reload at a `localhost` URL. Because a `Sim` fills the browser window and reads query parameters at startup (see [Preferences and Feature Flags](/guides/preferences-and-feature-flags)), the dev server URL is where you append query parameters while iterating, e.g. `http://localhost:5173/?ea` to enable assertions. ::: tip Enable assertions during development Append `?ea` to the dev URL to turn on SceneryStack's runtime assertions. They catch invalid option values and API misuse immediately instead of failing silently or much later — always develop with `?ea` on, and rely on production builds (which strip it) for the assertion-free path users get. ::: ## Production build The scaffolded build script runs the bundler in production mode: ```bash npm run build ``` This produces a single self-contained bundle (plus any referenced assets) under the bundler's default output directory (`dist/` for Vite/Esbuild, `build/` for some Webpack configs). The output is static HTML/JS/CSS/media — it can be hosted from any static file server or CDN with no server-side runtime. Two things distinguish a SceneryStack production build from a generic web app build: | Aspect | Effect | | --- | --- | | Assertions stripped | Runtime `assert && assert(...)` checks used throughout SceneryStack compile away, so production is both smaller and faster than a dev build | | Asset inlining | Images, sounds, and generated string modules referenced via `scenerystack/*` imports get bundled the same way any other module import does — no separate asset pipeline to configure | ## Where to go next - [Supported Browsers](/getting-started/supported-browsers) — the platform matrix a production build should target - [Preferences and Feature Flags](/guides/preferences-and-feature-flags) — query parameters available at both dev and production URLs - [Project Structure Conventions](/getting-started/project-structure-conventions) — the source layout the bundler consumes ======================================================================== Page: Scenery Application vs. Standalone Library URL: https://veillette.github.io/Almanach/getting-started/scenery-application-vs-standalone-library Source: docs/getting-started/scenery-application-vs-standalone-library.md Category: Getting Started | Tags: joist, scenery, standalone, architecture | Status: verified ======================================================================== # Scenery Application vs. Standalone Library SceneryStack can be used two very different ways, and picking the right one up front saves you from unpicking a `Sim` later. The rule of thumb: **if you need multiple screens, a navigation bar, a home screen, PhET-iO instrumentation, or the Preferences dialog, use a `Sim` application. If you just need an interactive scene graph inside a page you already control, use `scenery` standalone.** ## Full application: `Sim` + `Screen` + `ScreenView` This is the shape of every PhET simulation: a `Sim` owns one or more `Screen`s, each with a `ScreenView`, and `joist`/`sim` supplies the navigation bar, home screen, keyboard help, and Preferences dialog for free. See [Your First Simulation](/getting-started/your-first-simulation) for the full wiring — you don't manage a `Display` yourself, `Sim` does it internally via `SimDisplay`. Reach for this when your project is a self-contained, full-window interactive experience — especially if you want the accessibility, internationalization, and PhET-iO scaffolding that come with `joist` "for free." ## Standalone: `scenery` without `joist` If you only need a scene graph embedded in a larger page (a widget, a figure, an existing web app), skip `Sim`/`Screen` entirely and drive a `Display` yourself: ```html
``` ```ts import { Display, Node, Rectangle, Text } from 'scenerystack/scenery'; const container = document.getElementById( 'app' )!; const rootNode = new Node(); const display = new Display( rootNode, { width: container.clientWidth, height: container.clientHeight } ); container.appendChild( display.domElement ); // Example content const rotatingRectangle = new Rectangle( -150, -20, 300, 40, { fill: '#ccc' } ); const contentText = new Text( 'Content goes here', { font: '24px sans-serif' } ); rootNode.children = [ rotatingRectangle, contentText ]; contentText.center = display.bounds.center; rotatingRectangle.translation = display.bounds.center; display.initializeEvents(); // enable pointer/touch input display.updateOnRequestAnimationFrame( dt => { rotatingRectangle.rotation += 2 * dt; // radians per second } ); ``` Key differences from a `Sim`: | | Full application (`Sim`) | Standalone (`scenery` only) | | --- | --- | --- | | Owns the `Display` | Yes, internally (`SimDisplay`) | You create and size it | | Multi-screen / navigation bar / home screen | Built in | Not applicable — build your own UI | | Preferences dialog, PhET-iO | Built in | Opt in manually if needed | | Sizing | Fills the browser window | You size the container and `Display` | | Render/animation loop | Managed by `Sim` | You call `display.updateOnRequestAnimationFrame(...)` | | Input events | Enabled automatically | You call `display.initializeEvents()` | ::: tip Both ways to get the npm package are the same `npm create scenerystack@latest` scaffolds either shape — it asks what kind of project you want. For an existing project, `npm install scenerystack` and importing `scenerystack/scenery` directly works identically whether you build a `Sim` or go standalone; the two paths diverge in application code, not installation. ::: ## Where to go next - [Scenery Basics](/guides/scenery-basics) — the Node tree, coordinate frames, and Display fundamentals used by both approaches - [Your First Simulation](/getting-started/your-first-simulation) — the full-application path in detail - [Scenery Layout](/guides/scenery-layout) — laying out content inside either a `ScreenView` or a standalone root `Node` ======================================================================== Page: Supported Browsers URL: https://veillette.github.io/Almanach/getting-started/supported-browsers Source: docs/getting-started/supported-browsers.md Category: Getting Started | Tags: browsers, compatibility, deployment | Status: verified ======================================================================== # Supported Browsers SceneryStack simulations target modern evergreen browsers on desktop and mobile — the same browsers PhET's own published sims support. Because `scenery` renders through Canvas, SVG, and WebGL rather than a single fixed technology, most compatibility differences are handled by scenery's renderer selection rather than by your application code. ## The platform matrix | Platform | Support | | --- | --- | | Chrome / Chromium (desktop & Android) | Fully supported | | Firefox (desktop & Android) | Fully supported | | Safari (desktop & iOS/iPadOS) | Fully supported, including `mobileSafari` | | Edge (Chromium-based) | Fully supported | | Internet Explorer | Not supported | `phet-core`'s `platform` object gives you runtime feature/browser detection when you need to special-case something (a workaround for a specific engine quirk, not a support gate): ```ts import { platform } from 'scenerystack/phet-core'; if ( platform.safari ) { // e.g. work around a Safari-specific rendering quirk } ``` | Property | Detects | | --- | --- | | `platform.safari` | Any Safari, including mobile (`platform.mobileSafari`) | | `platform.firefox` | Firefox, desktop or mobile | | `platform.chromium` | Chrome/Chromium-based browsers (excludes Edge) | | `platform.edge` | Chromium-based Edge | | `platform.android` | Android's WebView/browser | | `platform.ie` | Internet Explorer (present for detection only — not a supported target) | ## Graceful degradation via renderer fallback Every `Node` has a `renderer` option — `'svg' | 'canvas' | 'webgl' | 'dom' | null` — that scenery uses as a hint, not a hard requirement. Leaving it `null` (the default) lets scenery pick the best available backend per-subtree and fall back automatically if, say, WebGL isn't available: ```ts import { Node } from 'scenerystack/scenery'; const heavyNode = new Node( { renderer: 'webgl' // hint only; scenery falls back to canvas/svg if WebGL is unavailable } ); ``` ::: tip Don't force a renderer unless you've measured a problem The historical `platform.firefox` example in scenery's own source (`if (platform.firefox) { node.renderer = 'canvas'; }`) is a workaround for a specific old performance issue, not a general pattern. Leave `renderer` unset by default; only pin it after profiling shows a specific subtree needs a specific backend. ::: ## Where to go next - [Scenery Basics](/guides/scenery-basics) — how the Node tree maps to Canvas/SVG/WebGL output - [Running and Building a Simulation](/getting-started/running-and-building-a-simulation) — the build step that ships to these browsers ======================================================================== Page: Troubleshooting Common Setup Errors URL: https://veillette.github.io/Almanach/getting-started/troubleshooting-common-setup-errors Source: docs/getting-started/troubleshooting-common-setup-errors.md Category: Getting Started | Tags: troubleshooting, setup, typescript, tooling | Status: verified ======================================================================== # Troubleshooting Common Setup Errors Most setup problems with a new SceneryStack project fall into a handful of recurring shapes: a Node version too old for the tooling, an import written against the package root instead of a subpath, a peer dependency the scaffolder assumed but didn't install, or a `tsconfig.json` module-resolution setting that doesn't understand package subpath exports. This page walks through each, with the error text you'll actually see and the fix. ## "Cannot find module 'scenerystack'" or "'scenerystack/scenery' has no exported member" This is almost always an import written against the wrong place. SceneryStack does not export anything usable from its bare package root — every library is its own subpath: ```ts // Wrong: nothing useful lives at the package root import { Node } from 'scenerystack'; // Right: import from the subpath for the library you need import { Node } from 'scenerystack/scenery'; import { Property } from 'scenerystack/axon'; ``` If you're getting the "no exported member" flavor of this error rather than "cannot find module," double check you're importing the *class* from the *library* that actually exports it — a common slip is assuming `Sim`/`Screen`/`ScreenView` live in `scenerystack/joist` (the underlying repository's name) when the published package actually exports them from `scenerystack/sim`; `scenerystack/joist` exists but only exports supporting pieces. See [What is SceneryStack?](/getting-started/what-is-scenerystack) for the full library-to-subpath table. ## "SyntaxError: Unexpected token 'export'" or the dev server won't start This is usually a Node version too old for the scaffolded tooling. SceneryStack's published package and its scaffolder (`npm create scenerystack@latest`) target current Node LTS releases; an old Node binary can fail confusingly rather than with a clear version error, especially inside a bundler's own dependency chain. Check your version and, if it's old, use a version manager to get current LTS: ```bash node --version # if it's below the current LTS line, e.g. Node 16 or 18: nvm install --lts nvm use --lts ``` Delete `node_modules` and reinstall after switching Node versions — native/binary dependencies (esbuild, some Vite optionals) are compiled per Node ABI version and a stale install can produce obscure runtime errors that look unrelated to Node at all: ```bash rm -rf node_modules package-lock.json npm install ``` ## TypeScript can't resolve `scenerystack/*` subpaths If plain imports work at runtime (the bundler resolves them fine) but the TypeScript language server or `tsc` reports `Cannot find module 'scenerystack/scenery' or its corresponding type declarations`, the project's `tsconfig.json` is using a `moduleResolution` mode that predates Node's package `exports` map support. SceneryStack's package.json declares its subpaths via `exports`, which requires one of the newer resolution modes: ```json // tsconfig.json { "compilerOptions": { "moduleResolution": "bundler", "module": "ESNext", "target": "ES2022" } } ``` `"moduleResolution": "node"` (the old default in many hand-written configs) only understands the legacy `main`/`module` fields, not subpath `exports`, and will fail to find types for anything under `scenerystack/*` even though the JavaScript resolves fine at bundle time. `"bundler"` (introduced in TypeScript 5.0) or `"node16"`/`"nodenext"` both understand `exports` maps correctly; `"bundler"` is what the official scaffolder generates and is the safer default if you're hand-writing a `tsconfig.json` for an existing project instead of scaffolding a new one. ## Missing peer dependency warnings after `npm install scenerystack` Adding SceneryStack to an existing project (rather than scaffolding fresh) means you're responsible for the bundler and dev tooling yourself — `scenerystack` itself has no bundler as a dependency, so `npm install scenerystack` alone gets you the library but not something that can serve or build it. If you see the app fail to start with errors pointing at a missing bundler binary or config file, you likely skipped setting up Vite/Webpack/Esbuild/Parcel: ```bash npm install scenerystack npm install --save-dev vite # or webpack, esbuild, parcel — whichever you're standardizing on ``` If you're not sure which bundler to use, `npm create scenerystack@latest` in a throwaway directory and copying its generated config is the fastest way to get a known-working baseline rather than assembling one from scratch — see [Installation and Setup](/getting-started/installation-and-setup). ## Assertions silently doing nothing If invalid option values or misuse of the SceneryStack API aren't producing the errors you'd expect, check whether you're running with assertions enabled. Assertions are opt-in via a query parameter at dev time and compiled away entirely in production builds — a dev server URL without `?ea` behaves like a production build with respect to error-catching, which can make a real bug look like it "just doesn't happen" until it does, at a less convenient time: ``` http://localhost:5173/?ea ``` ## Blank page with no console errors at all If the page loads (no 404s, no thrown errors) but nothing renders, check that you're constructing the `Sim` inside `onReadyToLaunch`, not at module scope. SceneryStack's asset loader (fonts, images, generated strings) is asynchronous; a `Sim` constructed before it resolves can fail to paint without throwing, because the loader itself hasn't errored — it just hasn't finished. See [Your First Simulation](/getting-started/your-first-simulation) for the correct `onReadyToLaunch` wiring. ::: tip When in doubt, isolate with a fresh scaffold If an error doesn't match anything above, scaffold a brand-new project with `npm create scenerystack@latest` in a scratch directory and reproduce the minimal failing case there. It rules out an entire category of causes — a stale lockfile, an accidentally hand-edited config, a Node/npm version mismatch specific to the broken project — in one step, and gives you a known-good baseline to diff your actual project's config against. ::: ## Where to go next - [Installation and Setup](/getting-started/installation-and-setup) — the scaffolding and manual-install paths these errors most often stem from - [Supported Browsers](/getting-started/supported-browsers) — platform-level compatibility issues, as distinct from local build/setup issues - [Running and Building a Simulation](/getting-started/running-and-building-a-simulation) — the dev-server/build workflow referenced throughout this page ======================================================================== Page: What is SceneryStack? URL: https://veillette.github.io/Almanach/getting-started/what-is-scenerystack Source: docs/getting-started/what-is-scenerystack.md Category: Getting Started | Tags: overview, installation, scenery, axon, dot, sun, joist | Status: verified ======================================================================== # What is SceneryStack? SceneryStack is the open-source TypeScript framework behind [PhET Interactive Simulations](https://phet.colorado.edu). It provides everything needed to build accessible, multi-modal, interactive HTML5 applications: a scene graph renderer, a reactive state system, math utilities, UI components, and an application shell. It is published as a single npm package, [`scenerystack`](https://www.npmjs.com/package/scenerystack), with each library exposed as a subpath export. ## The libraries | Library | Import path | Purpose | | --- | --- | --- | | **axon** | `scenerystack/axon` | Reactive state: `Property`, `DerivedProperty`, `Emitter`, `Multilink` | | **dot** | `scenerystack/dot` | Math: `Vector2`, `Bounds2`, `Matrix3`, `Range`, `Utils` | | **kite** | `scenerystack/kite` | Geometric shapes and boolean operations: `Shape`, `LineStyles` | | **scenery** | `scenerystack/scenery` | Scene graph: `Node`, `Path`, `Text`, input listeners, accessibility (PDOM) | | **sun** | `scenerystack/sun` | UI components: buttons, sliders, checkboxes, combo boxes | | **scenery-phet** | `scenerystack/scenery-phet` | Simulation-specific reusable components: arrows, thermometers, `PhetFont` | | **twixt** | `scenerystack/twixt` | Animation and easing | | **joist** | `scenerystack/sim` (app shell classes) / `scenerystack/joist` (preferences panels, locale utilities) | Application shell: `Sim`, `Screen`, `ScreenView`, navigation bar — see [Troubleshooting](/getting-started/troubleshooting-common-setup-errors) for why the import subpath differs from the library name | | **tandem** | `scenerystack/tandem` | PhET-iO instrumentation and serialization | | **phetcommon** | `scenerystack/phetcommon` | Shared utilities, notably `ModelViewTransform2` | ## Installation ```bash npm install scenerystack ``` ## A minimal example ```ts import { Property } from 'scenerystack/axon'; import { Circle, Node } from 'scenerystack/scenery'; import { Vector2 } from 'scenerystack/dot'; // Model: reactive state, no view knowledge const positionProperty = new Property( new Vector2( 0, 0 ) ); // View: a scenery Node that observes the model const ball = new Circle( 20, { fill: 'crimson' } ); positionProperty.link( position => { ball.translation = position; } ); ``` This model/view split is the foundational pattern of every SceneryStack application — see [Model-View Separation](/patterns/model-view-separation). ## Where to go next - [Model-View Separation](/patterns/model-view-separation) — the architecture every page in Almanach assumes - [ModelViewTransform2](/api/phetcommon/model-view-transform) — mapping between model and view coordinates - [Drag Listeners](/patterns/drag-listeners) — making nodes interactive - [The Parallel DOM (PDOM)](/accessibility/pdom) — building accessible simulations ======================================================================== Page: Your First Simulation URL: https://veillette.github.io/Almanach/getting-started/your-first-simulation Source: docs/getting-started/your-first-simulation.md Category: Getting Started | Tags: joist, sim, screen, tutorial | Status: verified ======================================================================== # Your First Simulation A SceneryStack simulation is a `Sim` containing one or more `Screen`s, each pairing a plain model class with a `ScreenView`. This page walks through the smallest possible one-screen simulation, wired together by hand so you can see every piece that `npm create scenerystack@latest` normally generates for you. ::: warning `Sim`, `Screen`, and `ScreenView` live in `scenerystack/sim`, not `scenerystack/joist` It's easy to assume the application shell classes are exported from `scenerystack/joist` since the underlying repository is named `joist`. In the published package they are actually exported from the **`scenerystack/sim`** subpath. `scenerystack/joist` exists too, but only exports supporting pieces (preferences panels, `CreditsNode`, locale utilities, and similar) — not `Sim`, `Screen`, or `ScreenView` themselves. ::: ## The model Every screen's model just needs a `reset()` method (and, optionally, a `step(dt)` method if it animates): ```ts class MyModel { public reset(): void { // reset any Properties here } public step( dt: number ): void { // advance the model by dt seconds, if needed } } ``` ## The view A `ScreenView` is a `Node` with a `layoutBounds` describing its design-time coordinate frame: ```ts import { ScreenView, type ScreenViewOptions } from 'scenerystack/sim'; import { Text } from 'scenerystack/scenery'; class MyScreenView extends ScreenView { public constructor( model: MyModel, providedOptions: ScreenViewOptions ) { super( providedOptions ); const greetingText = new Text( 'Hello, SceneryStack!', { font: '24px sans-serif' } ); greetingText.center = this.layoutBounds.center; this.addChild( greetingText ); } } ``` ## Wiring up the Screen and Sim ```ts import { Sim, Screen, onReadyToLaunch } from 'scenerystack/sim'; import { Tandem } from 'scenerystack/tandem'; import { Property } from 'scenerystack/axon'; const screenTandem = Tandem.ROOT.createTandem( 'myScreen' ); const myScreen = new Screen( () => new MyModel(), model => new MyScreenView( model, { tandem: screenTandem.createTandem( 'view' ) } ), { name: new Property( 'My Screen' ), backgroundColorProperty: new Property( 'white' ), tandem: screenTandem } ); onReadyToLaunch( () => { const sim = new Sim( new Property( 'My First Simulation' ), [ myScreen ] ); sim.start(); } ); ``` A few things worth noting: | Piece | Why it's there | | --- | --- | | `onReadyToLaunch` | Waits for SceneryStack's asynchronous asset loader (fonts, images, strings) before constructing the `Sim`. Always launch this way, never call `new Sim(...)` directly at module scope. | | `Tandem` | PhET-iO's instrumentation identifier. Every `Screen` requires one, even if you never enable instrumentation — see [PhET-iO and Instrumentation](/guides/phet-io-and-instrumentation). | | `name` | A `TReadOnlyProperty`, not a plain string — this is what lets the sim title be translated. See [Translation and Localization](/guides/translation-and-localization). | Since a single-screen sim has no home screen or navigation-bar screen icons, `homeScreenIcon`/`navigationBarIcon` can be omitted; they become required in practice once you add a second screen (see [Building Your First Screen](/guides/building-your-first-screen)). ## Where to go next - [Building Your First Screen](/guides/building-your-first-screen) — adding a second screen and the model/view split in more depth - [Model-View Separation](/patterns/model-view-separation) — the architecture this example already follows - [Scenery Basics](/guides/scenery-basics) — everything you can put inside a `ScreenView` - [PhET-iO and Instrumentation](/guides/phet-io-and-instrumentation) — what `Tandem` actually buys you ======================================================================== Page: Accessible Interaction Tutorial URL: https://veillette.github.io/Almanach/guides/accessible-interaction-tutorial Source: docs/guides/accessible-interaction-tutorial.md Category: Guides | Tags: accessibility, tutorial, pdom, keyboard | Status: verified ======================================================================== # Accessible Interaction Tutorial This walks through making one custom interaction — a draggable "planet" — fully accessible: operable with a mouse, a touchscreen, and a keyboard alone, and correctly announced by a screen reader. Each step below is necessary; skipping any one of them leaves a subset of users unable to use the interaction. ## Step 0: the model and view, unaccessible Start from an ordinary [model-view separation](/patterns/model-view-separation) setup — a `positionProperty` and a `Circle` observing it: ```ts import { Property } from 'scenerystack/axon'; import { Vector2 } from 'scenerystack/dot'; import { Circle } from 'scenerystack/scenery'; const positionProperty = new Property( new Vector2( 0, 0 ) ); const planetNode = new Circle( 20, { fill: 'steelblue', cursor: 'pointer' } ); positionProperty.link( position => { planetNode.translation = position; } ); ``` At this point nothing is accessible: there's no pointer interaction yet, and the Node doesn't exist in the [Parallel DOM](/accessibility/pdom) at all. ## Step 1: pointer + keyboard dragging together Use `RichDragListener` so pointer and keyboard dragging are wired from one declaration instead of two separately-maintained listeners: ```ts import { RichDragListener } from 'scenerystack/scenery'; import { Bounds2 } from 'scenerystack/dot'; planetNode.addInputListener( new RichDragListener( { positionProperty: positionProperty, dragBoundsProperty: new Property( new Bounds2( -200, -200, 200, 200 ) ), keyboardDragListenerOptions: { dragSpeed: 150, // model units/second while a key is held shiftDragSpeed: 50 // finer control with Shift held } } ) ); ``` `RichDragListener`'s keyboard half only works once the Node is actually focusable — that's the next step. ## Step 2: joining the Parallel DOM Give the Node a `tagName` (placing it in the PDOM at all) and make it `focusable`: ```ts planetNode.tagName = 'div'; planetNode.focusable = true; ``` Without this, `RichDragListener`'s pointer half still works, but there is nothing in the tab order for a keyboard user to reach — the keyboard half of the listener never fires because the Node can never receive focus. ## Step 3: an accessible name and help text A focusable, unnamed element is barely better than an unfocusable one — a screen reader announces it as "group" or similar, with no indication of what it is or does: ```ts planetNode.accessibleName = 'Planet'; planetNode.accessibleHelpText = 'Move with arrow keys. Drag with a mouse or touch.'; ``` `accessibleName` is what's announced when focus lands on the element; `accessibleHelpText` is supplementary guidance announced alongside it — keep the name short (a noun phrase) and put instructions in the help text, not the name. ## Step 4: focus order If this Node isn't naturally reached in a sensible order via scene-graph traversal (e.g. it's added before some controls that should logically come first), set `pdomOrder` explicitly on its `ScreenView`: ```ts screenView.pdomOrder = [ massControlPanel, planetNode, resetAllButton ]; ``` ## Putting it together ```ts import { Property } from 'scenerystack/axon'; import { Vector2, Bounds2 } from 'scenerystack/dot'; import { Circle, RichDragListener } from 'scenerystack/scenery'; const positionProperty = new Property( new Vector2( 0, 0 ) ); const planetNode = new Circle( 20, { fill: 'steelblue', cursor: 'pointer', // Parallel DOM tagName: 'div', focusable: true, accessibleName: 'Planet', accessibleHelpText: 'Move with arrow keys. Drag with a mouse or touch.' } ); positionProperty.link( position => { planetNode.translation = position; } ); planetNode.addInputListener( new RichDragListener( { positionProperty: positionProperty, dragBoundsProperty: new Property( new Bounds2( -200, -200, 200, 200 ) ), keyboardDragListenerOptions: { dragSpeed: 150, shiftDragSpeed: 50 } } ) ); ``` Scenery draws a visible focus highlight around focusable Nodes automatically once they're focused; override it with the `focusHighlight` option only if the default rectangle is visually wrong for your shape (e.g. a highlight that should hug a circular Node more tightly). ::: tip Verify with a keyboard, not just by reading the code Unplug your mouse and tab to the Node: it should receive a visible focus highlight, be operable with arrow keys (and Shift for fine control), and a screen reader (or the browser's accessibility inspector) should announce the `accessibleName`/`accessibleHelpText` you set. Each of Steps 1–3 above is independently testable and independently easy to forget — verify all three, not just that dragging "works" with a mouse. ::: ## Where to go next - [The Parallel DOM (PDOM)](/accessibility/pdom) — the full accessibility-option reference this tutorial draws from - [Drag Listeners](/patterns/drag-listeners) — `DragListener`/`KeyboardDragListener`/`RichDragListener` in more depth - [Scenery Input](/guides/scenery-input) — the lower-level pointer/event model these listeners are built on ======================================================================== Page: Advanced Layout Techniques URL: https://veillette.github.io/Almanach/guides/advanced-layout-techniques Source: docs/guides/advanced-layout-techniques.md Category: Guides | Tags: scenery, layout, manual-constraint, align-group | Status: verified ======================================================================== # Advanced Layout Techniques [Scenery Layout](/guides/scenery-layout) covers `FlowBox`, `GridBox`, and `AlignBox` as if each layout problem needs exactly one container. Real screens usually need containers nested inside each other, the occasional position that no container option quite expresses, and content in *separate* containers that still needs to agree on a common size. This page covers the three techniques that come up once you're past the basics: nesting `FlowBox`/`GridBox`, dropping to `ManualConstraint` for the cases layout containers don't model, and `AlignGroup` for cross-container sizing. ## Nesting FlowBox and GridBox A `FlowBox` or `GridBox` doesn't care whether its children are leaf Nodes or other layout containers — nesting is just composition. This is how you build a two-axis layout out of two one-axis containers, or a toolbar-with-groups shape where each group has its own internal spacing: ```ts import { HBox, VBox, Text, Rectangle } from 'scenerystack/scenery'; const colorSwatch = ( color: string ) => new Rectangle( 0, 0, 16, 16, { fill: color } ); // A row of "swatch + label" pairs, each pair itself laid out as a small HBox. const legendRow = ( color: string, label: string ) => new HBox( { spacing: 6, children: [ colorSwatch( color ), new Text( label, { font: '14px sans-serif' } ) ] } ); // The rows stack vertically as a VBox, each row internally an HBox. const legend = new VBox( { spacing: 4, align: 'left', children: [ legendRow( 'crimson', 'Reactant' ), legendRow( 'royalblue', 'Product' ), legendRow( 'gray', 'Catalyst' ) ] } ); ``` Each inner `HBox` reports its own computed bounds up to the outer `VBox` exactly as any other child would — the outer container has no idea it's laying out other layout containers rather than leaf Nodes. This is the main tool for building anything more structured than "one row" or "one column": decompose the screen into the smallest one-axis pieces, then compose them. The one thing to watch when nesting is that both levels are trying to manage size. If an inner `HBox` has `stretch: true` children but the outer `VBox` never gives it a `preferredWidth` to stretch into (because, say, the outer container's own `align` is `'left'` rather than `'stretch'`), the inner stretch option silently has nothing to do. Match `align`/`stretch` intentionally through the nesting rather than copying it reflexively at every level. ## ManualConstraint: escaping to imperative positioning without losing reactivity `FlowBox`/`GridBox`/`AlignBox` cover flow and grid relationships, but not every layout is a flow or a grid — sometimes you need "this label's left edge is 5 pixels right of that icon's right edge, regardless of either one's size," expressed once and kept correct as either Node's bounds change. That's what `ManualConstraint` is for: it re-runs an arbitrary positioning callback you write, every time any of the Nodes it watches changes bounds, using `LayoutProxy` objects that behave like the positional getters/setters on `Node` (`left`, `right`, `centerY`, etc.) but work correctly even across a shared ancestor rather than requiring a direct parent-child relationship. ```ts import { ManualConstraint, Node, Circle, Text } from 'scenerystack/scenery'; const root = new Node(); const icon = new Circle( 12, { fill: 'orange' } ); const label = new Text( 'Temperature', { font: '16px sans-serif' } ); root.children = [ icon, label ]; new ManualConstraint( root, [ icon, label ], ( iconProxy, labelProxy ) => { labelProxy.left = iconProxy.right + 5; labelProxy.centerY = iconProxy.centerY; } ); ``` The first argument is the common ancestor Node whose coordinate frame the constraint reasons in — it doesn't need to be the direct parent of either watched Node, only an ancestor of both. The constraint re-runs the callback automatically whenever `icon` or `label`'s bounds change (a translated string swapping in a longer label, an icon resizing), so `labelProxy.left`/`centerY` stay correct without you re-wiring a bounds listener by hand. It's tempting to solve this same problem with `node.boundsProperty.link(...)` and manual `x`/`y` math instead — `ManualConstraint` is that same idea, generalized and already handling the cross-coordinate-frame case correctly, so prefer it over a bespoke listener unless you have a reason its callback shape genuinely can't express. ## AlignGroup: matching sizes across independent containers `AlignBox` (covered briefly in [Scenery Layout](/guides/scenery-layout)) wraps one child and reserves alignment space for it — but the interesting behavior shows up when several `AlignBox`es share one `AlignGroup`. Every box in the group reports the same size to whatever lays *it* out: the largest content among them, recomputed as any member's content changes. This is exactly the case where a set of sibling panels (say, one per element in a periodic-table-style grid, or one per row of a settings panel) need to occupy equal-sized cells even though their actual content — a short label versus a long one — doesn't. ```ts import { AlignGroup, AlignBox, HBox, Text, Panel } from 'scenerystack/scenery'; const group = new AlignGroup( { matchHorizontal: true, matchVertical: true } ); const makePanel = ( labelText: string ) => new Panel( group.createBox( new Text( labelText, { font: '16px sans-serif' } ), { xAlign: 'left', xMargin: 10 } ) ); const panels = new HBox( { spacing: 8, children: [ makePanel( 'On' ), makePanel( 'A much longer setting label' ), makePanel( 'Off' ) ] } ); ``` `group.createBox(...)` is the same as constructing an `AlignBox` directly with `{ group }` passed in its options — it's a convenience for the common case of "make a box in this group." All three panels above end up exactly as wide and tall as the longest/tallest one needs, even though each is built independently and doesn't know about its siblings. `matchHorizontal`/`matchVertical` (both default `true`) control which axis participates in the shared sizing — set one to `false` if you only want, say, matching height with each box free to be its natural width. Because `AlignGroup` recomputes its shared size whenever *any* member's content changes, order matters when content is populated dynamically: per the class's own guidance, if a group is populated incrementally and a UI component doesn't support resizing its own contents after the fact (some `sun` components), populate the group largest-content-first so the shared size is correct from the start rather than growing after initial layout. ::: warning AlignGroup sizing can lag one layout pass behind a bounds change The class doc for `AlignGroup` calls out that resizes may not take effect immediately — they can be deferred until the next bounds computation for a member box's content, though anything already connected to a `Display` picks this up automatically on `Display.updateDisplay()`. If you need the recomputed size synchronously (rare — usually only relevant in tests or before the group's boxes are attached to a Display), call `group.updateLayout()` explicitly rather than assuming the size is current the instant you swap a member's content. ::: ## Where to go next - [Scenery Layout](/guides/scenery-layout) — `FlowBox`/`GridBox`/`AlignBox` fundamentals this page builds on - [Scenery Basics](/guides/scenery-basics) — the `bounds`/`localBounds` distinction that all of these layout mechanisms depend on - [AlignBox](/api/scenery/align-box) — the per-child container `AlignGroup` coordinates across ======================================================================== Page: Building Your First Screen URL: https://veillette.github.io/Almanach/guides/building-your-first-screen Source: docs/guides/building-your-first-screen.md Category: Guides | Tags: screen, tutorial, joist | Status: verified ======================================================================== # Building Your First Screen [Your First Simulation](/getting-started/your-first-simulation) builds a single screen with no home screen. Adding a **second** screen changes two things: `homeScreenIcon`/`navigationBarIcon` become effectively required (there's now a home screen to show them on), and each screen's model/view pair needs to be self-contained enough to run independently, since users can jump between screens freely. ## 1. The model Keep the model free of any `scenery`/`sun` imports — see [Model-View Separation](/patterns/model-view-separation): ```ts // model/BounceScreenModel.ts import { NumberProperty, Property } from 'scenerystack/axon'; import { Vector2 } from 'scenerystack/dot'; export default class BounceScreenModel { public readonly positionProperty = new Property( new Vector2( 0, 0 ) ); public readonly speedProperty = new NumberProperty( 5 ); // m/s public step( dt: number ): void { // advance position by speed * dt, bounce off walls, etc. } public reset(): void { this.positionProperty.reset(); this.speedProperty.reset(); } } ``` ## 2. The view A `ScreenView` composes your custom `Node`s plus the shared components every screen needs — commonly a `ResetAllButton` wired to the [Reset-All Pattern](/patterns/reset-all-pattern): ```ts // view/BounceScreenView.ts import { ScreenView, type ScreenViewOptions } from 'scenerystack/sim'; import { Circle } from 'scenerystack/scenery'; import { ResetAllButton } from 'scenerystack/scenery-phet'; import type BounceScreenModel from '../model/BounceScreenModel.js'; export default class BounceScreenView extends ScreenView { public constructor( model: BounceScreenModel, providedOptions: ScreenViewOptions ) { super( providedOptions ); const ball = new Circle( 15, { fill: 'crimson' } ); model.positionProperty.link( position => { ball.translation = position; // through a ModelViewTransform2 in a real sim with non-1:1 scale } ); const resetAllButton = new ResetAllButton( { listener: () => model.reset(), right: this.layoutBounds.maxX - 20, bottom: this.layoutBounds.maxY - 20 } ); this.children = [ ball, resetAllButton ]; } public step( dt: number ): void { // view-only animation, if any; model stepping happens via Screen's own step wiring } } ``` Use [`FlowBox`/`GridBox`](/guides/scenery-layout) instead of hand-placed `Circle`s once a screen has more than a couple of controls. ## 3. Icons for the home screen and navigation bar With more than one screen, each `Screen` needs a `ScreenIcon` — a small preview shown on the home screen and (scaled down) in the navigation bar: ```ts import { ScreenIcon } from 'scenerystack/sim'; import { Rectangle } from 'scenerystack/scenery'; const bounceScreenIcon = new ScreenIcon( new Rectangle( 0, 0, 100, 100, { fill: 'crimson' } ), { maxIconWidthProportion: 0.85, maxIconHeightProportion: 0.85, fill: 'white' // background behind the icon content } ); ``` If `navigationBarIcon` is omitted, it defaults to `homeScreenIcon`, scaled down — supply a distinct one only if the default is illegible at navigation-bar size. ## 4. Wiring the Screen ```ts // bounce-screen-main.ts import { Screen } from 'scenerystack/sim'; import { Tandem } from 'scenerystack/tandem'; import { Property } from 'scenerystack/axon'; import BounceScreenModel from './model/BounceScreenModel.js'; import BounceScreenView from './view/BounceScreenView.js'; const screenTandem = Tandem.ROOT.createTandem( 'bounceScreen' ); export const bounceScreen = new Screen( () => new BounceScreenModel(), model => new BounceScreenView( model, { tandem: screenTandem.createTandem( 'view' ) } ), { name: new Property( 'Bounce' ), backgroundColorProperty: new Property( 'white' ), homeScreenIcon: bounceScreenIcon, tandem: screenTandem } ); ``` ## 5. Adding it to the Sim Pass every screen's exported `Screen` instance to `Sim` in array order — that order becomes both the home screen layout and the navigation bar order: ```ts import { Sim, onReadyToLaunch } from 'scenerystack/sim'; import { Property } from 'scenerystack/axon'; import { firstScreen } from './first-screen-main.js'; import { bounceScreen } from './bounce-screen-main.js'; onReadyToLaunch( () => { const sim = new Sim( new Property( 'My Simulation' ), [ firstScreen, bounceScreen ] ); sim.start(); } ); ``` ::: tip Each screen's model/view pair should assume nothing about other screens Since `Screen`'s `createModel`/`createView` factories run lazily and users can switch screens at any point, a screen's model and view must be constructible and steppable in isolation — don't reach into another screen's model directly. Share state across screens (if genuinely needed) via a model object constructed once in the main file and passed to both screens' factories, not via one screen importing another's internals. ::: ## Where to go next - [PhET-iO and Instrumentation](/guides/phet-io-and-instrumentation) — what the `tandem` wiring above actually enables - [Scenery Layout](/guides/scenery-layout) — arranging a screen's controls with `FlowBox`/`GridBox` instead of manual positions - [The Reset-All Pattern](/patterns/reset-all-pattern) — the full contract for `model.reset()` and `ResetAllButton` ======================================================================== Page: Chart and Data Visualization with Bamboo URL: https://veillette.github.io/Almanach/guides/chart-and-data-visualization-with-bamboo Source: docs/guides/chart-and-data-visualization-with-bamboo.md Category: Guides | Tags: bamboo, chart, data-visualization, ChartTransform | Status: complete ======================================================================== # Chart and Data Visualization with Bamboo `bamboo` (`scenerystack/bamboo`) is SceneryStack's charting library — it renders datasets as line/bar/scatter/area plots, draws axes and gridlines, and does it all through one shared coordinate object every piece of a chart is built around. This page is a subsystem-level tour of how the pieces fit together; each linked API page documents one class in depth. ## ChartTransform: the object everything else is built on Every bamboo Node — every plot type, every axis, every gridline set — takes a [`ChartTransform`](/api/bamboo/chart-transform) in its constructor and rebuilds itself whenever that transform's `changedEmitter` fires. `ChartTransform` is not itself a `Node` — it's a plain object holding the chart's model-coordinate ranges (`modelXRange`, `modelYRange`) and view-coordinate size (`viewWidth`, `viewHeight`), plus the conversion math between the two: ```ts import { ChartTransform, ChartRectangle, LinePlot, AxisArrowNode } from 'scenerystack/bamboo'; import { Orientation } from 'scenerystack/phet-core'; import { Range, Vector2 } from 'scenerystack/dot'; const chartTransform = new ChartTransform( { viewWidth: 400, viewHeight: 200, modelXRange: new Range( 0, 10 ), modelYRange: new Range( -1, 1 ) } ); const chartRectangle = new ChartRectangle( chartTransform, { stroke: 'gray' } ); const xAxis = new AxisArrowNode( chartTransform, Orientation.HORIZONTAL ); const dataSet: ( Vector2 | null )[] = []; for ( let x = 0; x <= 10; x += 0.1 ) { dataSet.push( new Vector2( x, Math.sin( x ) ) ); } const linePlot = new LinePlot( chartTransform, dataSet, { stroke: 'blue' } ); // A resize (or panning/zooming the chart) is one call, and every attached Node redraws together. chartTransform.setViewWidth( 600 ); ``` This is the single most important thing to internalize about bamboo: **a chart is a loose collection of independent Nodes that happen to share one `ChartTransform`**, not one monolithic "Chart" class you configure. Adding a second plot type to the same chart is just constructing another Node against the same `chartTransform` — there's no chart-level API to register plots with. ## The plot Node family Each plot type is a distinct `Node` subclass specialized for one data shape — pick based on what the dataset actually represents, not by look alone: | Plot | Dataset shape | Use it for | | --- | --- | --- | | [`LinePlot`](/api/bamboo/line-plot) | `(Vector2 \| null)[]`, connected by straight segments | A sampled continuous function or time series; `null` entries create gaps for missing/discontinuous data | | [`AreaPlot`](/api/bamboo/area-plot) | Same shape as `LinePlot` | The same kind of data as `LinePlot`, shaded down to a baseline — cumulative quantities, or emphasizing magnitude rather than just the curve's shape | | [`BarPlot`](/api/bamboo/bar-plot) | `Vector2[]`, one bar per point, numeric x only | Discrete quantities at discrete x-values, growing from a shared baseline | | [`ScatterPlot`](/api/bamboo/scatter-plot) | `Vector2[]`, one circle per point | Unconnected discrete data points — measurements, samples — where connecting them with lines would imply a relationship that isn't there | | [`LinearEquationPlot`](/api/bamboo/linear-equation-plot) | A slope `m` and intercept `b`, no dataset at all | A reference/threshold/best-fit line spanning the chart's entire model range, not a curve through sampled points | All five extend either `scenery`'s `Path` or `Node` and rebuild their drawn geometry in an internal `update()`, called on construction and whenever `chartTransform.changedEmitter` fires — so once a plot is constructed against a transform, updating the transform (a resize, a pan) or calling the plot's own `setDataSet()` is all that's needed to keep it visually correct; nothing needs to be manually re-laid-out. ## Axes and gridlines: decoration, not structure [`AxisLine`/`AxisArrowNode`](/api/bamboo/axis-nodes) draw the x/y axis lines themselves (plain or arrow-tipped), and [`GridLineSet`, `TickMarkSet`, and `TickLabelSet`](/api/bamboo/gridlines-and-tick-marks) draw the repeating reference lines, tick marks, and numeric tick labels behind or alongside the data, all at a fixed model-coordinate spacing. All of these are, like the plots, independent Nodes constructed against the same `ChartTransform` — a chart with no axis or gridline Nodes at all is still a perfectly valid, if sparse, bamboo chart; they're additive decoration, not something a plot depends on to render correctly. ## Composing a full chart A typical bamboo chart is a `Node` (or the `ScreenView` itself) with several bamboo Nodes as children, all built from one `ChartTransform`, layered in the usual scenery stacking order — background/gridlines first, data plots in the middle, axes and labels on top: ```ts import { GridLineSet } from 'scenerystack/bamboo'; const verticalGridLines = new GridLineSet( chartTransform, Orientation.VERTICAL, 1, { stroke: 'lightGray' } ); const chartNode = new Node( { children: [ chartRectangle, // background + border verticalGridLines, // reference gridlines, behind the data linePlot, // the data itself xAxis // axis line/arrow on top ] } ); ``` Panning or zooming the whole chart is then a single `chartTransform.setModelXRange( newRange )` (or `setViewWidth`/`setViewHeight` for a resize) — every child Node listening to `changedEmitter` redraws itself in response, with no coordination code needed between them. ## Large datasets: CanvasLinePlot instead of LinePlot Every plot type above extends an ordinary scenery `Path`/`Node`, which means a dataset large enough (many thousands of points, redrawn every frame) can start to show up as real overhead — one scenery drawable's bookkeeping cost, multiplied by every point. [`CanvasLinePlot`](/api/bamboo/canvas-line-plot) is bamboo's answer for that specific case: it paints the same kind of `(Vector2 | null)[]` dataset as `LinePlot`, but directly to a `CanvasRenderingContext2D` via a shared `ChartCanvasNode`, rather than building a scenery `Shape`. Reach for it only once profiling (see [Performance and Profiling](/guides/performance-and-profiling)) actually shows `LinePlot`'s per-point overhead as the bottleneck — for anything smaller, `LinePlot`'s automatic redraw wiring and ordinary `Path` semantics (hit-testing, easy composition with other Nodes) are simpler to work with. ::: tip Dispose a chart's Nodes the same as any dynamically-created view Every bamboo Node holds a listener on its `ChartTransform`'s `changedEmitter`, which its own `dispose()` removes — but if a chart itself is created and destroyed dynamically (not for the sim's lifetime), the usual [Dispose and Memory Management](/patterns/dispose-and-memory-management) discipline still applies: dispose each plot/axis Node (and the `ChartTransform` itself, via its own `dispose()`) when the chart is torn down, the same as any other transient view. ::: ## Where to go next - [ChartTransform](/api/bamboo/chart-transform) — the full API for the shared coordinate object this page is built around - [LinePlot](/api/bamboo/line-plot), [BarPlot](/api/bamboo/bar-plot), [ScatterPlot](/api/bamboo/scatter-plot), [AreaPlot](/api/bamboo/area-plot), [LinearEquationPlot](/api/bamboo/linear-equation-plot) — the individual plot types - [AxisLine and AxisArrowNode](/api/bamboo/axis-nodes) — axis decoration - [GridLineSet, TickMarkSet, and TickLabelSet](/api/bamboo/gridlines-and-tick-marks) — gridline/tick decoration - [CanvasLinePlot](/api/bamboo/canvas-line-plot) — the Canvas-painted variant for very large datasets ======================================================================== Page: Continuous Integration for SceneryStack Projects URL: https://veillette.github.io/Almanach/guides/continuous-integration-for-scenerystack-projects Source: docs/guides/continuous-integration-for-scenerystack-projects.md Category: Guides | Tags: ci, testing, build, tooling, quality | Status: complete ======================================================================== # Continuous Integration for SceneryStack Projects A SceneryStack simulation is a normal bundler-based TypeScript web app (see [Running and Building a Simulation](/getting-started/running-and-building-a-simulation)), so its CI pipeline looks like any other TypeScript project's — the checks below aren't SceneryStack-specific machinery, they're the standard set applied to a project that happens to be a simulation. What *is* worth calling out is the order and the specific failure modes each check is aimed at, since a sim repo has a couple of failure modes (assertion-only bugs, fuzz-crashes) a generic web app CI checklist wouldn't otherwise think to include. ## What a pipeline typically checks, in order | Check | Catches | Roughly maps to | | --- | --- | --- | | Type-check (`tsc --noEmit`, or the bundler's own type-check step) | Type errors that would only surface at runtime otherwise | Fastest, cheapest check — run first so a trivial mistake fails in seconds | | Lint (ESLint, or whatever the project scaffolded) | Style/consistency issues, common bug patterns (unused variables, missing `dispose()` overrides caught by custom rules, etc.) | Also fast, independent of the other checks | | Unit tests | Model logic regressions | [Testing Model Logic Headlessly](/patterns/testing-model-logic-headlessly) — runs in plain Node.js, no browser needed | | Production build (`npm run build`) | Bundler configuration errors, missing assets, import errors that only show up once the whole dependency graph is resolved | [Running and Building a Simulation](/getting-started/running-and-building-a-simulation) | | Automated fuzz-testing (optional but recommended) | Crashes from input sequences no test author thought to write by hand | [Fuzz-Testing a Simulation Locally](/cookbook/fuzz-testing-a-simulation-locally), run headlessly against a built or served sim | Ordering matters mostly for feedback speed: type-checking and linting fail in seconds and don't require installing a browser or building anything, so they should fail first and fast, before a slower build or fuzz step ever starts. A CI configuration that runs the production build before the type-check just means a contributor waits longer to find out about a typo. ## Why the production build itself is a check, not just an artifact `npm run build` catches a category of bug that neither the type-checker nor unit tests can see: a bundler misconfiguration (a wrong base path, an unresolvable asset import, a circular dependency the dev server tolerated but the production bundler doesn't) only surfaces when the whole project is actually bundled for production. Running `npm run build` in CI — even if nothing downstream of it deploys the result — is what guarantees "it built on my machine" isn't the first time a break in the build is discovered. ## Automated fuzzing in CI Once a sim can be served (locally, or from the CI job's own build output), running it under `?fuzz` for a fixed duration and failing the job on any uncaught exception (see [Fuzz-Testing a Simulation Locally](/cookbook/fuzz-testing-a-simulation-locally)) extends the "does this crash" question from "did a human happen to try this sequence of clicks" to "did we throw a few thousand randomized ones at it." This is the one check in the table that needs an actual browser (typically headless, via Playwright/Puppeteer or similar) rather than just Node.js — budget it as a separate, slower CI job if the rest of the pipeline is otherwise browser-free. ## A concrete illustration: Almanach's own CI This documentation site isn't a simulation, but its own CI pipeline (`.github/workflows/ci.yml` in this repository) follows the same shape at a smaller scale, and is a real, inspectable example of the pattern: checkout, install with `npm ci` (not `npm install` — reproducible against the lockfile), then two validation steps (`npm run generate`, which fails loudly on frontmatter/link errors, and `npx vitepress build docs`, the production build) — both gating whether a pull request can merge. A separate workflow (`.github/workflows/deploy.yml`) only runs the production build and publishes it, on pushes to `main`, after those checks would already have passed on the corresponding PR. The same two-workflow split — one that verifies on every change, one that builds-and-publishes only on `main` — is a reasonable default for a sim repo too: run type-check/lint/tests/build/fuzz on every PR, and reserve the publish step (see [Deploying a Simulation to GitHub Pages](/getting-started/deploying-to-github-pages)) for a separate workflow gated on the first one passing. ::: tip Use `npm ci`, not `npm install`, in every CI job `npm ci` installs exactly the versions recorded in `package-lock.json` and fails outright if the lockfile and `package.json` are out of sync, rather than silently resolving new versions the way `npm install` can. A CI pipeline's entire value proposition — "if it's green, it works the same way it will everywhere else" — depends on every run installing identical dependencies, not whatever happened to be newest that day. ::: ## Where to go next - [Testing and QA Strategy Overview](/guides/testing-and-qa-strategy-overview) — how CI's automated checks fit alongside manual QA passes - [Running and Building a Simulation](/getting-started/running-and-building-a-simulation) — the build step CI is verifying - [Fuzz-Testing a Simulation Locally](/cookbook/fuzz-testing-a-simulation-locally) — the recipe a fuzz-testing CI job automates ======================================================================== Page: Debugging Common Scenery Rendering Issues URL: https://veillette.github.io/Almanach/guides/debugging-common-scenery-rendering-issues Source: docs/guides/debugging-common-scenery-rendering-issues.md Category: Guides | Tags: scenery, debugging, rendering, layout, troubleshooting | Status: complete ======================================================================== # Debugging Common Scenery Rendering Issues Most "why isn't this showing up" or "why is this in the wrong place" bugs in a scenery scene graph fall into a small number of recurring shapes. This page walks through the three most common ones and the specific thing to check for each, before reaching for a debugger or filing a bug against scenery itself — the overwhelming majority of these turn out to be one property on one Node. ## "My Node isn't visible" Work through these in order — each is more specific than the last, and any one of them alone is enough to make otherwise-correct content invisible: | Check | Why it hides content | | --- | --- | | `node.visible` / `node.visibleProperty.value` | `false` removes the Node from painting (and, by default, from input) while leaving it in the tree — this is the most common cause, especially when visibility is driven by a `DerivedProperty` that isn't computing what you expect | | `node.opacity` | `0` paints nothing even though the Node is technically `visible`; easy to leave at `0` after a fade-out animation that never reset it | | `node.bounds` / `node.localBounds` | A Node with empty or `NaN` bounds (e.g. a `Path` built from an empty `Shape`, or geometry computed from a not-yet-loaded value) often silently draws nothing rather than throwing — check `bounds` in the console for `NaN` or a zero-size rectangle | | Ancestor visibility | `visible`/`opacity` on this Node don't matter if an **ancestor** in the tree is invisible or fully transparent — walk up the parent chain, not just the Node itself, when a whole region is missing | | Being added at all | A Node constructed but never `addChild`-ed anywhere in the displayed tree is, correctly, never painted — confirm the Node is actually reachable from the `Display`'s root, not just that it exists | | `pickable` (a distinct but related symptom) | `pickable = false` doesn't hide anything visually, but if the *symptom* you're chasing is actually "clicks pass through" rather than "nothing draws," this — not `visible` — is the property to check; see [Hit-Testing and Picking](/api/scenery/hit-testing-and-picking) | ## "My layout isn't updating" Layout bugs are usually a mismatch between what you changed and what the layout container actually watches: - **Check `preferredWidth`/`preferredHeight` are actually being set on the container that should resize.** Scenery's [`Sizable`](/guides/scenery-layout) trait-based Nodes (`FlowBox`, `GridBox`, and anything opting into `WidthSizable`/`HeightSizable`) resize their content in response to their own `preferredWidth`/`preferredHeight` being set by *something* — usually a parent layout container, or the `ScreenView`'s `visibleBoundsProperty` for top-level content. If nothing ever sets a preferred dimension on a Sizable Node, it stays at its content's natural size no matter how much surrounding space is available. - **Check you're not fighting the container by setting `x`/`y`/`translation` directly.** As [Scenery Layout](/guides/scenery-layout#margins-shared-across-containers) points out, once a Node is a child of a `FlowBox`/`GridBox`, the container sets its position every layout pass — any `translation` you set afterward is silently overwritten on the next pass, which reads exactly like "layout isn't updating" when the actual cause is "layout keeps winning." - **Check `layoutOptions` are on the child, not the container**, for per-child overrides (`grow`, per-child margins). A `layoutOptions` object set on the wrong Node — the container instead of the child, or vice versa — is silently ignored rather than erroring. - **Confirm the thing that changed size is actually observed.** If a child's own content changed (a `Text` Node's string got longer after a translated-string update), its `bounds` change automatically and its parent `FlowBox`/`GridBox` re-lays-out on the next frame — but a manually-managed layout (Nodes positioned by hand, not inside a layout container) has no such mechanism and needs its position recomputed explicitly wherever the content change is handled. ## "Things are drawn in the wrong stacking order" Scenery has no z-index — stacking order is purely the order of entries in each Node's `children` array, drawn first-to-last (see [Scenery Basics](/guides/scenery-basics#the-node-tree)). A Node "behind" something it should be in front of (or vice versa) is a `children` ordering bug, not a separate property to set: - `node.addChild( child )` always appends to the *end* — the topmost stacking position — regardless of the child's position in space. If a new Node needs to render underneath existing siblings, use `insertChild( 0, child )` (or the appropriate index) instead of `addChild`. - Reordering existing children (moving one to the front without removing/re-adding it) is `node.moveChildToFront( child )` / `moveChildToBack( child )`, not a style property. - Remember this is a **per-parent** ordering: two Nodes with no common parent don't have a meaningful relative stacking order to reason about — if two subtrees visually overlap and need a deterministic order, they need a common ancestor whose `children` array you control. ## Tools beyond eyeballing the tree Once the checks above don't turn up the cause, [Performance and Profiling](/guides/performance-and-profiling#finding-the-bottleneck-built-in-profiling-query-parameters) documents several built-in debug query parameters that also help narrow down rendering (not just performance) problems — `?showPointerAreas` and `?showHitAreas` for input-region mismatches, `?showCanvasNodeBounds` for a `CanvasNode` drawing outside its declared bounds, and `?dev` for a broader bundle of developer-facing overlays. For content painted through the non-standard renderer escape hatches, see [`WebGLNode`, `CanvasNode`, and `DOM`](/api/scenery/rendering-backends) — bugs in a `CanvasNode`'s `paintCanvas` or a `WebGLNode`'s painter are ordinary drawing-code bugs in your own callback, not scenery issues, and are usually easiest to isolate by temporarily swapping the custom Node for a plain `Rectangle` at the same bounds to confirm the surrounding layout/visibility is correct first. ::: tip Bisect with a plain Rectangle When it's unclear whether a missing/misplaced Node is a visibility, layout, or stacking-order bug, temporarily replace the suspect Node's content with a brightly-colored plain `Rectangle` sized to its expected `bounds`. If the rectangle shows up correctly, the problem is specific to the original Node's own content/geometry; if the rectangle is also missing or misplaced, the problem is upstream — visibility, layout, or an ancestor — and you've eliminated an entire category without reading more code. ::: ## Where to go next - [Scenery Basics](/guides/scenery-basics) — the Node tree, coordinate frames, and children-order stacking model this page assumes - [Scenery Layout](/guides/scenery-layout) — the layout containers and options referenced above - [Performance and Profiling](/guides/performance-and-profiling) — the built-in debug query parameters, applied to slowness rather than incorrectness - [Display](/api/scenery/display) — the object that actually paints the tree these bugs live in - [Hit-Testing and Picking](/api/scenery/hit-testing-and-picking) — the full `pickable`/`mouseArea`/`touchArea` model referenced above ======================================================================== Page: Haptics and Alternative Feedback Channels URL: https://veillette.github.io/Almanach/guides/haptics-and-alternative-feedback-channels Source: docs/guides/haptics-and-alternative-feedback-channels.md Category: Guides | Tags: tambo, tappi, sound, vibration, haptics, accessibility | Status: verified ======================================================================== # Haptics and Alternative Feedback Channels Everything covered in [Scenery Basics](/guides/scenery-basics) is visual — Nodes, painted pixels, a `Display`. Not every user perceives a simulation that way, and even for users who do, a purely visual interaction misses feedback channels that a physical object would naturally provide. SceneryStack has two built-in, non-visual feedback channels: sound, via `tambo`, and vibration, via `tappi`. Both exist for the same underlying reason — giving a user information about what just happened without requiring them to be looking at (or looking closely at) the screen — but they suit different situations and hardware. ## Sound: tambo `tambo` (`scenerystack/tambo`) is the more broadly applicable of the two — it works on essentially any device with audio output, requires no special hardware, and is useful for both sighted and low-vision/blind users. [Working with Sound](/guides/working-with-sound) covers the subsystem in full: a central `soundManager` that mixes together individually-registered **sound generators** (`SoundClip` for recorded assets, `PitchedPopGenerator` for pitch-continuous synthesized feedback, and others), grouped into categories a user can enable/disable independently. Two distinct reasons a sim reaches for sound: - **Confirmation and delight** — a click, a chime on success, an ambient hum while something is running — feedback that enhances the experience for every user, sighted or not, the same way physical toys and instruments make sound as a byproduct of interaction. - **Accessibility (sonification)** — using the same tambo machinery specifically so a non-visual user (or a sighted user not looking at the screen) can perceive continuous state or discrete events that would otherwise be visual-only. [Sound Design](/accessibility/sound-design) covers this application specifically, including the `sharedSoundPlayers` registry that keeps common interactions (checkbox toggles, button presses, reset) sounding consistent across every sim without each one authoring its own. ## Vibration: tappi `tappi`'s `vibrationManager` (`scenerystack/tappi`) is the haptic equivalent of `soundManager` — a central point that drives the device's vibration hardware in response to simulation events, using vibration "patterns" (a sequence of on/off durations and intensities) rather than raw single buzzes. See [vibrationManager and Vibration Patterns](/api/tappi/vibration-manager-and-patterns) for the API itself. Vibration is a narrower tool than sound for a simple hardware reason: it requires a device with a vibration motor the browser can actually drive (most phones/tablets; effectively no desktop browsers), so it's necessarily a *supplementary* channel layered on top of visual and/or sound feedback, never the only way an interaction communicates something. Where it earns its place is touch-first interactions where the device itself is already in the user's hand — confirming a drag has snapped into place, signaling a boundary has been reached, or reinforcing a discrete event (a collision, a successful match) with a physical pulse that doesn't require audio output at all (useful in shared/classroom settings where sound might be muted or inappropriate). ## Choosing a channel | Channel | Requires | Reaches | Typical use | | --- | --- | --- | --- | | Visual (scenery) | Any display | Every user by default | The primary channel; everything else is supplementary | | Sound (tambo) | Audio output, not muted | Sighted and non-sighted users alike | Confirmation/delight, and sonification for low-vision/blind users | | Vibration (tappi) | A device with a vibration motor (mobile/touch) | Users on supported touch hardware, sound-independent | Reinforcing a touch interaction physically, especially where audio may be muted or unavailable | None of these are exclusive — a well-designed touch interaction on mobile might pair a visual snap-into-place animation, a short confirmation sound, and a vibration pulse for the same event, so the feedback reaches the user regardless of which channel they happen to be attending to (or which channels their device/settings support) at that moment. ::: tip Non-visual channels are additive, not a replacement for the PDOM Sound and vibration are useful accessibility *supplements*, but neither substitutes for the structured, navigable accessibility tree the [PDOM](/accessibility/pdom) provides for screen-reader users, or for [Voicing](/accessibility/voicing)'s spoken descriptions. A collision sound tells a user *something* happened; it doesn't tell them *what* the current state is in a way a screen reader can query on demand. Treat tambo/tappi feedback as reinforcement layered on top of a genuinely accessible PDOM structure, not a shortcut around building one. ::: ## Where to go next - [Working with Sound](/guides/working-with-sound) — the full tambo subsystem tour - [Sound Design](/accessibility/sound-design) — sound specifically as an accessibility channel - [vibrationManager and Vibration Patterns](/api/tappi/vibration-manager-and-patterns) — the tappi API this page summarizes ======================================================================== Page: Internationalization Deep Dive URL: https://veillette.github.io/Almanach/guides/internationalization-deep-dive Source: docs/guides/internationalization-deep-dive.md Category: Guides | Tags: localization, i18n, rtl, strings | Status: verified ======================================================================== # Internationalization Deep Dive [Translation and Localization](/guides/translation-and-localization) covers the core idea — every displayed string is a `Property`, never a literal — and the pipeline that turns per-locale JSON files into those Properties at build/runtime. This page goes one level deeper into three things that pipeline alone doesn't handle: how the *active* locale is actually determined and switched, what changes about layout once a right-to-left language is involved, and how to build translated strings that need pluralization or multiple placeholders correctly, rather than by string concatenation. ## Runtime locale: where it comes from, and whether it can change `localeProperty` (from `scenerystack/joist`) is the single source of truth for the sim's active locale — every generated `StringProperty` reflects the string data for whatever locale this Property currently holds: ```ts import { localeProperty } from 'scenerystack/joist'; console.log( localeProperty.value ); // e.g. 'en', 'fr', 'es', 'zh_CN' ``` It's initialized once at startup from the `?locale=` query parameter (falling back to the browser's own locale, then to `'en'`), which is why [Translation and Localization](/guides/translation-and-localization) describes it as "normally set once at startup" rather than something application code changes mid-session. `localeProperty` is a real, settable `Property` — nothing in the type system stops you from writing to it after startup — but production PhET simulations don't do this as a live "language switcher" feature; a full reload with a different `?locale=` value is the supported path for changing languages, since a substantial amount of layout (see below) is easiest to get right when it's computed fresh rather than live-relaid mid-session. If you do need to read the list of locales a specific build actually has string data for (rather than assuming every locale is available), that's exposed alongside `localeProperty` rather than hardcoded — check the actual runtime value rather than assuming a fixed list, since which locales are available for a given build is a locale-file/build-time concern (see [Project Structure Conventions](/getting-started/project-structure-conventions)), not something this API layer fixes at compile time. ## Right-to-left languages Some locales (Arabic, Hebrew, and others) lay out text right-to-left rather than left-to-right, and a simulation whose layout was only ever built and tested against English can end up with UI that's technically translated but reads backwards. `isLeftToRightProperty` (also from `scenerystack/joist`) is a derived boolean tracking the current locale's writing direction, recomputed automatically whenever `localeProperty` changes: ```ts import { isLeftToRightProperty } from 'scenerystack/joist'; isLeftToRightProperty.link( isLTR => { // true for locales like 'en', 'fr'; false for RTL locales like 'ar', 'he' } ); ``` `RichText` and the rest of scenery's text-rendering Nodes already handle bidirectional *text* rendering correctly (Arabic/Hebrew characters shape and flow right-to-left within a `Text`/`RichText` Node on their own). What `isLeftToRightProperty` is for is *layout structure* that your own code built assuming left-to-right — a row of icon-then-label pairs, a "Play" button placed at the visual left of a control strip, a `FlowBox` whose `justify` was chosen assuming reading order flows left to right. For content where left/right placement carries meaning (not just text shaping), branch on this Property rather than assuming your layout is direction-agnostic by default: ```ts import { HBox } from 'scenerystack/scenery'; const controlBox = new HBox( { spacing: 8, // FlowBox's own layout math doesn't auto-mirror; reverse the children order for RTL locales children: isLeftToRightProperty.value ? [ icon, label ] : [ label, icon ] } ); ``` ::: warning Test with a real RTL locale, not just a mirrored screenshot It's tempting to "check RTL support" by visually flipping a screenshot. Actually running with `?locale=ar` (or whichever RTL locale the sim ships strings for) surfaces the cases a mirrored screenshot can't: text truncation from different average string lengths, numerals that stay left-to-right even inside RTL text, and layout containers that were never told to reverse their child order. ::: ## Pluralization and multiple placeholders: PatternStringProperty [Translation and Localization](/guides/translation-and-localization) shows `StringUtils.fillIn` for one-off placeholder substitution. For a *reactive* translated string — one that needs to update automatically as the values feeding it change, without you re-calling `fillIn` by hand on every change — [`PatternStringProperty`](/api/axon/pattern-string-property) is the idiomatic tool, and it's also where pluralization-style patterns are handled correctly across languages: ```ts import { PatternStringProperty, NumberProperty } from 'scenerystack/axon'; const countProperty = new NumberProperty( 1 ); // patternProperty is itself a translated StringProperty, e.g. '{{count}} particles remaining' const remainingMessageProperty = new PatternStringProperty( patternProperty, { count: countProperty } ); ``` The reason this matters beyond convenience: word order and pluralization rules vary across languages in ways that a single hardcoded English sentence shape can't represent. A translator working from `'{{count}} particles remaining'` can reorder or restructure the pattern entirely for a language with different pluralization/word-order rules — the *placeholder* is what's fixed, not the surrounding sentence structure — whereas code that concatenates `count + ' particles remaining'` gives translators nothing to restructure. `PatternStringProperty` also supports `decimalPlaces` (rounding numeric values consistently at substitution time) and a `maps` option (transforming a value before substitution, e.g. converting grams to kilograms) — see [`PatternStringProperty`](/api/axon/pattern-string-property) for the full option set. Because `PatternStringProperty` is a `DerivedProperty` under the hood, treat it the same way: it can't be set directly, it lazily links to every dependency Property you pass in, and any instance you create dynamically (per list item, per dynamically-created model element) needs `dispose()` — see [Dispose and Memory Management](/patterns/dispose-and-memory-management). ## Where to go next - [Translation and Localization](/guides/translation-and-localization) — the `StringProperty` foundation and the translation pipeline this page assumes - [`PatternStringProperty`](/api/axon/pattern-string-property) — full constructor/options reference for pattern substitution - [`StringProperty`](/api/axon/string-property) — the base Property type every translated string, plural or not, is built on ======================================================================== Page: Modifying SceneryStack URL: https://veillette.github.io/Almanach/guides/modifying-scenerystack Source: docs/guides/modifying-scenerystack.md Category: Guides | Tags: contributing, open-source, workflow | Status: verified ======================================================================== # Modifying SceneryStack The `scenerystack` npm package is a **built artifact**: its `dist/` bundles and `src/` barrel files are compiled from dozens of separate PhET repositories (`scenery`, `axon`, `joist`, `sun`, `scenery-phet`, and more, all under [github.com/phetsims](https://github.com/phetsims)). If you ever need to patch a bug in one of those libraries — not just work around it in your own code — you check out the source repositories and rebuild the package locally, rather than editing files inside `node_modules`. ::: warning Never hand-edit `node_modules/scenerystack` Any change made directly inside `node_modules` is silently lost the next time anything runs `npm install`. If you need to change SceneryStack's own code, follow the checkout/build workflow below in a separate directory and point your project at the result — don't patch the installed copy in place. ::: ## Checking out the source repositories In an empty directory (not your project's `node_modules`): ```bash mkdir scenerystack-src && cd scenerystack-src npx scenerystack checkout ``` This clones every underlying repository (`scenery`, `axon`, `joist`, `sun`, `scenery-phet`, `dot`, `kite`, `tandem`, `phetcommon`, and the rest) into the current directory. Running it again later pulls newer sources into the same checkout. To pin a specific released version instead of the latest sources: ```bash npx scenerystack checkout 0.0.14 ``` ## Making your change Edit the relevant file in whichever cloned repository owns it — e.g. a `scenery` drag-interaction bug lives under `scenery/js/listeners/`, a `sun` component bug under `sun/js/`. Since these are real git checkouts of the PhET repositories, normal git workflow applies: branch, commit, and (if you intend to contribute upstream) open a pull request against the corresponding `phetsims/` repository. ## Rebuilding the package From the same directory: ```bash npx scenerystack build ``` This builds the entire stack — including your modified repository — into a local `scenerystack` package output. ## Pointing your project at the local build In the project that needs the patched behavior, replace the registry dependency with a file reference in `package.json`: ```json { "dependencies": { "scenerystack": "file:../scenerystack-src/scenerystack" } } ``` Then reinstall so npm picks up the local copy: ```bash npm install ``` Your project now imports the patched code through the exact same `scenerystack/` subpaths as before — nothing about your application code changes, only where the package resolves from. ## Where to go next - [Installation and Setup](/getting-started/installation-and-setup) — the normal (registry) installation path this workflow diverges from - [What is SceneryStack?](/getting-started/what-is-scenerystack) — the full list of libraries bundled into the package ======================================================================== Page: Performance and Profiling URL: https://veillette.github.io/Almanach/guides/performance-and-profiling Source: docs/guides/performance-and-profiling.md Category: Guides | Tags: scenery, performance, profiling, rendering | Status: verified ======================================================================== # Performance and Profiling A slow SceneryStack simulation is almost never slow because scenery itself is slow — it's slow because the scene graph is doing more repaint/relayout work than the visible result needs, or because the model's `step`/listener graph is doing more computation per frame than the animation actually requires. This page covers how to *see* where the time goes before guessing, and the handful of causes that account for most real slowdowns: Node count and structure, `Property`/listener overhead, and choosing the wrong renderer for a subtree. ::: warning Profile before you optimize Every technique below (picking a `renderer`, flattening Node structure, batching `Property` updates) has a real cost in code complexity or flexibility. Applied to a subtree that was never the bottleneck, it's pure overhead with no payoff. Use the query parameters in the next section to find the actual slow part first — guessing which Node or Property is "probably expensive" reliably wastes effort on the wrong thing. ::: ## Finding the bottleneck: built-in profiling query parameters Every SceneryStack simulation ships with debug instrumentation reachable via query parameters — no extra setup required, since these are wired into the shared `joist`/`chipper` initialization every `Sim` goes through: | Query parameter | Shows | | --- | --- | | `?profiler` | An FPS/ms-per-frame histogram in the corner of the screen, updated every 60 frames — the first thing to check for "is it actually slow, and when" | | `?showPointerAreas` | Each Node's `mouseArea`/`touchArea`, useful for confirming input regions rather than raw render cost | | `?showHitAreas` | Actual hit-testing regions being used for pointer input | | `?showCanvasNodeBounds` | Bounds overlays for `CanvasNode` subtrees specifically | | `?showFittedBlockBounds` | The boundaries of scenery's internal rendering "blocks" — useful for seeing how much of the tree got batched into one Canvas/SVG/WebGL block versus split into many | | `?dev` | A bundle of internal developer-facing overlays and checks, useful when narrowing down where in the tree a problem lives | Start with `?profiler`. If frame time is consistently high rather than spiking occasionally, you're CPU- or GPU-bound on steady-state work (too many Nodes repainting, too much `step`/listener computation per frame). If it's fine most frames but spikes occasionally, that's usually garbage collection pressure from allocating in a hot path (a `step` function or `Property` listener that creates new objects every frame) rather than a rendering problem at all — the profiler's own doc calls this out explicitly, which is why it reports a full histogram rather than just an average. ## Node count and tree shape Scenery batches adjacent, renderer-compatible Nodes into as few underlying Canvas/SVG/WebGL surfaces as it can (see [Scenery Basics](/guides/scenery-basics#render-layers-canvas-svg-and-webgl)), but every Node still costs *something* — a bounds computation, a transform, participation in hit-testing. A few thousand simple Nodes rarely matters; tens of thousands, or a few thousand Nodes each with complex `Shape`-based geometry, starts to show up in the profiler. The usual fix is reducing Node count for content that doesn't need to be individually addressable: - **Static decorative content** that never changes and never needs independent input handling is a candidate for a single `Path`/`Image` rather than many small Nodes assembled at runtime. - **Large repeated structures** (a grid of many small identical icons, for instance) are a good candidate for `CanvasNode` with hand-written `paintCanvas` logic instead of one scenery `Node` per repeated element — trading scenery's automatic per-Node input/bounds handling for one manual draw routine, in exchange for far fewer objects for scenery to track. - **Visibility toggling** should use `visible`, not adding/removing children repeatedly — `node.visible = false` keeps the Node in the tree (and its subtree structure intact) while skipping paint and, if `pickable` is also affected, input; repeated `addChild`/`removeChild` churn is more expensive than toggling a flag. ## Property and listener overhead `Property.set()`/`.value =` notifies every registered listener synchronously, in registration order. That's usually cheap — but two patterns turn it into a real cost: - **A `DerivedProperty` (or manual listener) that does expensive work on every dependency change**, when the dependency changes far more often than the derived result actually needs to update. If a derived value only matters once per animation frame, deriving it from `stepTimer`/an explicit per-frame recompute rather than from a rapidly-changing input Property (e.g. a raw pointer-position Property firing many times per frame) avoids redundant recomputation between frames nobody observes. - **Registering listeners you never remove.** A `Property` (or `Emitter`) that accumulates listeners across the sim's lifetime — because a transient object linked to it but never called `unlink`/`dispose` — both leaks memory and means every future `.set()` does more work than it should. See [Dispose and Memory Management](/patterns/dispose-and-memory-management) for the disposal convention that prevents this. ```ts import { DerivedProperty } from 'scenerystack/axon'; // Expensive: recomputes on every change to either dependency, even many times per frame. const derivedProperty = new DerivedProperty( [ rawPointerPositionProperty, zoomLevelProperty ], ( position, zoom ) => expensiveLayoutComputation( position, zoom ) ); ``` If `rawPointerPositionProperty` updates on every pointer-move event rather than once per frame, `expensiveLayoutComputation` runs far more often than the screen actually repaints. Debouncing to a per-frame recompute (driven from the model's own `step(dt)`, reading the latest pointer position without re-deriving on every intermediate update) is the usual fix once profiling actually shows this Property's listeners as the hot path. ## Canvas, SVG, and WebGL: matching the renderer to the content Every `Node` has a `renderer` option (`'svg' | 'canvas' | 'webgl' | 'dom' | null`), normally left `null` so scenery picks automatically (see [Supported Browsers](/getting-started/supported-browsers#graceful-degradation-via-renderer-fallback)). The tradeoffs, for the rare case profiling shows you need to pin one: | Renderer | Best for | Cost | | --- | --- | --- | | SVG (scenery's default for most vector content) | Crisp vector shapes/text at any zoom, resolution-independent | Each distinct element is a real DOM node; very large numbers of separately-styled SVG elements get expensive to keep in sync | | Canvas | Many similar elements painted together, or custom per-pixel drawing (`CanvasNode`) | Cheaper per-element than SVG at scale, but loses SVG's automatic hit-testing/accessibility hooks — you draw pixels, not addressable elements | | WebGL | Large numbers of simple shapes needing GPU-accelerated compositing (particle-like effects, many identical sprites) | Highest ceiling for raw throughput, but the least forgiving to author for and not guaranteed available (falls back automatically per [Supported Browsers](/getting-started/supported-browsers)) | The practical rule: leave `renderer` unset until `?profiler` and Node-count investigation point at a *specific* subtree, then reach for `CanvasNode` (for many similar small elements) or a pinned `renderer: 'canvas'`/`'webgl'` hint (for scenery-managed Nodes that would benefit from being batched onto a different surface than their neighbors) only for that subtree — not the whole sim. ## Where to go next - [Scenery Basics](/guides/scenery-basics) — the Node tree and render-layer model this page assumes - [Dispose and Memory Management](/patterns/dispose-and-memory-management) — the disposal convention that prevents listener/Property leaks from becoming a performance problem over a long-running sim - [Supported Browsers](/getting-started/supported-browsers) — renderer fallback behavior across platforms ======================================================================== Page: PhET-iO and Instrumentation URL: https://veillette.github.io/Almanach/guides/phet-io-and-instrumentation Source: docs/guides/phet-io-and-instrumentation.md Category: Guides | Tags: phet-io, tandem, instrumentation | Status: verified ======================================================================== # PhET-iO and Instrumentation ::: tip When to read the related pages This page is the **conceptual entry point** — what PhET-iO buys you and how `Tandem` fits in. For runtime dynamic elements and custom `IOType`s, read [PhET-iO Deep Dive](/guides/phet-io-deep-dive). For day-to-day wiring conventions (what to instrument, how to structure tandems), read [PhET-iO Instrumentation Pattern](/patterns/phet-io-instrumentation-pattern). ::: PhET-iO is the layer that turns a running simulation into something an external program can inspect, record, and control: every instrumented object's state can be serialized, saved, restored, and set remotely — the same mechanism that powers PhET's state-saving, data-collection, and interoperability features. **Instrumentation is opt-in and additive**: an uninstrumented simulation runs identically, just without that external surface. ## Tandem: the instrumentation identifier Every instrumentable object — `Property`, `Node`, `Screen`, `Emitter`, and the PhET-iO base class `PhetioObject` they build on — takes a `tandem` option. A `Tandem` is a hierarchical path name (mirroring where the object lives conceptually, not necessarily in the scene graph) that becomes that object's stable identifier in the PhET-iO API: ```ts import { Tandem } from 'scenerystack/tandem'; import { NumberProperty } from 'scenerystack/axon'; const screenTandem = Tandem.ROOT.createTandem( 'myScreen' ); const modelTandem = screenTandem.createTandem( 'model' ); const massProperty = new NumberProperty( 5, { tandem: modelTandem.createTandem( 'massProperty' ), phetioDocumentation: 'the mass of the object, in kilograms' } ); ``` | Tandem | Meaning | | --- | --- | | `Tandem.ROOT` | The root of the tandem tree for the whole simulation | | `Tandem.REQUIRED` | Placeholder meaning "this tandem must be supplied by the caller" — used in components designed to always be instrumented | | `Tandem.OPTIONAL` | Placeholder meaning "instrumentation is optional here" | | `someTandem.createTandem( name )` | Creates a child tandem, extending the hierarchical path | Every `Screen` requires a `tandem`, even in a simulation that never enables PhET-iO — see [Your First Simulation](/getting-started/your-first-simulation). Uninstrumented (`tandem: Tandem.OPTIONAL`, unsupplied) objects simply don't appear in the PhET-iO API; nothing breaks by omitting instrumentation, but nothing is inspectable/settable externally either. ## What instrumentation buys you | Capability | How | | --- | --- | | Save/restore full simulation state | Every instrumented `Property`'s value is captured and can be reapplied later | | External control | Another program can set an instrumented `Property`'s value directly, driving the sim the same as a user interaction would | | A documented, stable API | `phetioDocumentation` and the tandem hierarchy together describe the sim's state surface for tooling and other consumers | | Data collection / logging | Instrumented `Emitter`s and state changes can be observed externally without modifying simulation code | ## IOType: describing serialization Plain `Property`/`Property` instances serialize with the built-in `NumberIO`/`StringIO` `IOType`s automatically. Custom classes that extend `PhetioObject` (rather than composing existing instrumented Properties) describe their own serialization with an `IOType`: ```ts import { IOType, NumberIO } from 'scenerystack/tandem'; type MyThingState = { x: number; y: number }; const MyThingIO = new IOType( 'MyThingIO', { valueType: MyThing, documentation: 'Serializes the position of a MyThing.', toStateObject: myThing => ( { x: myThing.x, y: myThing.y } ), applyState: ( myThing, state ) => { myThing.x = state.x; myThing.y = state.y; }, stateSchema: { x: NumberIO, y: NumberIO } } ); ``` Most simulations never need to author a custom `IOType` directly — composing already-instrumented `Property`s (each with its own built-in `IOType`) inside a plain model class covers the overwhelming majority of cases, consistent with the [model-view separation](/patterns/model-view-separation) pattern where all state lives in `Property` instances. ::: tip Instrument by default, even before you need PhET-iO Adding `tandem`/`phetioDocumentation` after the fact to a model that already has dozens of Properties is far more tedious than threading tandems through from the start. Most PhET simulations instrument their full model tree from day one, whether or not a given release actually ships PhET-iO features — the incremental cost per `Property` is one option. ::: ## Where to go next - [Building Your First Screen](/guides/building-your-first-screen) — threading tandems through a new screen's model and view - [Model-View Separation](/patterns/model-view-separation) — why instrumentable state lives in `Property` instances, not scattered fields - [Your First Simulation](/getting-started/your-first-simulation) — the minimum tandem wiring every `Screen` requires ======================================================================== Page: PhET-iO Deep Dive URL: https://veillette.github.io/Almanach/guides/phet-io-deep-dive Source: docs/guides/phet-io-deep-dive.md Category: Guides | Tags: phet-io, tandem, instrumentation, phetio-group, phetio-capsule | Status: verified ======================================================================== # PhET-iO Deep Dive [PhET-iO and Instrumentation](/guides/phet-io-and-instrumentation) covers the basics: every instrumentable object takes a `Tandem`, and instrumentation is opt-in and additive. That page's model is *static* — a fixed set of `Property`s and `Node`s, each with a tandem assigned once at construction. This page covers what happens once your model creates and destroys elements at runtime (particles, graph points, anything with a variable count), how to describe serialization for a class that isn't just composed `Property`s, and what "state save/restore" is actually doing under the hood. ## The problem with dynamic elements A `Tandem` is a fixed, hierarchical path assigned once. That works fine for a model with a fixed shape — one `massProperty`, one `positionProperty` — but breaks down the moment the model creates elements at runtime: if a user can add particles to a simulation, each particle needs *some* tandem, but you don't know at construction time how many there will be or what to name them. `PhetioGroup` and `PhetioCapsule` (both from `scenerystack/tandem`) solve this by taking over both creation *and* tandem-naming for exactly this case. ## PhetioGroup: a homogeneous, growable collection `PhetioGroup` manages a runtime-created array of `PhetioObject`s, generating each one's tandem automatically: ```ts import { PhetioGroup, Tandem } from 'scenerystack/tandem'; import { NumberProperty } from 'scenerystack/axon'; class Particle extends PhetioObject { public readonly massProperty: NumberProperty; public constructor( tandem: Tandem, initialMass: number ) { super( { tandem, phetioType: Particle.ParticleIO } ); this.massProperty = new NumberProperty( initialMass, { tandem: tandem.createTandem( 'massProperty' ) } ); } public static ParticleIO = /* an IOType, see below */ null!; } const particleGroup = new PhetioGroup( ( tandem: Tandem, initialMass: number ) => new Particle( tandem, initialMass ), [ 1 ], // default arguments, used to build the group's "archetype" — see below { tandem: someParentTandem.createTandem( 'particleGroup' ) } ); const newParticle = particleGroup.createNextElement( 2.5 ); ``` A few things are worth understanding, not just copying: - **`createElement` never wires up listeners itself** — the constructor function's only job is building the element and giving it its tandem. If code elsewhere needs to react whenever a new element is created (adding a view Node for each new particle, for instance), listen to the group's own creation notifications rather than adding side effects inside `createElement` — mixing "create the object" and "wire up its effects" into one function is a documented anti-pattern for `PhetioGroup`, because the PhET-iO state engine re-invokes `createElement` when *restoring* saved state, and any side effect baked into it fires again on every restore, not just on genuine user-driven creation. - **The tandem name must end in the container's suffix** (`'Group'` by default) — `particleGroup`'s own tandem name ends in `Group`, and each created element's base tandem name is that group name with the suffix stripped, followed by an index (`particle_0`, `particle_1`, ...). This is why the naming isn't arbitrary: it's what lets the PhET-iO API tooling recognize "this whole namespace is one dynamic group" rather than treating each element as an unrelated fixed object. - **`defaultArguments` build the group's archetype** — an extra, always-present instance created from those defaults purely so the PhET-iO API has a static example of what elements in this group look like (their nested Properties, their `IOType`), even though the group might have zero real elements at a given moment. You don't interact with the archetype directly; it exists for API generation and validation. - **`countProperty`** is a built-in, read-only `NumberProperty` tracking how many elements currently exist — useful for a view layer (or a PhET-iO client) to observe the group's size without walking the array itself. ## PhetioCapsule: a single, replaceable dynamic element `PhetioCapsule` is `PhetioGroup`'s sibling for the "exactly zero or one" case rather than "many": a dialog that's created lazily on first open, or a single dynamically-swapped model element, rather than an array of many. Its shape mirrors `PhetioGroup` closely — same `createElement`/`defaultArguments`/`options` constructor — but instead of `createNextElement` appending to an array, `create(...)` (re-)creates the single managed instance: ```ts import { PhetioCapsule } from 'scenerystack/tandem'; const dialogCapsule = new PhetioCapsule( ( tandem: Tandem ) => new MyDialog( { tandem } ), [], { tandem: someParentTandem.createTandem( 'dialogCapsule' ) } ); const dialog = dialogCapsule.create( [] ); ``` Reach for `PhetioCapsule` when "at most one instance exists, created on demand" is the actual shape of the problem (a modal dialog, a singleton sub-panel) and `PhetioGroup` when the real shape is "an arbitrary, growing/shrinking collection" (particles, graph series, anything list-like). ## Authoring a custom IOType [PhET-iO and Instrumentation](/guides/phet-io-and-instrumentation#iotype-describing-serialization) shows the shape of an `IOType`: `toStateObject`/`applyState` plus a `stateSchema`. The one thing worth adding here is **when to reach for `ReferenceIO`** instead of writing full state serialization yourself. If your class's meaningful state already lives entirely in child `PhetioObject`s (nested `Property`s, for instance), those children already know how to serialize themselves — your `IOType`'s job is just to describe *which* children matter, via `stateSchema`, rather than manually re-implementing their serialization: ```ts import { IOType, NumberIO, ReferenceIO } from 'scenerystack/tandem'; const ParticleIO = new IOType( 'ParticleIO', { valueType: Particle, stateSchema: { massProperty: NumberIO // delegates entirely to NumberProperty's own IOType } } ); ``` `ReferenceIO` (also from `scenerystack/tandem`) is for the opposite situation: a field that points *at* another already-instrumented `PhetioObject` elsewhere in the tree, rather than owning its own copy of that state. Serializing that field as a reference (its `phetioID`) rather than embedding the referenced object's full state avoids duplicating state that's already captured once, at the referenced object's own tandem path. ## How state save/restore actually works Every instrumented object's `IOType` (built-in or custom) contributes one entry to the sim's overall state object when a save is triggered — essentially a big `phetioID -> stateObject` map, built by calling `toStateObject` on every instrumented `PhetioObject` in the tree. Restoring reverses this: for each entry, the corresponding object's `applyState` is called with the saved value. The part that trips people up is dynamic elements: restoring state for a `PhetioGroup`/`PhetioCapsule` doesn't just call `applyState` on existing elements — the group first has to *re-create* the right number of elements (by re-invoking `createElement`, the same function used for ordinary runtime creation) before `applyState` has anything to apply state to. This is precisely why `createElement` must be a pure "build the object" function: it runs during ordinary interactive use *and* during every state restore, and `isSettingPhetioStateProperty` (readable, though not normally something application code needs to check directly) is `true` for the duration of a restore specifically so framework code — like the assertion inside `PhetioGroup` guarding its `countProperty` — can distinguish "state is being restored" from "the user is doing this interactively" when the distinction matters. ::: tip Most simulations never author a custom IOType or a PhetioGroup As [PhET-iO and Instrumentation](/guides/phet-io-and-instrumentation) notes, composing already-instrumented `Property`s inside plain model classes covers most simulations without a custom `IOType` at all. `PhetioGroup`/`PhetioCapsule` specifically are for the subset of sims whose model genuinely creates/destroys elements at runtime — reach for them because the model shape requires it, not as a default pattern to apply everywhere. ::: ## Where to go next - [PhET-iO and Instrumentation](/guides/phet-io-and-instrumentation) — `Tandem` fundamentals and the basic `IOType` shape this page builds on - [PhET-iO Instrumentation Pattern](/patterns/phet-io-instrumentation-pattern) — deciding what to instrument in the first place - [`Tandem`](/api/tandem/tandem) — the naming primitive both `PhetioGroup` and `PhetioCapsule` build on ======================================================================== Page: Preferences and Feature Flags URL: https://veillette.github.io/Almanach/guides/preferences-and-feature-flags Source: docs/guides/preferences-and-feature-flags.md Category: Guides | Tags: preferences, query-parameters, joist | Status: verified ======================================================================== # Preferences and Feature Flags SceneryStack simulations expose two different mechanisms for configuring behavior that aren't part of the core interaction: the **Preferences dialog**, a user-facing settings panel built into every `Sim`, and **query parameters**, developer/deployment-facing flags read once at startup. Both exist so features like localization, sound, and accessibility options don't need bespoke UI in every simulation. ## The Preferences dialog Every `Sim` gets a Preferences button in its navigation bar automatically; what appears inside it is configured by a `PreferencesModel` passed to `Sim`: ```ts import { Sim, onReadyToLaunch, PreferencesModel } from 'scenerystack/sim'; import { Property } from 'scenerystack/axon'; const preferencesModel = new PreferencesModel( { visualOptions: { supportsProjectorMode: true // adds a high-contrast "projector mode" toggle }, audioOptions: { supportsSound: true, supportsExtraSound: true } } ); onReadyToLaunch( () => { const sim = new Sim( new Property( 'My Simulation' ), [ /* screens */ ], { preferencesModel } ); sim.start(); } ); ``` | `PreferencesModel` section | Surfaces | | --- | --- | | `simulationOptions` | Sim-specific custom preferences you supply yourself | | `visualOptions` | Projector/high-contrast mode, interactive highlights | | `audioOptions` | Sound on/off, extra sound, voicing (speech output) | | `inputOptions` | Alternative input customization (e.g. gesture control settings) | | `localizationOptions` | Locale switching, region-and-culture selection | If none of these `Options` are supplied, the corresponding Preferences tab simply doesn't appear — the dialog only shows tabs relevant to what the simulation actually supports, so passing an empty `new PreferencesModel()` (the `Sim` default) yields a minimal dialog. ## Query parameters Query parameters are read once, at launch, from the page URL — they're the mechanism for developer tooling and deployment configuration, not for anything a shipped simulation should let end users toggle from within the running page. Two you'll use constantly during development: | Query parameter | Effect | | --- | --- | | `?ea` | Enables assertions (`assert && assert(...)` checks throughout SceneryStack) — see [Running and Building a Simulation](/getting-started/running-and-building-a-simulation) | | `?locale=fr` | Launches directly in a given locale, bypassing the Preferences dialog's language picker | | `?screens=1,2` | Restricts which screens are available, by 1-based index — useful for testing one screen in isolation | Simulation-specific flags follow the same pattern using `QueryStringMachine` (from `scenerystack/query-string-machine`), the schema-validated query parameter parser SceneryStack itself uses internally: ```ts import { QueryStringMachine } from 'scenerystack/query-string-machine'; const myQueryParameters = QueryStringMachine.getAll( { // ?showAnswers=true showAnswers: { type: 'flag' }, // ?initialSpeed=2.5 initialSpeed: { type: 'number', defaultValue: 1 } } ); if ( myQueryParameters.showAnswers ) { // reveal debug-only content } ``` `QueryStringMachine.getAll` validates every parameter against its declared `type`/`defaultValue` at startup, so a malformed URL fails fast with a clear error rather than silently misconfiguring the sim. ::: warning Query parameters are a developer/QA surface, not an end-user settings UI It's tempting to add a query parameter as a quick way to make a feature "configurable" and stop there. If end users are meant to change the setting, it belongs in the **Preferences dialog** (or in-sim UI) instead — query parameters aren't discoverable, aren't documented in-product, and are easy to lose (e.g. on a bookmarked or shared URL that omits them). Reserve query parameters for developer flags, QA/testing switches, and deployment-time configuration. ::: ## Where to go next - [Translation and Localization](/guides/translation-and-localization) — how `?locale=` interacts with the string pipeline - [Running and Building a Simulation](/getting-started/running-and-building-a-simulation) — where query parameters get appended during development - [Your First Simulation](/getting-started/your-first-simulation) — the minimal `Sim` this guide's `preferencesModel` option extends ======================================================================== Page: Publishing and Distributing a Simulation URL: https://veillette.github.io/Almanach/guides/publishing-and-distributing-a-simulation Source: docs/guides/publishing-and-distributing-a-simulation.md Category: Guides | Tags: deployment, build, hosting, distribution | Status: complete ======================================================================== # Publishing and Distributing a Simulation Once a simulation is built, "publishing" it is entirely a static-hosting problem — there is no SceneryStack-specific server, database, or runtime to stand up. This page covers what a production build actually is, and the tradeoffs between the handful of common places to put it. ## What you're distributing As covered in [Running and Building a Simulation](/getting-started/running-and-building-a-simulation), `npm run build` runs the project's bundler in production mode and produces a self-contained folder of static HTML, JS, CSS, and media (`dist/` for Vite/Esbuild, `build/` for some Webpack configs). Every `scenerystack/*` import, image, and sound file the bundler could resolve is already inlined or copied alongside the built JS, and runtime assertions are stripped. That folder is the entire artifact — there is nothing else to package, containerize, or configure server-side before a browser can load it. This has one direct consequence for distribution: **anything that can serve static files can host a SceneryStack simulation.** The choice of where to host isn't a technical constraint on the sim itself, it's a question of audience, update frequency, and who's already paying for what infrastructure. ## Where a built sim can live | Option | Good fit when | Tradeoff | | --- | --- | --- | | [GitHub Pages](/getting-started/deploying-to-github-pages) | The project is already on GitHub, wants free hosting, and doesn't need a custom domain or server-side logic | Static only, and (for project sites) served from a `/repo-name/` subpath — the build's base path must match, see the linked page | | A generic static host (Netlify, Vercel, Cloudflare Pages, an S3 bucket + CDN, etc.) | You want a custom domain, preview deployments per branch/PR, or hosting independent of a specific git forge | Slightly more setup than GitHub Pages, but usually still just "point it at the build output folder" | | Your own web server (nginx, Apache, or any static file server) | The sim needs to live alongside other institutional content, behind an existing domain/auth layer, or fully under your own infrastructure control | You own the uptime, TLS certificates, and cache headers yourself instead of a managed provider handling them | | Opening `dist/index.html` directly from disk, or a local static server (`npx serve dist`) | Local testing, or distributing to a single person/classroom without any hosting at all | No shareable URL; some browsers restrict `file://`-loaded assets (module imports, in particular) more than an actual HTTP server would | None of these change what gets built — the same `dist/` folder from `npm run build` is what you'd push to a `gh-pages` branch, upload to Netlify, or `scp` onto your own server. The only per-target adjustment is usually the bundler's base-path/`publicPath` setting (see the GitHub Pages page for the concrete example), which needs to match wherever the files will actually be served from. ## Versioning what you distribute A sim distributed publicly will eventually need more than one version live at once — a "current" version and an archived prior release someone has bookmarked or cited. Since a build is just a static folder, the simplest version scheme is directory-based: publish each release to its own path (`/my-sim/1.2.0/`, `/my-sim/1.1.0/`) rather than overwriting the same location on every release, and point a `/my-sim/latest/` alias (a redirect, or a copy of the newest build) at whichever one is current. This costs nothing extra at build time — it's purely a decision about *where* each build's output is uploaded — and it means an old link never silently starts serving different content than what was originally shared. ::: tip Verify the deployed build before announcing it, not after Every distribution target strips runtime assertions the same way (see [Running and Building a Simulation](/getting-started/running-and-building-a-simulation)) and serves case-sensitive file paths even if your local development machine doesn't enforce that. Serve the built output locally (`npx serve dist` or your bundler's `preview` command) and click through the sim once before publishing, rather than discovering a production-only bug for the first time in front of the sim's actual audience — see the verification checklist in [Deploying a Simulation to GitHub Pages](/getting-started/deploying-to-github-pages) for the specific failure modes to check. ::: ## Where to go next - [Running and Building a Simulation](/getting-started/running-and-building-a-simulation) — how the artifact this page distributes gets built - [Deploying a Simulation to GitHub Pages](/getting-started/deploying-to-github-pages) — the concrete, step-by-step path for one specific (free, common) hosting target - [Supported Browsers](/getting-started/supported-browsers) — the platform matrix to verify a distributed build against ======================================================================== Page: Scenery Basics URL: https://veillette.github.io/Almanach/guides/scenery-basics Source: docs/guides/scenery-basics.md Category: Guides | Tags: scenery, scene-graph, rendering | Status: verified ======================================================================== # Scenery Basics `scenery` is a retained-mode scene graph: instead of issuing draw calls yourself, you build a tree of `Node` instances describing *what* to show, and a `Display` figures out how to paint it (via Canvas, SVG, or WebGL) and keeps the painting in sync as the tree changes. Every visual element in a SceneryStack application — shapes, text, images, and every UI component from `sun`/`scenery-phet` — is a `Node`. ## The Node tree ```ts import { Node, Rectangle, Circle, Text } from 'scenerystack/scenery'; const root = new Node(); const background = new Rectangle( 0, 0, 400, 300, { fill: '#eef' } ); const ball = new Circle( 20, { fill: 'crimson', center: background.center } ); const label = new Text( 'Scene graph demo', { font: '18px sans-serif', top: 10, left: 10 } ); root.children = [ background, ball, label ]; ``` A `Node` with no visual content of its own (like `root` above) is a plain container — its only job is grouping children so you can move, hide, or style them together. `children` is drawn in array order: **later entries paint on top of earlier ones**, exactly like DOM stacking order or z-index by declaration order. | Way to change children | Effect | | --- | --- | | `node.children = [ a, b, c ]` | Replaces the entire children array | | `node.addChild( child )` | Appends one Node to the end (drawn on top) | | `node.insertChild( index, child )` | Inserts at a specific stacking position | | `node.removeChild( child )` | Removes one Node | A `Node` can have multiple parents (it's a DAG, not strictly a tree) — the same `Node` instance can appear in two different places in the graph and both stay in sync, though this is an advanced technique most simulations don't need. ## Render layers: Canvas, SVG, and WebGL Scenery doesn't require you to pick a rendering technology per element. Each `Node` has a `renderer` option (`'svg' | 'canvas' | 'webgl' | 'dom' | null`) that's normally left `null`, letting scenery choose — and internally batch adjacent compatible Nodes into as few underlying Canvas/SVG/WebGL "layers" as it can, to minimize the number of separate surfaces the browser has to composite. You rarely think about layers directly; the practical knobs you do use are `visible`, `opacity`, and `pickable`: ```ts ball.visible = false; // removed from painting and from input, but stays in the tree ball.opacity = 0.5; // still painted, semi-transparent ball.pickable = false; // still painted, but input events pass through it ``` See [Supported Browsers](/getting-started/supported-browsers) for how renderer fallback behaves across platforms. ## Coordinate frames Every `Node` has its own **local coordinate frame** — the one its constructor arguments and internal geometry are expressed in — and a transform (translation, rotation, scale) that places it within its **parent's** coordinate frame: ```ts ball.translation = ball.translation.plusXY( 50, 0 ); // move 50 units right, in the parent's frame ball.rotation = Math.PI / 4; // rotate 45 degrees about its own origin ball.scale( 1.5 ); // scale about its own origin ``` | Property | Meaning | | --- | --- | | `x` / `y` / `translation` | Position of this Node's origin in its parent's coordinate frame | | `rotation` | Rotation in radians about the local origin | | `localBounds` | Bounding box of this Node's own content, in its own local frame | | `bounds` | Bounding box of this Node *and all its children*, in the parent's frame | Converting a point between frames uses the Node's transform chain directly: ```ts const parentPoint = ball.localToParentPoint( someLocalPoint ); const globalPoint = ball.localToGlobalPoint( someLocalPoint ); // all the way to the Display's root frame ``` ::: tip `bounds` is not the same as `localBounds` `localBounds` is one Node's own content, unaffected by its children. `bounds` includes every descendant, already transformed into the parent's frame — it's what determines layout and hit-testing for anything that treats this Node as a single unit (see [Scenery Layout](/guides/scenery-layout)). ::: ## Where to go next - [Scenery Layout](/guides/scenery-layout) — arranging children automatically instead of setting `x`/`y` by hand - [Scenery Input](/guides/scenery-input) — pointers, listeners, and how events traverse this same tree - [Scenery Application vs. Standalone Library](/getting-started/scenery-application-vs-standalone-library) — putting a Node tree in a page via `Display` ======================================================================== Page: Scenery Input URL: https://veillette.github.io/Almanach/guides/scenery-input Source: docs/guides/scenery-input.md Category: Guides | Tags: scenery, input, pointers, listeners | Status: verified ======================================================================== # Scenery Input Scenery unifies mouse, touch, and pen input behind one event model: every physical input source becomes a `Pointer` (a `Mouse`, `Touch`, or `Pen` instance), every DOM event becomes a `SceneryEvent` carrying the `Trail` of Nodes it hit, and any `Node` can attach listeners that react to it — no direct DOM event handling required. ## Attaching a listener ```ts import { Node, Circle } from 'scenerystack/scenery'; const ball = new Circle( 20, { fill: 'crimson', cursor: 'pointer' } ); ball.addInputListener( { down: event => console.log( 'pressed', event.pointer.point ), up: event => console.log( 'released' ), over: event => { ball.fill = 'orange'; }, out: event => { ball.fill = 'crimson'; } } ); ``` An input listener is a plain object (or class instance) whose keys are event names. Scenery calls the matching key when that event reaches a Node the listener is attached to. | Event | Fires on | | --- | --- | | `down` / `up` / `cancel` | Pointer press, release, or interrupted press (any pointer type) | | `move` | Pointer moved while over this Node | | `enter` / `exit` | Pointer entered/left this Node's subtree (fires once per subtree, unlike `over`/`out`) | | `over` / `out` | Pointer entered/left this specific Node | | `click` | A full press-and-release on the same target | | `focus` / `blur` | Keyboard/PDOM focus gained or lost | | `keydown` / `keyup` | Key events while this Node has focus | Type-specific variants (`mousedown`, `touchdown`, `pendown`, ...) exist for every generic event when you need to branch on pointer type without inspecting `event.pointer.type` yourself. ## SceneryEvent and the hit trail The object passed to a listener is a `SceneryEvent`, carrying everything about *this* dispatch: ```ts ball.addInputListener( { down: event => { event.pointer; // the Pointer (Mouse | Touch | Pen) that triggered this event.trail; // Trail: ordered list of Nodes from the Display's root to the leaf hit event.target; // the leaf-most (deepest) Node in the trail event.currentTarget; // the Node this particular listener is attached to event.domEvent; // the underlying raw MouseEvent/TouchEvent/PointerEvent, or null } } ); ``` Events bubble: a listener on a parent `Node` still receives events that were hit-tested against one of its descendants, with `currentTarget` reflecting which ancestor the listener is actually attached to and `target`/`trail` reflecting where the pointer actually hit. Call `event.handled = true` inside a listener to stop the event bubbling to ancestors, or `event.abort()` to stop it from reaching any further listeners at all. ## Pointer areas and pickability Two Node options control *whether* and *where* a Node can be hit at all: ```ts import { Shape } from 'scenerystack/kite'; ball.touchArea = ball.localBounds.dilated( 10 ); // easier to tap on touchscreens than the visual bounds ball.mouseArea = ball.localBounds; // mouse hit area, independently of touch ball.pickable = false; // excluded from hit-testing (still painted, if visible) ball.inputEnabled = false; // subtree stops responding to input, without changing pickable ``` `touchArea`/`mouseArea` accept a `kite` `Shape` or a `Bounds2` and are commonly dilated beyond the visual bounds, since touch targets smaller than ~44px are hard to hit reliably. ## Structured listeners: PressListener, FireListener, DragListener Handling `down`/`up`/`cancel` manually gets repetitive and easy to get subtly wrong (interrupted presses, multitouch). The `scenery` listener classes wrap the raw events into higher-level behavior: ```ts import { FireListener, PressListener, DragListener } from 'scenerystack/scenery'; button.addInputListener( new FireListener( { fire: () => console.log( 'fired!' ) // press-and-release, handles interruption for you } ) ); someNode.addInputListener( new PressListener( { press: event => { /* pressed down */ }, release: event => { /* released, still over the node */ } } ) ); ``` `DragListener` (and its keyboard-accessible counterpart `KeyboardDragListener`) is the most common of these — see [Drag Listeners](/patterns/drag-listeners) for the full pattern of wiring a listener to a model `positionProperty` through a `ModelViewTransform2`. ::: tip Pointer input alone is never accessible Every listener shown above only responds to mouse/touch/pen. Keyboard users and screen reader users interact through the [Parallel DOM](/accessibility/pdom) instead — a fully accessible custom interaction needs a `tagName`, `focusable: true`, and a keyboard-driven listener (`KeyboardListener` or `KeyboardDragListener`) in addition to any pointer listener. See the [Accessible Interaction Tutorial](/guides/accessible-interaction-tutorial) for a worked example. ::: ## Where to go next - [Drag Listeners](/patterns/drag-listeners) — the standard pattern for draggable model-bound Nodes - [The Parallel DOM (PDOM)](/accessibility/pdom) — the accessible half of input handling - [Accessible Interaction Tutorial](/guides/accessible-interaction-tutorial) — pointer and keyboard input on the same custom Node ======================================================================== Page: Scenery Layout URL: https://veillette.github.io/Almanach/guides/scenery-layout Source: docs/guides/scenery-layout.md Category: Guides | Tags: scenery, layout, flow-box, grid-box | Status: verified ======================================================================== # Scenery Layout Setting every child's `x`/`y` by hand works for a handful of Nodes but falls apart once content changes size dynamically (translated strings, resizable panels). Scenery's layout containers — `FlowBox`, `GridBox`, and `AlignBox` — solve this the way CSS flexbox/grid do: you describe *relationships* between children, and the container keeps positions correct as content changes. ## FlowBox: one-dimensional flow (like flexbox) `FlowBox` lays children out in a single row or column, in `children` array order: ```ts import { FlowBox, Rectangle, Text } from 'scenerystack/scenery'; const row = new FlowBox( { orientation: 'horizontal', spacing: 8, align: 'center', children: [ new Rectangle( 0, 0, 20, 20, { fill: 'red' } ), new Text( 'Label', { font: '16px sans-serif' } ), new Rectangle( 0, 0, 20, 20, { fill: 'blue' } ) ] } ); ``` | Option | Effect | | --- | --- | | `orientation` | `'horizontal'` or `'vertical'` — which axis children flow along | | `spacing` | Gap between adjacent children | | `align` | Cross-axis alignment: `'start'` / `'center'` / `'end'` / `'stretch'`, etc. | | `justify` | Main-axis distribution: `'left'`/`'top'`, `'center'`, `'right'`/`'bottom'`, `'spaceBetween'`, `'spaceAround'`, `'spaceEvenly'` | | `wrap` | Whether children wrap to a new line when they overflow the preferred size | | `stretch` | Whether children stretch to fill the cross-axis by default | `HBox` and `VBox` are `FlowBox` with `orientation` fixed to `'horizontal'`/`'vertical'` — reach for them when you never need to change orientation dynamically: ```ts import { VBox } from 'scenerystack/scenery'; const column = new VBox( { spacing: 4, children: [ /* ... */ ] } ); ``` A child can override its own contribution via `layoutOptions`, without the `FlowBox` needing to know about it: ```ts someChild.layoutOptions = { grow: 1 }; // this child expands to absorb extra space ``` ## GridBox: two-dimensional layout `GridBox` positions children in a grid, either by nested `rows`/`columns` arrays or by giving each child explicit `layoutOptions`: ```ts import { GridBox, Text } from 'scenerystack/scenery'; const grid = new GridBox( { xSpacing: 10, ySpacing: 6, rows: [ [ new Text( 'Name' ), new Text( 'Value' ) ], [ new Text( 'Mass' ), new Text( '5 kg' ) ] ] } ); ``` | Option | Effect | | --- | --- | | `rows` | `Node[][]`, outer array is rows, inner array is that row's columns | | `columns` | `Node[][]`, outer array is columns instead — use one or the other, not both | | `xSpacing` / `ySpacing` | Gaps between columns / rows | | `autoRows` / `autoColumns` | Auto-place a flat `children` array into a grid with a fixed number of rows/columns, instead of positioning explicitly | ## AlignBox: aligning within reserved space `AlignBox` wraps a single child and gives it a fixed alignment inside a bounding box — commonly used to keep same-size content aligned across several sibling panels: ```ts import { AlignBox, AlignGroup, Text } from 'scenerystack/scenery'; const group = new AlignGroup(); // shares one common size across boxes const leftLabel = new AlignBox( new Text( 'Short' ), { group, xAlign: 'left' } ); const rightLabel = new AlignBox( new Text( 'A much longer label' ), { group, xAlign: 'left' } ); // Both AlignBoxes now report the same (largest) size to whatever lays them out. ``` | Option | Effect | | --- | --- | | `xAlign` / `yAlign` | `'left'\|'center'\|'right'\|'stretch'` and `'top'\|'center'\|'bottom'\|'stretch'` | | `xMargin` / `yMargin` | Padding around the content inside the alignment box | | `group` | An `AlignGroup` shared by multiple `AlignBox`es so they all report a common (max) size | ## Margins, shared across containers `FlowBox` and `GridBox` cells both support per-child margin options — `xMargin`, `yMargin`, `leftMargin`, `rightMargin`, `topMargin`, `bottomMargin`, `minContentWidth`/`minContentHeight` — set either on the container (as a default) or per-child via `layoutOptions`. ::: tip Layout containers manage their children's transform — don't fight them Once a `Node` is a child of a `FlowBox`/`GridBox`, that container sets its `x`/`y` every layout pass. Setting `translation` on the child directly afterwards is overwritten on the next layout update; adjust `spacing`, `align`, `layoutOptions`, or wrap the child in its own `Node` if you need an offset the container doesn't already model. ::: ## Where to go next - [Building Your First Screen](/guides/building-your-first-screen) — laying out a `ScreenView`'s content with these containers - [Scenery Basics](/guides/scenery-basics) — the Node tree and coordinate frames layout containers operate on - [UI Components Overview](/guides/ui-components-overview) — `sun` components that are commonly composed inside `FlowBox`/`GridBox` ======================================================================== Page: Sim Lifecycle and Startup Sequence URL: https://veillette.github.io/Almanach/guides/sim-lifecycle-and-startup-sequence Source: docs/guides/sim-lifecycle-and-startup-sequence.md Category: Guides | Tags: joist, Sim, Screen, lifecycle, animation-loop | Status: complete ======================================================================== # Sim Lifecycle and Startup Sequence Every SceneryStack simulation goes through the same handful of phases between the page loading and the animation loop running steadily at 60fps. Knowing the real order — not just "it starts" — matters for questions like "why isn't my model constructed yet" or "why did this listener fire before the thing it depends on existed." ## Phase 1: asset loading, via `onReadyToLaunch` A simulation never constructs its `Sim` directly at module scope — it wraps construction in `onReadyToLaunch` (from `scenerystack/sim`), which waits for SceneryStack's asynchronous asset loader (fonts, images, translated string files) to finish before the callback runs: ```ts import { Sim, Screen, onReadyToLaunch } from 'scenerystack/sim'; onReadyToLaunch( () => { const sim = new Sim( /* ... */ ); sim.start(); } ); ``` Nothing in the scene graph is safe to build before this callback fires — a `Text` Node created too early could measure its bounds against a font that hasn't finished loading, and a string Property could briefly hold an untranslated fallback. This is why every [`Screen`](/api/joist/screen)'s model/view factories are deferred functions rather than eagerly-run code: they're only invoked once `Sim` decides a screen is actually needed, which is always after this phase has completed. ## Phase 2: `new Sim(...)` construction Constructing `Sim` itself does relatively little work: it records the list of `Screen`s, builds a `HomeScreen` and navigation bar if there's more than one, and sets up its own top-level Properties (`selectedScreenProperty`, `activeProperty`, `dimensionProperty`) — see [`Sim`](/api/joist/sim) for the full option/member list. Crucially, **no individual screen's `createModel`/`createView` factory has run yet** at this point; `Screen` only holds those factories, it doesn't call them. ## Phase 3: `sim.start()` — screen initialization Calling `start()` is what actually triggers construction. `Sim` initializes screens (by default, every screen eagerly rather than only the one first shown — see [`detachInactiveScreenViews`](/api/joist/sim) for the option that keeps inactive screens' views out of the scene graph once built), calling each `Screen`'s `createModel()` and then `createView( model )` in turn. `Sim.isConstructionCompleteProperty` becomes `true` once this has finished for every screen — code that needs to know "has everything actually been built yet" (some PhET-iO tooling, in particular) waits on that Property rather than assuming construction is synchronous with `start()` returning. Once construction finishes, `start()` begins the `requestAnimationFrame` loop described below. `selectedScreenProperty` is set (to the `HomeScreen` for a fresh multi-screen sim, or the single screen for a one-screen sim, unless a query parameter like `?screens=2` requests a specific starting screen) as part of this same phase. ## Phase 4: the running animation loop Once started, a `Sim` drives one `requestAnimationFrame`-based loop for the lifetime of the page. Each frame: 1. The browser calls the loop's callback with an elapsed time; `Sim` computes `dt` (elapsed seconds, capped per-screen by that `Screen`'s `maxDT` option to avoid a huge jump after e.g. a backgrounded tab resumes). 2. `Sim` steps the currently-selected screen's model (if it has a `step( dt )` method) and then its [`ScreenView`](/api/joist/screen-view)'s own `step( dt )` — only the active screen steps; a screen the user isn't looking at does no per-frame work. 3. `stepTimer` (from `scenerystack/axon`) emits `dt`, firing anything scheduled via `stepTimer.setTimeout`/`setInterval` whose accumulated time has elapsed — see [Timer, animationFrameTimer, and stepTimer](/api/axon/timer) for how this differs from `animationFrameTimer`, which fires every frame independent of which screen (or whether any screen) is active. 4. The [`Display`](/api/scenery/display) underlying the sim repaints, syncing any Node whose visual state changed as a result of the above back to actual pixels — this is the same `updateDisplay()`/`updateOnRequestAnimationFrame()` mechanism described in [Scenery Basics](/guides/scenery-basics), just already wired up for you by `Sim` rather than something you drive yourself. Model and view `step` functions are the only place per-frame model mutation should happen — a `Property` set directly from an input listener (a drag, a slider) is fine and expected, but time-based motion (an object coasting, an animation easing toward a target) belongs in `step(dt)`, driven by the loop above, not in a `setInterval` or a listener that assumes a fixed frame rate. ::: tip `activeProperty` pauses the loop's effects, not the loop itself Setting `sim.activeProperty = false` doesn't stop `requestAnimationFrame` from firing — `Sim` keeps receiving frames, but skips stepping the model/view while inactive, which is what "pausing" a sim actually means at this layer. This is distinct from `browserTabVisibleProperty`, which reflects whether the browser tab itself is visible and is one of the inputs `Sim` uses to decide whether to set `activeProperty` automatically. ::: ## Where to go next - [Sim](/api/joist/sim) — the full constructor/option/member reference for the class this page walks through - [Screen](/api/joist/screen) — the lazy model/view factory pattern `start()` invokes - [Timer, animationFrameTimer, and stepTimer](/api/axon/timer) — the two timer singletons driven by (or independent of) this loop - [Scenery Basics](/guides/scenery-basics) — the `Display`/repaint mechanism the loop drives each frame ======================================================================== Page: Testing and QA Strategy Overview URL: https://veillette.github.io/Almanach/guides/testing-and-qa-strategy-overview Source: docs/guides/testing-and-qa-strategy-overview.md Category: Guides | Tags: testing, qa, fuzz-testing, qunit, quality | Status: complete ======================================================================== # Testing and QA Strategy Overview No single kind of test catches every class of bug in a SceneryStack simulation. A model's physics can be perfectly correct while a control panel silently fails to render on a narrow window; a screen can look flawless while an aggressive sequence of clicks and drags crashes it after ninety seconds. This page is a map of the three complementary layers a healthy sim project relies on — headless unit tests, manual QA passes, and automated fuzz-testing — and what each one is (and isn't) good for. ## The three layers | Layer | Catches | Cost | Runs | | --- | --- | --- | --- | | Unit-testing model logic headlessly | Physics/logic bugs, regressions in `step()`, incorrect `reset()` | Cheapest — no browser, milliseconds per test | Every commit, in CI | | Manual QA passes | Visual bugs, UX confusion, accessibility gaps, "does this feel right" | Most expensive — a person's time | Before a release, after major feature work | | Automated fuzz-testing (`?fuzz`) | Crashes and exceptions from unexpected/rapid input sequences no one thought to test by hand | Cheap to run, requires a browser | Regularly in CI or ahead of a release | None of the three substitutes for the others: a model can be exhaustively unit-tested and still crash the moment a real user drags two things at once during a screen transition, and a sim can survive an hour of fuzzing without a single exception while still being confusing or ugly. Treat them as different nets, each catching what falls through the others. ## Layer 1: unit-testing model logic headlessly The cheapest and fastest layer is also the one to lean on hardest. Because [Model-View Separation](/patterns/model-view-separation) keeps model classes free of `scenery`/`sun` imports, model logic can be constructed, stepped, and asserted against in plain Node.js — see [Testing Model Logic Headlessly](/patterns/testing-model-logic-headlessly) for the full pattern and a worked `QUnit` example. This is where the bulk of a project's automated test coverage should live: it's fast enough to run on every commit and precise enough that a failure points at the actual broken logic, not a rendering symptom. ## Layer 2: manual QA passes Some things can only be judged by a person looking at (or listening to, or navigating with a keyboard through) the running sim: does this layout hold up at an unusual window size, is this control's purpose obvious without a caption, does this sound cue feel right paired with this animation. PhET's own process for this is a structured manual QA pass — a checklist walked by a human across every screen, every supported browser/platform combination, and every accessibility mode (keyboard-only, screen reader) before a release. A project without dedicated QA staff can still get most of the value by treating "click through every screen with `?stringTest=double` and a screen reader on" as a release-blocking checklist item, not an afterthought. ## Layer 3: automated fuzz-testing Between "the model is unit-tested" and "a human clicked through it once" is a large space of input sequences nobody thought to try by hand: rapid clicking during a screen transition, dragging while a reset is in progress, opening a dialog and immediately switching screens. SceneryStack simulations have a built-in tool for probing that space automatically — appending `?fuzz` to the sim's URL drives a stream of randomized pointer and keyboard events at the running sim and reports the first uncaught exception (typically paired with `?ea` so assertions catch problems as early as possible rather than corrupting state silently). See [Fuzz-Testing a Simulation Locally](/cookbook/fuzz-testing-a-simulation-locally) for the concrete recipe — running it locally for a few minutes, and reading query-parameter-driven fuzzing in general as one more application of the [Query Parameters Pattern](/patterns/query-parameters-pattern). Fuzzing is a crash-finder, not a correctness-checker: it tells you the sim *didn't throw*, not that the physics is right or the UI makes sense — that's still the job of layers 1 and 2. Its value is coverage breadth at near-zero authoring cost: once wired into a CI job (see [Continuous Integration for SceneryStack Projects](/guides/continuous-integration-for-scenerystack-projects)), it runs the same randomized stress test on every push without anyone having to imagine the failing sequence in advance. ::: tip Layer the checks so the cheapest one runs most often Run headless model tests on every commit, fuzz-testing on a schedule or before merging significant changes, and reserve full manual QA passes for pre-release milestones. Trying to run a full manual pass on every commit is too slow to sustain; relying only on fuzzing and unit tests risks shipping something technically crash-free but confusing or visually broken. ::: ## Where to go next - [Testing Model Logic Headlessly](/patterns/testing-model-logic-headlessly) — the concrete pattern for layer 1 - [Fuzz-Testing a Simulation Locally](/cookbook/fuzz-testing-a-simulation-locally) — the concrete recipe for layer 3 - [Continuous Integration for SceneryStack Projects](/guides/continuous-integration-for-scenerystack-projects) — wiring these layers into a pipeline that runs automatically ======================================================================== Page: Translation and Localization URL: https://veillette.github.io/Almanach/guides/translation-and-localization Source: docs/guides/translation-and-localization.md Category: Guides | Tags: localization, i18n, strings | Status: verified ======================================================================== # Translation and Localization Every piece of user-visible text in a SceneryStack simulation is a [`StringProperty`](/api/axon/string-property) (or a `TReadOnlyProperty` derived from one), never a plain string baked into a `Text` node. That single decision is what makes translation possible: swapping the *value* underneath an existing `Property` re-renders anywhere that string is displayed, with no manual re-rendering step. ## Strings live in Properties, not literals ```ts import { Text } from 'scenerystack/scenery'; import { StringProperty } from 'scenerystack/axon'; const greetingProperty = new StringProperty( 'Hello!' ); const greetingText = new Text( greetingProperty, { font: '20px sans-serif' } ); // Changing the underlying value updates every Node displaying it automatically. greetingProperty.value = 'Bonjour !'; ``` `Text` and `RichText` both accept a `TReadOnlyProperty` directly wherever they'd otherwise take a plain string, so passing a `StringProperty` (or a `DerivedProperty`/`PatternStringProperty` built from one) is idiomatic, not a special case. ## The PhET translation pipeline (conceptual) In a full PhET-style project, you don't hand-construct every `StringProperty` yourself. The workflow is: 1. Author English strings once, in a JSON file at the project root (conventionally `-strings_en.json`) — see [Project Structure Conventions](/getting-started/project-structure-conventions). 2. A build-time step generates a strongly-typed strings module from that file, exposing one `StringProperty` per key. 3. Translators submit additional per-locale JSON files (`-strings_fr.json`, etc.) through PhET's translation tooling, without touching any TypeScript. 4. At runtime, the active locale determines which JSON file's values populate the generated `StringProperty` instances — the application code that consumes them (`Text`, `RichText`, any `TReadOnlyProperty` consumer) never changes. The exact generation step is part of the PhET build tooling and out of scope for simulation-author-facing code in Almanach, but the contract you write against is stable regardless: **every generated string is a `Property`**, with the same `.value`/`.link()` API described in [`StringProperty`](/api/axon/string-property) and [`Property`](/api/axon/property). ## Interpolating values into translated strings Never build a translated string with template literals or concatenation — word order and pluralization vary across languages, so a fixed English sentence shape breaks translation. Use `StringUtils.fillIn` (or the axon `PatternStringProperty`, which wraps the same mechanism as a reactive `Property`) with named placeholders instead: ```ts import { StringUtils } from 'scenerystack/phetcommon'; // pattern (usually itself a translated StringProperty's value): '{{name}} scored {{points}} points' const message = StringUtils.fillIn( messagePatternProperty.value, { name: 'Ada', points: 42 } ); ``` ```ts import { PatternStringProperty } from 'scenerystack/axon'; // Reactive version: recomputes whenever the pattern or values change. const scoreMessageProperty = new PatternStringProperty( messagePatternProperty, { name: playerNameProperty, points: pointsProperty } ); ``` ## Runtime locale `localeProperty` (from `scenerystack/joist`) reflects the simulation's active locale and is normally set once at startup from the `?locale=` query parameter, not changed mid-session by application code: ```ts import { localeProperty } from 'scenerystack/joist'; console.log( 'current locale:', localeProperty.value ); // e.g. 'en', 'fr', 'es' ``` ::: tip Don't hand-roll a second translation mechanism It's tempting to reach for `Intl` or a homegrown lookup table for "just this one string." Keeping every displayed string on the `StringProperty` pipeline — even ones you're sure will never need translation — means the whole simulation's localization surface stays discoverable and translatable in one pass, rather than some strings quietly falling outside the pipeline. ::: ## Where to go next - [`StringProperty`](/api/axon/string-property) — the Property type every translated string is built on - [Preferences and Feature Flags](/guides/preferences-and-feature-flags) — where locale and other runtime settings are surfaced to users - [Project Structure Conventions](/getting-started/project-structure-conventions) — where the source strings JSON file lives in a repo ======================================================================== Page: UI Components Overview URL: https://veillette.github.io/Almanach/guides/ui-components-overview Source: docs/guides/ui-components-overview.md Category: Guides | Tags: sun, scenery-phet, ui-components | Status: verified ======================================================================== # UI Components Overview SceneryStack splits reusable, pre-built `Node`s across two libraries with different scopes: `sun` provides generic, domain-agnostic controls (the things any application needs — buttons, sliders, checkboxes), while `scenery-phet` provides PhET-simulation-specific visualizations and controls built on top of `sun` and `scenery`. Knowing which library a component lives in tells you how broadly reusable it's meant to be. ## `sun`: generic UI controls Everything in `sun` is bound to an axon `Property` and otherwise has no domain knowledge — a `Checkbox` doesn't know or care whether it's toggling gravity or sound. ```ts import { Checkbox, HSlider, RectangularPushButton, ComboBox, AccordionBox, Panel } from 'scenerystack/sun'; ``` | Component | Use it for | | --- | --- | | [`Checkbox`](/api/sun/checkbox) | A single boolean toggle with a label Node | | [`HSlider`](/api/sun/hslider) / `VSlider` | Continuous numeric input bound to a `NumberProperty` and `Range` | | [`RectangularPushButton`](/api/sun/rectangular-push-button) / `TextPushButton` | A momentary action button | | `RectangularRadioButtonGroup` / [`RadioButtonGroup`](/api/sun/radio-button-group) | Mutually-exclusive selection from a small fixed set of options | | [`ComboBox`](/api/sun/combo-box) | Mutually-exclusive selection from a longer list, in a dropdown | | [`Panel`](/api/sun/panel) | Bordered/background container that auto-sizes to its content | | [`AccordionBox`](/api/sun/accordion-box) | Collapsible titled container, e.g. an optional "advanced controls" section | | `ABSwitch` / `ToggleSwitch` | A labeled two-state switch, an alternative to `Checkbox` for some layouts | ## Live demos These controls have interactive embeds on their API pages. Try them here, then follow the links for full option tables. ### `sun` [Checkbox API](/api/sun/checkbox) [HSlider API](/api/sun/hslider) [RectangularPushButton API](/api/sun/rectangular-push-button) ### `scenery-phet` [NumberControl API](/api/scenery-phet/number-control) [ResetAllButton API](/api/scenery-phet/reset-all-button) Compose these with [`FlowBox`/`GridBox`](/guides/scenery-layout) rather than positioning each one by hand: ```ts import { VBox } from 'scenerystack/scenery'; import { Checkbox, HSlider } from 'scenerystack/sun'; import { Text } from 'scenerystack/scenery'; const controlPanel = new VBox( { spacing: 8, align: 'left', children: [ new Checkbox( gravityEnabledProperty, new Text( 'Gravity' ) ), new HSlider( massProperty, massProperty.range! ) ] } ); ``` ## `scenery-phet`: simulation-specific visualizations `scenery-phet` builds on `sun` and `scenery` to provide components that show up across many PhET simulations but aren't generic enough for `sun` — visual metaphors for physical quantities, and the handful of controls every sim needs regardless of subject matter. ```ts import { ResetAllButton, NumberControl, ThermometerNode, ArrowNode, PhetFont } from 'scenerystack/scenery-phet'; ``` | Component | Use it for | | --- | --- | | [`ResetAllButton`](/api/scenery-phet/reset-all-button) | The standard "reset the whole screen" button — see the [Reset-All Pattern](/patterns/reset-all-pattern) | | [`NumberControl`](/api/scenery-phet/number-control) | A labeled slider plus numeric readout plus increment/decrement, bundled together | | [`ThermometerNode`](/api/scenery-phet/thermometer-node) | A fluid-level thermometer bound to a temperature `Property` | | [`ArrowNode`](/api/scenery-phet/arrow-node) | A vector/force-style arrow, sized and rotated from model data | | `FaceNode` | A smiley/frowny face, common in game-style feedback | | `HeaterCoolerNode` | The heater/cooler control used across thermodynamics sims | | [`PhetFont`](/api/scenery-phet/phet-font) | The standard PhET typeface — use it instead of ad hoc `font` strings for visual consistency | ## Choosing between them ::: tip If it doesn't mention a physical quantity, it probably belongs in `sun` A rule of thumb: if you can describe a component without referencing anything domain-specific ("a checkbox," "a slider," "a bordered panel"), it's `sun`. If describing it requires a physical concept ("a thermometer," "a force arrow," "a heater/cooler"), it's `scenery-phet`. When you need something novel to your sim's domain that isn't in either library, build it as a plain `Node`/`Path` composition in your own `view/` folder rather than trying to force-fit an existing component — see [Project Structure Conventions](/getting-started/project-structure-conventions). ::: ## Where to go next - [Scenery Layout](/guides/scenery-layout) — arranging these components with `FlowBox`/`GridBox`/`AlignBox` - [The Reset-All Pattern](/patterns/reset-all-pattern) — the standard use of `ResetAllButton` - [Building Your First Screen](/guides/building-your-first-screen) — assembling these into a full `ScreenView` ======================================================================== Page: Working with Model-View Transforms URL: https://veillette.github.io/Almanach/guides/working-with-model-view-transforms Source: docs/guides/working-with-model-view-transforms.md Category: Guides | Tags: phetcommon, dot, coordinates, transform, ModelViewTransform2 | Status: complete ======================================================================== # Working with Model-View Transforms [`ModelViewTransform2`](/api/phetcommon/model-view-transform) is a small, specific class — a handful of factory methods and `modelToView*`/`viewToModel*` conversions. This guide is the broader "why," and a tour of where a single transform instance actually shows up across a screen's model and view code, once you've adopted the convention. ## Why separate model coordinates from view pixels at all [Model-View Separation](/patterns/model-view-separation) already establishes that a model's state should be plain data with no scenery imports. Coordinates are where that principle is easiest to violate by accident: it's tempting for a model's `positionProperty` to just hold "pixels from the top-left," since that's ultimately what gets drawn. Two things go wrong once you do that: - **The model becomes coupled to one specific view size.** A `positionProperty` holding view pixels has no meaning independent of how large the `Display` happens to be — resizing the browser window, or reusing the model in a different layout, means the model's own numbers stop making physical sense. - **Physics stops reading like physics.** A projectile's launch angle, a planet's orbital radius, a chemical concentration — these have natural units (degrees, meters, moles) with real-world magnitudes and, often, a "up is positive" convention. Scenery pixels are y-down and have no inherent unit; forcing model state into that frame means every physics formula in the model has to fight the coordinate system instead of expressing the physics directly. Keeping the model in physical, y-up units and doing every pixel conversion at the view boundary — through exactly one `ModelViewTransform2` instance per screen — means the model reads like the physics it represents, and the *only* place "pixels" and "y-down" enter the codebase at all is the small set of view classes that call the transform. ## Where one transform instance travels A screen typically constructs exactly one `ModelViewTransform2` (in the `ScreenView`'s constructor, sized against `this.layoutBounds`) and threads that same instance to everywhere it's needed — it's not recreated per view Node: ```ts import { ModelViewTransform2 } from 'scenerystack/phetcommon'; import { Vector2 } from 'scenerystack/dot'; import { ScreenView, type ScreenViewOptions } from 'scenerystack/sim'; class ProjectileScreenView extends ScreenView { public constructor( model: ProjectileModel, providedOptions: ScreenViewOptions ) { super( providedOptions ); const modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( Vector2.ZERO, new Vector2( this.layoutBounds.centerX, this.layoutBounds.maxY - 100 ), 50 ); // The same instance flows to every view class that needs to convert coordinates. const projectileNode = new ProjectileBodyNode( model, modelViewTransform ); // ...and to any drag listener moving a model Property through pointer input. } } ``` The three places it typically shows up: | Consumer | What it converts | | --- | --- | | A view `Node`'s constructor, linking `model.positionProperty` | Model position → `translation`, via `modelToViewPosition` (see [Model-View Separation](/patterns/model-view-separation)) | | A [drag listener](/patterns/drag-listeners)'s `transform` option | Pointer motion in view pixels → the model `Property` it writes, automatically, in both directions | | Anything sizing a Node from a model quantity (a radius, a velocity vector) | Model-space *deltas* → view-space deltas, via `modelToViewDeltaX`/`modelToViewDelta` — never the position methods, which also apply translation | Passing the transform down as a constructor argument (rather than each view class constructing its own) is what guarantees every Node in a screen agrees on the same mapping — two Nodes built from different transform instances would each convert correctly in isolation but disagree with each other about where the same model point belongs on screen. ## Choosing y-inverted vs. not Most physics-flavored simulations want `createSinglePointScaleInvertedYMapping` — model y increases upward (matching how most physics is taught and written), view y increases downward (matching scenery/CSS/canvas convention), and the transform's job is exactly to reconcile that mismatch once, in one place. A simulation whose "model" is itself already screen-like (a diagram editor, something with no physical y-up convention to preserve) can reach for the non-inverted `createSinglePointScaleMapping` instead — the choice is about whether the model's own y-axis has an inherent "up" direction worth preserving, not a default to apply unconditionally. ::: tip One transform per screen is the common case, not a strict rule A screen with multiple independent visual regions at different scales (a small inset chart alongside a large main view, for instance) may legitimately need more than one `ModelViewTransform2` — one per region, each mapping the same underlying model quantity to that region's own view scale. What doesn't scale is constructing a *new* transform per Node for the same region — that reintroduces exactly the risk (Nodes disagreeing about the mapping) the shared-instance convention exists to prevent. ::: ## Where to go next - [ModelViewTransform2](/api/phetcommon/model-view-transform) — the class reference: constructors, methods, and the positions-vs-deltas distinction in full - [Drag Listeners](/patterns/drag-listeners) — the `transform` option that lets a `DragListener` convert pointer motion automatically - [Model-View Separation](/patterns/model-view-separation) — the broader architecture this coordinate convention is one instance of ======================================================================== Page: Working with Sound (tambo Overview) URL: https://veillette.github.io/Almanach/guides/working-with-sound Source: docs/guides/working-with-sound.md Category: Guides | Tags: tambo, sound, audio | Status: verified ======================================================================== # Working with Sound (tambo Overview) `tambo` (`scenerystack/tambo`) is SceneryStack's sound-generation library: instead of every part of a simulation calling into the Web Audio API directly, each sound-producing thing in the sim is a small **sound generator** object, and a single global **`soundManager`** owns the actual audio graph — enabling/disabling sound globally, mixing generators together, applying per-category gain, and connecting the result to the browser's audio output. This page is a subsystem-level tour of that architecture; for using sound specifically as an accessibility channel (sonification alongside Voicing and the PDOM), see [Sound Design](/accessibility/sound-design). ## soundManager: the mixer `soundManager` is a singleton — you don't construct it, you register generators with it. Conceptually it's the mixing desk: every sound generator you create gets `add`ed to it once, and from then on `soundManager` controls whether it's audible, how loud, and through which category-level gain it's routed: ```ts import { soundManager } from 'scenerystack/tambo'; import { SoundClip } from 'scenerystack/tambo'; import popSound_mp3 from './sounds/popSound_mp3.js'; const popSoundClip = new SoundClip( popSound_mp3 ); soundManager.addSoundGenerator( popSoundClip, { categoryName: 'sim-specific' } ); ``` A generator that's never added to `soundManager` never produces audible output — construction alone doesn't connect it to anything. `removeSoundGenerator` reverses this, and should be called as part of disposing whatever owns the generator (see [Dispose and Memory Management](/patterns/dispose-and-memory-management)) so a long-running sim doesn't accumulate generators nothing plays through anymore. `soundManager` also exposes `setOutputLevelForCategory( categoryName, outputLevel )`, letting you (or a Preferences panel) turn a whole category up or down together — the standard categories are `'sim-specific'` and `'user-interface'`, so a user can, for instance, mute UI click sounds while keeping model-driven sonification audible. ## The sound generator hierarchy Every concrete generator (`SoundClip`, `PitchedPopGenerator`, `OscillatorSoundGenerator`, and others) extends a common `SoundGenerator` base class, which is what `soundManager.addSoundGenerator` actually expects. That shared base is what lets `soundManager` treat "play a recorded clip" and "synthesize a tone" identically for the purposes of enabling, disabling, and mixing — the differences between generator types are only in *how* each produces its waveform, not in how it's registered or controlled. | Generator | Produces | Typical use | | --- | --- | --- | | `SoundClip` | Plays a decoded audio asset, one-shot or looping | The default choice for any pre-recorded sound effect | | `SoundClipPlayer` | A thin wrapper managing a pool of `SoundClip`-like players for rapid repeated triggering | Sounds fired often enough that overlapping playback needs pooling rather than one clip instance | | `PitchedPopGenerator` | A synthesized "pop" with a controllable pitch, no audio asset needed | Feedback tied to a continuous value (a slider position, a count) where discrete recorded pitches would be impractical | | `OscillatorSoundGenerator` | A raw oscillator tone | Low-level procedural sound, rarely used directly outside other generators built on it | | `NoiseGenerator` | Synthesized noise (not a recorded sample) | Ambient/textural sound rather than a discrete event | | `CompositeSoundClip` | Plays several `SoundClip`s together as one logical unit | Layering (e.g. a base hit sound plus a variable layer) without hand-managing multiple independent clips | ## Playing a recorded sound: SoundClip `SoundClip` is the generator you reach for most: it wraps a single decoded audio asset (imported the same way an image asset would be) and exposes `play()`/`stop()`: ```ts import { SoundClip, soundManager } from 'scenerystack/tambo'; import collisionSound_mp3 from './sounds/collisionSound_mp3.js'; const collisionSoundClip = new SoundClip( collisionSound_mp3, { loop: false, initialPlaybackRate: 1 } ); soundManager.addSoundGenerator( collisionSoundClip ); model.collisionEmitter.addListener( () => collisionSoundClip.play() ); ``` `play( delay )` and `stop( delay )` both accept an optional delay in seconds — useful for lining a sound up with an animation rather than triggering it the instant the model event fires. Setting `loop: true` turns the same class into a continuous ambient sound (a running motor, an idle hum) instead of a one-shot effect; a looping clip is started once and left playing (or its `initialOutputLevel`/gain adjusted) rather than re-triggered per event. ## Synthesized feedback: PitchedPopGenerator Not every sound corresponds to a discrete recorded asset — feedback tied to a continuously varying value (dragging a slider, a running count) is often better served by a synthesized tone whose pitch tracks the value than by trying to pre-record enough discrete pitches to sound smooth. `PitchedPopGenerator` is tambo's built-in example of this pattern: it produces a short "pop" whose pitch you control per-call, pooling a small number of oscillator/gain pairs internally so rapid successive calls don't cut each other off: ```ts import { PitchedPopGenerator, soundManager } from 'scenerystack/tambo'; import { Range } from 'scenerystack/dot'; const popGenerator = new PitchedPopGenerator( { pitchRange: new Range( 220, 660 ) } ); soundManager.addSoundGenerator( popGenerator ); // relativePitch is 0-1; 0 maps to pitchRange.min, 1 to pitchRange.max countProperty.link( count => { const relativePitch = countProperty.range!.getNormalizedValue( count ); popGenerator.playPop( relativePitch ); } ); ``` This is the general shape for any procedurally-driven feedback: construct one generator, register it once with `soundManager`, then call its play method repeatedly as the underlying value changes, rather than constructing a new sound generator per event. ## Where to go next - [Sound Design](/accessibility/sound-design) — using `soundManager`/`SoundClip`/shared sound players specifically as an accessibility (sonification) channel, and the `sharedSoundPlayers` registry for common UI sounds - [Dispose and Memory Management](/patterns/dispose-and-memory-management) — why generators need `removeSoundGenerator` on disposal, same as any other listener-holding object - [Scenery Basics](/guides/scenery-basics) — the Node tree that sound-triggering interactions (drag, press, model events) live alongside ======================================================================== Page: BooleanProperty URL: https://veillette.github.io/Almanach/api/axon/boolean-property Source: docs/api/axon/boolean-property.md Category: API | Tags: axon, BooleanProperty | Status: verified ======================================================================== # BooleanProperty `BooleanProperty` (from `scenerystack/axon`) is a [`Property`](/api/axon/property) whose value is constrained to `boolean` — truthy/falsy non-boolean values are rejected by its built-in validator, not silently coerced. It's the standard type for simulation flags like "is the sim paused," "is this checkbox checked," or "is this Node visible." ```ts import { BooleanProperty } from 'scenerystack/axon'; const isPlayingProperty = new BooleanProperty( true ); isPlayingProperty.link( isPlaying => { console.log( isPlaying ? 'playing' : 'paused' ); } ); isPlayingProperty.toggle(); // logs "paused" ``` ## Methods `BooleanProperty` adds exactly one method beyond what it inherits from [`Property`](/api/axon/property): | Method | Effect | | --- | --- | | `toggle()` | Sets `value = !value` | Everything else — `value`, `link`, `lazyLink`, `reset`, `dispose`, and so on — comes from `Property`; see the [Property reference](/api/axon/property) for the full list. ## Options `BooleanPropertyOptions` is `PropertyOptions` with `isValidValue`, `valueType`, and `phetioValueType` removed — `BooleanProperty` sets `valueType: 'boolean'` and the PhET-iO `BooleanIO` type internally, so you cannot override them. ```ts const isVisibleProperty = new BooleanProperty( false, { // any remaining PropertyOptions, e.g.: reentrant: false } ); ``` ::: tip Combining booleans To derive a boolean from other Properties (`a && b`, `a || b`, `!a`), don't write your own listener — use [`DerivedProperty.and`](/api/axon/derived-property), `.or`, and `.not`, which return a read-only `TReadOnlyProperty` kept automatically in sync. ::: ======================================================================== Page: DerivedProperty URL: https://veillette.github.io/Almanach/api/axon/derived-property Source: docs/api/axon/derived-property.md Category: API | Tags: axon, DerivedProperty | Status: verified ======================================================================== # DerivedProperty `DerivedProperty` (from `scenerystack/axon`) is a read-only Property whose value is automatically recomputed whenever any of its dependency Properties change. It extends the same `ReadOnlyProperty` base as [`Property`](/api/axon/property), so it supports `.value`, `.link()`, `.lazyLink()`, etc. — but calling `.value = ...` on it is a programming error; the value can only change via the derivation function. ```ts import { DerivedProperty, NumberProperty } from 'scenerystack/axon'; const widthProperty = new NumberProperty( 4 ); const heightProperty = new NumberProperty( 3 ); const areaProperty = new DerivedProperty( [ widthProperty, heightProperty ], ( width, height ) => width * height ); areaProperty.link( area => console.log( 'area:', area ) ); // logs "area: 12" immediately widthProperty.value = 5; // logs "area: 15" ``` The derivation function receives the current `.value` of each dependency, in the same order as the `dependencies` array, and its return type determines `T`. ## Constructor ```ts new DerivedProperty( dependencies, derivation, providedOptions? ) ``` `dependencies` is an array of up to 15 `TReadOnlyProperty<...>` instances (any Property, including another `DerivedProperty`); overloads pick the matching arity so `derivation`'s parameter types line up automatically. ## Members | Member | Effect | | --- | --- | | `recomputeDerivation()` | Force a recompute — useful when a non-Property event (an [`Emitter`](/api/axon/emitter), an observable array's `elementAddedEmitter`) should also trigger recomputation | | `hasDependency( dependency )` | Whether a given Property is one of this DerivedProperty's dependencies | | `dispose()` | Unlinks from every dependency; always dispose a `DerivedProperty` you own once it's no longer needed | ## Static factories Rather than writing a derivation by hand for common cases, `DerivedProperty` offers static helpers that return a `TReadOnlyProperty`: | Static method | Produces | | --- | --- | | `DerivedProperty.and( properties )` | `true` iff every boolean-valued Property is `true` | | `DerivedProperty.or( properties )` | `true` iff any boolean-valued Property is `true` | | `DerivedProperty.not( property )` | The logical inverse of a boolean Property | | `DerivedProperty.valueEquals( a, b )` | `true` iff `a.value === b.value` | | `DerivedProperty.valueNotEquals( a, b )` | `true` iff `a.value !== b.value` | | `DerivedProperty.valueEqualsConstant( a, value )` | `true` iff `a.value === value` | | `DerivedProperty.add( properties )` / `.multiply( properties )` | Sum / product of number-valued Properties | | `DerivedProperty.toFixedProperty( numberProperty, decimalPlaces )` | A `string` Property formatting the number to a fixed number of decimals | | `DerivedProperty.fromRecord( key, record )` | Looks up `record[key.value]`, following through if the record's value is itself a Property | | `DerivedProperty.deriveAny( dependencies, derivation )` | Escape hatch for more than 15 dependencies or a dynamically-sized dependency array | ```ts import { BooleanProperty } from 'scenerystack/axon'; const isRunningProperty = new BooleanProperty( true ); const isEnabledProperty = new BooleanProperty( true ); const isActiveProperty = DerivedProperty.and( [ isRunningProperty, isEnabledProperty ] ); ``` ::: warning Read-only — and remember to dispose `DerivedProperty` overrides `set`/`value=` to throw; it's read-only by design (`phetioReadOnly: true` internally). It also `lazyLink`s to every dependency in its constructor, so an undisposed `DerivedProperty` keeps its dependencies alive — always call `.dispose()` when the derived value is no longer needed, symmetric with any Property you construct yourself. ::: If you need to react to several Properties without producing a *new* Property value — just running a side-effecting callback — use [`Multilink`](/api/axon/multilink) instead. ======================================================================== Page: DerivedStringProperty URL: https://veillette.github.io/Almanach/api/axon/derived-string-property Source: docs/api/axon/derived-string-property.md Category: API | Tags: axon, DerivedStringProperty, DerivedProperty, PatternStringProperty, i18n, strings | Status: complete ======================================================================== # DerivedStringProperty `DerivedStringProperty` (from `scenerystack/axon`) is a thin subclass of [`DerivedProperty`](/api/axon/derived-property) whose derivation is constrained to return a `string`. It exists so that string-valued derived Properties — typically built from a translated `StringProperty` and other dependencies — carry the right PhET-iO metadata by default and are easy to spot in code as "this is a derived, translated-or-formatted string," rather than a generic `DerivedProperty`. ```ts import { DerivedStringProperty, NumberProperty } from 'scenerystack/axon'; const countProperty = new NumberProperty( 3 ); const summaryStringProperty = new DerivedStringProperty( [ countProperty ], count => `${count} item${count === 1 ? '' : 's'} remaining` ); summaryStringProperty.value; // '3 items remaining' countProperty.value = 1; summaryStringProperty.value; // '1 item remaining' ``` Structurally, `DerivedStringProperty` takes the exact same `dependencies` and `derivation` constructor arguments as `DerivedProperty` — it's `DerivedProperty` with `T` pinned to a string subtype, plus two default options applied for you: | Default | Value | Why | | --- | --- | --- | | `phetioValueType` | `StringIO` | So you don't have to specify it yourself every time | | `phetioFeatured` | `true` | Translated/derived strings are treated as PhET-iO-featured by default (override if this is an internal, non-featured string) | ## When to reach for this vs. `PatternStringProperty` If your derivation is "substitute values into a translated pattern string with `{{placeholder}}` tokens," use [`PatternStringProperty`](/api/axon/pattern-string-property) instead — it's *itself* a `DerivedStringProperty` under the hood, and handles the placeholder-filling, `maps`, and `decimalPlaces` machinery for you. Reach for `DerivedStringProperty` directly when your string logic is closer to a one-off formatting function than a pattern substitution — for example, choosing between several already-translated strings based on a state Property, or building a plain (non-pattern) string from numeric data. ::: tip Constrain `T` to the string literals you actually produce Because `DerivedStringProperty` is generic in `T`, you can (and often should) narrow it to a union of literal strings your derivation actually returns, e.g. `DerivedStringProperty<'small' | 'medium' | 'large', ...>`, rather than letting it widen to the bare `string` type. This gives downstream code (and Storybook-style PhET-iO tooling) a much more useful type than "any string." ::: ======================================================================== Page: DynamicProperty URL: https://veillette.github.io/Almanach/api/axon/dynamic-property Source: docs/api/axon/dynamic-property.md Category: API | Tags: axon, DynamicProperty, Property, MappedProperty | Status: verified ======================================================================== # DynamicProperty `DynamicProperty` (from `scenerystack/axon`) solves a specific problem [`DerivedProperty`](/api/axon/derived-property) can't: following a Property *of* Properties. Given a `Property>` — for example a `currentSceneProperty` whose value is itself a scene's `backgroundColorProperty` — `DynamicProperty` produces a plain `Property` that automatically re-subscribes to whichever inner Property is current, so consumers never have to unlink from the old scene and relink to the new one by hand. ```ts import { DynamicProperty, Property } from 'scenerystack/axon'; const firstProperty = new Property( 'red' ); const secondProperty = new Property( 'blue' ); const currentProperty = new Property( firstProperty ); // Property> const colorProperty = new DynamicProperty( currentProperty ); colorProperty.value; // 'red' — currentProperty.value is firstProperty firstProperty.value = 'yellow'; colorProperty.value; // 'yellow' — still following firstProperty currentProperty.value = secondProperty; // switch which Property is active colorProperty.value; // 'blue' ``` If `currentProperty.value` is ever `null`, `DynamicProperty` falls back to its `defaultValue` option rather than throwing. ## Constructor ```ts new DynamicProperty( valuePropertyProperty, providedOptions? ) ``` `valuePropertyProperty` is the outer, "Property of Properties" source; its value may be `null` to mean "disconnected." ## Options | Option | Default | Effect | | --- | --- | --- | | `derive` | identity | A function `(outerValue) => TReadOnlyProperty`, or a string key, for reaching an inner Property that's nested inside the outer value (e.g. `derive: 'backgroundColorProperty'` when the outer Property holds a `Scene` object rather than a Property directly) | | `defaultValue` | `null` | The `InnerValueType` used (before `map`) when `valuePropertyProperty.value` is `null` | | `map` | identity | Maps the inner Property's value to `ThisValueType` — lets `DynamicProperty` change type, not just source | | `inverseMap` | identity | Required alongside `map` only if `bidirectional: true` and the mapping isn't its own inverse | | `bidirectional` | `false` | If `true`, setting `dynamicProperty.value` writes back through `inverseMap` into the currently-active inner Property, instead of throwing | ```ts // Bidirectional example, plus a map that changes the exposed type const dynamicProperty = new DynamicProperty( currentProperty, { bidirectional: true, map: ( n: number ) => `${n}`, inverseMap: ( s: string ) => Number.parseFloat( s ) } ); ``` ## Methods | Method | Effect | | --- | --- | | `reset()` | Resets whichever inner Property is currently active (asserts if not `bidirectional`) | | `dispose()` | Unlinks from both the outer Property and the currently-active inner Property | ::: tip MappedProperty is DynamicProperty with the wrapping done for you [`UnitConversionProperty`](/api/axon/unit-conversion-property) and the general-purpose `MappedProperty` are both literally `DynamicProperty` subclasses that wrap a single source Property in a `TinyProperty` internally, so you get `map`/`inverseMap`/`bidirectional` without needing an outer "Property of Properties." Reach for `DynamicProperty` directly only when the thing you're following can actually change — a `currentSceneProperty`-style source — not just when you want to transform one Property's value into another. ::: ======================================================================== Page: Emitter URL: https://veillette.github.io/Almanach/api/axon/emitter Source: docs/api/axon/emitter.md Category: API | Tags: axon, Emitter, events | Status: verified ======================================================================== # Emitter `Emitter` (from `scenerystack/axon`) is a typed event bus for discrete occurrences that carry no persistent state — a reset button being pressed, a collision happening, a level being completed. Unlike [`Property`](/api/axon/property), an `Emitter` has no "current value": a newly-added listener does *not* get called back with anything, it only hears about events that happen after it subscribes. ```ts import { Emitter } from 'scenerystack/axon'; const resetEmitter = new Emitter(); resetEmitter.addListener( () => console.log( 'sim was reset' ) ); resetEmitter.emit(); // logs "sim was reset" ``` ## Typed parameters `Emitter` is generic over the tuple of arguments passed to `emit()`. Declare the parameter types as a tuple: ```ts const scoredEmitter = new Emitter<[ points: number, isBonus: boolean ]>(); scoredEmitter.addListener( ( points, isBonus ) => { console.log( `scored ${points} points${isBonus ? ' (bonus!)' : ''}` ); } ); scoredEmitter.emit( 10, false ); ``` ## Methods | Method | Effect | | --- | --- | | `emit( ...args )` | Calls every listener synchronously with `args` | | `addListener( listener )` | Registers a listener to be called on `emit()` | | `removeListener( listener )` | Removes a specific listener | | `removeAllListeners()` | Removes every listener | | `hasListener( listener )` | Checks whether a listener is registered | | `hasListeners()` | Whether any listener is registered | | `getListenerCount()` | Number of registered listeners | | `dispose()` | Removes all listeners; call this when the Emitter's owner is disposed | ## Options `EmitterOptions` is mostly PhET-iO metadata (`tandem`, `phetioDocumentation`, `parameters` describing each argument for the data stream) plus two low-level tuning knobs inherited from `TinyEmitter`: | Option | Effect | | --- | --- | | `reentrantNotificationStrategy` | `'stack'` (default) or `'queue'` — controls ordering when a listener triggers another `emit()` of the same Emitter while still notifying | | `disableListenerLimit` | Disables the dev-mode assertion that catches an unbounded number of listeners (a common memory-leak symptom) | ```ts const explodedEmitter = new Emitter( { reentrantNotificationStrategy: 'queue' } ); ``` ::: tip Emitter vs. Property If what you're modeling has a "current value" that late-arriving listeners should immediately see — visibility, a numeric setting, a mode — use [`Property`](/api/axon/property) (or a typed subclass). Reach for `Emitter` only for momentary occurrences with no meaningful "current state." See [Emitter vs. Property](/patterns/emitter-vs-property) for more on making this call. [`createObservableArray`](/api/axon/observable-array) uses `Emitter`s internally (`elementAddedEmitter`, `elementRemovedEmitter`) as a good worked example of the distinction — the array's contents are state (its `lengthProperty`), but each add/remove is a discrete event. ::: ======================================================================== Page: EnabledComponent and EnabledProperty URL: https://veillette.github.io/Almanach/api/axon/enabled-component Source: docs/api/axon/enabled-component.md Category: API | Tags: axon, EnabledComponent, EnabledProperty, BooleanProperty, Disposable | Status: complete ======================================================================== # EnabledComponent and EnabledProperty `EnabledProperty` (from `scenerystack/axon`) is a thin `BooleanProperty` subclass that standardizes what an "enabled" Property looks like: it defaults `tandem` name-checking (it asserts, if instrumented, that its tandem is named `enabledProperty`), sets `phetioFeatured: true`, and supplies default `phetioDocumentation`. `EnabledComponent` is a small base class built on top of it — extend `EnabledComponent` (instead of hand-rolling `public readonly enabledProperty = new BooleanProperty( true )`) to get a consistent `enabledProperty`, `enabled` getter/setter, and disposal wired up automatically, with the same PhET-iO conventions every other `EnabledComponent` subclass follows. ```ts import { EnabledComponent } from 'scenerystack/axon'; class Thruster extends EnabledComponent { public constructor() { super( { enabled: true } ); } public fire(): void { if ( this.isEnabled() ) { // ... apply thrust } } } const thruster = new Thruster(); thruster.enabled = false; thruster.isEnabled(); // false ``` ## `EnabledComponent` options | Option | Default | Effect | | --- | --- | --- | | `enabledProperty` | `null` | Supply your own `TReadOnlyProperty` instead of letting `EnabledComponent` create one (useful to share/derive enabled-ness from elsewhere); if provided, `EnabledComponent` does not own or dispose it | | `enabled` | `true` | Initial value of the auto-created `enabledProperty`; ignored if `enabledProperty` is supplied | | `enabledPropertyOptions` | `null` | Options forwarded to the auto-created `EnabledProperty` (e.g. `phetioDocumentation`); ignored if `enabledProperty` is supplied | | `phetioEnabledPropertyInstrumented` | `true` | Whether the auto-created `enabledProperty` gets a PhET-iO tandem (`options.tandem.createTandem( 'enabledProperty' )`) or `Tandem.OPT_OUT` | | `tandem` | — | Standard PhET-iO tandem, used to namespace the auto-created `enabledProperty` | ## Public API | Member | Effect | | --- | --- | | `enabledProperty` | The underlying `TProperty` — either the one you supplied or an auto-created `EnabledProperty` | | `enabled` (getter/setter) | Convenience accessor for `enabledProperty.value` | | `isEnabled()` | Same as reading `.enabled` | | `dispose()` | Disposes the auto-created `enabledProperty` (only if `EnabledComponent` created it — a caller-supplied `enabledProperty` is left alone) | ## `GatedEnabledProperty` and friends If you need an `enabledProperty` whose value combines an internal ("self") flag with an external, PhET-iO-controllable gate, see `GatedEnabledProperty` (and the parallel `GatedBooleanProperty`/`GatedVisibleProperty`) — also exported from `scenerystack/axon`. Public fields exposing a gated result should be typed [`ReadOnlyProperty`](/api/axon/read-only-property), matching the general rule that anything not meant to be set directly from outside advertises `ReadOnlyProperty`, not `Property`. ::: tip Passing your own `enabledProperty` opts you out of ownership If you supply `enabledProperty` yourself (rather than letting `EnabledComponent` construct one), `EnabledComponent` will neither instrument nor dispose it — that Property's lifecycle is entirely your responsibility. This is the usual way to make several related objects share one `enabledProperty`, but forgetting to dispose the shared Property yourself is a common leak. ::: ======================================================================== Page: EnumerationProperty URL: https://veillette.github.io/Almanach/api/axon/enumeration-property Source: docs/api/axon/enumeration-property.md Category: API | Tags: axon, EnumerationProperty | Status: verified ======================================================================== # EnumerationProperty `EnumerationProperty` (from `scenerystack/axon`) is a [`Property`](/api/axon/property) whose value is restricted to the instances of a rich [`EnumerationValue`](https://www.npmjs.com/package/scenerystack) subclass, PhET's typed alternative to string-union "enums." It automatically fills in `validValues` from the enumeration and wires up the matching PhET-iO `EnumerationIO`. ```ts import { EnumerationProperty } from 'scenerystack/axon'; import { Enumeration, EnumerationValue } from 'scenerystack/phet-core'; class SimSpeed extends EnumerationValue { public static readonly NORMAL = new SimSpeed(); public static readonly SLOW = new SimSpeed(); // Must be declared last, once all values above exist. public static readonly enumeration = new Enumeration( SimSpeed ); } const simSpeedProperty = new EnumerationProperty( SimSpeed.NORMAL ); simSpeedProperty.link( speed => console.log( 'speed:', speed.toString() ) ); simSpeedProperty.value = SimSpeed.SLOW; ``` The PhET enumeration pattern (from `EnumerationValue`'s own documentation): declare each value as a `static readonly` instance of the class, then declare `static readonly enumeration = new Enumeration( MyClass )` last, after every value exists. `EnumerationValue` seals the class at that point — you can no longer construct new instances of it directly (subclassing is still allowed). ## Options `EnumerationPropertyOptions` extends [`PropertyOptions`](/api/axon/property) (minus `phetioValueType`, which `EnumerationProperty` fills in for you) with one addition: | Option | Effect | | --- | --- | | `enumeration` | The `Enumeration` to validate against. Defaults to `value.enumeration` (read off the initial value); provide it explicitly only if you're subtyping an enumeration and need to pin the subtype's enumeration | ```ts const speedProperty = new EnumerationProperty( SimSpeed.NORMAL, { enumeration: SimSpeed.enumeration // usually unnecessary — inferred from `value` } ); ``` ::: tip Exhaustiveness over string unions Prefer `EnumerationProperty` over a [`StringProperty`](/api/axon/string-property) with `validValues` whenever the value represents a closed set of named states. TypeScript can exhaustively check `switch` statements over `EnumerationValue` instances, and the enumeration's `.values` array (`SimSpeed.enumeration.values`) is available at runtime for building UI like radio button groups. ::: ======================================================================== Page: Multilink URL: https://veillette.github.io/Almanach/api/axon/multilink Source: docs/api/axon/multilink.md Category: API | Tags: axon, Multilink | Status: verified ======================================================================== # Multilink `Multilink` (from `scenerystack/axon`) links a callback to multiple Properties at once, calling it whenever any of them change. It's the callback-only sibling of [`DerivedProperty`](/api/axon/derived-property): use it when you need a side effect (repositioning a Node, playing a sound, logging) rather than a new derived value. ```ts import { Multilink, NumberProperty } from 'scenerystack/axon'; const xProperty = new NumberProperty( 0 ); const yProperty = new NumberProperty( 0 ); Multilink.multilink( [ xProperty, yProperty ], ( x, y ) => { console.log( `position: (${x}, ${y})` ); } ); // logs "position: (0, 0)" immediately xProperty.value = 5; // logs "position: (5, 0)" ``` ## Static convenience methods (preferred) The static factories are preferred over `new Multilink(...)` directly, both because they read better and because a bare `new Multilink(...)` with no captured reference trips lint rules for side-effect-only construction: | Method | Behavior | | --- | --- | | `Multilink.multilink( dependencies, callback )` | Links and calls `callback` immediately with current values | | `Multilink.lazyMultilink( dependencies, callback )` | Links without an immediate callback | | `Multilink.multilinkAny( dependencies, callback )` | Same as `multilink`, but for a dynamically-sized array of dependencies (callback takes no arguments) | | `Multilink.lazyMultilinkAny( dependencies, callback )` | Lazy version of `multilinkAny` | | `Multilink.unmultilink( multilink )` | Disposes a Multilink instance — equivalent to calling `.dispose()` on it | ```ts const multilinkHandle = Multilink.multilink( [ xProperty, yProperty ], ( x, y ) => console.log( x, y ) ); // later Multilink.unmultilink( multilinkHandle ); ``` ## Constructor ```ts new Multilink( dependencies, callback, lazy? ) ``` Up to 15 dependencies are supported via overloads (like [`DerivedProperty`](/api/axon/derived-property)), with `callback`'s parameter types inferred from the dependency array. Pass `lazy: true` as the third argument to suppress the initial callback. ## Members | Member | Effect | | --- | --- | | `dispose()` | Unlinks the callback from every dependency. Calling `dispose()` twice throws an assertion error | ::: tip Multilink vs. DerivedProperty Reach for [`DerivedProperty`](/api/axon/derived-property) when the combination of dependencies *is* a value other code should read and link to (e.g. an `areaProperty` other Nodes bind their size to). Reach for `Multilink` when you only need to *run something* when several Properties change together and there's no resulting value worth exposing as its own Property. ::: ======================================================================== Page: NumberProperty URL: https://veillette.github.io/Almanach/api/axon/number-property Source: docs/api/axon/number-property.md Category: API | Tags: axon, NumberProperty, range | Status: verified ======================================================================== # NumberProperty `NumberProperty` (from `scenerystack/axon`) is a [`Property`](/api/axon/property) whose value must be a number, with an optional `range` that both validates the value and is exposed to UI components (like sliders) that need to know the legal bounds. The range itself is stored as its own nested `rangeProperty`, so range and value can each be observed independently. ```ts import { NumberProperty } from 'scenerystack/axon'; import { Range } from 'scenerystack/dot'; const volumeProperty = new NumberProperty( 0.5, { range: new Range( 0, 1 ) } ); volumeProperty.link( volume => console.log( 'volume:', volume ) ); volumeProperty.value = 1.5; // throws an assertion error in dev builds: outside rangeProperty's range ``` ## Options | Option | Effect | | --- | --- | | `range` | A `Range` (or a `Property` for a dynamic range) that values are validated against | | `numberType` | `'FloatingPoint'` (default) or `'Integer'` — `'Integer'` adds a validator requiring `value % 1 === 0` | | `rangePropertyOptions` | Options forwarded to the internally-created `rangeProperty`, ignored if you passed a `Property` for `range` | All other [`PropertyOptions`](/api/axon/property) (e.g. `units`, `reentrant`) are supported too; `valueType` and `phetioValueType` are fixed to `'number'` / `NumberIO` internally. ## Members and methods | Member | Effect | | --- | --- | | `range` (getter/setter) | Reads or replaces the current `Range` value (shorthand for `rangeProperty.value`) | | `rangeProperty` | The underlying `Property` — link to it directly if a UI element needs to react to range changes | | `setValueAndRange( value, range )` | Atomically sets both value and range so an intermediate state never fails validation | | `resetValueAndRange()` | Resets both `value` and `range` back to their initial values together | | `reset()` | Overridden to reset value and range atomically, then defer to `Property.reset()` | ```ts const angleProperty = new NumberProperty( 0, { range: new Range( 0, 90 ) } ); // Grow the range and set an out-of-old-range value without a transient validation error: angleProperty.setValueAndRange( 120, new Range( 0, 180 ) ); ``` ::: warning Range changes are validated immediately Setting `.range = ...` directly re-validates the *current* value against the new range right away. If the new range wouldn't contain the current value, use `setValueAndRange()` (or `setDeferred`) to change both together — otherwise you'll trip an assertion in dev builds. ::: ======================================================================== Page: Observable Array URL: https://veillette.github.io/Almanach/api/axon/observable-array Source: docs/api/axon/observable-array.md Category: API | Tags: axon, observable-array, collections | Status: verified ======================================================================== # Observable Array (createObservableArray) `createObservableArray` (from `scenerystack/axon`) builds an object with the exact same API as a native JavaScript `Array` — you can `push`, `pop`, `splice`, index with `[i]`, iterate, etc. — but that also notifies listeners whenever its contents change. Internally it wraps a real array in a `Proxy` and exposes an [`Emitter`](/api/axon/emitter) for additions, an `Emitter` for removals, and a [`NumberProperty`](/api/axon/number-property) tracking the length. ```ts import { createObservableArray } from 'scenerystack/axon'; const particles = createObservableArray<{ id: number }>(); particles.elementAddedEmitter.addListener( particle => console.log( 'added', particle.id ) ); particles.elementRemovedEmitter.addListener( particle => console.log( 'removed', particle.id ) ); particles.lengthProperty.link( length => console.log( 'count:', length ) ); particles.push( { id: 1 } ); // logs "added 1", then "count: 1" particles.pop(); // logs "removed 1", then "count: 0" ``` `lengthProperty` always updates *before* the corresponding `elementAddedEmitter`/`elementRemovedEmitter` fires, so a listener on either emitter can safely read the up-to-date `lengthProperty.value`. ## Observable members | Member | Type | Notes | | --- | --- | --- | | `elementAddedEmitter` | `TEmitter<[T]>` | Fires once per element added (`push`, `unshift`, `splice` insertions, index assignment, etc.) | | `elementRemovedEmitter` | `TEmitter<[T]>` | Fires once per element removed | | `lengthProperty` | `NumberProperty` | Mirrors `.length`; read-only in practice — don't set it directly | ## Array-like and convenience methods All standard `Array` methods work as expected (`push`, `pop`, `shift`, `unshift`, `splice`, `map`, `filter`, `forEach`, indexing, `for...of`, ...). `createObservableArray` also layers on PhET-specific convenience methods: | Method | Effect | | --- | --- | | `add( element )` | Alias for `push( element )` | | `addAll( elements )` | Pushes every element in `elements` | | `remove( element )` | Removes the first matching element | | `removeAll( elements )` | Removes every matching element in `elements` | | `clear()` | Empties the array (pops until empty, so removal notifications still fire) | | `count( predicate )` | Number of elements matching `predicate` | | `find( predicate, fromIndex? )` | First matching element, or `undefined` | | `get( index )` | Same as `array[index]` | | `getArrayCopy()` | A plain (non-observable) `T[]` snapshot | | `shuffle( random )` | Reorders in place via a `{ shuffle }`-shaped random source, without firing add/remove notifications | | `addItemAddedListener` / `removeItemAddedListener` | Legacy aliases for `elementAddedEmitter.addListener` / `removeListener` | | `addItemRemovedListener` / `removeItemRemovedListener` | Legacy aliases for `elementRemovedEmitter.addListener` / `removeListener` | | `reset()` | Restores the array to the `elements`/`length` it was constructed with | | `dispose()` | Disposes `elementAddedEmitter`, `elementRemovedEmitter`, and `lengthProperty` | ## Options ```ts const readingsArray = createObservableArray( { elements: [ 0, 0, 0 ] } ); ``` | Option | Effect | | --- | --- | | `elements` | Initial contents (mutually exclusive with `length`) | | `length` | Initial length, filled with `undefined` (mutually exclusive with `elements`) | | `elementAddedEmitterOptions` | Options forwarded to the internal `elementAddedEmitter` | | `elementRemovedEmitterOptions` | Options forwarded to the internal `elementRemovedEmitter` | | `lengthPropertyOptions` | Options forwarded to the internal `lengthProperty` | ::: tip It's a function, not a class There is no `ObservableArray` constructor to `new` up — call `createObservableArray(options?)` and you get back a value that satisfies both `T[]` and the observable API table above. The exported `ObservableArray` name from `scenerystack/axon` is a *type*, useful for annotating a field or parameter, not a runtime class. ::: ::: warning Prefer this over an array of Properties for "add/remove" collections If you find yourself keeping a plain `T[]` alongside a manually-maintained `Emitter` for adds and another for removes, that's exactly what `createObservableArray` already does — and it keeps `lengthProperty` in sync for you, which is easy to get wrong by hand. See [DerivedProperty](/api/axon/derived-property) if you need a *value* computed from the array's contents (e.g. a sum), by pairing `DerivedProperty.deriveAny` with `lengthProperty` or `recomputeDerivation()` triggered from `elementAddedEmitter`/`elementRemovedEmitter`. ::: ======================================================================== Page: PatternStringProperty URL: https://veillette.github.io/Almanach/api/axon/pattern-string-property Source: docs/api/axon/pattern-string-property.md Category: API | Tags: axon, PatternStringProperty, DerivedProperty, i18n, strings | Status: verified ======================================================================== # PatternStringProperty `PatternStringProperty` (from `scenerystack/axon`) is a specialized [`DerivedProperty`](/api/axon/derived-property) (technically, a `DerivedStringProperty`) that fills `{{placeholder}}` tokens in a translated pattern string with values pulled from other Properties, automatically re-deriving whenever the pattern string or any value Property changes. This is the standard way to build a translated, dynamic UI string like "Score: {{score}}" or "{{count}} particles remaining" without hand-rolling a `DerivedProperty` and a `StringUtils.fillIn` call yourself. ```ts import { PatternStringProperty, NumberProperty, TinyProperty } from 'scenerystack/axon'; const scoreProperty = new NumberProperty( 0 ); const patternProperty = new TinyProperty( '{{score}} points' ); // typically a translated StringProperty const scoreStringProperty = new PatternStringProperty( patternProperty, { score: scoreProperty } ); scoreStringProperty.value; // '0 points' scoreProperty.value = 12; scoreStringProperty.value; // '12 points' ``` Values may be plain strings/numbers or Properties of strings/numbers — both work in the `values` record, and only the Property ones become dependencies that trigger re-derivation. ## Constructor ```ts new PatternStringProperty( patternProperty, values, providedOptions? ) ``` | Parameter | Effect | | --- | --- | | `patternProperty` | A `TReadOnlyProperty` holding the pattern, e.g. a localized `...StringProperty` with `{{name}}`-style placeholders | | `values` | A record mapping each placeholder name to a `string \| number \| TReadOnlyProperty` (or, with a `maps` entry, any other Property type) | | `providedOptions` | See below | ## Options | Option | Default | Effect | | --- | --- | --- | | `maps` | `{}` | Per-key functions converting a value to `string \| number` before substitution — **required** for any key whose value isn't already `string`, `number`, or a Property of those | | `decimalPlaces` | `null` | Rounds numeric values (after any `map`) to a fixed number of decimals; either one number applied to every numeric value, or a per-key record | | `formatNames` | `[]` | Back-compat shim for patterns written for `StringUtils.format`'s `{0}`/`{1}` style — lists the placeholder names those positional indices map to | ```ts const gramsProperty = new NumberProperty( 2143 ); const kilogramsStringProperty = new PatternStringProperty( new TinyProperty( '{{kilograms}} kg' ), { kilograms: gramsProperty }, { maps: { kilograms: ( grams: number ) => grams / 1000 }, decimalPlaces: 2 } ); kilogramsStringProperty.value; // '2.14 kg' ``` ::: tip It's a DerivedProperty — dispose it Because `PatternStringProperty` is built on `DerivedProperty`, it lazy-links to `patternProperty` and every value Property passed in, and its value can't be set directly. Always `dispose()` a `PatternStringProperty` you create dynamically (e.g. per list item), the same as any other `DerivedProperty`. ::: ======================================================================== Page: Property URL: https://veillette.github.io/Almanach/api/axon/property Source: docs/api/axon/property.md Category: API | Tags: axon, Property, reactive | Status: verified ======================================================================== # Property `Property` (from `scenerystack/axon`) is the base observable value wrapper that all reactive state in a SceneryStack simulation is built on. It holds a single current value, notifies listeners when that value changes, and remembers its initial value so it can be `reset()`. Almost every other axon class — [`BooleanProperty`](/api/axon/boolean-property), [`NumberProperty`](/api/axon/number-property), [`StringProperty`](/api/axon/string-property), [`EnumerationProperty`](/api/axon/enumeration-property) — is a thin, typed subclass of `Property`, and [`DerivedProperty`](/api/axon/derived-property) is a read-only sibling built on the same base (`ReadOnlyProperty`). ```ts import { Property } from 'scenerystack/axon'; const isPausedProperty = new Property( false ); isPausedProperty.link( isPaused => { console.log( 'paused:', isPaused ); } ); isPausedProperty.value = true; // logs "paused: true" ``` ## Reading and writing `value` is the idiomatic get/set (there are also `get()`/`set()` methods that do the same thing, useful in hot inner loops): ```ts const speedProperty = new Property( 1 ); speedProperty.value = 2; // triggers listeners if the value actually changed console.log( speedProperty.value ); // 2 speedProperty.reset(); // back to the initial value (1) ``` ## Listening for changes | Method | Behavior | | --- | --- | | `link( listener )` | Adds a listener and immediately calls it once with `( value, null, this )` | | `lazyLink( listener )` | Adds a listener without an immediate callback | | `unlink( listener )` | Removes a specific listener | | `unlinkAll()` | Removes every listener | | `hasListener( listener )` | Checks whether a listener is registered | | `linkAttribute( object, attributeName )` | Convenience: sets `object[attributeName] = value` whenever the Property changes | ```ts const listener = ( newValue: number, oldValue: number | null ) => { console.log( `${oldValue} -> ${newValue}` ); }; speedProperty.link( listener ); // fires immediately with (1, null) speedProperty.lazyLink( listener ); // does not fire immediately speedProperty.unlink( listener ); ``` ## Other members | Member | Effect | | --- | --- | | `initialValue` (getter) | The value the Property was constructed with | | `getInitialValue()` / `setInitialValue( value )` | Explicit accessors for the initial value; use `setInitialValue` sparingly, only when the "reset" value isn't known until after construction | | `reset()` | Sets the value back to `initialValue` | | `isSettable()` | Returns `true` for `Property` (subclasses like `DerivedProperty` override this to `false`) | | `dispose()` | Removes all listeners and marks the Property as disposed | | `isValueValid( value )` / `getValidationError( value )` | Check a candidate value against the Property's validator without setting it | ## Options `Property` accepts a `PropertyOptions` object as its second constructor argument. The most commonly used options for simulation code: | Option | Effect | | --- | --- | | `valueType` | A validator shorthand, e.g. `'boolean'`, `'number'`, or a constructor, checked on every `set()` | | `validValues` | Restrict the value to a fixed array of allowed values | | `isValidValue` | A custom `( value ) => boolean` predicate | | `units` | Physical units string for PhET-iO/documentation purposes | | `reentrant` | Allow a listener to set the Property's own value again while notifications are in progress (default `false`) | ::: tip Prefer a typed subclass Reach for `Property` directly only when there isn't a more specific subclass. For booleans, numbers, strings, and enum values, use [`BooleanProperty`](/api/axon/boolean-property), [`NumberProperty`](/api/axon/number-property), [`StringProperty`](/api/axon/string-property), or [`EnumerationProperty`](/api/axon/enumeration-property) instead — they bake in the right validators and PhET-iO serialization automatically. ::: ::: warning Property vs. Emitter A `Property` always has a *current value* that new listeners immediately receive on `link()`. If you need to broadcast a discrete occurrence that has no persistent state — a "button pressed" or "collision happened" moment — use [`Emitter`](/api/axon/emitter) instead. See [Emitter vs. Property](/patterns/emitter-vs-property) for the full guidance. ::: ======================================================================== Page: ReadOnlyProperty URL: https://veillette.github.io/Almanach/api/axon/read-only-property Source: docs/api/axon/read-only-property.md Category: API | Tags: axon, ReadOnlyProperty, Property, DerivedProperty, reactive | Status: complete ======================================================================== # ReadOnlyProperty `ReadOnlyProperty` (from `scenerystack/axon`) is the base class that [`Property`](/api/axon/property) extends. It implements everything about an observable value — `.value`/`.get()`, `link()`, `lazyLink()`, `unlink()`, `dispose()`, PhET-iO serialization — except the ability to *set* the value from outside: its `set()`/`value=` accessors are `protected`, and its constructor is `protected` too, so you never instantiate `ReadOnlyProperty` directly. Subclasses fall into two families: `Property` (and its typed siblings `BooleanProperty`, `NumberProperty`, ...) re-expose `set`/`value=` as `public`, making the value externally settable; [`DerivedProperty`](/api/axon/derived-property) and [`DynamicProperty`](/api/axon/dynamic-property) leave them protected, so the only way their value changes is through their own internal logic. ```ts import { ReadOnlyProperty, DerivedProperty, NumberProperty } from 'scenerystack/axon'; const widthProperty = new NumberProperty( 4 ); const heightProperty = new NumberProperty( 3 ); const areaProperty: ReadOnlyProperty = new DerivedProperty( [ widthProperty, heightProperty ], ( width, height ) => width * height ); areaProperty.isSettable(); // false - DerivedProperty never overrides this // areaProperty.value = 100; // compile error: 'value' is protected on ReadOnlyProperty ``` ## Why public APIs say `ReadOnlyProperty`, not `Property` When a class exposes a derived or gated value — `isValidProperty`, `areaProperty`, anything built with `DerivedProperty` or `DynamicProperty` — its public field should be typed `ReadOnlyProperty` (or the narrower `TReadOnlyProperty` interface), never `Property`. Typing it as `Property` would be a lie: it advertises a settable `.value=` that either doesn't exist at runtime (TypeScript would happily compile `areaProperty.value = 5`, since `Property` widens the type) or, worse, silently succeeds and desyncs from the derivation. Reserve the `Property` type annotation for fields you actually construct as a plain, externally-settable `Property` (or subclass). ## Public API `ReadOnlyProperty` provides the full read side of the Property API; only `set`/`value=` are missing (protected) compared to `Property`: | Member | Effect | | --- | --- | | `value` (getter) / `get()` | The current value | | `link( listener )` / `lazyLink( listener )` | Subscribe to changes, with or without an immediate callback | | `unlink( listener )` / `unlinkAll()` | Remove one or all listeners | | `hasListener( listener )` / `hasListeners()` | Inspect subscriptions | | `isSettable()` | Returns `false` on `ReadOnlyProperty` and any subclass that doesn't override it (e.g. `DerivedProperty`); `Property` overrides it to `true` | | `isValueValid( value )` / `getValidationError( value )` | Check a candidate value against the Property's validator without setting it | | `linkAttribute( object, attributeName )` | Convenience: sets `object[attributeName] = value` on every change | | `toString()` / `debug( name )` | Console-debugging helpers | | `dispose()` | Removes all listeners and marks the instance disposed | ::: tip Use `isSettable()` to branch on capability at runtime Because both `Property` and `ReadOnlyProperty` share this same base type, `isSettable()` is the correct runtime check when code receives a `TReadOnlyProperty` and needs to know whether it's safe to attempt a cast-and-set (some PhET-iO and Studio tooling does exactly this). Don't rely on `instanceof Property` for this — prefer the method, since it's what the class itself uses internally to guard `PhetioStateEngine` value-setting. ::: ======================================================================== Page: StringProperty URL: https://veillette.github.io/Almanach/api/axon/string-property Source: docs/api/axon/string-property.md Category: API | Tags: axon, StringProperty, localization | Status: verified ======================================================================== # StringProperty `StringProperty` (from `scenerystack/axon`) is a [`Property`](/api/axon/property) whose value must be a `string`. It adds no methods beyond `Property` — its entire job is fixing `valueType: 'string'` and the PhET-iO `StringIO` type so every string-valued piece of simulation state validates and serializes consistently. ```ts import { StringProperty } from 'scenerystack/axon'; const labelProperty = new StringProperty( 'Start' ); labelProperty.link( label => console.log( 'button label:', label ) ); labelProperty.value = 'Resume'; ``` ## Options `StringPropertyOptions` is `PropertyOptions` with `valueType` and `phetioValueType` removed, since `StringProperty` sets both internally (`'string'` and `StringIO`, respectively). All other [`PropertyOptions`](/api/axon/property) — `validValues`, `isValidValue`, `units`, `reentrant`, etc. — pass through normally: ```ts const modeProperty = new StringProperty( 'idle', { validValues: [ 'idle', 'running', 'paused' ] } ); ``` ## Role in localization Displayed, translatable UI text in SceneryStack simulations is generally exposed through generated `StringProperty` instances (from the sim's `-strings_en.json` and Fluent-based string modules), not through this constructor directly — but the underlying contract is the same `Property` API described here and on the [Property](/api/axon/property) page: `.value`, `.link()`, `.dispose()`. Any `scenery` Node that displays text (e.g. `Text`, `RichText`) accepts a `TReadOnlyProperty` for its `string`/`stringProperty` option, so a plain `StringProperty` is a fine stand-in during development before the translated strings pipeline is wired up. ::: tip Restricting to a fixed set of strings If a string Property should only ever hold one of a small, fixed set of values (e.g. `'idle' | 'running' | 'paused'`), consider whether an [`EnumerationProperty`](/api/axon/enumeration-property) backed by an `EnumerationValue` subclass is a better fit than a raw `StringProperty` with `validValues` — the enumeration gives you compile-time exhaustiveness checks that a string union does not. ::: ======================================================================== Page: StringUnionProperty URL: https://veillette.github.io/Almanach/api/axon/string-union-property Source: docs/api/axon/string-union-property.md Category: API | Tags: axon, StringUnionProperty, Property, enumeration, TypeScript | Status: verified ======================================================================== # StringUnionProperty `StringUnionProperty` (from `scenerystack/axon`) is a thin [`Property`](/api/axon/property) subclass for the common TypeScript idiom of using a string literal union (`'small' | 'medium' | 'large'`) as a lightweight enumeration, instead of an [`EnumerationProperty`](/api/axon/enumeration-property) backed by a `Rich Enumeration` class. It requires `validValues` at construction (there's no way to construct one without it) and derives the PhET-iO `phetioValueType` from that list automatically via `StringUnionIO`. ```ts import { StringUnionProperty } from 'scenerystack/axon'; type SizeChoice = 'small' | 'medium' | 'large'; const sizeProperty = new StringUnionProperty( 'medium', { validValues: [ 'small', 'medium', 'large' ] } ); sizeProperty.value = 'large'; // fine // sizeProperty.value = 'huge'; // would throw an assertion error — not in validValues ``` ## Constructor ```ts new StringUnionProperty( value: T, providedOptions: StringEnumerationPropertyOptions ) ``` Unlike plain `Property`, the options argument is **required** — `validValues` has no default and must be supplied. ## Options `StringUnionProperty` accepts the same [`PropertyOptions`](/api/axon/property) as `Property`, with two differences: | Option | Effect | | --- | --- | | `validValues` | **Required.** The full list of legal string literal values — also used to build the `phetioValueType` | | `phetioValueType` | Not settable — `StringUnionProperty` always computes it from `validValues` via `StringUnionIO` | Everything else (`tandem`, `units`, `reentrant`, …) works exactly as it does on `Property`. ::: tip When to reach for this instead of EnumerationProperty Use `StringUnionProperty` when the set of choices is small, has no associated behavior/data beyond the label itself, and is naturally expressed as a TypeScript string literal union already used elsewhere in your model's types. Use [`EnumerationProperty`](/api/axon/enumeration-property) instead when the values need to carry additional data or methods (a full PhET "Rich Enumeration" class) — see the [Enumeration Pattern](/patterns/enumeration-pattern) for the tradeoffs between the two approaches. ::: ======================================================================== Page: Timer, animationFrameTimer, and stepTimer URL: https://veillette.github.io/Almanach/api/axon/timer Source: docs/api/axon/timer.md Category: API | Tags: axon, Timer, animationFrameTimer, stepTimer, scheduling | Status: complete ======================================================================== # Timer, animationFrameTimer, and stepTimer `Timer` (from `scenerystack/axon`) is a small `TinyEmitter<[ number ]>` subclass that adds `setTimeout`/`clearTimeout` and `setInterval`/`clearInterval`-style scheduling on top of a simple `dt`-driven `emit`. Rather than construct your own, simulation code almost always reaches for one of the two ready-made singleton instances axon exports: `animationFrameTimer` and `stepTimer`. Both are plain `Timer` instances; they differ only in *who calls `emit()` on them and when*. ```ts import { stepTimer, animationFrameTimer } from 'scenerystack/axon'; // Fires once, ~500ms of sim time after this call, driven by the sim's step loop: const timeoutListener = stepTimer.setTimeout( () => { console.log( 'sim has stepped for 500ms' ); }, 500 ); // Fires every frame, whether or not the sim is currently active/running: animationFrameTimer.addListener( dt => { // dt is in seconds } ); ``` ## `stepTimer` vs. `animationFrameTimer` | | `stepTimer` | `animationFrameTimer` | | --- | --- | --- | | Driven by | The running `Sim`'s `stepSimulation` call (also ticks in accessibility tests and bare `Display` animation loops) | The browser's `requestAnimationFrame` loop directly, independent of whether a `Sim` is active | | Ticks while paused / inactive | No — tied to the active screen's step | Yes — keeps firing even when the sim is paused or backgrounded | | Typical use | Model-time scheduling: "wait 400ms of sim time," debounced UI updates tied to simulation stepping | Animation/UI effects that must keep running regardless of play/pause state (e.g. a `stepTimer`-independent visual pulse) | Both are global, screen-independent singletons — a `setTimeout` registered on `stepTimer` from one screen will still fire even if the user has navigated to a different screen, as long as the sim keeps stepping at all. ## `Timer`'s public API | Member | Effect | | --- | --- | | `setTimeout( listener, timeout )` | Calls `listener()` once, after `timeout` milliseconds of accumulated `dt` have elapsed; returns an internal callback handle | | `clearTimeout( handle )` | Cancels a pending `setTimeout`, using the handle it returned | | `setInterval( listener, interval )` | Calls `listener()` repeatedly every `interval` milliseconds of accumulated `dt`; returns a handle | | `clearInterval( handle )` | Cancels a running `setInterval` | | `runOnNextTick( listener )` | Convenience for `setTimeout( listener, 0 )` | | `addListener( listener )` / `removeListener( listener )` | Inherited from `TinyEmitter` — the low-level `( dt: number ) => void` hook that `setTimeout`/`setInterval` are themselves built on | `CallbackTimer` (also exported from `scenerystack/axon`) is a related but distinct higher-level utility built on top of `stepTimer`: it models "press-and-hold" style repeated firing (an initial `delay`, then continuous firing at `interval`), the same pattern used by `sun`'s press-and-hold buttons. ::: warning `timeout`/`interval` are in milliseconds; `dt` is in seconds `setTimeout`/`setInterval`'s duration arguments are milliseconds (matching the browser APIs they mimic), but the underlying `emit( dt )` — and any listener you add directly via `addListener` — receives `dt` in **seconds**, matching the rest of axon/scenery's simulation-time conventions. Mixing up the units is a common source of "my timeout fires 1000x too fast/slow" bugs. ::: ======================================================================== Page: TinyEmitter URL: https://veillette.github.io/Almanach/api/axon/tiny-emitter Source: docs/api/axon/tiny-emitter.md Category: API | Tags: axon, TinyEmitter, Emitter, events, performance | Status: verified ======================================================================== # TinyEmitter `TinyEmitter` (from `scenerystack/axon`) is the same discrete-event abstraction as [`Emitter`](/api/axon/emitter) — `addListener`, `removeListener`, `emit`, `dispose` — but without the PhET-iO instrumentation, tandem, or `parameters` metadata layered on top. `Emitter` actually *is* a thin `TinyEmitter` wrapper internally; reach for `TinyEmitter` directly when you're writing axon-internal or high-frequency code (per-frame or per-listener-call overhead matters) and don't need PhET-iO data-stream support. ```ts import { TinyEmitter } from 'scenerystack/axon'; const resizeEmitter = new TinyEmitter<[ width: number, height: number ]>(); resizeEmitter.addListener( ( width, height ) => { console.log( `resized to ${width}x${height}` ); } ); resizeEmitter.emit( 800, 600 ); ``` ## Constructor Unlike `Emitter`'s single config-object constructor, `TinyEmitter` takes up to four positional arguments (any of which may be omitted or passed as `null`) instead of an options object — this avoids allocating an options object on hot construction paths: ```ts new TinyEmitter( onBeforeNotify?: TEmitterListener | null, hasListenerOrderDependencies?: boolean | null, reentrantNotificationStrategy?: 'stack' | 'queue' | null, disableListenerLimit?: boolean | null ) ``` | Parameter | Default | Effect | | --- | --- | --- | | `onBeforeNotify` | `undefined` | Called with the same arguments as `emit()`, just before listeners are notified | | `hasListenerOrderDependencies` | `undefined` | Set `true` to assert that listener order must stay fixed (relevant only under the `?listenerOrder` debug query parameter) | | `reentrantNotificationStrategy` | `'stack'` | `'stack'`: a nested `emit()` call (triggered from within a listener) fully resolves before the outer `emit()` continues notifying. `'queue'`: nested `emit()` calls are queued and notified breadth-first, after the current round finishes | | `disableListenerLimit` | `undefined` | Skips the dev-mode assertion that flags runaway listener counts | ## Methods | Method | Effect | | --- | --- | | `emit( ...args )` | Calls every listener synchronously with `args` | | `addListener( listener )` | Registers a listener; asserts if the same listener is already registered | | `removeListener( listener )` | Removes a specific listener | | `removeAllListeners()` | Removes every listener | | `hasListener( listener )` | Checks whether a listener is registered | | `hasListeners()` | Whether any listener is registered | | `getListenerCount()` | Number of registered listeners | | `forEachListener( callback )` | Invokes `callback` once per listener — used internally by [`TinyProperty`](/api/axon/tiny-property) | | `dispose()` | Removes all listeners | ::: tip TinyEmitter is what Emitter is built from `Emitter` is a `TinyEmitter` plus PhET-iO `tandem`/`parameters` options and `phetioReadOnly` bookkeeping — the actual `emit()`/`addListener()` mechanics (including the `'stack'` vs. `'queue'` reentrancy handling) live entirely in `TinyEmitter`. Use `Emitter` for anything that needs a `tandem` for the data stream or PhET-iO API docs; use `TinyEmitter` for internal plumbing (as [`TinyProperty`](/api/axon/tiny-property), `TinyForwardingProperty`, and `TinyStaticProperty` all do) where allocating that metadata on every instance would be wasted memory. ::: ======================================================================== Page: TinyProperty URL: https://veillette.github.io/Almanach/api/axon/tiny-property Source: docs/api/axon/tiny-property.md Category: API | Tags: axon, TinyProperty, Property, performance | Status: verified ======================================================================== # TinyProperty `TinyProperty` (from `scenerystack/axon`) is a lightweight observable value wrapper that implements enough of the `Property` interface (`.value`, `.get()`/`.set()`, `link()`/`lazyLink()`/`unlink()`) to be used interchangeably in many places, but without [`Property`](/api/axon/property)'s validation, PhET-iO instrumentation, or `reset()`/`initialValue` bookkeeping. It directly extends [`TinyEmitter`](/api/axon/tiny-emitter) rather than composing one, specifically to save memory — scenery uses a `TinyProperty` for every Node's dozens of internal state values (`opacityProperty`, `visibleProperty`'s underlying storage, `pickableProperty`, etc.), so the per-instance overhead matters at that scale. ```ts import { TinyProperty } from 'scenerystack/axon'; const opacityProperty = new TinyProperty( 1 ); opacityProperty.link( opacity => console.log( 'opacity:', opacity ) ); // logs "opacity: 1" opacityProperty.value = 0.5; // logs "opacity: 0.5" ``` ## Constructor ```ts new TinyProperty( value: T, onBeforeNotify?: TinyEmitterOptions<...>['onBeforeNotify'] | null, hasListenerOrderDependencies?: boolean | null, reentrantNotificationStrategy?: 'stack' | 'queue' | null, disableListenerLimit?: boolean | null ) ``` The trailing four parameters are forwarded straight to the underlying [`TinyEmitter`](/api/axon/tiny-emitter) — `TinyProperty` itself only adds the first `value` argument. Unlike plain `TinyEmitter` (which defaults to `'stack'`), `TinyProperty` defaults `reentrantNotificationStrategy` to `'queue'`, so that if a listener changes the value again while still notifying (a → b, and a listener sets b → c), all listeners see the a→b change in order before any b→c notification begins. ## Methods and members | Member | Effect | | --- | --- | | `value` (getter/setter) | Reads or writes the current value; setting is a no-op if the new value `areValuesEqual` the old one | | `get()` / `set( value )` | Same as `.value`, provided as explicit methods for hot inner loops | | `link( listener )` | Adds a listener and immediately calls it with `( value, null, this )` | | `lazyLink( listener )` | Adds a listener without an immediate callback | | `unlink( listener )` / `unlinkAll()` | Removes one or all listeners | | `linkAttribute( object, attributeName )` | Sets `object[attributeName] = value` whenever the value changes | | `valueComparisonStrategy` | Get/set the equality strategy (defaults to `'reference'`) used by `areValuesEqual` to decide whether to notify | | `isSettable()` | Always returns `true` for `TinyProperty` | | `dispose()` | Unlinks all listeners, then disposes the underlying `TinyEmitter` | ::: warning Not validated, not instrumented, no reset `TinyProperty` has no `validValues`/`valueType`/`isValidValue` checking, no `tandem`, and no `initialValue`/`reset()` — assigning any value of the declared type always succeeds silently. It exists purely as a memory-lean building block for framework internals (scenery Node state, [`DynamicProperty`](/api/axon/dynamic-property)'s internal wrapping, [`MappedProperty`](/api/axon/unit-conversion-property)). For simulation model state that simulation-author code creates directly, use [`Property`](/api/axon/property) (or a typed subclass) instead — the safety and PhET-iO support are almost always worth the extra bytes. ::: ======================================================================== Page: UnitConversionProperty URL: https://veillette.github.io/Almanach/api/axon/unit-conversion-property Source: docs/api/axon/unit-conversion-property.md Category: API | Tags: axon, UnitConversionProperty, MappedProperty, Property, units | Status: verified ======================================================================== # UnitConversionProperty `UnitConversionProperty` (from `scenerystack/axon`) is a `MappedProperty` (itself built on [`DynamicProperty`](/api/axon/dynamic-property)) specialized for the single most common numeric-derivation need: showing the same underlying quantity in different units. Give it a source Property and a `factor`, and it produces a new `number` Property that stays in sync in both directions — including tracking the source's `Range`, if it has one. ```ts import { UnitConversionProperty, NumberProperty } from 'scenerystack/axon'; import { Range } from 'scenerystack/dot'; const metersProperty = new NumberProperty( 0.5, { range: new Range( 0, 1 ) } ); const centimetersProperty = new UnitConversionProperty( metersProperty, { factor: 100 } ); centimetersProperty.value; // 50 centimetersProperty.range; // Range( 0, 100 ) — derived from metersProperty's range metersProperty.value = 0.25; centimetersProperty.value; // 25 — one-way sync happens automatically centimetersProperty.value = 100; // bidirectional by default metersProperty.value; // 1 ``` ## Constructor ```ts new UnitConversionProperty( property: TReadOnlyProperty | TRangedProperty, providedOptions: UnitConversionPropertyOptions ) ``` `property` may be any numeric Property, or specifically a ranged one (like [`NumberProperty`](/api/axon/number-property)) to get automatic range conversion. ## Options | Option | Default | Effect | | --- | --- | --- | | `factor` | — | **Required.** The multiplicative factor from the source's units to this Property's units — `this.value === factor * property.value` | | `bidirectional` | `true` | Unlike the general `MappedProperty`/`DynamicProperty` (which default to one-way), `UnitConversionProperty` defaults to two-way sync, since `map`/`inverseMap` are always derivable from `factor` | | `map` / `inverseMap` | derived from `factor` | Overridable, but normally left alone — `UnitConversionProperty` builds `value => value * factor` and `value => value / factor` for you | ## Members | Member | Effect | | --- | --- | | `range` (getter/setter) | The converted `Range`, tracked automatically from the source Property's `rangeProperty` if it has one (otherwise a default unbounded range) | | `rangeProperty` | The underlying `Property` backing `.range` | ::: warning Setting `.range` directly is not yet bidirectional The doc comment on the source is explicit about this: assigning `centimetersProperty.range = ...` updates this Property's own `rangeProperty` but does **not** push a converted range back onto the source Property's range. Range synchronization only flows from the source outward; only the `.value` sync is bidirectional. ::: ======================================================================== Page: Validation and validate URL: https://veillette.github.io/Almanach/api/axon/validation Source: docs/api/axon/validation.md Category: API | Tags: axon, Validation, validate, Property | Status: complete ======================================================================== # Validation and validate `Validation` (from `scenerystack/axon`) is a static-methods-only class defining the shared "validator" object schema — `{ valueType, validValues, isValidValue, phetioType, valueComparisonStrategy, validators }` — that [`Property`](/api/axon/property) accepts directly as construction options, and that is reused verbatim by `Emitter`'s parameter validation. `validate()` is a small assertion-only wrapper around `Validation.getValidationError()`: with assertions enabled it throws when a value doesn't satisfy a validator, and is a no-op otherwise. You'll rarely call either directly — mostly you pass `valueType`/`validValues`/`isValidValue` straight into a `Property` constructor — but understanding this layer explains exactly what those options accept and how they combine. ```ts import { Validation } from 'scenerystack/axon'; const validator = { valueType: 'number', isValidValue: ( n: number ) => n >= 0 }; Validation.isValueValid( 5, validator ); // true Validation.isValueValid( -1, validator ); // false Validation.getValidationError( -1, validator ); // 'value failed isValidValue: -1' ``` ## The `Validator` shape This is the same object shape accepted by `Property`'s second constructor argument (see [Property's options](/api/axon/property#options)): | Key | Effect | | --- | --- | | `valueType` | A primitive type string (`'number'`, `'string'`, `'boolean'`, `'function'`), a constructor (checked via `instanceof`), `null`, or an array of any of those (value must match at least one) | | `validValues` | A fixed array of allowed values; membership is checked according to `valueComparisonStrategy` | | `valueComparisonStrategy` | How `validValues` membership (and Property change-detection) compares values: `'reference'` (default, `===`), `'equalsFunction'` (calls `.equals()` on both sides), `'lodashDeep'` (`_.isEqual`), or a custom `(a, b) => boolean` | | `isValidValue` | A custom `( value ) => boolean` predicate, checked in addition to any `valueType`/`validValues` | | `phetioType` | An `IOType` whose own `.validator` is checked as well — lets PhET-iO type definitions double as value validators | | `validators` | An array of nested `Validator` objects, all of which must pass | A validator object must specify at least one of these keys — `Validation.getValidatorValidationError()` (which `Validation.validateValidator()` asserts against) rejects an empty `{}`. ## Static API | Method | Effect | | --- | --- | | `Validation.isValueValid( value, validator, options? )` | `true`/`false` — whether `value` satisfies `validator` | | `Validation.getValidationError( value, validator, options? )` | `null` if valid, otherwise a human-readable string describing the failure | | `Validation.validateValidator( validator )` | Asserts that the *validator object itself* is well-formed (not that a value matches it) | | `Validation.containsValidatorKey( obj )` | Whether `obj` has at least one recognized validator key — used internally to detect "was a validator even supplied" | | `Validation.equalsForValidationStrategy( a, b, strategy )` | The comparison primitive backing `valueComparisonStrategy`, usable standalone | | `Validation.VALIDATOR_KEYS` | The list of recognized keys, e.g. for `_.pick`-ing a validator out of a larger options object (this is exactly how `ReadOnlyProperty` extracts its `valueValidator` from constructor options) | | `validate( value, validator, options? )` | Assertion-only convenience: throws (with assertions enabled) if `value` fails `validator`; otherwise a no-op | ::: warning `validate()` is deprecated in favor of calling `Validation` directly The source marks `validate()` `@deprecated`, recommending a direct assertion using `Validation.getValidationError()` instead — e.g. `assert && assert( Validation.isValueValid( value, validator ) )`. New code documenting or building on this layer should prefer `Validation`'s static methods; `validate()` remains only for existing call sites. ::: ======================================================================== Page: AreaPlot URL: https://veillette.github.io/Almanach/api/bamboo/area-plot Source: docs/api/bamboo/area-plot.md Category: API | Tags: bamboo, AreaPlot, chart, plot | Status: complete ======================================================================== # AreaPlot `AreaPlot` (from `scenerystack/bamboo`) combines the look of a [`LinePlot`](/api/bamboo/line-plot) with the shading of a [`BarPlot`](/api/bamboo/bar-plot): it renders a `(Vector2 | null)[]` dataset as a filled region bounded above by the line through the data and below by a horizontal `baseline` value. It extends `scenerystack/scenery`'s `Path`, rebuilding its `Shape` whenever the dataset, `baseline`, or the chart transform changes. As with `LinePlot`, a `null` entry in the dataset closes off the current shaded region and starts a new one on the next non-null point, rather than plotting a point at the origin. ```ts import { ChartTransform, AreaPlot } from 'scenerystack/bamboo'; import { Range, Vector2 } from 'scenerystack/dot'; ``` ## A minimal example ```ts const chartTransform = new ChartTransform( { viewWidth: 400, viewHeight: 200, modelXRange: new Range( 0, 10 ), modelYRange: new Range( -1, 1 ) } ); const dataSet: ( Vector2 | null )[] = []; for ( let x = 0; x <= 10; x += 0.1 ) { dataSet.push( new Vector2( x, Math.sin( x ) ) ); } const areaPlot = new AreaPlot( chartTransform, dataSet, { baseline: 0, fill: 'rgba(0,100,255,0.4)' } ); ``` ## Constructor ```ts new AreaPlot( chartTransform: ChartTransform, dataSet: ( Vector2 | null )[], providedOptions?: AreaChartOptions ) ``` ## Options | Option | Default | Effect | | --- | --- | --- | | `baseline` | `0` | The y-value (in model coordinates) that forms the foundation of the shaded region — each shaded run of points is closed by dropping straight down to this value at its first and last x | | `fill` (inherited `PathOptions`) | `'black'` | Fill color of the shaded region | ## Methods | Member | Description | | --- | --- | | `dataSet` | `(Vector2 \| null)[]` — public field. Safe to read directly; if you mutate it in place, you're responsible for calling `update()` yourself | | `baseline` | `number` — public field, mirrors the `baseline` option. Same caveat as `dataSet` if mutated directly | | `setDataSet( dataSet )` | Replaces the dataset and immediately calls `update()` | | `setBaseline( baseline )` | Replaces the baseline and immediately calls `update()` | | `update()` | Recomputes the shaded `Shape` from the current `dataSet` and `baseline`; called automatically on construction and whenever the `ChartTransform`'s `changedEmitter` fires | | `dispose()` | Removes the `chartTransform.changedEmitter` listener before calling `Path.dispose()` | ::: tip Each run of non-null points closes independently at the baseline `AreaPlot` walks the dataset and, on hitting a `null` (or the end of the array), draws a line straight down to `baseline` at the last point's x-coordinate and closes that subpath — then starts a fresh shaded region at the next non-null point. This is what makes `null` entries produce separate shaded islands rather than one continuous fill with a hole punched through the gap, and it's the same convention `LinePlot` uses for line segments, just with an extra close-to-baseline step. ::: ======================================================================== Page: AxisLine and AxisArrowNode URL: https://veillette.github.io/Almanach/api/bamboo/axis-nodes Source: docs/api/bamboo/axis-nodes.md Category: API | Tags: bamboo, AxisLine, AxisArrowNode, chart, axis | Status: verified ======================================================================== # AxisLine and AxisArrowNode Bamboo has no single `AxisNode` — instead, `scenerystack/bamboo` exports two interchangeable axis-drawing Nodes, both driven by a `ChartTransform` plus an `Orientation` (`scenerystack/phet-core`) saying which axis they represent: - **`AxisLine`** extends `scenerystack/scenery`'s `Line` — a plain stroked line, no arrowheads. - **`AxisArrowNode`** extends `scenerystack/scenery-phet`'s `ArrowNode` — the same idea, but double-headed by default, for charts that want to emphasize the axis extending indefinitely. Both reposition themselves whenever the chart transform changes, and both auto-hide (`setVisible( false )`) when their position would fall outside the transform's current view bounds — so an axis pinned at, say, `x = 0` disappears cleanly if the visible model range pans away from zero. ```ts import { ChartTransform, AxisLine, AxisArrowNode, ChartRectangle } from 'scenerystack/bamboo'; import { Orientation } from 'scenerystack/phet-core'; import { Range } from 'scenerystack/dot'; ``` ## A minimal example ```ts const chartTransform = new ChartTransform( { viewWidth: 400, viewHeight: 200, modelXRange: new Range( -5, 5 ), modelYRange: new Range( -1, 1 ) } ); const chartRectangle = new ChartRectangle( chartTransform, { stroke: 'gray' } ); // A vertical AxisLine (the "y axis"), positioned at model x = 0. const yAxis = new AxisLine( chartTransform, Orientation.VERTICAL, { value: 0 } ); // A horizontal AxisArrowNode (the "x axis"), positioned at model y = 0. const xAxis = new AxisArrowNode( chartTransform, Orientation.HORIZONTAL, { value: 0 } ); ``` ## Constructors ```ts new AxisLine( chartTransform: ChartTransform, axisOrientation: Orientation, providedOptions?: AxisLineOptions ) new AxisArrowNode( chartTransform: ChartTransform, axisOrientation: Orientation, providedOptions?: AxisArrowNodeOptions ) ``` ## Options | Option | `AxisLine` default | `AxisArrowNode` default | Effect | | --- | --- | --- | --- | | `value` | `0` | `0` | The position of the axis, in model coordinates **on the opposite axis** from `axisOrientation` (see tip below) | | `extension` | `0` | `20` | How far past the `ChartRectangle`'s edge (in view coordinates) the line/arrow is drawn | | `stroke` / `lineWidth` (`AxisLine`, from `LineOptions`) | `'black'` / `2` | — | Line styling | | `doubleHead` / `headHeight` / `headWidth` / `tailWidth` (`AxisArrowNode`, from `ArrowNodeOptions`) | — | `true` / `10` / `10` / `2` | Arrowhead styling | ## Methods Both classes share the same shape: they recompute their geometry in a private `update()` (called on construction and on every `chartTransform.changedEmitter` firing), and expose only: | Member | Description | | --- | --- | | `dispose()` | Removes the `chartTransform.changedEmitter` listener before calling the superclass's `dispose()` | ::: tip `value` is a coordinate on the *opposite* axis For a `VERTICAL` `AxisLine`/`AxisArrowNode` (a vertical line representing the "y axis"), `value` is an **x** model coordinate — it's where that vertical line crosses the horizontal axis. Symmetrically, a `HORIZONTAL` axis's `value` is a **y** model coordinate. This is why the default `value: 0` conventionally draws each axis through the origin, and why changing `value` moves the axis sideways/vertically rather than stretching it. ::: ======================================================================== Page: BarPlot URL: https://veillette.github.io/Almanach/api/bamboo/bar-plot Source: docs/api/bamboo/bar-plot.md Category: API | Tags: bamboo, BarPlot, chart, plot | Status: verified ======================================================================== # BarPlot `BarPlot` (from `scenerystack/bamboo`) shows numerical data (an x-value plus a height) as one `Rectangle` per point, extending `scenerystack/scenery`'s `Node` rather than `Path` since it manages a pool of child `Rectangle`s instead of a single `Shape`. Each bar's tail sits at a shared `barTailValue` (the baseline, e.g. `y = 0`) and its tip is the data point itself, so bars can grow up or down from the baseline. It only supports numeric x-values — not categorical data. ```ts import { ChartTransform, BarPlot } from 'scenerystack/bamboo'; import { Range, Vector2 } from 'scenerystack/dot'; ``` ## A minimal example ```ts const chartTransform = new ChartTransform( { viewWidth: 300, viewHeight: 200, modelXRange: new Range( 0, 5 ), modelYRange: new Range( 0, 10 ) } ); const dataSet = [ new Vector2( 0, 3 ), new Vector2( 1, 7 ), new Vector2( 2, 5 ), new Vector2( 3, 9 ), new Vector2( 4, 2 ) ]; const barPlot = new BarPlot( chartTransform, dataSet, { barWidth: 20, pointToPaintableFields: point => ( { fill: point.y > 5 ? 'orange' : 'gray' } ) } ); ``` ## Constructor ```ts new BarPlot( chartTransform: ChartTransform, dataSet: Vector2[], providedOptions?: BarPlotOptions ) ``` ## Options | Option | Default | Effect | | --- | --- | --- | | `barWidth` | `10` | Width of each bar, in view coordinates | | `barTailValue` | `0` | The model-coordinate baseline each bar's tail is drawn from | | `pointToPaintableFields` | `() => ({ fill: 'black' })` | Maps a data point to `PaintableOptions` (e.g. `{ fill, stroke }`) applied to that bar's `Rectangle` | ## Methods | Member | Description | | --- | --- | | `dataSet` | `Vector2[]` — public field. Mutate in place only if you also call `update()` yourself | | `rectangles` | `Rectangle[]` — the current child rectangles, one per data point, kept in sync by `update()` | | `setDataSet( dataSet )` | Replaces the dataset and calls `update()` | | `update()` | Adds/removes `Rectangle` children to match `dataSet.length`, repositions/resizes each one from the chart transform, and re-applies `pointToPaintableFields` | | `dispose()` | Removes the `chartTransform.changedEmitter` listener before calling `Node.dispose()` | ::: warning `pointToPaintableFields` is restricted to paintable keys `update()` asserts that every key returned by `pointToPaintableFields` is one of the `Rectangle`'s default paintable options (`fill`, `stroke`, and similar) before calling `.mutate()` with it — this guards against accidentally passing an option (like `children` or a layout option) that would be unsafe to `mutate()` onto an internally-managed `Rectangle`. Stick to fill/stroke-style fields. ::: ======================================================================== Page: CanvasLinePlot URL: https://veillette.github.io/Almanach/api/bamboo/canvas-line-plot Source: docs/api/bamboo/canvas-line-plot.md Category: API | Tags: bamboo, CanvasLinePlot, ChartCanvasNode, chart, plot, canvas, performance | Status: complete ======================================================================== # CanvasLinePlot `CanvasLinePlot` (from `scenerystack/bamboo`) draws the same kind of `(Vector2 | null)[]` dataset as [`LinePlot`](/api/bamboo/line-plot) — straight segments between consecutive points, with `null` entries creating gaps — but paints directly to a `CanvasRenderingContext2D` instead of building a scenery `Shape`/`Path`. It is **not** a `Node` itself: `CanvasLinePlot` extends the abstract `CanvasPainter` base class, and one or more painters are handed to a `ChartCanvasNode` (which *is* the actual scenery `Node`, extending `CanvasNode`) that iterates its `painters` array and calls each one's `paintCanvas()` on every repaint. ```ts import { ChartTransform, ChartCanvasNode, CanvasLinePlot } from 'scenerystack/bamboo'; import { Range, Vector2 } from 'scenerystack/dot'; ``` ## A minimal example ```ts const chartTransform = new ChartTransform( { viewWidth: 400, viewHeight: 200, modelXRange: new Range( 0, 1000 ), modelYRange: new Range( -1, 1 ) } ); // 10,000 points -- too many for per-point scenery Nodes to stay responsive. const dataSet: ( Vector2 | null )[] = []; for ( let x = 0; x <= 1000; x += 0.1 ) { dataSet.push( new Vector2( x, Math.sin( x ) ) ); } const canvasLinePlot = new CanvasLinePlot( chartTransform, dataSet, { stroke: 'blue', lineWidth: 1 } ); // The ChartCanvasNode is what actually gets added to the scene graph. const chartCanvasNode = new ChartCanvasNode( chartTransform, [ canvasLinePlot ] ); ``` ## Constructor ```ts new CanvasLinePlot( chartTransform: ChartTransform, dataSet: ( Vector2 | null )[], providedOptions?: CanvasLinePlotOptions ) ``` ## Options | Option | Default | Effect | | --- | --- | --- | | `stroke` | `'black'` | CSS color string or `Color`; validated with `Color.isCSSColorString()` when assertions are enabled | | `lineWidth` | `1` | Stroke width in view (pixel) coordinates | | `lineDash` | `[]` (solid) | Passed straight through to `context.setLineDash()` | ## Methods | Member | Description | | --- | --- | | `dataSet` | `(Vector2 \| null)[]` — public field, read directly by `paintCanvas()`. Mutating it (or calling `setDataSet()`/`setStroke()`) does **not** trigger a repaint by itself | | `setDataSet( dataSet )` | Replaces the dataset | | `setStroke( stroke )` / `stroke` (setter) | Replaces the stroke | | `paintCanvas( context )` | Called by the owning `ChartCanvasNode` on every repaint; walks `dataSet`, moving to the next point after each `null` and line-ing to it otherwise | | `dispose()` | Marks the painter disposed; asserts if called twice | ::: warning You must call `update()` on the `ChartCanvasNode` yourself Unlike `LinePlot`, which listens to `chartTransform.changedEmitter` and rebuilds its own `Shape` automatically, `CanvasLinePlot` has no such wiring — it's a passive painter, not a `Node`. `ChartCanvasNode` is the one that listens to `changedEmitter` and calls `this.invalidatePaint()` for *its own* size/transform changes, but if you mutate a painter's `dataSet`, `lineWidth`, `lineDash`, or call `setStroke()` after construction, you must call `update()` on the owning `ChartCanvasNode` yourself (or `setPainters()`, if you're also changing which painters it holds) — nothing does it for you. ::: Reach for `CanvasLinePlot` (plus `ChartCanvasNode`) only once profiling shows a `LinePlot`'s per-point scenery drawable overhead is the bottleneck — typically datasets in the thousands-of-points range redrawn every frame. For anything smaller, `LinePlot`'s automatic redraw wiring and ordinary scenery `Path` semantics (hit testing, easy composition with other Nodes) are simpler to work with. ======================================================================== Page: ChartTransform URL: https://veillette.github.io/Almanach/api/bamboo/chart-transform Source: docs/api/bamboo/chart-transform.md Category: API | Tags: bamboo, ChartTransform, coordinates, chart | Status: verified ======================================================================== # ChartTransform `ChartTransform` (from `scenerystack/bamboo`) is a plain (non-`Node`) object that defines a chart's model-coordinate ranges (`modelXRange`, `modelYRange`) and view-coordinate size (`viewWidth`, `viewHeight`), and converts between the two. Every bamboo rendering primitive — `LinePlot`, `BarPlot`, `ScatterPlot`, `ChartRectangle`, `AxisLine`/`AxisArrowNode`, grid and tick sets — takes a `ChartTransform` in its constructor and listens to its `changedEmitter` so the whole chart redraws together when the transform changes (e.g. on a resize or a pan/zoom). ```ts import { ChartTransform, ChartRectangle, LinePlot } from 'scenerystack/bamboo'; import { Range, Vector2 } from 'scenerystack/dot'; ``` ## A minimal example ```ts const chartTransform = new ChartTransform( { viewWidth: 400, viewHeight: 200, modelXRange: new Range( 0, 10 ), modelYRange: new Range( -1, 1 ) } ); // ChartRectangle gives the chart a visible background/border sized to the transform. const chartRectangle = new ChartRectangle( chartTransform, { fill: 'white', stroke: 'black' } ); const dataSet = []; for ( let x = 0; x <= 10; x += 0.1 ) { dataSet.push( new Vector2( x, Math.sin( x ) ) ); } const linePlot = new LinePlot( chartTransform, dataSet, { stroke: 'blue' } ); // Later, e.g. on a layout resize: chartTransform.setViewWidth( 600 ); // -> chartRectangle and linePlot both redraw automatically via changedEmitter. ``` ## Constructor ```ts new ChartTransform( providedOptions?: ChartTransformOptions ) ``` ## Options | Option | Default | Effect | | --- | --- | --- | | `viewWidth` | `100` | Width of the chart in view (pixel) coordinates | | `viewHeight` | `100` | Height of the chart in view (pixel) coordinates | | `modelXRange` | `new Range( -1, 1 )` | Range of the x axis, in model coordinates | | `modelYRange` | `new Range( -1, 1 )` | Range of the y axis, in model coordinates | | `modelXRangeInverted` | `false` | Flips which view edge corresponds to `modelXRange.min` | | `modelYRangeInverted` | `false` | Flips which view edge corresponds to `modelYRange.min` | | `xTransform` | identity `Transform1` | Nonlinear model-to-view scaling function for x (e.g. for a log axis), applied before the linear view-range mapping | | `yTransform` | identity `Transform1` | Nonlinear model-to-view scaling function for y | ## Methods | Member | Description | | --- | --- | | `modelToView( orientation, value )` / `modelToViewX/Y( value )` | Converts a scalar model coordinate to a view coordinate along the given `Orientation` (`scenerystack/phet-core`) | | `modelToViewXY( x, y )` / `modelToViewPosition( Vector2 )` | Converts a model point to a view `Vector2` | | `modelToViewDelta/DeltaX/DeltaY/DeltaXY` | Converts a model-space *delta* (not an absolute position) to view space | | `viewToModel*` | The inverse of each `modelToView*` method | | `setViewWidth( w )` / `setViewHeight( h )` | Updates the view size and fires `changedEmitter` if it actually changed | | `setModelXRange( range )` / `setModelYRange( range )` | Updates a model range and fires `changedEmitter` if it actually changed | | `setXTransform( transform )` / `setYTransform( transform )` | Swaps the nonlinear scaling function for an axis | | `getModelRange( orientation )` | Returns `modelXRange` or `modelYRange` for the given orientation | | `forEachSpacing( orientation, spacing, origin, clippingType, callback )` | Iterates evenly-spaced model/view coordinate pairs across a range — what `TickMarkSet`/`GridLineSet` use internally | | `changedEmitter` | `TEmitter` — fires whenever any dimension, range, or transform changes; plots/axes/grids listen to this to know when to redraw | | `dispose()` | Disposes `changedEmitter` | ::: warning Mutate through the setters, not the public fields `viewWidth`, `modelXRange`, etc. are public fields (readable directly, e.g. by a plot's `update()`), but they are also directly *writable* — assigning `chartTransform.viewWidth = 600` will **not** fire `changedEmitter`, so nothing downstream redraws. Always go through `setViewWidth()`, `setModelXRange()`, `setXTransform()`, and friends when changing a `ChartTransform` after construction. ::: ======================================================================== Page: GridLineSet, TickMarkSet, and TickLabelSet URL: https://veillette.github.io/Almanach/api/bamboo/gridlines-and-tick-marks Source: docs/api/bamboo/gridlines-and-tick-marks.md Category: API | Tags: bamboo, GridLineSet, TickMarkSet, TickLabelSet, chart, axis | Status: complete ======================================================================== # GridLineSet, TickMarkSet, and TickLabelSet `GridLineSet`, `TickMarkSet`, and `TickLabelSet` (all from `scenerystack/bamboo`) are the three repeating-decoration primitives every bamboo chart uses alongside [`ChartTransform`](/api/bamboo/chart-transform) and the [axis Nodes](/api/bamboo/axis-nodes): they draw evenly-spaced lines, tick marks, and tick labels respectively, all driven by the same `(chartTransform, axisOrientation, spacing)` triple. Internally, each one calls `chartTransform.forEachSpacing()` to walk the model/view coordinate pairs at that spacing and clipping behavior, then builds its `Shape` (or, for `TickLabelSet`, its `Text` children) from those pairs. All three listen to `changedEmitter` and redraw automatically. ```ts import { ChartTransform, GridLineSet, TickMarkSet, TickLabelSet } from 'scenerystack/bamboo'; import { Orientation } from 'scenerystack/phet-core'; import { Range } from 'scenerystack/dot'; ``` ## A minimal example ```ts const chartTransform = new ChartTransform( { viewWidth: 400, viewHeight: 200, modelXRange: new Range( 0, 10 ), modelYRange: new Range( -1, 1 ) } ); // Vertical lines every 1 model unit along x. const verticalGridLines = new GridLineSet( chartTransform, Orientation.VERTICAL, 1, { stroke: 'lightGray' } ); // Tick marks along the x axis (at model y = 0), every 1 unit. const xTickMarks = new TickMarkSet( chartTransform, Orientation.HORIZONTAL, 1, { edge: 'min' } ); // Numeric labels below the x tick marks. const xTickLabels = new TickLabelSet( chartTransform, Orientation.HORIZONTAL, 1, { edge: 'min' } ); ``` All three constructors share the same shape: ```ts new GridLineSet( chartTransform: ChartTransform, axisOrientation: Orientation, spacing: number, providedOptions?: GridLineSetOptions ) new TickMarkSet( chartTransform: ChartTransform, axisOrientation: Orientation, spacing: number, providedOptions?: TickMarkSetOptions ) new TickLabelSet( chartTransform: ChartTransform, axisOrientation: Orientation, spacing: number, providedOptions?: TickLabelSetOptions ) ``` `axisOrientation` is the axis the ticks/gridlines *progress along* — a `HORIZONTAL` set has ticks marching along x (at x=0,1,2,…), producing vertical gridlines; a `VERTICAL` set marches along y and produces horizontal gridlines. `spacing` is in model coordinates. ## Options | Option | Applies to | Default | Effect | | --- | --- | --- | --- | | `value` | all three | `0` | Model coordinate on the *opposite* axis where the ticks/gridlines are positioned (mutually exclusive with `edge`) | | `edge` | all three | `null` | `'min'` or `'max'` pins the ticks/gridlines to that edge of the chart instead of a `value` | | `origin` | all three | `0` | Model-coordinate offset the spacing is measured from | | `skipCoordinates` | `TickMarkSet`, `TickLabelSet` | `[]` | Model coordinates to skip — typically `[0]`, to avoid drawing a redundant tick where the axes cross | | `extent` | `TickMarkSet`, `TickLabelSet` | `10` (`TickMarkSet.DEFAULT_EXTENT`) | Length of each tick mark in view coordinates (or, for `TickLabelSet`, the extent an implied tick would have, used only for label positioning) | | `clippingType` | all three | `'strict'` | Rounding/clipping behavior for `ChartTransform.forEachSpacing()` — whether partially-out-of-range ticks are included | | `createLabel` | `TickLabelSet` | `value => new Text( toFixed( value, 1 ), { fontSize: 12 } )` | Builds the label `Node` for a given model coordinate; returning `null` omits that label | | `positionLabel` | `TickLabelSet` | positions below (`HORIZONTAL`) or to the left (`VERTICAL`) of the tick | Positions a created label relative to its tick's bounds | ## Methods | Member | Description | | --- | --- | | `setSpacing( spacing )` / `getSpacing()` | Replaces the spacing (a no-op if unchanged) and recomputes; shared across all three | | `getSpacingBorders()` | Returns the `Range` of model coordinates the current spacing/origin/clippingType actually produces ticks for; shared across all three | | `setCreateLabel( createLabel )` | `TickLabelSet` only — swaps the label-creation function and calls `invalidateTickLabelSet()` | | `invalidateTickLabelSet()` | `TickLabelSet` only — disposes every cached label and rebuilds, needed if `createLabel`'s *output* for an already-seen value should change (e.g. switching from numeric to symbolic labels) | | `dispose()` | Removes the `chartTransform.changedEmitter` listener before calling the superclass's `dispose()` | ::: tip `TickLabelSet` caches label Nodes by model coordinate `TickLabelSet` keeps a `Map` from model coordinate to the `Node` last built by `createLabel`, and reuses it across `update()` calls rather than reconstructing every label on every transform change — labels are only rebuilt when a coordinate scrolls out of range and a different one scrolls in. If `createLabel`'s logic depends on external state (not just the coordinate value), call `invalidateTickLabelSet()` after that state changes, or stale labels will stick around. ::: ======================================================================== Page: LinearEquationPlot URL: https://veillette.github.io/Almanach/api/bamboo/linear-equation-plot Source: docs/api/bamboo/linear-equation-plot.md Category: API | Tags: bamboo, LinearEquationPlot, chart, plot | Status: complete ======================================================================== # LinearEquationPlot `LinearEquationPlot` (from `scenerystack/bamboo`) renders the line described by a slope `m` and y-intercept `b` — `y = mx + b` — rather than a dataset of discrete points. Unlike [`LinePlot`](/api/bamboo/line-plot), which connects an array of `(Vector2 | null)` samples, `LinearEquationPlot` only needs the two numbers describing the equation: it extends `scenerystack/scenery`'s `Line` and recomputes its endpoints to span the chart transform's full `modelXRange` (or `modelYRange`, for a vertical line) whenever `m`, `b`, or the transform changes. Passing `m: Infinity` or `m: -Infinity` draws a vertical line instead of throwing, which is the idiomatic way to represent an undefined slope. ```ts import { ChartTransform, LinearEquationPlot } from 'scenerystack/bamboo'; import { Range } from 'scenerystack/dot'; ``` ## A minimal example ```ts const chartTransform = new ChartTransform( { viewWidth: 400, viewHeight: 200, modelXRange: new Range( -10, 10 ), modelYRange: new Range( -10, 10 ) } ); // y = 2x + 1, spanning the chart's full model x-range. const linearEquationPlot = new LinearEquationPlot( chartTransform, 2, 1, { stroke: 'red', lineWidth: 2 } ); // A vertical line at x = 3. const verticalLine = new LinearEquationPlot( chartTransform, Infinity, 3 ); ``` ## Constructor ```ts new LinearEquationPlot( chartTransform: ChartTransform, m: number, b: number, providedOptions?: LinearEquationPlotOptions ) ``` `m` is the slope (`Infinity`/`-Infinity` for a vertical line), and `b` is the y-intercept. `LinearEquationPlotOptions` adds no options of its own beyond whatever it inherits from `LineOptions` (e.g. `stroke`, `lineWidth`); `stroke` defaults to `'black'` and `lineWidth` defaults to `1`. ## Methods | Member | Description | | --- | --- | | `m` (getter/setter) | The slope. Setting it calls `setSlope()`, which recomputes the endpoints | | `setSlope( m )` | Replaces the slope and redraws | | `b` (getter/setter) | The y-intercept. Setting it calls `setYIntercept()`, which recomputes the endpoints | | `setYIntercept( b )` | Replaces the y-intercept and redraws | | `solve( x )` | Returns `m * x + b` — the y-value the equation predicts for a given x, without touching the rendered line | | `dispose()` | Removes the `chartTransform.changedEmitter` listener before calling `Line.dispose()` | ::: tip Use it for reference lines and fits, not for arbitrary curves Because `LinearEquationPlot` always spans the transform's entire model range, it's a natural fit for things a dataset would be overkill for: a best-fit line overlaid on a `ScatterPlot`, a target/threshold line, or a y = x reference line. For anything that isn't representable as a single global slope and intercept — a curve, a piecewise function, or real sampled data — reach for [`LinePlot`](/api/bamboo/line-plot) instead. ::: ======================================================================== Page: LinePlot URL: https://veillette.github.io/Almanach/api/bamboo/line-plot Source: docs/api/bamboo/line-plot.md Category: API | Tags: bamboo, LinePlot, chart, plot | Status: verified ======================================================================== # LinePlot `LinePlot` (from `scenerystack/bamboo`) renders a dataset of `(Vector2 | null)[]` by connecting consecutive points with straight line segments, using a `ChartTransform` to convert each model-space point to view coordinates. It extends `scenerystack/scenery`'s `Path`, rebuilding its `Shape` whenever the dataset or the chart transform changes. A `null` entry breaks the line: `[ (0,0), (0,1), null, (0,2), (0,3) ]` draws two separate segments instead of three connected ones. ```ts import { ChartTransform, LinePlot } from 'scenerystack/bamboo'; import { Range, Vector2 } from 'scenerystack/dot'; ``` ## A minimal example ```ts const chartTransform = new ChartTransform( { viewWidth: 400, viewHeight: 200, modelXRange: new Range( 0, 10 ), modelYRange: new Range( -1, 1 ) } ); const dataSet: ( Vector2 | null )[] = []; for ( let x = 0; x <= 10; x += 0.1 ) { dataSet.push( new Vector2( x, Math.sin( x ) ) ); } const linePlot = new LinePlot( chartTransform, dataSet, { stroke: 'blue', lineWidth: 2 } ); ``` ## Constructor ```ts new LinePlot( chartTransform: ChartTransform, dataSet: ( Vector2 | null )[], providedOptions?: LinePlotOptions ) ``` `LinePlotOptions` adds no options of its own beyond whatever it inherits from `PathOptions` (e.g. `stroke`, `lineWidth`, `lineDash`); `stroke` defaults to `'black'`. ## Methods | Member | Description | | --- | --- | | `dataSet` | `( Vector2 \| null )[]` — public field. Safe to read directly; if you mutate it in place (rather than calling `setDataSet`), you're responsible for calling `update()` yourself | | `setDataSet( dataSet )` | Replaces the dataset and immediately calls `update()` | | `update()` | Recomputes the line's `Shape` from the current `dataSet`; called automatically on construction and whenever the `ChartTransform`'s `changedEmitter` fires | | `dispose()` | Removes the `chartTransform.changedEmitter` listener before calling `Path.dispose()` | ::: tip `null` entries create gaps, not zero-value points Each `null` in `dataSet` starts a new, disconnected subpath on the next non-null point — it does not plot a point at the origin. This is the standard way to represent missing data or a discontinuous function on a bamboo chart, and it's the main behavioral difference from `ScatterPlot`, whose `dataSet` has no such gap mechanism. ::: ======================================================================== Page: ScatterPlot URL: https://veillette.github.io/Almanach/api/bamboo/scatter-plot Source: docs/api/bamboo/scatter-plot.md Category: API | Tags: bamboo, ScatterPlot, chart, plot | Status: verified ======================================================================== # ScatterPlot `ScatterPlot` (from `scenerystack/bamboo`) renders a `Vector2[]` dataset as one filled circle per point, using a `ChartTransform` to convert each model point to a view position. It extends `scenerystack/scenery`'s `Path`, building a single `Shape` containing one `circle()` subpath per data point. Unlike `LinePlot`, its dataset has no `null`-gap convention — non-finite points (`NaN` or infinite components) are simply skipped and draw nothing. ```ts import { ChartTransform, ScatterPlot } from 'scenerystack/bamboo'; import { Range, Vector2 } from 'scenerystack/dot'; ``` ## A minimal example ```ts const chartTransform = new ChartTransform( { viewWidth: 300, viewHeight: 300, modelXRange: new Range( 0, 10 ), modelYRange: new Range( 0, 10 ) } ); const dataSet = []; for ( let i = 0; i < 50; i++ ) { dataSet.push( new Vector2( Math.random() * 10, Math.random() * 10 ) ); } const scatterPlot = new ScatterPlot( chartTransform, dataSet, { radius: 3, fill: 'purple' } ); ``` ## Constructor ```ts new ScatterPlot( chartTransform: ChartTransform, dataSet: Vector2[], providedOptions?: ScatterPlotOptions ) ``` ## Options | Option | Default | Effect | | --- | --- | --- | | `radius` | `2` | Radius of each plotted circle, in view coordinates | | `fill` (inherited `PathOptions`) | `'black'` | Fill color of the circles | ## Methods | Member | Description | | --- | --- | | `dataSet` | `Vector2[]` — public field. Mutate in place only if you also call `update()` yourself | | `setDataSet( dataSet )` | Replaces the dataset and calls `update()` | | `update()` | Recomputes the circles' `Shape` from the current `dataSet`, skipping non-finite points | | `dispose()` | Removes the `chartTransform.changedEmitter` listener before calling `Path.dispose()` | ::: warning `fill` and `stroke` together can render incorrectly on overlapping points Because all circles are packed into one `Shape` on a single `Path`, if you set both `fill` and `stroke` to different colors, overlapping points will not composite the way separate individually-stroked circles would (this is a known upstream limitation, not a configuration mistake). If your data can produce overlapping points and the visual difference matters, prefer a fill-only or stroke-only style, or lay out separate `Circle` nodes instead of `ScatterPlot`. ::: ======================================================================== Page: Bounds2 URL: https://veillette.github.io/Almanach/api/dot/bounds2 Source: docs/api/dot/bounds2.md Category: API | Tags: dot, Bounds2, bounds | Status: verified ======================================================================== # Bounds2 `Bounds2` (from `scenerystack/dot`) is an axis-aligned bounding box, stored as `minX`/`minY`/`maxX`/`maxY` (not `x`/`y`/`width`/`height`, though those are available as convenience getters). It's used constantly for layout bounds, drag bounds, hit-testing, and `Node.bounds`/`localBounds`. ```ts import { Bounds2 } from 'scenerystack/dot'; const dragBounds = new Bounds2( 0, 0, 768, 504 ); dragBounds.width; // 768 dragBounds.center; // Vector2(384, 252) dragBounds.containsCoordinates( 800, 10 ); // false — outside on the right const clamped = dragBounds.closestPointTo( new Vector2( 900, 10 ) ); // Vector2(768, 10) ``` ## Constructing bounds The constructor takes the four extremes directly: `new Bounds2( minX, minY, maxX, maxY )`. For the more familiar `(x, y, width, height)` rectangle shape, use the static factory instead: ```ts const bounds = Bounds2.rect( 10, 10, 200, 100 ); // minX:10, minY:10, maxX:210, maxY:110 ``` | Static | Meaning | | --- | --- | | `Bounds2.rect( x, y, width, height )` | Build from x/y/width/height | | `Bounds2.point( x, y )` / `Bounds2.point( vector )` | A zero-area bounds containing exactly one point, useful as a starting point to `dilate()` | | `Bounds2.NOTHING` | `minX = minY = +Infinity`, `maxX = maxY = -Infinity` — the identity for `union()` | | `Bounds2.EVERYTHING` | `minX = minY = -Infinity`, `maxX = maxY = +Infinity` — the identity for `intersection()` | ## Reading properties | Getter | Meaning | | --- | --- | | `width`, `height` | `maxX - minX`, `maxY - minY` | | `x`, `y`, `left`, `top`, `right`, `bottom` | Aliases for `minX`, `minY`, `minX`, `minY`, `maxX`, `maxY` | | `centerX`, `centerY`, `center` | Midpoints, as numbers or a `Vector2` | | `leftTop`, `centerTop`, `rightTop`, `leftCenter`, `rightCenter`, `leftBottom`, `centerBottom`, `rightBottom` | The eight named corners/edge-midpoints as `Vector2` | | `xRange`, `yRange` | The `[minX, maxX]` / `[minY, maxY]` extents as a [`Range`](/api/dot/range) | | `isEmpty()` | `true` if width or height is negative (`NOTHING` is empty; a zero-area point bounds is **not**) | | `isFinite()` | `true` unless it's `NOTHING`/`EVERYTHING`-like | ## Immutable vs. mutable methods Like the vector types, most operations come in an immutable form (new `Bounds2`) and a mutable form (mutates `this`): | Immutable | Mutable | Effect | | --- | --- | --- | | `union( bounds )` | `includeBounds( bounds )` | Smallest bounds containing both | | `intersection( bounds )` | `constrainBounds( bounds )` | Largest bounds contained in both | | `withPoint( point )` / `withCoordinates( x, y )` | `addPoint( point )` / `addCoordinates( x, y )` | Expand to include a point | | `dilated( d )` / `dilatedXY( x, y )` | `dilate( d )` / `dilateXY( x, y )` | Expand on all sides | | `eroded( d )` / `erodedXY( x, y )` | `erode( d )` / `erodeXY( x, y )` | Contract on all sides | | `shifted( v )` / `shiftedXY( x, y )` | `shift( v )` / `shiftXY( x, y )` | Translate | | `roundedOut()` / `roundedIn()` | `roundOut()` / `roundIn()` | Snap to integer boundaries, expanding or contracting | | `transformed( matrix )` | `transform( matrix )` | Apply a [`Matrix3`](/api/dot/matrix3) affine transform, re-fitting to axis-alignment | | `copy()` | `set( bounds )` / `setMinMax( ... )` | Copy / assign | Queries: `containsCoordinates( x, y )`, `containsPoint( point )`, `containsBounds( bounds )`, `intersectsBounds( bounds )`, `closestPointTo( point )`, `getConstrainedPoint( point )` (clamp a point into the bounds), `equals`/`equalsEpsilon`, and `toString()`. ::: warning `Bounds2.NOTHING` and `Bounds2.EVERYTHING` are frozen Both constants throw if you call a mutating method (`setMinMax`, `dilate`, `transform`, ...) on them directly — they exist purely as identity elements for `union`/`intersection` chains. Start an accumulation with `Bounds2.NOTHING.copy()` (or just `Bounds2.NOTHING.union(...)`, which returns a fresh instance) rather than mutating the shared constant. ::: ## Related - [Vector2](/api/dot/vector2) — corners and centers of a `Bounds2` are returned as `Vector2` instances. - [Range](/api/dot/range) — `bounds.xRange` / `bounds.yRange` expose each axis as a `Range`. - [Matrix3](/api/dot/matrix3) — `bounds.transformed( matrix )` re-fits a bounds after an affine transform. - [Drag Listeners](/patterns/drag-listeners) — drag bounds are typically expressed as a `Bounds2`. ======================================================================== Page: Bounds3 URL: https://veillette.github.io/Almanach/api/dot/bounds3 Source: docs/api/dot/bounds3.md Category: API | Tags: dot, Bounds3, bounds | Status: verified ======================================================================== # Bounds3 `Bounds3` (from `scenerystack/dot`) is the 3D counterpart to [`Bounds2`](/api/dot/bounds2): an axis-aligned cuboid stored as `minX`/`minY`/`minZ`/`maxX`/`maxY`/`maxZ` (plus `width`/`height`/`depth` convenience getters). It shows up far less often than `Bounds2` — mainly in 3D-flavored simulation content alongside [`Vector3`](/api/dot/vector3) and [`Matrix4`](/api/dot/matrix4) — but shares the same shape and naming conventions. ```ts import { Bounds3 } from 'scenerystack/dot'; const box = new Bounds3( 0, 0, 0, 10, 20, 30 ); box.width; // 10 box.height; // 20 box.depth; // 30 box.volume; // 6000 box.center; // Vector3(5, 10, 15) ``` ## Constructing bounds The constructor takes all six extremes directly: `new Bounds3( minX, minY, minZ, maxX, maxY, maxZ )`. For the `(x, y, z, width, height, depth)` cuboid shape, use the static factory instead: ```ts const box = Bounds3.cuboid( 0, 0, 0, 10, 20, 30 ); // same box as above ``` | Static | Meaning | | --- | --- | | `Bounds3.cuboid( x, y, z, width, height, depth )` | Build from x/y/z/width/height/depth | | `Bounds3.point( x, y, z )` | A zero-volume bounds containing exactly one point | | `Bounds3.NOTHING` | All mins `+Infinity`, all maxes `-Infinity` — the identity for `union()` | | `Bounds3.EVERYTHING` | All mins `-Infinity`, all maxes `+Infinity` — the identity for `intersection()` | ## Reading properties | Getter | Meaning | | --- | --- | | `width`, `height`, `depth` | `maxX - minX`, `maxY - minY`, `maxZ - minZ` | | `x`, `y`, `z`, `left`, `top`, `back`, `right`, `bottom`, `front` | Aliases for `minX`, `minY`, `minZ`, `minX`, `minY`, `minZ`, `maxX`, `maxY`, `maxZ` | | `centerX`, `centerY`, `centerZ`, `center` | Midpoints, as numbers or a [`Vector3`](/api/dot/vector3) | | `volume` | `width * height * depth` | | `isEmpty()` | `true` if any dimension is negative | | `isFinite()` | `true` unless it's `NOTHING`/`EVERYTHING`-like | | `hasNonzeroVolume()` | `true` if width, height, and depth are all strictly positive | | `isValid()` | `!isEmpty() && isFinite()` | ## Immutable vs. mutable methods Like `Bounds2`, most operations come in an immutable form (new `Bounds3`) and a mutable form (mutates `this`). Note that `Bounds3` doesn't have the full set of named corner getters (`leftTop`, etc.) that `Bounds2` has — the model is otherwise identical: | Immutable | Mutable | Effect | | --- | --- | --- | | `union( bounds )` | `includeBounds( bounds )` | Smallest bounds containing both | | `intersection( bounds )` | `constrainBounds( bounds )` | Largest bounds contained in both | | `withCoordinates( x, y, z )` / `withPoint( point )` | `addCoordinates( x, y, z )` / `addPoint( point )` | Expand to include a point | | `dilated( d )` / `dilatedXYZ( x, y, z )` | `dilate( d )` / `dilateXYZ( x, y, z )` | Expand on all sides | | `eroded( d )` / `erodedXYZ( x, y, z )` | `erode( d )` / `erodeXYZ( x, y, z )` | Contract on all sides | | `shifted( v )` / `shiftedXYZ( x, y, z )` | `shift( v )` / `shiftXYZ( x, y, z )` | Translate | | `roundedOut()` / `roundedIn()` | `roundOut()` / `roundIn()` | Snap to integer boundaries, expanding or contracting | | `transformed( matrix )` | `transform( matrix )` | Apply a [`Matrix4`](/api/dot/matrix4) affine transform, re-fitting to axis-alignment | | `copy()` | `set( bounds )` / `setMinMax( ... )` | Copy / assign | Queries: `containsCoordinates( x, y, z )`, `containsPoint( point )`, `containsBounds( bounds )`, `intersectsBounds( bounds )`, `equals`/`equalsEpsilon`, and `toString()`. ::: tip `transformed`/`transform` check all 8 corners Just like `Bounds2.transformed()`, `Bounds3.transformed( matrix )` re-fits the box after an arbitrary [`Matrix4`](/api/dot/matrix4) affine transform by transforming all 8 corners and taking the min/max of the results — so it stays axis-aligned even after a rotation. Composing a transform with its inverse (`bounds.transformed( m ).transformed( m.inverted() )`) can therefore return a *larger* box than the original if `m` includes a rotation that isn't a multiple of a right angle. ::: ## Related - [Bounds2](/api/dot/bounds2) — the 2D counterpart; same shape and naming conventions, one dimension fewer. - [Vector3](/api/dot/vector3) — `Bounds3`'s corners and center are `Vector3` instances. - [Matrix4](/api/dot/matrix4) — `bounds.transformed( matrix )` re-fits a `Bounds3` after an affine transform. ======================================================================== Page: Complex URL: https://veillette.github.io/Almanach/api/dot/complex Source: docs/api/dot/complex.md Category: API | Tags: dot, Complex, math | Status: verified ======================================================================== # Complex `Complex` (from `scenerystack/dot`) is a complex number with a `real` and `imaginary` part, supporting the usual arithmetic (add, subtract, multiply, divide, conjugate, powers, trig, exponentiation) plus static solvers for the roots of linear, quadratic, and cubic equations with complex coefficients. It follows the same immutable/mutable method-naming convention as dot's vector types. ```ts import { Complex } from 'scenerystack/dot'; const a = new Complex( 3, 4 ); // 3 + 4i a.magnitude; // 5 a.phase(); // Math.atan2(4, 3) const product = a.times( Complex.I ); // new Complex, a unchanged: rotates a by 90 degrees -> -4 + 3i a.multiply( Complex.I ); // mutates a in place to -4 + 3i ``` ## Constructing The constructor takes `real` and `imaginary` directly: `new Complex( real, imaginary )`. Both are plain mutable public fields. | Static | Meaning | | --- | --- | | `Complex.real( r )` | `r + 0i` | | `Complex.imaginary( i )` | `0 + ri` | | `Complex.createPolar( magnitude, phase )` | Builds from magnitude/phase (radians) instead of real/imaginary | | `Complex.ZERO` / `Complex.ONE` / `Complex.I` | The constants `0`, `1`, and the imaginary unit `i` | ## Immutable vs. mutable methods The convention matches `Vector2`/`Vector3`: the immutable form returns a new `Complex`, the mutable form changes `this` and returns it. | Immutable (returns new `Complex`) | Mutable (changes `this`, returns `this`) | Effect | | --- | --- | --- | | `plus( c )` | `add( c )` | Addition | | `minus( c )` | `subtract( c )` | Subtraction | | `times( c )` | `multiply( c )` | Complex multiplication | | `dividedBy( c )` | `divide( c )` | Complex division | | `negated()` | `negate()` | Negation | | `conjugated()` | `conjugate()` | Complex conjugate (flips the sign of `imaginary`) | | `squared()` | `square()` | Self times self | | `sqrtOf()` | `sqrt()` | Principal square root | | `powerByReal( realPower )` | — (no mutable form) | Raises to a real-valued power | | `sinOf()` / `cosOf()` | `sin()` / `cos()` | Complex sine/cosine | | `exponentiated()` | `exponentiate()` | $e^{a+bi} = e^a(\cos b + i\sin b)$ | | `copy()` | `set( c )` / `setRealImaginary( r, i )` / `setReal( r )` / `setImaginary( i )` / `setPolar( magnitude, phase )` | Copy / assign | Read-only queries: `magnitude`, `magnitudeSquared`, `phase()` (alias `argument`/`getArgument()`), `equals( other )` / `equalsEpsilon( other, epsilon )`, `getCubeRoots()` (the three cube roots of this complex number), and `toString()`. ## Solving polynomial roots `Complex` exposes static solvers that work over complex coefficients, each returning an array of roots or `null` if every value is a solution: | Static method | Solves | | --- | --- | | `Complex.solveLinearRoots( a, b )` | $ax + b = 0$ | | `Complex.solveQuadraticRoots( a, b, c )` | $ax^2 + bx + c = 0$ | | `Complex.solveCubicRoots( a, b, c, d )` | $ax^3 + bx^2 + cx + d = 0$ | ::: tip For real-only coefficients, dot's free functions are simpler If your coefficients and expected roots are all real numbers, `scenerystack/dot`'s free functions `solveLinearRootsReal`, `solveQuadraticRootsReal`, and `solveCubicRootsReal` (see [Utils](/api/dot/dot-utils)) skip the `Complex` wrapping entirely and return `number[] | null` directly. Reach for `Complex.solve*Roots` only when coefficients or roots may genuinely be complex (not just real values that happen to be represented as numbers). ::: ## Related - [Utils](/api/dot/dot-utils) — the real-only root solvers (`solveQuadraticRootsReal`, etc.) that skip `Complex` when all values are real. ======================================================================== Page: ConvexHull2 URL: https://veillette.github.io/Almanach/api/dot/convex-hull2 Source: docs/api/dot/convex-hull2.md Category: API | Tags: dot, ConvexHull2, Vector2, geometry, math | Status: complete ======================================================================== # ConvexHull2 `ConvexHull2` (from `scenerystack/dot`) is a namespace object (not a class you instantiate) with a single method, `grahamScan()`, implementing the [Graham Scan](http://en.wikipedia.org/wiki/Graham_scan) algorithm: given an arbitrary set of 2D points, it returns the ordered subset of those points that form the smallest convex polygon containing all of them. ```ts import { ConvexHull2, Vector2 } from 'scenerystack/dot'; import { Shape } from 'scenerystack/kite'; const points = [ new Vector2( 0, 0 ), new Vector2( 4, 0 ), new Vector2( 2, 2 ), new Vector2( 2, 4 ), new Vector2( 1, 1 ) // interior point - excluded from the hull ]; const hullPoints = ConvexHull2.grahamScan( points, false ); // an ordered Vector2[] tracing the outer boundary, excluding the interior point const hullShape = Shape.polygon( hullPoints ); // draw it with kite ``` ## `grahamScan( points, includeCollinear )` | Parameter | Effect | | --- | --- | | `points` | The `Vector2[]` to compute a hull over | | `includeCollinear` | If `true`, points lying exactly along a hull edge (not at a vertex) are kept in the result; if `false`, only the "true" corner vertices are kept | Returns an ordered `Vector2[]` (counter-clockwise) tracing the hull boundary. Arrays of 2 or fewer points are returned unchanged (there's no meaningful "hull" to compute). ## When you'd reach for this Convex hulls come up whenever you need the smallest polygon enclosing a scattered set of points — for example, drawing a boundary/highlight around a cluster of draggable objects, computing a rough bounding shape for collision purposes that's tighter than an axis-aligned [`Bounds2`](/api/dot/bounds2) but simpler than the objects' exact outlines, or building the correct arm/interaction shape for a Voronoi- or hull-based interactive diagram. ::: tip Collinear points are excluded by default Pass `includeCollinear: false` (as most callers do) unless you specifically need every point that lies exactly on a hull edge represented in the result — the default (excluding them) gives you the minimal vertex set describing the same polygon, which is usually what you want for drawing or further geometric processing. ::: ======================================================================== Page: DampedHarmonic URL: https://veillette.github.io/Almanach/api/dot/damped-harmonic Source: docs/api/dot/damped-harmonic.md Category: API | Tags: dot, DampedHarmonic, differential-equations, physics, math | Status: complete ======================================================================== # DampedHarmonic `DampedHarmonic` (from `scenerystack/dot`) solves the general damped harmonic oscillator differential equation `a·x'' + b·x' + c·x = 0` in closed form, given initial conditions `x(0)` and `x'(0)`. This is the same equation governing a mass on a damped spring, a damped pendulum (small-angle approximation), or an RLC circuit — anywhere a sim's model has a restoring force plus a velocity-proportional damping force and needs `x(t)` evaluated analytically (no numerical integration, no accumulated per-step error) at any time `t`. ```ts import { DampedHarmonic } from 'scenerystack/dot'; // A mass-spring-damper: mx'' + bx' + kx = 0, released from x=1 with zero velocity const mass = 1; const damping = 0.5; const springConstant = 4; const oscillator = new DampedHarmonic( mass, damping, springConstant, 1, 0 ); oscillator.getValue( 0 ); // 1 (the initial position) oscillator.getValue( 2 ); // position at t=2 seconds oscillator.getDerivative( 2 ); // velocity at t=2 seconds ``` ## Constructor ```ts new DampedHarmonic( a: number, b: number, c: number, initialValue: number, initialDerivative: number ) ``` | Parameter | Effect | | --- | --- | | `a` | Coefficient on `x''` (e.g. mass) — must be finite and non-zero | | `b` | Coefficient on `x'` (damping) — must be finite; `a` and `b` must share the same sign, since negative damping (relative to `a`) doesn't correspond to a physical damped system this class solves | | `c` | Coefficient on `x` (restoring force / spring constant) — must be finite and non-zero; `a` and `c` must share the same sign | | `initialValue` | `x(0)` | | `initialDerivative` | `x'(0)` | Internally, the constructor normalizes to `x'' + dampingConstant·x' + angularFrequencySquared·x = 0`, computes the discriminant `dampingConstant² - 4·angularFrequencySquared`, and picks the matching closed-form solution: **over-damped** (`discriminant > 0`, two real exponential decay rates), **critically damped** (`discriminant ≈ 0`, a linear-times-exponential solution), or **under-damped** (`discriminant < 0`, an oscillating exponentially-decaying solution) — this is standard textbook damped-oscillator classification, chosen once at construction and then used by both query methods below. ## Methods | Method | Effect | | --- | --- | | `getValue( t )` | The solution `x(t)` at time `t` | | `getDerivative( t )` | The velocity `x'(t)` at time `t` | Both are pure functions of `t` — evaluating at an arbitrary time doesn't require stepping through every intermediate time, unlike a numerically-integrated model. ::: tip This is an analytic solver, not a stepping integrator Unlike model code that advances state with `step( dt )` on every animation frame, `DampedHarmonic` computes the exact solution at any requested `t` directly from the initial conditions — there's no accumulated integration error, and no need to call it every frame if you only need the value at a few specific times. If your model already advances other quantities with discrete `dt` steps, you can still track a single elapsed-time accumulator and call `getValue( elapsedTime )` each frame for the oscillating quantity specifically. ::: ======================================================================== Page: Dimension2 URL: https://veillette.github.io/Almanach/api/dot/dimension2 Source: docs/api/dot/dimension2.md Category: API | Tags: dot, Dimension2, bounds | Status: verified ======================================================================== # Dimension2 `Dimension2` (from `scenerystack/dot`) is a bare `{ width, height }` pair — a size with no position, unlike [`Bounds2`](/api/dot/bounds2) which anchors its extent at specific `minX`/`minY` coordinates. Reach for it when you need to describe or pass around "how big," not "where," e.g. layout preferred sizes, image dimensions, or screen/window sizes. ```ts import { Dimension2 } from 'scenerystack/dot'; const screenSize = new Dimension2( 1024, 768 ); screenSize.width; // 1024 screenSize.swapped(); // Dimension2(768, 1024) — new instance, screenSize unchanged const bounds = screenSize.toBounds(); // Bounds2(0, 0, 1024, 768) const shifted = screenSize.toBounds( 100, 50 ); // Bounds2(100, 50, 1124, 818) ``` ## Constructing The constructor takes `width` and `height` directly: `new Dimension2( width, height )`. Both are plain mutable public fields (`dimension.width = 10`), not getter/setter pairs. ## Methods | Method | Effect | | --- | --- | | `copy( dimension? )` | Returns a new `Dimension2` equal to this one, or (if `dimension` is passed) mutates `dimension` to match this one and returns it | | `set( dimension )` | Mutates this `Dimension2` to match another, returning `this` | | `setWidth( width )` / `setHeight( height )` | Mutable single-field setters, returning `this` | | `swapped()` | A new `Dimension2` with `width` and `height` exchanged | | `toBounds( x?, y? )` | A new [`Bounds2`](/api/dot/bounds2) of this size, anchored with its minimum corner at `(x, y)` (both default to `0`) | | `equals( that )` / `equalsEpsilon( that, epsilon? )` | Comparison | | `toString()` | Debug string, e.g. `[1024w, 768h]` | ::: tip There's no immutable/mutable method split here Unlike `Vector2`/`Bounds2`, `Dimension2` doesn't have paired immutable+mutable variants for its operations — `width`/`height` are plain public fields you can assign directly, and `set`/`setWidth`/`setHeight` are the only mutators. `copy()` and `swapped()` are the only two methods that return a new instance; everything else either mutates in place or (like `toBounds`) is a one-way conversion. ::: ## Related - [Bounds2](/api/dot/bounds2) — `dimension.toBounds( x, y )` builds a `Bounds2` of this size at a given position; conversely, `bounds.width`/`bounds.height` give you the size of an existing `Bounds2`. ======================================================================== Page: dotRandom URL: https://veillette.github.io/Almanach/api/dot/dot-random Source: docs/api/dot/dot-random.md Category: API | Tags: dot, dotRandom, Random, seeding, phet-io | Status: complete ======================================================================== # dotRandom `dotRandom` (from `scenerystack/dot`) is a single, shared instance of [`Random`](/api/dot/random) that every part of a SceneryStack simulation should use for pseudo-randomness, rather than calling `Math.random()` directly or constructing a private `new Random()`. It's created once, seeded from the `randomSeed` PhET-iO query parameter when running under PhET-iO (falling back to an unpredictable seed otherwise), and then reused everywhere. ```ts import { dotRandom } from 'scenerystack/dot'; // Anywhere in model code that needs randomness: const angle = dotRandom.nextDoubleBetween( 0, Math.PI * 2 ); const startingLane = dotRandom.nextIntBetween( 0, 3 ); const shuffledDeck = dotRandom.shuffle( cards ); ``` `dotRandom` exposes the exact same API as any `Random` instance — `nextDouble()`, `nextBoolean()`, `nextIntBetween()`, `nextGaussian()`, `sample()`, `shuffle()`, `sampleProbabilities()`, and so on. See [Random](/api/dot/random) for the full method table. ## Why the shared instance matters `Random`'s own doc comment is explicit about this: "If you are developing a PhET Simulation, you should probably use the global `DOT/dotRandom`" rather than a private instance, because `dotRandom` provides built-in support for PhET-iO seeding. When every random draw in a simulation funnels through the one shared, seedable generator, PhET-iO's state-restore and record-and-replay tooling can reproduce an identical run just by re-supplying the same seed — a `Vector2Property` moving randomly, a probabilistic model event, and a shuffled list all become deterministic together. Scattering `Math.random()` calls (or several independent `new Random()` instances) throughout a codebase breaks that guarantee: even with the same seed, the *order* in which different generators are drawn from can vary run to run, and any plain `Math.random()` call can never be seeded or reproduced at all. ::: warning Construct your own `Random` only for genuinely independent randomness Reach for `new Random( { seed } )` instead of `dotRandom` only when you deliberately want an isolated sequence that must not perturb (or be perturbed by) the rest of the sim's random draws — for example, a self-contained unit test asserting on a specific sequence of values. For anything that runs as part of a live simulation, use `dotRandom`. ::: ======================================================================== Page: LinearFunction URL: https://veillette.github.io/Almanach/api/dot/linear-function Source: docs/api/dot/linear-function.md Category: API | Tags: dot, LinearFunction, math | Status: verified ======================================================================== # LinearFunction `LinearFunction` (from `scenerystack/dot`) packages up a linear mapping between two numeric domains — "domain a" and "domain b" — as a reusable object with both a forward (`evaluate`) and inverse (`inverse`) direction. It's the object-oriented sibling of the free function [`linear()`](/api/dot/dot-utils): where `linear()` takes all five numbers every call, `LinearFunction` remembers the two domains once and lets you call `.evaluate()`/`.inverse()` repeatedly, which reads better when the same mapping is reused (e.g. a value-to-pixel conversion applied across many data points). ```ts import { LinearFunction } from 'scenerystack/dot'; // Maps a model domain [0, 100] to a view/pixel domain [0, 200] const f = new LinearFunction( 0, 100, 0, 200 ); f.evaluate( 50 ); // 100 — maps a value from domain a to domain b f.inverse( 100 ); // 50 — maps a value from domain b back to domain a ``` ## Constructing ```ts new LinearFunction( a1, a2, b1, b2, clamp = false ) ``` `a1`/`a2` are two points in the first domain, `b1`/`b2` are the corresponding two points in the second domain — so `evaluate( a1 ) === b1` and `evaluate( a2 ) === b2`, with everything else linearly interpolated (or extrapolated) between them. The optional fifth argument `clamp` (default `false`), if `true`, constrains every result of `evaluate`/`inverse` to stay within `[min(b1,b2), max(b1,b2)]` or `[min(a1,a2), max(a1,a2)]` respectively, instead of extrapolating past the given endpoints. ## Methods | Method | Effect | | --- | --- | | `evaluate( a3 )` | Maps a value from domain a to domain b | | `inverse( b3 )` | Maps a value from domain b back to domain a — the literal inverse of `evaluate` | That's the entire public surface — `LinearFunction` is intentionally minimal. Both methods respect the `clamp` flag passed to the constructor. ```ts // A slider's model range maps to its track's pixel width, clamped so // out-of-range model values don't push the thumb off the track. const modelToTrackX = new LinearFunction( 0, 100, 0, 300, true ); modelToTrackX.evaluate( 150 ); // 300, not 450 — clamped to the b-domain ``` ::: tip Order of arguments controls direction, not just endpoints `evaluate` always maps *from* the `a1`/`a2` domain *to* the `b1`/`b2` domain; `inverse` always goes the other way. There's no ambiguity about which domain is "the input" at call time — it's fixed by the order of arguments to the constructor. If you find yourself calling `inverse` far more often than `evaluate`, consider swapping the constructor arguments so the more common direction is the default `evaluate` call. ::: ## Related - [Utils](/api/dot/dot-utils) — `linear( a1, a2, b1, b2, a3 )` is the underlying free function `LinearFunction` wraps; reach for it directly for a one-off mapping that doesn't need to be reused. - [Range](/api/dot/range) — `range.getNormalizedValue()`/`expandNormalizedValue()` cover the common special case of mapping to/from `[0, 1]`, which overlaps with what `LinearFunction` can express. ======================================================================== Page: Matrix Decompositions URL: https://veillette.github.io/Almanach/api/dot/matrix-decompositions Source: docs/api/dot/matrix-decompositions.md Category: API | Tags: dot, Matrix, LUDecomposition, QRDecomposition, EigenvalueDecomposition, SingularValueDecomposition, linear-algebra | Status: complete ======================================================================== # Matrix Decompositions `scenerystack/dot` exports a general, arbitrary-dimensional `Matrix` class (distinct from the fixed-size [`Matrix3`](/api/dot/matrix3)/`Matrix4` used for 2D/3D transforms — `Matrix` is for numerical linear algebra on `m x n` data, ported from [Jama](http://math.nist.gov/javanumerics/jama/)) plus four decomposition classes built on it: `LUDecomposition`, `QRDecomposition`, `EigenvalueDecomposition`, and `SingularValueDecomposition`. Most simulation code never needs any of these directly — they exist to support `Matrix.solve()`/`Matrix.inverse()` and any sim that does real numerical linear algebra (curve fitting, physics models expressed as linear systems, PCA-style data analysis). This page is a map of what each one is for, not a numerical-methods tutorial. ```ts import { Matrix } from 'scenerystack/dot'; const A = new Matrix( 2, 2, [ 2, 1, 1, 3 ] ); // row-major: [[2,1],[1,3]] const b = new Matrix( 2, 1, [ 3, 5 ] ); const x = A.solve( b ); // solves Ax = b - internally picks LU (square) or QR (non-square) ``` ## `Matrix` itself `new Matrix( m, n, filler?, fast? )` builds an `m`-row by `n`-column matrix backed by a flat, row-major `Float64Array`. Beyond basic algebra (`plus`, `minus`, `times`, `transpose`, `trace`, ...), its most-used members delegate to the decompositions below: | Method | Delegates to | | --- | --- | | `solve( b )` | `LUDecomposition` if square, otherwise `QRDecomposition` (least-squares) | | `inverse()` | `solve( Matrix.identity( ... ) )` | | `det()` | `LUDecomposition.det()` | | `rank()` | `SingularValueDecomposition.rank()` | | `cond()` | `SingularValueDecomposition.cond()` | You'll reach for a specific decomposition class directly only when you need something `Matrix`'s convenience methods don't expose — the individual `L`/`U`, `Q`/`R`, eigenvector, or singular-value matrices themselves, not just a solved system or a scalar. ## `LUDecomposition` — solving square linear systems Factors a square matrix `A` into a lower-triangular `L` and upper-triangular `U` (with row pivoting) such that `A = L * U` (up to a permutation). This is the classical "solve `Ax = b`" workhorse for square systems, and what `Matrix.solve()`/`det()` use whenever the matrix is square. | Member | Effect | | --- | --- | | `new LUDecomposition( matrix )` | Computes the factorization of a square `Matrix` | | `isNonsingular()` | Whether the matrix is invertible (no zero pivot) | | `getL()` / `getU()` | The lower/upper triangular factor matrices | | `getPivot()` / `getDoublePivot()` | The row-pivot permutation applied during factorization | | `det()` | The determinant, computed from the pivoted diagonal | | `solve( b )` | Solves `Ax = b` for `x`, given the already-computed factorization | A separate `LUDecompositionDecimal` export provides the same factorization using arbitrary-precision decimal arithmetic instead of floating point, for cases where floating-point rounding error in an ordinary `LUDecomposition` would matter. ## `QRDecomposition` — solving non-square (least-squares) systems Factors any `m x n` matrix `A` (via Householder reflections) into an orthogonal `Q` and upper-triangular `R` such that `A = Q * R`. Where `LUDecomposition` requires a square matrix, `QRDecomposition` works for any shape, and `Matrix.solve()` falls back to it whenever the system is over- or under-determined, producing a least-squares solution. | Member | Effect | | --- | --- | | `new QRDecomposition( matrix )` | Computes the factorization of any `Matrix` | | `isFullRank()` | Whether `R`'s diagonal has no (near-)zero entries | | `getQ()` / `getR()` | The orthogonal / upper-triangular factor matrices | | `getH()` | The raw Householder vectors used internally | | `solve( b )` | Least-squares solution to `Ax = b` | ## `EigenvalueDecomposition` — eigenvalues and eigenvectors Computes the eigenvalues and eigenvectors of a square matrix `A`: `A = V * D * V⁻¹`, where `D` is (block-)diagonal and `V`'s columns are the eigenvectors. For symmetric `A`, `D` is purely diagonal with real eigenvalues and `V` is orthogonal; for non-symmetric `A`, complex eigenvalue pairs show up as 2x2 blocks in `D`. | Member | Effect | | --- | --- | | `new EigenvalueDecomposition( matrix )` | Computes the decomposition of a square `Matrix` | | `getV()` | The eigenvector matrix | | `getD()` | The (block-)diagonal eigenvalue matrix | | `getRealEigenvalues()` / `getImagEigenvalues()` | The eigenvalues' real and imaginary parts as plain number arrays | Reach for this when a sim needs the actual eigenstructure of a system — e.g. finding the natural modes/frequencies of a coupled-oscillator model, or the principal axes of a data set. ## `SingularValueDecomposition` — rank, conditioning, and general factorization Factors any `m x n` matrix into `A = U * S * Vᵀ`, where `S` is diagonal (the singular values) and `U`/`V` are orthogonal. This is the most numerically robust of the four — `Matrix.rank()` and `Matrix.cond()` both delegate to it — at the cost of being the most expensive to compute. | Member | Effect | | --- | --- | | `new SingularValueDecomposition( matrix )` | Computes the decomposition of any `Matrix` | | `getU()` / `getV()` | The two orthogonal factor matrices | | `getSingularValues()` / `getS()` | The singular values as a plain array, or as the diagonal `S` matrix | | `rank()` | The matrix's numerical rank (count of singular values above a tolerance) | | `cond()` | The condition number (`largest / smallest` singular value) — a large value signals a numerically unstable/near-singular matrix | ::: tip Reach for `Matrix.solve()`/`.det()`/`.rank()` first Unless you specifically need the factor matrices themselves (`L`/`U`, `Q`/`R`, eigenvectors, or `U`/`S`/`V`), use `Matrix`'s own convenience methods (`solve`, `inverse`, `det`, `rank`, `cond`) — they pick the appropriate decomposition internally. Constructing a decomposition class directly is for the minority of cases (eigenstructure analysis, needing `Q` or `L`/`U` explicitly) where the convenience methods don't expose what you need. ::: ======================================================================== Page: Matrix3 URL: https://veillette.github.io/Almanach/api/dot/matrix3 Source: docs/api/dot/matrix3.md Category: API | Tags: dot, Matrix3, transform | Status: verified ======================================================================== # Matrix3 `Matrix3` (from `scenerystack/dot`) is a 3x3 matrix, most commonly used to represent a 2D affine transform (rotation + scale + translation, or shear) via homogeneous coordinates. It's the type behind `Node.matrix`/`Node.transform` in scenery and the machinery inside `ModelViewTransform2`, and it's the argument type for `Bounds2.transformed()` and `Shape.transformed()`. ```ts import { Matrix3 } from 'scenerystack/dot'; import { Vector2 } from 'scenerystack/dot'; const matrix = Matrix3.translation( 100, 50 ).timesMatrix( Matrix3.rotation2( Math.PI / 4 ) ); matrix.timesVector2( new Vector2( 1, 0 ) ); // rotate then translate the point (1,0) matrix.getTranslation(); // Vector2(100, 50) matrix.getRotation(); // Math.PI / 4 ``` ## Constructing matrices The bare constructor `new Matrix3()` only ever produces the identity matrix — you build real matrices with static factories, which also let dot track the matrix's `type` (`IDENTITY`, `TRANSLATION_2D`, `SCALING`, `AFFINE`, `OTHER`) for fast-path optimizations: | Static factory | Produces | | --- | --- | | `Matrix3.identity()` | The identity matrix | | `Matrix3.translation( x, y )` / `Matrix3.translationFromVector( vector )` | Pure translation | | `Matrix3.scaling( x, y? )` (alias `Matrix3.scale`) | Pure scale (`y` defaults to `x` for uniform scale) | | `Matrix3.rotation2( angle )` | Pure 2D rotation, radians | | `Matrix3.rotationAround( angle, x, y )` / `Matrix3.rotationAroundPoint( angle, point )` | Rotation about an arbitrary point | | `Matrix3.translationRotation( x, y, angle )` | Combined translate + rotate | | `Matrix3.rotationX/Y/Z( angle )` | 3D-style axis rotations (still a 3x3, useful for the z-rotation case above) | | `Matrix3.affine( m00, m01, m02, m10, m11, m12 )` | Arbitrary 2x3 affine part directly | | `Matrix3.rowMajor( v00, ..., v22 )` | All nine entries, row-major | ## Reading a matrix | Accessor | Meaning | | --- | --- | | `m00()`...`m22()` | Individual entries, row/column indexed | | `translation` | `(m02, m12)` as a `Vector2` | | `rotation` | `Math.atan2( m10, m00 )`, in radians | | `scaleVector` | `(|column 0|, |column 1|)` as a `Vector2` | | `determinant` | The determinant; also the signed area scale factor | | `isIdentity()` / `isTranslation()` / `isAffine()` / `isAligned()` | Structural queries used for fast-path code | ## Applying and combining transforms | Method | Effect | | --- | --- | | `timesMatrix( matrix )` | Returns `this * matrix` as a new `Matrix3` | | `timesVector2( vector )` / `multiplyVector2( vector )` | Apply the transform to a point; `timesVector2` is immutable (new `Vector2`), `multiplyVector2` mutates the vector argument in place | | `inverted()` | Returns the inverse transform as a new `Matrix3` | | `transposed()` | Returns the transpose | | `plus( matrix )` / `minus( matrix )` | Entry-wise addition/subtraction | | `equals( matrix )` / `equalsEpsilon( matrix, epsilon )` | Comparison | Mutating counterparts exist for the constructor-style operations too — `setToTranslation`, `setToScale`, `setToRotationZ`, `setToTranslationRotation`, and the low-level `rowMajor(...)` that all of the mutators funnel through. ::: tip Order matters: `A.timesMatrix(B)` applies B first `A.timesMatrix( B )` computes the matrix product `A * B`. When applied to a point via homogeneous coordinates, that means **B's transform is applied first, then A's** — so `Matrix3.translation( 100, 0 ).timesMatrix( Matrix3.rotation2( angle ) )` rotates a point about the origin and *then* translates it, not the other way around. This is the same convention used to compose [`ModelViewTransform2`](/api/phetcommon/model-view-transform)-style transforms and scenery `Node` transforms. ::: ## Related - [Vector2](/api/dot/vector2) — the point/vector type that `Matrix3` transforms. - [Bounds2](/api/dot/bounds2) — `bounds.transformed( matrix )` re-fits an axis-aligned box after an affine transform. - [ModelViewTransform2](/api/phetcommon/model-view-transform) — wraps a `Matrix3` (or matrix pair) to convert between model and view coordinates. ======================================================================== Page: Matrix4 URL: https://veillette.github.io/Almanach/api/dot/matrix4 Source: docs/api/dot/matrix4.md Category: API | Tags: dot, Matrix4, transform | Status: verified ======================================================================== # Matrix4 `Matrix4` (from `scenerystack/dot`) is a 4x4 matrix, the 3D counterpart to [`Matrix3`](/api/dot/matrix3): it represents affine transforms (rotation + scale + translation) in homogeneous coordinates over `(x, y, z, w)`. It's the type used by `Bounds3.transformed()`, and is also useful directly for 3D-flavored simulation content and WebGL-adjacent math (it has a `gluPerspective` factory for building projection matrices). ```ts import { Matrix4 } from 'scenerystack/dot'; import { Vector3 } from 'scenerystack/dot'; const matrix = Matrix4.translation( 0, 0, -10 ).timesMatrix( Matrix4.rotationY( Math.PI / 4 ) ); matrix.timesVector3( new Vector3( 1, 0, 0 ) ); // rotate then translate, as a homogeneous (x,y,z,1) point matrix.getTranslation(); // Vector3(0, 0, -10) ``` ## Constructing matrices The bare constructor `new Matrix4()` produces the identity matrix; passing 16 entries (row-major) builds an arbitrary matrix directly. As with `Matrix3`, static factories are the common path and let dot tag the result's `type` (`IDENTITY`, `TRANSLATION_3D`, `SCALING`, `AFFINE`, `OTHER`, all under `Matrix4.Types`) for fast-path optimizations: | Static factory | Produces | | --- | --- | | `Matrix4.identity()` | The identity matrix | | `Matrix4.translation( x, y, z )` / `Matrix4.translationFromVector( vector )` | Pure translation | | `Matrix4.scaling( x, y?, z? )` | Pure scale (`y`/`z` default to `x` for uniform scale) | | `Matrix4.rotationAxisAngle( axis, angle )` | Rotation about an arbitrary normalized axis | | `Matrix4.rotationX/Y/Z( angle )` | Axis-aligned rotations, radians | | `Matrix4.gluPerspective( fovYRadians, aspect, zNear, zFar )` | A WebGL-style perspective projection matrix | | `Matrix4.rowMajor( v00, ..., v33, type? )` / `columnMajor( ... )` | All sixteen entries directly | ## Reading a matrix | Accessor | Meaning | | --- | --- | | `m00()` ... `m33()` | Individual entries, row/column indexed | | `translation` | `(m03, m13, m23)` as a [`Vector3`](/api/dot/vector3) | | `scaleVector` | Per-axis scale magnitude, as a `Vector3` | | `determinant` | The determinant | | `isIdentity()` / `isFinite()` | Structural queries | | `cssTransform` | A `matrix3d(...)` CSS transform string | ## Applying and combining transforms | Method | Effect | | --- | --- | | `timesMatrix( matrix )` | Returns `this * matrix` as a new `Matrix4` | | `timesVector4( v )` | Full homogeneous multiplication of a [`Vector4`](/api/dot/vector4) | | `timesVector3( v )` | Treats `v` as `(x, y, z, 1)` — a point — and returns a `Vector3` | | `timesRelativeVector3( v )` | Treats `v` as `(x, y, z, 0)` — a direction, ignoring translation | | `timesTransposeVector4( v )` / `timesTransposeVector3( v )` | Multiplication by this matrix's transpose instead | | `inverted()` | Returns the inverse as a new `Matrix4` (throws if the determinant is 0) | | `transposed()` | Returns the transpose | | `plus( matrix )` / `minus( matrix )` | Entry-wise addition/subtraction | | `equals( matrix )` / `equalsEpsilon( matrix, epsilon )` | Comparison | | `makeImmutable()` | Marks the instance so further mutation throws (used for `Matrix4.IDENTITY`) | All of the above are immutable-returning; `Matrix4` also supports in-place mutation via `set( matrix )`, `rowMajor(...)`, and `columnMajor(...)`, which funnel through the same entry-setting path. ::: tip `timesVector3` assumes `w = 1`, not `w = 0` `matrix.timesVector3( v )` implicitly promotes `v` to `Vector4( v.x, v.y, v.z, 1 )` before multiplying — correct for transforming a *point*, but wrong for a *direction* (like a surface normal or a velocity), where translation shouldn't apply. Use `timesRelativeVector3( v )` for directions, the same way `Transform3`/`ModelViewTransform2` distinguish "position" methods from "delta" methods in 2D. ::: ## Related - [Matrix3](/api/dot/matrix3) — the 2D counterpart, used far more often in typical (2D) SceneryStack sims. - [Vector3](/api/dot/vector3) — the point/vector type most `Matrix4` methods operate on. - [Vector4](/api/dot/vector4) — the native homogeneous-coordinate operand for `timesVector4`. - [Bounds3](/api/dot/bounds3) — `bounds.transformed( matrix )` re-fits a 3D bounding box after an affine transform. ======================================================================== Page: Permutation and Combination URL: https://veillette.github.io/Almanach/api/dot/permutation-and-combination Source: docs/api/dot/permutation-and-combination.md Category: API | Tags: dot, Permutation, Combination, combinatorics, math | Status: complete ======================================================================== # Permutation and Combination `Permutation` and `Combination` (both from `scenerystack/dot`) are small, immutable combinatorics helper classes. A `Permutation` describes one particular reordering of a list's indices; a `Combination` describes one particular yes/no inclusion of each index (a subset). Both are mostly useful for enumerating every possible reordering/subset of a small array — for example, generating every possible arrangement of a handful of draggable objects for a "try all combinations" puzzle checker, or exhaustively testing every way a small set of items can be grouped. ```ts import { Permutation, Combination } from 'scenerystack/dot'; // Every reordering of 3 items: const permutations = Permutation.permutations( 3 ); // 6 Permutation instances // Apply one directly to an array: Permutation.identity( 3 ).apply( [ 'a', 'b', 'c' ] ); // [ 'a', 'b', 'c' ] // Every subset of a 3-element array: Combination.combinationsOf( [ 'a', 'b', 'c' ] ); // [ [], ['c'], ['b'], ['b','c'], ['a'], ['a','c'], ['a','b'], ['a','b','c'] ] ``` ## `Permutation` A `Permutation` wraps an `indices: number[]` array such that applying it to a list produces `newList[i] = oldList[permutation.indices[i]]`. | Member | Effect | | --- | --- | | `new Permutation( indices )` | Wraps an explicit indices array | | `size()` | Number of elements this permutation rearranges | | `apply( arrayOrInt )` | Applied to an array, returns a new reordered array; applied to a single index, returns `indices[index]` | | `inverted()` | The permutation that undoes this one | | `withIndicesPermuted( indices )` | All `Permutation`s obtained by additionally permuting just the given subset of index positions | | `equals( other )` | Structural equality on `indices` | | `Permutation.identity( size )` | The no-op permutation of a given size | | `Permutation.permutations( size )` | Every `Permutation` of a given size (`size!` of them) | | `Permutation.permutationsOf( array )` | Every reordering of a specific array, as plain arrays (convenience over `permutations` + `apply`) | | `Permutation.forEachPermutation( array, callback )` | Calls `callback` once per permutation without allocating all of them up front | ## `Combination` A `Combination` wraps an `inclusions: boolean[]` array, one entry per index, marking whether that index is included in the subset. | Member | Effect | | --- | --- | | `new Combination( inclusions )` | Wraps an explicit inclusions array | | `size()` | Number of elements this combination is defined over | | `includes( index )` | Whether a given index is included | | `apply( array )` | Filters `array` down to just the included elements | | `inverted()` | The complementary combination (every included index becomes excluded, and vice versa) | | `getIncludedIndices()` | The included indices as a plain `number[]` | | `equals( other )` | Structural equality on `inclusions` | | `Combination.empty( size )` / `Combination.full( size )` | The all-excluded / all-included combination of a given size | | `Combination.combinations( size )` | Every `Combination` of a given size (`2^size` of them) | | `Combination.combinationsOf( array )` | Every subset of a specific array, as plain arrays | | `Combination.forEachCombination( array, callback )` | Calls `callback` once per subset without allocating all of them up front | ::: warning Both scale factorially/exponentially — don't reach for these on large arrays `Permutation.permutations( n )` produces `n!` instances and `Combination.combinations( n )` produces `2^n` — both classes are meant for exhaustively checking small, fixed-size sets (a handful of draggable pieces, a small puzzle's pieces), not for general-purpose array shuffling (use [`dotRandom.shuffle()`](/api/dot/dot-random) for that) or for enumerating anything with more than a dozen or so elements. ::: ======================================================================== Page: Quaternion and Plane3 URL: https://veillette.github.io/Almanach/api/dot/quaternion-and-plane3 Source: docs/api/dot/quaternion-and-plane3.md Category: API | Tags: dot, Quaternion, Plane3, Vector3, Matrix3, rotation, math | Status: complete ======================================================================== # Quaternion and Plane3 `Quaternion` and `Plane3` (both from `scenerystack/dot`) are two independent 3D-geometry primitives commonly used together in 3D-flavored code (such as mobius): `Quaternion` represents a rotation without the gimbal-lock and interpolation problems of Euler angles, while `Plane3` represents an infinite plane for ray/plane intersection tests. ## Quaternion A `Quaternion` holds four components `{x, y, z, w}` representing a rotation — informally, `{x, y, z}` as a (scaled) rotation axis and `w` encoding the rotation angle. Unlike a rotation matrix, quaternions compose cheaply and interpolate smoothly (`slerp`), which is why they're the standard representation for smoothly animating between two orientations. ```ts import { Quaternion, Vector3 } from 'scenerystack/dot'; const q = Quaternion.fromEulerAngles( Math.PI / 4, 0, 0 ); // 45-degree yaw const rotated: Vector3 = q.timesVector3( new Vector3( 1, 0, 0 ) ); const matrix = q.toRotationMatrix(); // convert to a Matrix3 for use with e.g. a transform // Smoothly blend between two orientations, t in [0, 1]: const halfway = Quaternion.slerp( q, Quaternion.fromEulerAngles( Math.PI / 2, 0, 0 ), 0.5 ); ``` | Member | Effect | | --- | --- | | `new Quaternion( x, y, z, w )` | Constructs from raw components (default `w = 1`, i.e. identity, if omitted) | | `plus( quat )` / `timesScalar( s )` | Component-wise addition / scaling, returning a new `Quaternion` | | `timesQuaternion( quat )` | Hamilton product — composes two rotations (rotate by `this`, then by `quat`) | | `timesVector3( v )` | Rotates a [`Vector3`](/api/dot/vector3) by this quaternion, returning a new `Vector3` | | `magnitude` / `magnitudeSquared` | The quaternion's norm | | `normalized()` | Rescales to magnitude 1 (a valid rotation must be a unit quaternion) | | `negated()` | The quaternion with every component negated | | `toRotationMatrix()` | Converts to an equivalent `Matrix3` rotation matrix | | `Quaternion.fromEulerAngles( yaw, roll, pitch )` | Builds a quaternion from Euler angles | | `Quaternion.fromRotationMatrix( matrix )` | Converts a `Matrix3` rotation matrix back to a quaternion | | `Quaternion.getRotationQuaternion( a, b )` | A quaternion rotating unit vector `a` onto unit vector `b` | | `Quaternion.slerp( a, b, t )` | Spherical linear interpolation between two quaternions, `t` in `[0, 1]` | `Quaternion` is also `Poolable` (via phet-core's `Poolable.mixInto`), so performance-sensitive code can use `Quaternion.pool.fetch()`/`.freeToPool()` instead of `new Quaternion(...)` to avoid allocation churn, the same pattern `Vector2`'s pooled `v2()` shorthand follows. ## Plane3 A `Plane3` is an infinite plane in 3D, represented by a unit `normal` vector and a signed `distance` from the origin (so that `normal.timesScalar( distance )` is a point on the plane). It's the natural counterpart to [`Ray3`](/api/dot/ray3) for ray/plane intersection. ```ts import { Plane3, Ray3, Vector3 } from 'scenerystack/dot'; const groundPlane = Plane3.XY; // the built-in x-y plane through the origin const ray = new Ray3( new Vector3( 0, 0, 5 ), new Vector3( 0, 0, -1 ) ); const hitPoint = groundPlane.intersectWithRay( ray ); // Vector3(0, 0, 0) ``` | Member | Effect | | --- | --- | | `new Plane3( normal, distance )` | `normal` must be a unit `Vector3`; `distance` is the signed distance from the origin | | `intersectWithRay( ray )` | The `Vector3` where a [`Ray3`](/api/dot/ray3) crosses this plane | | `getIntersection( plane )` | The `Ray3` where two planes intersect (`null` if parallel) | | `Plane3.fromTriangle( a, b, c )` | Builds the plane through three points, normal via `(c-a) x (b-a)` (`null` if the points are collinear) | | `Plane3.XY` / `Plane3.XZ` / `Plane3.YZ` | The three axis-aligned planes through the origin | ::: warning Both classes assert unit-length inputs, but only in development builds `Plane3`'s constructor asserts `normal` is a unit vector, exactly like `Ray3`'s `direction` and `Ray2`'s `direction` — but this check only runs with assertions enabled. Passing a non-normalized normal (or building a `Quaternion` you forgot to `.normalized()` before treating as a rotation) will silently produce wrong geometry in a production build instead of throwing. Normalize explicitly at the boundary where you construct these objects from arbitrary input. ::: ======================================================================== Page: Random URL: https://veillette.github.io/Almanach/api/dot/random Source: docs/api/dot/random.md Category: API | Tags: dot, Random, dotRandom, math | Status: verified ======================================================================== # Random `Random` (from `scenerystack/dot`) is a seedable pseudo-random number generator (built on [seedrandom.js](https://github.com/davidbau/seedrandom)), with helpers for booleans, integers, doubles in a range, gaussian samples, array sampling/shuffling, and weighted sampling. `scenerystack/dot` also exports `dotRandom`, a shared singleton instance of `Random` — sim code should reach for `dotRandom` rather than constructing a `new Random()` or calling `Math.random()` directly. ```ts import { dotRandom } from 'scenerystack/dot'; dotRandom.nextDouble(); // [0, 1) dotRandom.nextIntBetween( 1, 6 ); // an integer in [1, 6], inclusive dotRandom.nextBoolean(); // true or false dotRandom.sample( [ 'red', 'green', 'blue' ] ); // one randomly-chosen element ``` ## Constructing your own instance ```ts new Random( { seed?: number | null } ) ``` The single options object is optional; if `seed` is omitted or `null`, a random seed is generated via `Math.random()` once at construction. Passing a fixed numeric `seed` makes the entire sequence of subsequent calls deterministic and reproducible — useful for tests, or for anything that needs the same "random" sequence across runs. ## Methods | Method | Effect | | --- | --- | | `nextDouble()` | Next value in `[0, 1)`, uniformly distributed | | `nextBoolean()` | `nextDouble() >= 0.5` | | `nextInt( n )` | Next integer in `[0, n)` | | `nextIntBetween( min, max )` | Next integer in `[min, max]` inclusive (both must be integers) | | `nextDoubleBetween( min, max )` | Next double in `[min, max)` | | `nextDoubleInRange( range )` | Next double within a [`Range`](/api/dot/range)'s `[min, max)` (or exactly `min` if `min === max`) | | `nextGaussian()` | Next gaussian-distributed sample, mean 0, standard deviation 1 | | `nextPointInBounds( bounds )` | A random [`Vector2`](/api/dot/vector2) within a [`Bounds2`](/api/dot/bounds2), `[minX,maxX) x [minY,maxY)` | | `sample( array )` | One randomly-chosen element from a non-empty array | | `shuffle( array )` | A new array with the same elements in Fisher-Yates-shuffled order | | `sampleProbabilities( weights )` | A randomly-chosen index, weighted by the (not necessarily normalized) `weights` array | | `getSeed()` | The seed this instance was constructed (or re-seeded) with | | `setSeed( seed )` | Re-seeds the generator; `null` picks a new random seed via `Math.random()` | | `numberOfCalls` | Public field tracking how many times `nextDouble()` has been called — read-only by convention, not by enforcement | ::: tip Use `dotRandom`, not `Math.random()`, in simulation code SceneryStack simulations use the shared `dotRandom` singleton (not `new Random()` and not raw `Math.random()`) so that every "random" call in the sim funnels through one seed. This is what makes PhET-iO's seeded/reproducible playback and record-and-replay features work — if a sim's randomness comes from scattered `Math.random()` calls, a replayed session can diverge from the original. Reach for `new Random()` directly only when you deliberately want an isolated, independently-seeded generator (e.g. a self-contained test). ::: ## Related - [Vector2](/api/dot/vector2) — `nextPointInBounds()` returns a `Vector2`. - [Bounds2](/api/dot/bounds2) — the region `nextPointInBounds()` samples from. - [Range](/api/dot/range) — `nextDoubleInRange()` takes a `Range` directly instead of separate `min`/`max` arguments. ======================================================================== Page: Range URL: https://veillette.github.io/Almanach/api/dot/range Source: docs/api/dot/range.md Category: API | Tags: dot, Range | Status: verified ======================================================================== # Range `Range` (from `scenerystack/dot`) is a simple `[min, max]` interval. It's most commonly seen as the `range` option passed to a `NumberProperty`, which sliders and number spinners use for their bounds, but it's a general-purpose interval type used anywhere a valid numeric interval needs a name. ```ts import { Range } from 'scenerystack/dot'; const temperatureRange = new Range( 0, 100 ); temperatureRange.contains( 37 ); // true temperatureRange.constrainValue( 150 ); // 100 — clamps into the range temperatureRange.getCenter(); // 50 ``` ## Constructing a range The constructor takes `min` and `max` directly: `new Range( min, max )`. Both `min` and `max` are also settable properties (`range.min = 10`), with an internal assertion that `min <= max` is maintained at all times. | Static | Meaning | | --- | --- | | `Range.EVERYTHING` | `(-Infinity, Infinity)` | | `Range.NOTHING` | `(Infinity, -Infinity)` — an inverted, "empty" range | ## Methods | Method | Effect | | --- | --- | | `getLength()` | `max - min` | | `getCenter()` | `(min + max) / 2` | | `contains( value )` | Whether `value` is within `[min, max]`, inclusive | | `containsRange( range )` | Whether this range fully contains another range | | `intersects( range )` / `intersectsExclusive( range )` | Whether two ranges overlap | | `constrainValue( value )` | Clamp a value into `[min, max]` | | `getNormalizedValue( value )` / `expandNormalizedValue( t )` | Map a value to/from `[0, 1]` within the range | | `union( range )` / `includeRange( range )` | Smallest range containing both — immutable / mutable pair | | `intersection( range )` / `constrainRange( range )` | Largest range contained by both — immutable / mutable pair | | `shifted( n )` | New range with both endpoints offset by `n` | | `times( value )` / `multiply( value )` | Scale both endpoints — immutable / mutable pair | | `addValue( n )` | Mutable: expand min/max, if needed, to include `n` | | `withValue( n )` | Immutable counterpart of `addValue` | | `copy()` | Clone | | `equals( range )` / `equalsEpsilon( range, epsilon )` | Comparison | ## Common use: NumberProperty validation ```ts import { NumberProperty } from 'scenerystack/axon'; import { Range } from 'scenerystack/dot'; const temperatureProperty = new NumberProperty( 20, { range: new Range( 0, 100 ) } ); ``` `NumberProperty` uses its `range` option both to validate assignments in assertion-enabled builds and to give UI components (like sliders) their bounds automatically — see [NumberProperty](/api/axon/number-property). ::: tip `defaultValue` belongs to `RangeWithValue`, not `Range` Reaching for `range.defaultValue` on a plain `Range` throws immediately — that field only exists on the related `RangeWithValue` subclass (also exported from `scenerystack/dot`), which pairs a range with a starting value for controls that need one (e.g. a reset-able slider). If you need a default alongside min/max, reach for `RangeWithValue` instead of trying to bolt one onto `Range`. ::: ## Related - [NumberProperty](/api/axon/number-property) — the most common consumer of `Range`, via its `range` option. - [Bounds2](/api/dot/bounds2) — `bounds.xRange` / `bounds.yRange` expose each axis of a bounds as a `Range`. - [Utils](/api/dot/dot-utils) — `clamp()` is the free-function equivalent of `range.constrainValue()` when you don't have a `Range` object handy. ======================================================================== Page: RangeWithValue URL: https://veillette.github.io/Almanach/api/dot/range-with-value Source: docs/api/dot/range-with-value.md Category: API | Tags: dot, RangeWithValue, Range | Status: verified ======================================================================== # RangeWithValue `RangeWithValue` (from `scenerystack/dot`) extends [`Range`](/api/dot/range), adding a required `defaultValue` that must fall within `[min, max]`. It's the type [`Range`'s own docs](/api/dot/range) point to when you need a range *plus* a starting/reset value in one object — most useful for a control (like a slider) that needs to know both its bounds and where it should reset to. ```ts import { RangeWithValue } from 'scenerystack/dot'; const volumeRange = new RangeWithValue( 0, 11, 5 ); // min 0, max 11, defaults to 5 volumeRange.defaultValue; // 5 volumeRange.min; // 0 volumeRange.max; // 11 volumeRange.contains( 7 ); // true — inherited from Range ``` ## Constructing The constructor takes `min`, `max`, and `defaultValue` directly: `new RangeWithValue( min, max, defaultValue )`. An assertion throws immediately if `defaultValue` falls outside `[min, max]`. ## What's added over `Range` `RangeWithValue` is a `Range` — it inherits every method described on the [`Range`](/api/dot/range) page (`contains`, `constrainValue`, `union`, `getCenter`, etc.) unchanged. It adds: | Member | Meaning | | --- | --- | | `defaultValue` (getter) | The default value passed to the constructor; there is no public setter — it's fixed for the life of the instance | | `equals( other )` (overridden) | Also compares `defaultValue`, and requires `other` to be a `RangeWithValue` (not a plain `Range`) | | `toString()` (overridden) | Includes `defaultValue` in the debug string | `setMin`, `setMax`, and `setMinMax` are also overridden to add validation: each throws (via assertion) if the change would push `defaultValue` outside the new bounds. This is the one behavioral difference from `Range` beyond the extra field — a plain `Range` lets you move `min`/`max` freely, but a `RangeWithValue` won't let you strand its own default value outside the interval. ::: tip `defaultValue` has no setter — construct a new instance to change it Unlike `min`/`max` (settable, with the validation above), `defaultValue` is fixed at construction. If a control's reset value needs to change, create a new `RangeWithValue` rather than trying to mutate the existing one's default. ::: ## Related - [Range](/api/dot/range) — the base `[min, max]` interval type; `RangeWithValue` is a strict superset. - [NumberProperty](/api/axon/number-property) — takes a `Range` via its `range` option; a `RangeWithValue` satisfies that option too (since it *is* a `Range`) and additionally lets code that already has the range object read a sensible default/reset value off of it, instead of tracking that value separately. ======================================================================== Page: Ray2 URL: https://veillette.github.io/Almanach/api/dot/ray2 Source: docs/api/dot/ray2.md Category: API | Tags: dot, Ray2, math, hit-testing | Status: verified ======================================================================== # Ray2 `Ray2` (from `scenerystack/dot`) is a minimal 2D ray: an origin `position` and a unit-length `direction`, both [`Vector2`](/api/dot/vector2) instances. It's the type [`Transform3`](/api/dot/transform3)'s `transformRay2`/`inverseRay2` carry through a transform, and the natural representation for hit-testing/intersection queries (e.g. "does this pointer ray hit that shape"). ```ts import { Ray2 } from 'scenerystack/dot'; import { Vector2 } from 'scenerystack/dot'; const ray = new Ray2( new Vector2( 0, 0 ), new Vector2( 1, 0 ) ); // origin at (0,0), pointing along +x ray.pointAtDistance( 5 ); // Vector2(5, 0) — the point 5 units along the ray ray.shifted( 5 ); // a new Ray2 with the same direction, origin moved to (5, 0) ``` ## Constructing ```ts new Ray2( position: Vector2, direction: Vector2 ) ``` Both fields are plain public properties, not getter/setter pairs. `direction` **must** be a unit vector (magnitude 1) — an assertion in development builds checks `Math.abs( direction.magnitude - 1 ) < 0.01` and throws if it's off. Passing a non-normalized vector is a common mistake; call `.normalized()` on it first if you're not sure it's already unit length. ## Methods | Method | Effect | | --- | --- | | `pointAtDistance( distance )` | Returns `position.plus( direction.timesScalar( distance ) )` as a new `Vector2` — the point reached by walking `distance` along the ray | | `shifted( distance )` | Returns a new `Ray2` with the same `direction`, whose origin is `pointAtDistance( distance )` | | `toString()` | Debug string showing position and direction | That's the full public surface on `Ray2` itself — it's deliberately a thin data holder. Most of the interesting ray behavior (transforming a ray, or intersecting it against a shape) lives on the *other* object: [`Transform3.transformRay2`](/api/dot/transform3)/`inverseRay2` carry a `Ray2` through a matrix transform, and kite `Shape`s expose their own intersection methods that accept a `Ray2`. ::: warning `direction` is not automatically kept normalized Because `position` and `direction` are plain mutable fields, nothing stops you from reassigning `ray.direction` to a non-unit vector after construction — only the constructor asserts unit length, and only in assertion-enabled (development) builds. If you mutate a ray's direction after creating it, normalize it yourself first, or construct a fresh `Ray2` instead. ::: ## Related - [Vector2](/api/dot/vector2) — both `position` and `direction` are `Vector2` instances. - [Transform3](/api/dot/transform3) — `transformRay2`/`inverseRay2` carry a `Ray2` through a `Matrix3`-based transform. ======================================================================== Page: Ray3 URL: https://veillette.github.io/Almanach/api/dot/ray3 Source: docs/api/dot/ray3.md Category: API | Tags: dot, Ray3, Vector3, Plane3, math, hit-testing | Status: complete ======================================================================== # Ray3 `Ray3` (from `scenerystack/dot`) is the 3D counterpart to [`Ray2`](/api/dot/ray2): an origin `position` and a unit-length `direction`, both [`Vector3`](/api/dot/vector3) instances. It's the representation used for picking/intersection queries in 3D-flavored code — for example, mobius's 3D scenes — such as "does this mouse ray hit that sphere or plane." ```ts import { Ray3, Vector3 } from 'scenerystack/dot'; const ray = new Ray3( new Vector3( 0, 0, 5 ), new Vector3( 0, 0, -1 ) ); // pointing toward the origin ray.pointAtDistance( 5 ); // Vector3(0, 0, 0) — 5 units along the ray from its origin const shifted = ray.shifted( 2 ); // a new Ray3 with the same direction, origin moved 2 units along it ``` ## Constructing ```ts new Ray3( position: Vector3, direction: Vector3 ) ``` Both fields are plain public properties. `direction` **must** be a unit vector — an assertion in development builds checks `Math.abs( direction.magnitude - 1 ) < 0.01` and throws if it's off, exactly as with `Ray2`. ## Methods | Method | Effect | | --- | --- | | `pointAtDistance( distance )` | `position.plus( direction.timesScalar( distance ) )` — the point reached by walking `distance` along the ray | | `shifted( distance )` | A new `Ray3` with the same `direction`, whose origin is `pointAtDistance( distance )` | | `distanceToPlane( plane )` | The signed distance along the ray at which it crosses a given [`Plane3`](/api/dot/quaternion-and-plane3) | | `toString()` | Debug string showing position and direction | ## Intersecting a ray with a plane `Ray3` itself only computes the *distance* to a plane; combine it with [`Plane3.intersectWithRay()`](/api/dot/quaternion-and-plane3) to get the actual intersection point: ```ts import { Ray3, Plane3, Vector3 } from 'scenerystack/dot'; const ray = new Ray3( new Vector3( 0, 0, 5 ), new Vector3( 0, 0, -1 ) ); const hitPoint = Plane3.XY.intersectWithRay( ray ); // Vector3(0, 0, 0) ``` ::: tip There's no `Ray3`-vs-sphere or `Ray3`-vs-triangle method on `Ray3` itself `Ray3` is a minimal data holder, like `Ray2` — it only knows how to walk along itself and measure distance to a plane. Sphere-ray intersection lives as the standalone `sphereRayIntersection` function (also exported from `scenerystack/dot`), and triangle/mesh picking is handled by the consuming 3D code (e.g. mobius), not by `Ray3` itself. ::: ======================================================================== Page: Transform3 URL: https://veillette.github.io/Almanach/api/dot/transform3 Source: docs/api/dot/transform3.md Category: API | Tags: dot, Transform3, transform, matrix | Status: verified ======================================================================== # Transform3 `Transform3` (from `scenerystack/dot`) wraps a single [`Matrix3`](/api/dot/matrix3) and adds three things a bare matrix doesn't have: **lazily-cached** inverse/transpose/inverse-transpose matrices (recomputed only when the primary matrix changes), a `changeEmitter` that fires whenever the matrix is replaced, and a full family of typed convenience methods (`transformPosition2`, `transformBounds2`, `transformShape`, `transformRay2`, and their `inverse*` counterparts) instead of raw matrix multiplication. ::: tip Despite the name, this is a 2D (not 3D) transform The "3" in `Transform3` refers to the 3x3 [`Matrix3`](/api/dot/matrix3) it wraps (for 2D homogeneous coordinates), not to three spatial dimensions — it operates on [`Vector2`](/api/dot/vector2), [`Bounds2`](/api/dot/bounds2), and 2D `Shape`s. If you're looking for the sim-facing model/view coordinate converter built on similar ideas, see [`ModelViewTransform2`](/api/phetcommon/model-view-transform) — it's a different, higher-level class (not a subclass of `Transform3`) purpose-built for physical-unit-to-pixel conversion, whereas `Transform3` is the lower-level, general-purpose "matrix plus caching plus change notification" utility that generic transform math (including parts of scenery's own `Node` transform tracking) is built on. ::: ```ts import { Transform3 } from 'scenerystack/dot'; import { Matrix3 } from 'scenerystack/dot'; import { Vector2 } from 'scenerystack/dot'; const transform = new Transform3( Matrix3.rotation2( Math.PI / 2 ) ); transform.transformPosition2( new Vector2( 1, 0 ) ); // Vector2(0, 1) (approx) transform.inversePosition2( new Vector2( 0, 1 ) ); // Vector2(1, 0) (approx) — undoes the forward transform transform.changeEmitter.addListener( () => console.log( 'matrix changed' ) ); transform.append( Matrix3.scaling( 2 ) ); // notifies changeEmitter ``` ## Constructing and mutating The constructor optionally takes an initial `Matrix3` (defaults to the identity): `new Transform3( matrix? )`. | Method | Effect | | --- | --- | | `setMatrix( matrix )` | Replaces the primary matrix (copies values in; doesn't retain the passed-in instance), invalidates caches, notifies `changeEmitter` | | `prepend( matrix )` | `this.matrix = matrix * this.matrix` — applies `matrix` *after* whatever this transform already does | | `append( matrix )` | `this.matrix = this.matrix * matrix` — applies `matrix` *before* whatever this transform already does | | `prependTranslation( x, y )` | Optimized prepend of a pure translation | | `prependTransform( transform )` / `appendTransform( transform )` | Same as `prepend`/`append`, but taking another `Transform3`'s matrix | | `copy()` | A new `Transform3` with the same matrix (and cached state) | ## Reading the transform | Getter | Meaning | | --- | --- | | `getMatrix()` | The primary `Matrix3` | | `getInverse()` | The inverse matrix, computed on first access after a change and cached thereafter | | `getMatrixTransposed()` / `getInverseTransposed()` | Cached transpose / inverse-transpose | | `isIdentity()` | Whether the primary matrix is (very likely) the identity | | `isFinite()` | Whether all matrix entries are finite | ## Forward and inverse transform methods Every `transform*` method has an `inverse*` counterpart that applies the inverse matrix instead — conceptually, `transform.inverseThing( transform.transformThing( x ) )` should return something equal to `x`. | Forward | Inverse | Applies to | | --- | --- | --- | | `transformPosition2( v )` | `inversePosition2( v )` | A [`Vector2`](/api/dot/vector2) point (translation applied) | | `transformDelta2( v )` | `inverseDelta2( v )` | A `Vector2` displacement (translation **not** applied) | | `transformNormal2( v )` | `inverseNormal2( v )` | A `Vector2` surface normal (uses the transposed inverse) | | `transformX( x )` / `transformY( y )` | `inverseX( x )` / `inverseY( y )` | A single coordinate (throws in an assertion-enabled build if the matrix has rotation/shear, since the result would depend on the other axis) | | `transformDeltaX( x )` / `transformDeltaY( y )` | `inverseDeltaX( x )` / `inverseDeltaY( y )` | A single-axis delta | | `transformBounds2( bounds )` | `inverseBounds2( bounds )` | A [`Bounds2`](/api/dot/bounds2), re-fit to stay axis-aligned | | `transformShape( shape )` | `inverseShape( shape )` | A kite `Shape` | | `transformRay2( ray )` | `inverseRay2( ray )` | A [`Ray2`](/api/dot/ray2) | | `applyToCanvasContext( context )` | — | Calls `context.setTransform(...)` with this transform's matrix | ## Related - [Matrix3](/api/dot/matrix3) — the underlying matrix type; `Transform3` is a caching, observable wrapper around it. - [Vector2](/api/dot/vector2) — the point/delta type most `Transform3` methods operate on. - [Bounds2](/api/dot/bounds2) — `transformBounds2`/`inverseBounds2` re-fit a bounds through the transform. - [Ray2](/api/dot/ray2) — `transformRay2`/`inverseRay2` carry a ray through the transform. - [ModelViewTransform2](/api/phetcommon/model-view-transform) — the higher-level, sim-facing model/view coordinate converter with related but distinct semantics. ======================================================================== Page: Utils URL: https://veillette.github.io/Almanach/api/dot/dot-utils Source: docs/api/dot/dot-utils.md Category: API | Tags: dot, Utils, math | Status: verified ======================================================================== # Utils dot doesn't bundle its numeric helpers into a single `Utils` namespace object — instead, `scenerystack/dot` re-exports each helper as its own named function. They're grouped here as "the dot Utils" because that's how they're typically reached for: small, dependency-free numeric functions used throughout simulation code for clamping, mapping, and rounding numbers. ```ts import { clamp, linear, roundSymmetric, toDegrees, toRadians } from 'scenerystack/dot'; clamp( 150, 0, 100 ); // 100 linear( 0, 1, 0, 100, 0.37 ); // 37 — maps 0.37 in [0,1] to [0,100] roundSymmetric( -2.5 ); // -3 (not -2, unlike Math.round) toDegrees( Math.PI ); // 180 toRadians( 180 ); // Math.PI ``` ## Core helpers | Function | Signature | Effect | | --- | --- | --- | | `clamp` | `clamp( value, min, max ): number` | Constrains `value` to `[min, max]` | | `linear` | `linear( a1, a2, b1, b2, a3 ): number` | Linear map: given `f(a1) = b1` and `f(a2) = b2`, returns `f(a3)` | | `roundSymmetric` | `roundSymmetric( value ): number` | Rounds half-away-from-zero (symmetric for `+`/`-`, unlike `Math.round`) | | `roundToInterval` | `roundToInterval( value, interval ): number` | Rounds `value` to the nearest multiple of `interval` | | `toFixedNumber` | `toFixedNumber( value, decimalPlaces ): number` | Like `.toFixed()`, but returns a `number` and rounds symmetrically | | `toDegrees` / `toRadians` | `toDegrees( radians )`, `toRadians( degrees )` | Angle unit conversion | | `equalsEpsilon` | `equalsEpsilon( a, b, epsilon ): boolean` | Whether two numbers are within `epsilon` of each other | ## Other exported free functions `scenerystack/dot` also exports several more specialized numeric helpers directly (not methods on a class): `mod`, `gcd`, `lcm`, `cubeRoot`, `log10`, `sinh`, `cosh`, `factorial`, `numberOfDecimalPlaces`, `triangleArea`/`triangleAreaSigned`, `centroidOfPolygon`, `distToSegment`/`distToSegmentSquared`, `lineLineIntersection`, `lineSegmentIntersection`, `solveLinearRootsReal`/`solveQuadraticRootsReal`/`solveCubicRootsReal`, and `arePointsCollinear`. Import whichever you need directly from `scenerystack/dot` by name. ## Typical use: mapping a physical quantity onto a display range ```ts import { linear, clamp } from 'scenerystack/dot'; function temperatureToHue( temperatureCelsius: number ): number { // Map [0, 100] C onto a hue range [240, 0] (blue to red), clamping outliers first const clamped = clamp( temperatureCelsius, 0, 100 ); return linear( 0, 100, 240, 0, clamped ); } ``` ::: tip `roundSymmetric`, not `Math.round`, is the sim convention `Math.round` uses "round half up," so `Math.round( -2.5 ) === -2`. SceneryStack simulations consistently use `roundSymmetric` instead, which rounds `-2.5` to `-3` — symmetric behavior for positive and negative numbers. Reach for `roundSymmetric` (and `roundToInterval` for rounding to steps other than 1) instead of the built-in `Math.round` when displaying rounded values to users. ::: ## Related - [Range](/api/dot/range) — `range.constrainValue()` is the `Range`-aware equivalent of `clamp()`. - [Vector2](/api/dot/vector2) — `Vector2.roundedSymmetric()` / `roundSymmetric()` apply `roundSymmetric` component-wise. ======================================================================== Page: Vector2 URL: https://veillette.github.io/Almanach/api/dot/vector2 Source: docs/api/dot/vector2.md Category: API | Tags: dot, Vector2, math | Status: verified ======================================================================== # Vector2 `Vector2` (from `scenerystack/dot`) is the 2-dimensional `(x, y)` vector/point class used everywhere in SceneryStack — positions, velocities, drag deltas, and node translations all pass through it. It doubles as both a "point" and a "displacement" depending on context; there is no separate point type. ```ts import { Vector2 } from 'scenerystack/dot'; const position = new Vector2( 3, 4 ); position.magnitude; // 5, i.e. sqrt(3^2 + 4^2) position.angle; // Math.atan2( 4, 3 ), in radians const moved = position.plus( new Vector2( 1, 0 ) ); // (4, 4) — new vector, position unchanged position.add( new Vector2( 1, 0 ) ); // (4, 4) — mutates position in place ``` ## Constructing vectors The constructor takes `x` and `y` directly: `new Vector2( x, y )`. There's also the pooled shorthand `v2( x, y )` (exported alongside `Vector2`) which reuses pooled instances for performance-sensitive code, and static constants: | Static | Value | | --- | --- | | `Vector2.ZERO` | `(0, 0)` | | `Vector2.X_UNIT` | `(1, 0)` | | `Vector2.Y_UNIT` | `(0, 1)` | | `Vector2.createPolar( magnitude, angle )` | Builds a vector from magnitude/angle instead of x/y | ## Immutable vs. mutable methods Every operation on `Vector2` comes in two forms: an **immutable** version that returns a new vector, and a **mutable** version that changes `this` and returns it for chaining. The naming convention is consistent throughout dot: the immutable form usually reads as a noun/adjective (`plus`, `minus`, `times`, `normalized`, `rotated`), and the mutable form reads as a verb (`add`, `subtract`, `multiply`, `normalize`, `rotate`). | Immutable (returns new `Vector2`) | Mutable (changes `this`, returns `this`) | Effect | | --- | --- | --- | | `plus( v )` / `plusXY( x, y )` / `plusScalar( s )` | `add( v )` / `addXY( x, y )` / `addScalar( s )` | Addition | | `minus( v )` / `minusXY( x, y )` / `minusScalar( s )` | `subtract( v )` / `subtractXY( x, y )` / `subtractScalar( s )` | Subtraction | | `times( s )` / `timesScalar( s )` | `multiply( s )` / `multiplyScalar( s )` | Scalar multiplication | | `componentTimes( v )` | `componentMultiply( v )` | Component-wise multiplication | | `dividedScalar( s )` | `divideScalar( s )` | Scalar division | | `negated()` | `negate()` | Multiply by -1 | | `normalized()` | `normalize()` | Rescale to magnitude 1 (throws on a zero vector) | | `withMagnitude( m )` | `setMagnitude( m )` | Rescale to magnitude `m` | | `rotated( angle )` | `rotate( angle )` | Rotate about the origin, radians | | `rotatedAboutXY( x, y, angle )` / `rotatedAboutPoint( p, angle )` | `rotateAboutXY( x, y, angle )` / `rotateAboutPoint( p, angle )` | Rotate about an arbitrary point | | `roundedSymmetric()` | `roundSymmetric()` | Round both components | | `copy()` | `set( v )` / `setXY( x, y )` / `setX( x )` / `setY( y )` | Copy / assign | Read-only queries (no mutable counterpart, since they don't change the vector): `magnitude`, `magnitudeSquared`, `angle`, `dot( v )` / `dotXY( x, y )`, `distance( point )` / `distanceXY( x, y )`, `distanceSquared( point )`, `crossScalar( v )` (the z-component of the 3D cross product), `angleBetween( v )`, `equals( v )` / `equalsEpsilon( v, epsilon )`, `isFinite()`, `perpendicular` (rotated -π/2), `blend( v, ratio )` / `average( v )`, and `toString()`. ## Common uses ```ts import { Vector2 } from 'scenerystack/dot'; // Position tracked in a model, updated via a drag listener const velocity = new Vector2( 2, -1 ); const dt = 0.016; // Immutable style: compute a new position without touching the old one const nextPosition = position.plus( velocity.timesScalar( dt ) ); // Mutable style: update a stored vector in place (common in performance-sensitive step() loops) position.add( velocity.timesScalar( dt ) ); ``` ::: warning Mutable methods change shared state Because plain object references are passed around freely, calling a mutable method (`add`, `normalize`, `rotate`, `set...`) on a `Vector2` that's stored in a `Property` or shared elsewhere will silently change it for every holder of that reference, **without notifying listeners** (a `Vector2Property` only fires when you call `.value = newVector`, not when you mutate the existing value in place). When in doubt, prefer the immutable form (`plus`, `normalized`, `rotated`, ...) for anything backing observable state, and reserve the mutable form for local scratch vectors. ::: ## Related - [Vector3](/api/dot/vector3) — the 3D counterpart, used for color math and 3D-flavored sims. - [Bounds2](/api/dot/bounds2) — axis-aligned rectangles built from `Vector2` corners/centers. - [Matrix3](/api/dot/matrix3) — transforms that map `Vector2` positions via `multiplyVector2`. - [ModelViewTransform2](/api/phetcommon/model-view-transform) — converts `Vector2` positions between model and view coordinates. ======================================================================== Page: Vector2Property URL: https://veillette.github.io/Almanach/api/dot/vector2-property Source: docs/api/dot/vector2-property.md Category: API | Tags: dot, Vector2Property, Vector2, Property, position | Status: complete ======================================================================== # Vector2Property `Vector2Property` (from `scenerystack/dot`) is a `Property` subclass — the standard type for a model's `positionProperty`, a drag target's location, or any other `Vector2`-valued piece of observable state. Beyond fixing `valueType: Vector2` and wiring up `Vector2.Vector2IO` for PhET-iO, it adds two validators of its own: it always rejects a vector with a `NaN` component, and it optionally confines every value to a [`Bounds2`](/api/dot/bounds2) region via the `validBounds` option. ```ts import { Vector2Property } from 'scenerystack/dot'; import { Vector2, Bounds2 } from 'scenerystack/dot'; const positionProperty = new Vector2Property( new Vector2( 0, 0 ), { validBounds: new Bounds2( -100, -100, 100, 100 ) } ); positionProperty.link( position => console.log( 'position:', position ) ); positionProperty.value = new Vector2( 50, 50 ); // fine, inside validBounds // positionProperty.value = new Vector2( 500, 0 ); // assertion failure: outside validBounds ``` Aside from these extra validators, `Vector2Property` behaves exactly like a plain `Property`: `.value`, `.link()`, `.reset()`, and every other member documented on [`Property`](/api/axon/property) work the same way. ## Constructor ```ts new Vector2Property( initialValue: Vector2, providedOptions?: Vector2PropertyOptions ) ``` `Vector2PropertyOptions` is `PropertyOptions` (see [Property's options](/api/axon/property#options)) minus `valueType`/`phetioValueType` (which `Vector2Property` fixes for you), plus: | Option | Effect | | --- | --- | | `validBounds` | If provided, every value set on the Property must satisfy `validBounds.containsPoint( value )`; enforced via an assertion-only validator, so it has no effect in production builds without assertions | ::: tip Mutating the stored `Vector2` in place bypasses validation and listeners Because `Vector2Property`'s validators (including `validBounds`) only run inside `set()`/`value=`, calling a mutating method directly on the currently-stored vector — `positionProperty.value.add( delta )` instead of `positionProperty.value = positionProperty.value.plus( delta )` — skips both the bounds check and listener notification entirely. Always reassign `.value` with a new `Vector2` (via `plus`, `plusXY`, etc.) rather than mutating the existing one in place; see the same caveat on [Vector2](/api/dot/vector2#immutable-vs-mutable-methods). ::: ======================================================================== Page: Vector3 URL: https://veillette.github.io/Almanach/api/dot/vector3 Source: docs/api/dot/vector3.md Category: API | Tags: dot, Vector3, math | Status: verified ======================================================================== # Vector3 `Vector3` (from `scenerystack/dot`) is the 3-dimensional `(x, y, z)` counterpart to [`Vector2`](/api/dot/vector2). It shares the same immutable/mutable method-naming convention, plus a genuine 3D cross product. It shows up less often than `Vector2` in 2D sims, but is used for color math (RGB triples) and any 3D-flavored simulation content. ```ts import { Vector3 } from 'scenerystack/dot'; const a = new Vector3( 1, 0, 0 ); const b = new Vector3( 0, 1, 0 ); a.cross( b ); // Vector3(0, 0, 1) — new vector, a and b unchanged a.dot( b ); // 0 a.magnitude; // 1 ``` ## Constructing vectors The constructor takes `x`, `y`, and `z` directly: `new Vector3( x, y, z )`. The pooled shorthand `v3( x, y, z )` is also exported for allocation-sensitive code, along with static constants: | Static | Value | | --- | --- | | `Vector3.ZERO` | `(0, 0, 0)` | | `Vector3.X_UNIT` | `(1, 0, 0)` | | `Vector3.Y_UNIT` | `(0, 1, 0)` | | `Vector3.Z_UNIT` | `(0, 0, 1)` | ## Immutable vs. mutable methods Just like `Vector2`, every operation comes in an immutable form (returns a new `Vector3`) and a mutable form (changes `this` and returns it). | Immutable (returns new `Vector3`) | Mutable (changes `this`, returns `this`) | Effect | | --- | --- | --- | | `plus( v )` / `plusXYZ( x, y, z )` / `plusScalar( s )` | `add( v )` / `addXYZ( x, y, z )` / `addScalar( s )` | Addition | | `minus( v )` / `minusXYZ( x, y, z )` / `minusScalar( s )` | `subtract( v )` / `subtractXYZ( x, y, z )` / `subtractScalar( s )` | Subtraction | | `times( s )` / `timesScalar( s )` | `multiply( s )` / `multiplyScalar( s )` | Scalar multiplication | | `componentTimes( v )` | `componentMultiply( v )` | Component-wise multiplication | | `dividedScalar( s )` | `divideScalar( s )` | Scalar division | | `negated()` | `negate()` | Multiply by -1 | | `normalized()` | `normalize()` | Rescale to magnitude 1 (throws on a zero vector) | | `withMagnitude( m )` | `setMagnitude( m )` | Rescale to magnitude `m` | | `cross( v )` | `setCross( v )` | 3D cross product | | `roundedSymmetric()` | `roundSymmetric()` | Round all three components | | `copy()` | `set( v )` / `setXYZ( x, y, z )` / `setX( x )` / `setY( y )` / `setZ( z )` | Copy / assign | Read-only queries: `magnitude`, `magnitudeSquared`, `dot( v )` / `dotXYZ( x, y, z )`, `distance( point )` / `distanceXYZ( x, y, z )`, `distanceSquared( point )`, `angleBetween( v )`, `equals( v )` / `equalsEpsilon( v, epsilon )`, `isFinite()`, `blend( v, ratio )` / `average( v )`, and `toString()`. A static `Vector3.slerp( start, end, ratio )` does spherical linear interpolation between two unit vectors. ::: tip No 2D cross product here Unlike `Vector2.crossScalar()` (which returns a single number — the z-component of the equivalent 3D cross product), `Vector3.cross()` returns a full `Vector3`, since a genuine cross product only makes sense in three (or seven) dimensions. ::: ## Related - [Vector2](/api/dot/vector2) — the 2D counterpart used for almost all on-screen positioning. - [Matrix3](/api/dot/matrix3) — `Matrix3.rotationAxisAngle` and `Matrix3.rotateAToB` take `Vector3` axes. ======================================================================== Page: Vector4 URL: https://veillette.github.io/Almanach/api/dot/vector4 Source: docs/api/dot/vector4.md Category: API | Tags: dot, Vector4, math | Status: verified ======================================================================== # Vector4 `Vector4` (from `scenerystack/dot`) is the 4-dimensional `(x, y, z, w)` counterpart to [`Vector2`](/api/dot/vector2) and [`Vector3`](/api/dot/vector3). It follows the exact same immutable/mutable naming convention as its lower-dimensional siblings, but has no cross product (a 4D cross product isn't a single well-defined operation the way it is in 3D). It's used wherever a fourth homogeneous or alpha component is needed — most notably as the operand type for [`Matrix4`](/api/dot/matrix4)'s `timesVector4`. ```ts import { Vector4 } from 'scenerystack/dot'; const homogeneous = new Vector4( 1, 2, 3, 1 ); // a 3D point (1,2,3) in homogeneous form homogeneous.magnitude; // sqrt(1^2 + 2^2 + 3^2 + 1^2) const doubled = homogeneous.plus( new Vector4( 1, 0, 0, 0 ) ); // new vector, unchanged original homogeneous.add( new Vector4( 1, 0, 0, 0 ) ); // mutates in place ``` ## Constructing vectors The constructor takes all four components directly: `new Vector4( x, y, z, w )`. There's also the pooled shorthand `v4( x, y, z, w )` (exported alongside `Vector4`) for allocation-sensitive code, and static constants: | Static | Value | | --- | --- | | `Vector4.ZERO` | `(0, 0, 0, 0)` | | `Vector4.X_UNIT` | `(1, 0, 0, 0)` | | `Vector4.Y_UNIT` | `(0, 1, 0, 0)` | | `Vector4.Z_UNIT` | `(0, 0, 1, 0)` | | `Vector4.W_UNIT` | `(0, 0, 0, 1)` | | `Vector4.from( vector4Like, defaultW? )` | Builds a `Vector4` from any `{x?, y?, z?, w?}`-shaped object, defaulting missing `x`/`y`/`z` to `0` and missing `w` to `1` (or the given `defaultW`) | ## Immutable vs. mutable methods Same convention as `Vector2`/`Vector3`: the immutable form returns a new `Vector4` and reads as a noun/adjective; the mutable form changes `this` and returns it, reading as a verb. | Immutable (returns new `Vector4`) | Mutable (changes `this`, returns `this`) | Effect | | --- | --- | --- | | `plus( v )` / `plusXYZW( x, y, z, w )` / `plusScalar( s )` | `add( v )` / `addXYZW( x, y, z, w )` / `addScalar( s )` | Addition | | `minus( v )` / `minusXYZW( x, y, z, w )` / `minusScalar( s )` | `subtract( v )` / `subtractXYZW( x, y, z, w )` / `subtractScalar( s )` | Subtraction | | `times( s )` / `timesScalar( s )` | `multiply( s )` / `multiplyScalar( s )` | Scalar multiplication | | `componentTimes( v )` | `componentMultiply( v )` | Component-wise multiplication | | `dividedScalar( s )` | `divideScalar( s )` | Scalar division | | `negated()` | `negate()` | Multiply by -1 | | `normalized()` | `normalize()` | Rescale to magnitude 1 (throws on a zero vector) | | `withMagnitude( m )` | `setMagnitude( m )` | Rescale to magnitude `m` | | `roundedSymmetric()` | `roundSymmetric()` | Round all four components | | `copy()` | `set( v )` / `setXYZW( x, y, z, w )` / `setX( x )` / `setY( y )` / `setZ( z )` / `setW( w )` | Copy / assign | Read-only queries: `magnitude`, `magnitudeSquared`, `dot( v )` / `dotXYZW( x, y, z, w )`, `distance( point )` / `distanceXYZW( x, y, z, w )`, `distanceSquared( point )`, `angleBetween( v )`, `equals( v )` / `equalsEpsilon( v, epsilon )`, `isFinite()`, `blend( v, ratio )` / `average( v )`, and `toString()`. ::: tip Mostly a `Matrix4` operand, not a general-purpose vector Unlike `Vector2`/`Vector3`, `Vector4` doesn't show up often as application-level state in sims — it mainly exists as the natural operand for [`Matrix4`](/api/dot/matrix4)'s homogeneous 4x4 math (`timesVector4`, `timesTransposeVector4`), where `w = 1` represents a point and `w = 0` represents a direction. If you're modeling an RGBA color or a plain 4-tuple without matrix math in mind, a `Vector4` still works fine, but there's no dedicated color type here — reach for `scenerystack/scenery`'s `Color` for actual color values. ::: ## Related - [Vector2](/api/dot/vector2) — the 2D counterpart used for almost all on-screen positioning. - [Vector3](/api/dot/vector3) — the 3D counterpart, used for color math and 3D-flavored sims. - [Matrix4](/api/dot/matrix4) — the 4x4 matrix type that operates on `Vector4` via `timesVector4`. ======================================================================== Page: CreditsNode URL: https://veillette.github.io/Almanach/api/joist/credits-node Source: docs/api/joist/credits-node.md Category: API | Tags: joist, CreditsNode, credits, AboutDialog, Sim | Status: complete ======================================================================== # CreditsNode `CreditsNode` (from `scenerystack/joist`) is a `VBox` that renders a fixed, ordered set of credits fields — lead design, software development, team, contributors, quality assurance, graphic arts, sound design, and a final "thanks" note — as the content of the simulation's About dialog. `Sim`'s `credits` option (a `CreditsData` object) is what you actually populate; `CreditsNode` is what turns it into a laid-out `Node`, built automatically by joist's `AboutDialog` when credits are present. ```ts import { Sim, onReadyToLaunch } from 'scenerystack/sim'; import type { CreditsData } from 'scenerystack/joist'; import { Property } from 'scenerystack/axon'; const credits: CreditsData = { leadDesign: 'Alice Example', softwareDevelopment: 'Bob Example, Carol Example', team: 'Dan Example (accessibility), Erin Example (physics)', qualityAssurance: 'Frank Example', thanks: 'Thanks to the Foo Foundation for supporting this project.' }; onReadyToLaunch( () => { const sim = new Sim( new Property( 'My Simulation' ), [ /* screens */ ], { credits } ); sim.start(); // AboutDialog builds a CreditsNode from `credits` the first time it's opened } ); ``` You won't typically construct `CreditsNode` yourself — it's what `AboutDialog` builds internally from `Sim`'s `credits` option — but its shape is documented here since `CreditsData`'s fields are exactly what a sim author fills in. ## `CreditsData` Every field is an optional string; a field only renders a line if it's provided (non-empty): | Field | Rendered as | | --- | --- | | `leadDesign` | "Lead Design: ..." | | `softwareDevelopment` | "Software Development: ..." | | `team` | "Team: ..." | | `contributors` | "Contributors: ..." | | `qualityAssurance` | "Quality Assurance: ..." | | `graphicArts` | "Graphic Arts: ..." | | `soundDesign` | "Sound Design: ..." | | `thanks` | Rendered last, under its own "Thanks!" heading, with extra spacing above it if any other field was present | ## Constructor ```ts new CreditsNode( credits: CreditsData, options: CreditsNodeOptions ) ``` Unlike most Node subclasses, `options` here is not optional — `AboutDialog` always passes at least a `tandem`. ## Options `CreditsNodeOptions` extends `VBoxOptions` with: | Option | Default | Effect | | --- | --- | --- | | `titleFont` | 18px bold `PhetFont` | Font for the "Credits" heading and the "Thanks!" heading | | `textFont` | 16px `PhetFont` | Font for every credits line | `CreditsNode` also defaults `align: 'left'`, `spacing: 1`, and `maxWidth: 550` (each line wraps via `RichText`'s `lineWrap` at that width), and sets `children` internally. ::: tip Voicing and accessible structure come for free Each credits line is a `VoicingRichText`/`VoicingText`, and the "Credits"/"Thanks!" headings are exposed as accessible headings (`accessibleHeading`) — using `CreditsNode` (via `Sim`'s `credits` option) gets you PDOM structure and Voicing support without any extra accessibility work in sim code. ::: ======================================================================== Page: HighlightNode URL: https://veillette.github.io/Almanach/api/joist/highlight-node Source: docs/api/joist/highlight-node.md Category: API | Tags: joist, HighlightNode, navigation-bar, highlight | Status: complete ======================================================================== # HighlightNode `HighlightNode` (from `scenerystack/joist`) draws the soft highlight bar joist uses behind navigation-bar buttons (screen icons, the PhET menu button, and similar) to indicate hover/press state — two thin vertical `Rectangle`s, each filled with a `LinearGradient` that fades from transparent at the top and bottom to a solid color in the middle, separated by a gap equal to the requested width. ```ts import { HighlightNode } from 'scenerystack/joist'; const buttonHighlight = new HighlightNode( 40, 32, { fill: 'white', highlightWidth: 2 } ); buttonHighlight.visible = false; // toggle on hover/press, as NavigationBar's own buttons do ``` ## Constructor ```ts new HighlightNode( width: number, height: number, providedOptions?: HighlightNodeOptions ) ``` `width` becomes the `HBox`'s `spacing` between the two bars (so it's the gap the highlight leaves open for the button content), and `height` sets both bars' rectangle height. ## Options `HighlightNodeOptions` extends `HBoxOptions` with: | Option | Default | Effect | | --- | --- | --- | | `fill` | `'white'` | The color at the gradient's center; `HighlightNode` derives a fully-transparent version of the same color for the gradient's top/bottom stops | | `highlightWidth` | `1` | The width (thickness) of each of the two vertical bars | `HighlightNode` also forces `pickable: false` (a highlight should never intercept input meant for the button it's decorating) and sets `children` internally — passing either as an option is redundant with what the constructor already does. ::: tip `HighlightNode` is purely decorative — visibility is your responsibility `HighlightNode` doesn't listen to any pointer/focus state itself; it's a static pair of gradient rectangles. The Node that owns it (e.g. a navigation bar screen button) is responsible for toggling its `visible` property in response to hover/press/focus, typically via a [`PressListener`](/api/scenery/press-listener)'s `isHighlightedProperty`. ::: ======================================================================== Page: HomeScreen URL: https://veillette.github.io/Almanach/api/joist/home-screen Source: docs/api/joist/home-screen.md Category: API | Tags: joist, HomeScreen, Screen, Sim | Status: verified ======================================================================== # HomeScreen `HomeScreen` is itself a [`Screen`](/api/joist/screen) subclass — the one showing a grid of icon buttons, one per entry in `sim.simScreens`, so the user can pick which screen to run. `Sim` constructs a `HomeScreen` automatically whenever it's given more than one screen; with exactly one screen, there's nothing to choose between and no `HomeScreen` is created at all. You don't construct or configure a `HomeScreen` directly — it's entirely managed by `Sim` — but you do control what it shows via each screen's [`Screen`](/api/joist/screen) options (`name`, `homeScreenIcon`). ::: warning `HomeScreen` is exported from `scenerystack/sim`, not `scenerystack/joist` Same gotcha as [`Sim`](/api/joist/sim), [`Screen`](/api/joist/screen), and [`ScreenIcon`](/api/joist/screen-icon): despite living in the `joist` repository, it's exported from **`scenerystack/sim`**. ::: ```ts import { Sim, onReadyToLaunch } from 'scenerystack/sim'; import { Property } from 'scenerystack/axon'; onReadyToLaunch( () => { // Passing 2+ screens is what causes Sim to auto-create a HomeScreen. const sim = new Sim( new Property( 'My Simulation' ), [ firstScreen, secondScreen ] ); sim.start(); sim.screens[ 0 ]; // the auto-created HomeScreen (always first in sim.screens when one exists) } ); ``` ## Constructor ```ts new HomeScreen( simNameProperty: TReadOnlyProperty, getScreenProperty: () => Property, simScreens: AnyScreen[], activeSimScreensProperty: ReadOnlyProperty, providedOptions: HomeScreenOptions ) ``` Constructed once by `Sim` during startup, when `allSimScreens.length > 1`; not intended to be called from simulation code. ## Behavior worth knowing | Aspect | Detail | | --- | --- | | `sim.screens` vs `sim.simScreens` | `sim.screens` includes the auto-created `HomeScreen` first (when one exists); `sim.simScreens` excludes it — use `simScreens` when iterating "the sim's actual content screens" | | `BACKGROUND_COLOR` (static) | Black — also what [`NavigationBar`](/api/joist/navigation-bar)'s fill matches while the home screen is selected, so the bar blends in rather than reading as a separate strip | | `name` | Defaults to a localized "Home" label; not usually overridden | | Icon buttons | Each button is built from the corresponding screen's `homeScreenIcon` (a [`ScreenIcon`](/api/joist/screen-icon)) and `name`; clicking one sets `sim.selectedScreenProperty` to that screen | | `instrumentNameProperty` | `false` by default for `HomeScreen` (unlike ordinary screens) — its name isn't independently PhET-iO instrumented | ## Related - [Sim](/api/joist/sim) — decides whether to auto-create a `HomeScreen`, based on `allSimScreens.length`. - [Screen](/api/joist/screen) — the base class `HomeScreen` extends, and the source of the icons/names it displays. - [ScreenIcon](/api/joist/screen-icon) — builds each screen's `homeScreenIcon`. - [NavigationBar](/api/joist/navigation-bar) — matches its fill to `HomeScreen.BACKGROUND_COLOR` while the home screen is active. ======================================================================== Page: JoistButton URL: https://veillette.github.io/Almanach/api/joist/joist-button Source: docs/api/joist/joist-button.md Category: API | Tags: joist, JoistButton, Sim, NavigationBar, button | Status: complete ======================================================================== # JoistButton `JoistButton` (from `scenerystack/joist`) is the base class behind the chrome buttons that live directly in the [`NavigationBar`](/api/joist/navigation-bar) — `HomeButton`, the PhET-menu button, and similar — none of which use the ordinary `sun` button visuals. Instead of a fixed background rectangle, `JoistButton` draws two overlapping highlight shapes (one for a light background, one for a dark one) and shows whichever one is legible against the navigation bar's current fill, switching automatically as `sim.lookAndFeel`'s color changes (e.g. between the black home-screen background and a lighter in-sim navigation bar). You extend it directly only if you're building a custom navigation-bar-style button; ordinary in-sim UI should use `sun`'s regular button classes instead. ```ts import { JoistButton } from 'scenerystack/joist'; import type { TReadOnlyProperty } from 'scenerystack/axon'; import type { Color, Node } from 'scenerystack/scenery'; ``` ## A minimal example ```ts class MyChromeButton extends JoistButton { public constructor( content: Node, navigationBarFillProperty: TReadOnlyProperty, tandem: Tandem ) { super( content, navigationBarFillProperty, { tandem: tandem, listener: () => console.log( 'pressed' ) } ); } } ``` ## Constructor ```ts new JoistButton( content: Node, navigationBarFillProperty: TReadOnlyProperty, providedOptions: JoistButtonOptions // tandem is required ) ``` `navigationBarFillProperty` is what drives the light/dark highlight swap — pass `sim.lookAndFeel.navigationBarFillProperty` (a `TReadOnlyProperty`) when building a real navigation-bar button. ## Options | Option | Default | Effect | | --- | --- | --- | | `listener` | `null` | Callback invoked when the button fires | | `highlightExtensionWidth` / `highlightExtensionHeight` | `0` | Grows the highlight shape beyond `content`'s bounds | | `highlightCenterOffsetX` / `highlightCenterOffsetY` | `0` | Shifts the highlight's center relative to `content`'s center | | `pointerAreaDilationX` / `pointerAreaDilationY` | `0` | Expands mouse/touch areas beyond `content`'s bounds, to close gaps between adjacent chrome buttons and their labels | | `focusHighlightDilationX` / `focusHighlightDilationY` | `0` | Expands the keyboard focus highlight independently of the pointer areas | | `enabledPropertyOptions` | `{ phetioFeatured: false }` | `JoistButton`s intentionally default to a non-featured `enabledProperty`, unlike most `sun` buttons | ## Protected members (for subclasses) | Member | Description | | --- | --- | | `buttonModel` | The underlying `PushButtonModel` driving press/release behavior | | `interactionStateProperty` | A `PushButtonInteractionStateProperty` tracking over/pressed/idle state, used to decide which highlight is visible | ## Method | Method | Effect | | --- | --- | | `isPDOMClicking()` | Whether the button is currently firing due to keyboard/screen-reader (PDOM) activation rather than pointer input | ::: tip The two highlight `Node`s are both always in the scene graph `JoistButton` builds a `brightenHighlight` (white, for dark backgrounds) and a `darkenHighlight` (black, for light backgrounds) as siblings of `content` in every instance, and toggles their `visible` flags based on `navigationBarFillProperty` and interaction state rather than swapping children in and out. If you're customizing appearance by inspecting a `JoistButton`'s children, expect exactly three: `content`, `brightenHighlight`, `darkenHighlight`, in that order. ::: ======================================================================== Page: LookAndFeel URL: https://veillette.github.io/Almanach/api/joist/look-and-feel Source: docs/api/joist/look-and-feel.md Category: API | Tags: joist, LookAndFeel, Sim, Screen, color, navigation-bar | Status: complete ======================================================================== # LookAndFeel `LookAndFeel` (from `scenerystack/joist`) is a small class, one instance per running [`Sim`](/api/joist/sim), that owns the single source of truth for the currently-displayed screen's background color and derives the navigation bar's fill/text colors from it. A `Sim` creates its own `LookAndFeel` and exposes it as `sim.lookAndFeel`; each [`Screen`](/api/joist/screen)'s `backgroundColorProperty` is wired to update `lookAndFeel.backgroundColorProperty` whenever that screen is the one currently selected. ```ts import { LookAndFeel } from 'scenerystack/joist'; // Typically accessed from the running Sim, not constructed directly by sim code: declare const lookAndFeel: LookAndFeel; lookAndFeel.backgroundColorProperty.link( backgroundColor => { console.log( 'Current screen background:', backgroundColor.toCSS() ); } ); console.log( lookAndFeel.navigationBarDarkProperty.value ); // true when the nav bar background is black ``` ## Constructor ```ts new LookAndFeel() ``` Takes no options — sim code virtually never constructs a `LookAndFeel` itself; use the one already created by `Sim` (`sim.lookAndFeel`). ## Public API | Member | Description | | --- | --- | | `backgroundColorProperty` | `Property`, initially `Color.BLACK`. Set by `Sim` to track whichever screen is currently selected; this is also the `Color` applied to the `Display`'s `backgroundColor` | | `navigationBarDarkProperty` | `TReadOnlyProperty` — `true` exactly when `backgroundColorProperty` equals `Color.BLACK` | | `navigationBarFillProperty` | `TReadOnlyProperty` — `white` when the background is black, `black` otherwise (i.e. the navigation bar always contrasts with the current screen background) | | `navigationBarTextFillProperty` | `TReadOnlyProperty` — the readable text/icon color for whatever `navigationBarFillProperty` currently is (`white` text on a black nav bar, `black` text on a white nav bar) | | `reset()` | Resets `backgroundColorProperty` to its initial value (`Color.BLACK`); the three derived Properties update automatically since they're `DerivedProperty`s | ## How this ties into `Screen` Every `Screen` accepts a `backgroundColorProperty` option; whichever screen is currently active has its background color propagated into `sim.lookAndFeel.backgroundColorProperty` by `Sim`'s screen-selection logic. Because `navigationBarFillProperty`/`navigationBarTextFillProperty` are derived from that single Property, the navigation bar's contrast is automatically correct for every screen without each screen needing to compute or declare its own navigation bar colors. ::: tip Read from `LookAndFeel`, don't write to it, outside of `Sim` internals The only Property on `LookAndFeel` meant to be set from outside is `backgroundColorProperty`, and even that is normally driven by `Sim`'s screen-switching logic rather than sim code setting it directly. Screen and view code should treat `lookAndFeel`'s Properties as read-only signals for "what should currently contrast well," not as a general-purpose theming API — for broader theming, use [`ColorProperty`/`ProfileColorProperty`](/api/scenery/color-property-and-profile-color-property). ::: ======================================================================== Page: NavigationBar URL: https://veillette.github.io/Almanach/api/joist/navigation-bar Source: docs/api/joist/navigation-bar.md Category: API | Tags: joist, NavigationBar, Screen, Sim | Status: verified ======================================================================== # NavigationBar `NavigationBar` is the strip shown at the bottom of every running [`Sim`](/api/joist/sim): the sim's title on the left, a row of per-[`Screen`](/api/joist/screen) buttons and a home button for multi-screen sims, and the PhET/accessibility menu buttons on the right. `Sim` constructs exactly one `NavigationBar` internally — you never build one yourself, and its constructor even takes the owning `Sim` instance as an argument. Documented here because you'll see it referenced when reading `Sim`/`Screen` internals or customizing sim chrome via [`LookAndFeel`](/api/joist/sim), and because its static size constants are occasionally useful for layout math. ```ts import { NavigationBar } from 'scenerystack/sim'; NavigationBar.NAVIGATION_BAR_SIZE; // Dimension2 - the bar's un-scaled design size (width matches HomeScreenView's layout bounds; height 40) ``` ::: tip You configure the navigation bar through `Screen` and `Sim`, not `NavigationBar` itself As a sim author, everything you'd want to change about the navigation bar is exposed elsewhere: a screen's button icon and label come from [`Screen`](/api/joist/screen)'s `navigationBarIcon`/`name` options (built with [`ScreenIcon`](/api/joist/screen-icon)), and the bar's fill/text colors come from `Sim.lookAndFeel` (a `LookAndFeel` instance, exported from `scenerystack/joist`). There's no supported way to construct or restyle a `NavigationBar` directly. ::: ## Constructor ```ts new NavigationBar( sim: Sim, tandem: Tandem ) ``` Constructed once by `Sim` during startup; not intended to be called from simulation code. ## Behavior worth knowing | Aspect | Detail | | --- | --- | | Single-screen sims | Shows only the sim title (left) and PhET/a11y buttons (right) — no home button or per-screen buttons, since there's nothing to switch between | | Multi-screen sims | Adds a home button and one button per entry in `sim.simScreens`, laid out and centered via `ManualConstraint`, with each screen's width driven by its `navigationBarIcon`'s label | | Fill when on the home screen | The bar's background matches `HomeScreen.BACKGROUND_COLOR` (black) while the home screen is selected, so it visually blends into the [`HomeScreen`](/api/joist/home-screen) rather than showing as a separate bar | | `layout( scale, width, height )` | Called by `Sim` on window resize to rescale/reposition the bar's contents; not meant to be called directly by simulation code | ## Related - [Sim](/api/joist/sim) — owns the single `NavigationBar` instance and its `lookAndFeel`. - [Screen](/api/joist/screen) — supplies the `name`/`navigationBarIcon` each screen button displays. - [ScreenIcon](/api/joist/screen-icon) — builds the icon `navigationBarIcon` expects. - [HomeScreen](/api/joist/home-screen) — the screen the home button switches to, and whose background color the bar matches when selected. ======================================================================== Page: PreferencesModel and PreferencesDialog URL: https://veillette.github.io/Almanach/api/joist/preferences-model-and-dialog Source: docs/api/joist/preferences-model-and-dialog.md Category: API | Tags: joist, PreferencesModel, PreferencesDialog, Sim, preferences | Status: verified ======================================================================== # PreferencesModel and PreferencesDialog `PreferencesModel` is the configuration object you pass to [`Sim`](/api/joist/sim) to declare which Preferences features a simulation supports — projector mode, sound, voicing, gesture input, locale switching, and simulation-specific custom controls. `PreferencesDialog` is the tabbed dialog that renders whatever `PreferencesModel` describes; you never construct it yourself — `Sim` wires a `NavigationBarPreferencesButton` to build one lazily (`new PreferencesDialog( preferencesModel, ... )`) the first time the user opens it. This pairing is covered end-to-end, with a full example, in [Preferences and Feature Flags](/guides/preferences-and-feature-flags) — this page is the API-reference companion, focused on what each class actually exposes. ::: warning Both are exported from `scenerystack/sim`, not `scenerystack/joist` Same gotcha as [`Sim`](/api/joist/sim) and [`Screen`](/api/joist/screen): despite living in the `joist` repository, both `PreferencesModel` and `PreferencesDialog` are exported from **`scenerystack/sim`**. ::: ```ts import { Sim, onReadyToLaunch, PreferencesModel } from 'scenerystack/sim'; import { Property } from 'scenerystack/axon'; const preferencesModel = new PreferencesModel( { visualOptions: { supportsProjectorMode: true }, audioOptions: { supportsSound: true, supportsExtraSound: true } } ); onReadyToLaunch( () => { const sim = new Sim( new Property( 'My Simulation' ), [ /* screens */ ], { preferencesModel } ); sim.start(); // Sim wires up the Preferences button; PreferencesDialog is built lazily on first click } ); ``` ## `PreferencesModel` ### Constructor ```ts new PreferencesModel( providedOptions?: PreferencesModelOptions ) ``` ### Options | Option | Surfaces | Notable sub-options | | --- | --- | --- | | `simulationOptions` | The "Simulation" tab | `customPreferences` — your own controls; this tab only appears if you supply at least one | | `visualOptions` | The "Visual" tab | `supportsProjectorMode`, `supportsInteractiveHighlights`, plus `customPreferences` | | `audioOptions` | The "Audio" tab | `supportsSound`, `supportsExtraSound`, `supportsVoicing`, `supportsCoreVoicing`, plus `customPreferences` (which can specify a `column: 'left' \| 'right'`) | | `inputOptions` | The "Input" tab | `supportsGestureControl`, plus `customPreferences` | | `localizationOptions` | The "Localization" tab | `supportsDynamicLocale`, `includeLocalePanel`, plus `customPreferences` | Every one of these is optional — an untouched `new PreferencesModel()` supports nothing extra, and `Sim`'s Preferences button only appears at all if `shouldShowDialog()` (below) is `true`. ### Public API | Member | Description | | --- | --- | | `simulationModel`, `visualModel`, `audioModel`, `inputModel`, `localizationModel` | The resolved model for each tab (options merged with defaults, plus derived Properties like `visualModel.interactiveHighlightsEnabledProperty`) | | `supportsSimulationPreferences()` / `supportsVisualPreferences()` / `supportsAudioPreferences()` / `supportsInputPreferences()` / `supportsLocalizationPreferences()` | Whether each tab has anything to show — `PreferencesDialog` uses these to decide which tabs to build | | `shouldShowDialog()` | `true` if *any* tab would have content — `Sim` uses this to decide whether to show a Preferences button at all | ## `PreferencesDialog` ### Constructor ```ts new PreferencesDialog( preferencesModel: PreferencesModel, providedOptions?: PreferencesDialogOptions ) ``` You won't call this yourself in ordinary sim code — it's what `Sim`'s `NavigationBarPreferencesButton` constructs internally, once, the first time a user opens Preferences. `PreferencesDialog` is a `Dialog` subclass (see `scenerystack/sim`'s `Dialog`/`Popupable` re-exports), and is never disposed once created (`isDisposable: false`) — subsequent opens reuse the same instance. ### Behavior worth knowing | Aspect | Detail | | --- | --- | | Tab selection | Built from whichever `supports*Preferences()` calls on `preferencesModel` return `true`; the "Overview" tab is always present | | `focusSelectedTab()` | Public method to move keyboard focus to the currently selected tab — used when the dialog opens | | Keyboard navigation | Arrow-down from the tab row moves focus into the selected panel; arrow-up from the panel moves focus back to the tab row | ::: tip Configure preferences through `PreferencesModel`; you'll rarely touch `PreferencesDialog` Everything a sim author controls — which tabs appear, which toggles are in them, custom per-sim controls — is a `PreferencesModel` option. `PreferencesDialog` exists mostly so its shape is documented; the only thing you'd construct one for directly is testing/exploratory code, not ordinary sim wiring. ::: ## Related - [Preferences and Feature Flags](/guides/preferences-and-feature-flags) — the narrative walkthrough this reference page complements, including query-parameter interactions. - [Sim](/api/joist/sim) — takes `preferencesModel` as a `SimOptions` field and owns the Preferences button that lazily builds the dialog. ======================================================================== Page: PreferencesPanel, PreferencesPanelSection, PreferencesControl, PreferencesTabs, and PreferencesTab URL: https://veillette.github.io/Almanach/api/joist/preferences-panel-components Source: docs/api/joist/preferences-panel-components.md Category: API | Tags: joist, PreferencesPanel, PreferencesPanelSection, PreferencesControl, PreferencesTabs, PreferencesTab, PreferencesModel, PreferencesDialog | Status: complete ======================================================================== # PreferencesPanel, PreferencesPanelSection, PreferencesControl, PreferencesTabs, and PreferencesTab [`PreferencesModel` and `PreferencesDialog`](/api/joist/preferences-model-and-dialog) documents the dialog as a whole; this page covers the five smaller pieces (all from `scenerystack/joist`) it's assembled from. Two of them — **`PreferencesPanelSection`** and **`PreferencesControl`** — are the ones sim authors actually construct, via a `PreferencesModel` option's `customPreferences: [{ createContent: ( parentTandem ) => Node }]`. The other three — **`PreferencesPanel`**, **`PreferencesTabs`**, and **`PreferencesTab`** — are the scaffolding `PreferencesDialog` uses internally to build each tab and its panel; documented here for completeness, but you won't normally construct them yourself. ```ts import { PreferencesModel, Sim, onReadyToLaunch } from 'scenerystack/sim'; import { PreferencesPanelSection, PreferencesControl } from 'scenerystack/joist'; import { Text } from 'scenerystack/scenery'; import { ToggleSwitch } from 'scenerystack/sun'; import { BooleanProperty, Property } from 'scenerystack/axon'; const showGridProperty = new BooleanProperty( false ); const preferencesModel = new PreferencesModel( { simulationOptions: { customPreferences: [ { createContent: parentTandem => { const label = new Text( 'Show grid' ); const toggle = new ToggleSwitch( showGridProperty, false, true, { tandem: parentTandem.createTandem( 'showGridSwitch' ) } ); // PreferencesControl handles the label/control/description layout for you. const control = new PreferencesControl( { labelNode: label, controlNode: toggle } ); // PreferencesPanelSection adds the standard section spacing/indentation. return new PreferencesPanelSection( { contentNode: control } ); } } ] } } ); onReadyToLaunch( () => { const sim = new Sim( new Property( 'My Simulation' ), [ /* screens */ ], { preferencesModel } ); sim.start(); } ); ``` ## `PreferencesControl` A `GridBox` that lays out an optional `labelNode`, an optional `controlNode`, and an optional `descriptionNode` in the standard Preferences arrangement — label and control on one row, a full-width description below — and wires up sensible accessibility defaults between them. | Option | Default | Effect | | --- | --- | --- | | `labelNode` | — | Node placed at the left of the control's row | | `controlNode` | — | The actual UI control (a switch, radio buttons, combo box, …); right-aligned | | `descriptionNode` | — | Node placed below the label/control row, spanning both columns | | `labelSpacing` | `10` | Horizontal gap between `labelNode` and `controlNode` when there's no `descriptionNode` | | `ySpacing` | `5` | Vertical gap between the label/control row and `descriptionNode` | | `allowDescriptionStretch` | `true` | If `true`, the description cell stretches to a minimum content width (`480`) so the control aligns with other controls in the panel | | `headingControl` | `false` | Set `true` for a control acting as a section heading — it won't stretch to fill the row like an ordinary control does | If `controlNode` has no `accessibleName`/`accessibleHelpText` already set, `PreferencesControl` derives them from `labelNode`/`descriptionNode`'s text automatically. `PreferencesControl` also manages `disabledOpacity` on itself, so nesting it inside another disableable container doesn't compound dimming. ## `PreferencesPanelSection` A `VBox` providing the standard vertical spacing and title/content indentation for one logical group of preferences within a tab. | Option | Default | Effect | | --- | --- | --- | | `titleNode` | `null` | If provided, a heading for this section | | `contentNode` | `null` | If provided, the section's body — indented under `titleNode` by `contentLeftMargin` | | `contentLeftMargin` | `30` | Indentation applied to `contentNode` when a `titleNode` is also present | | `contentNodeOptions` | `{}` | Extra `NodeOptions` applied to the wrapper around `contentNode` | For a `customPreferences` entry with just one control and no separate heading (as in the example above), passing only `contentNode` (no `titleNode`) is the common case. ## `PreferencesPanel` The `Node` base class each of joist's built-in tab panels (Simulation, Visual, Audio, Input, Localization) extends. Its entire job is visibility: it shows itself only when `selectedTabProperty` matches its own `PreferencesType` **and** `tabVisibleProperty` is `true`. ```ts new PreferencesPanel( preferencesType: PreferencesType, selectedTabProperty: TReadOnlyProperty, tabVisibleProperty: TReadOnlyProperty, providedOptions?: PreferencesPanelOptions ) ``` You would only construct one directly if building an entirely custom Preferences tab structure outside of `PreferencesModel`'s supported tabs — ordinary `customPreferences` content does not need to touch `PreferencesPanel` at all, since it's nested inside whichever built-in panel (Simulation/Visual/Audio/Input/Localization) you attached it to. ## `PreferencesTabs` and `PreferencesTab` `PreferencesTabs` is the `HBox` of tab buttons across the top of the dialog (implemented as `role="tablist"`/`role="tab"` for accessibility, with arrow-key navigation between tabs built in); `PreferencesTab` is a single tab button within it, associated with one `PreferencesType` value. ```ts new PreferencesTabs( supportedTabs: PreferencesType[], selectedPanelProperty: TProperty, providedOptions: PreferencesTabsOptions // tandem is required ) new PreferencesTab( labelProperty: TReadOnlyProperty, property: TProperty, value: PreferencesType, providedOptions: PreferencesTabOptions // tandem is required ) ``` `PreferencesTabs` builds one `PreferencesTab` per entry in `supportedTabs` that matches a known `PreferencesType` (`OVERVIEW`, `SIMULATION`, `VISUAL`, `AUDIO`, `INPUT`, `LOCALIZATION` — the `LOCALIZATION` tab additionally gets a globe icon). `PreferencesTabs.focusSelectedTab()` (surfaced on `PreferencesDialog` too, see [PreferencesModel and PreferencesDialog](/api/joist/preferences-model-and-dialog)) moves keyboard focus to whichever tab is currently selected. ::: tip For ordinary custom preferences, you only need `PreferencesPanelSection` and `PreferencesControl` `PreferencesModel`'s `customPreferences.createContent` callback is expected to return a single `Node` — nesting your control(s) in `PreferencesControl` (for label/control/description layout) inside `PreferencesPanelSection` (for standard section spacing) is enough to match the built-in tabs' look. `PreferencesPanel`/`PreferencesTabs`/`PreferencesTab` are the dialog's own scaffolding, not something a `customPreferences` entry needs to construct. ::: ======================================================================== Page: Region, Culture, and Locale Selection UI URL: https://veillette.github.io/Almanach/api/joist/region-culture-and-locale-selection Source: docs/api/joist/region-culture-and-locale-selection.md Category: API | Tags: joist, LocalizedImageProperty, RegionAndCultureComboBox, LanguageSelectionNode, LocalePanel, localeProperty, regionAndCultureProperty, i18n | Status: complete ======================================================================== # Region, Culture, and Locale Selection UI SceneryStack draws a line between two independent runtime i18n concerns, each with its own global singleton `Property` (both exported from `scenerystack/joist`): **`localeProperty`** controls which *language* strings are displayed in, and **`regionAndCultureProperty`** controls how *images/illustrations* are styled — the region-and-culture of the people and objects depicted — independent of language. A sim can support one without the other. Both are surfaced to end users through the "Localization" tab of the Preferences dialog (see [PreferencesModel and PreferencesDialog](/api/joist/preferences-model-and-dialog)), built from the UI classes documented below. ```ts import { localeProperty, regionAndCultureProperty, LocalizedImageProperty, RegionAndCultureComboBox, LanguageSelectionNode, LocalePanel } from 'scenerystack/joist'; ``` ## `localeProperty` — which language is displayed `localeProperty` is a module-level singleton `LocaleProperty` (a `Property` subclass), initialized from `phet.chipper.locale` and instrumented at `Tandem.GENERAL_MODEL.createTandem('localeProperty')`. Setting its value re-renders every `StringProperty`-backed string in the sim — see [Translation and Localization](/guides/translation-and-localization) for how strings react to it. ```ts localeProperty.value; // e.g. 'es' — the current Locale localeProperty.availableRuntimeLocales; // Locale[] actually built into this sim localeProperty.supportsDynamicLocale; // false if only one locale was built — switching is pointless ``` | Member | Description | | --- | --- | | `availableRuntimeLocales` | `Locale[]`, sorted by each locale's localized display name — the full set this build supports | | `supportsDynamicLocale` | `true` only when `availableRuntimeLocales.length > 1` | | `isLocaleChanging` | `true` while a locale switch is in progress, so listeners can opt out of redundant work mid-switch | ## `regionAndCultureProperty` — how images are styled `regionAndCultureProperty` is a separate singleton `Property`, where `RegionAndCulture` is one of `'usa' | 'africa' | 'africaModest' | 'asia' | 'latinAmerica' | 'oceania' | 'random'`. Its runtime choices are narrowed to `supportedRegionAndCultureValues` — derived from `"supportedRegionsAndCultures"` in the sim's `package.json`, always including `'usa'` (`DEFAULT_REGION_AND_CULTURE`) as the guaranteed fallback. It's only instrumented (given a real tandem instead of `Tandem.OPT_OUT`) when more than one value is actually supported. ```ts import type { RegionAndCulture } from 'scenerystack/joist'; regionAndCultureProperty.value; // e.g. 'africa' ``` ### `LocalizedImageProperty` — region-varying images `LocalizedImageProperty` is a `DerivedProperty1` that picks an image out of a map keyed by region-and-culture, re-deriving whenever `regionAndCultureProperty`'s *concrete* value (the resolved value, with `'random'` already picked) changes. You build one per region-varying illustration: ```ts import handSoap_usa_png from '../../images/handSoap_usa_png.js'; import handSoap_africa_png from '../../images/handSoap_africa_png.js'; const handSoapImageProperty = new LocalizedImageProperty( 'handSoap', { usa: handSoap_usa_png, // required — the guaranteed fallback africa: handSoap_africa_png // any concrete RegionAndCulture you omit falls back to asserting at construction time } ); ``` `usa` is required in the image map (it's the fallback), and every other *concrete* region-and-culture value the sim declares support for must also have an entry, or construction asserts. ## The Preferences "Localization" UI Three joist classes build the panels a sim's Preferences dialog shows for these two Properties — you rarely construct them directly (they're wired up automatically when `PreferencesModel`'s `localizationOptions` are configured), but you'll encounter them reading Preferences internals: | Class | Role | | --- | --- | | `RegionAndCultureComboBox` | A `ComboBox` bound to `regionAndCultureProperty`, listing `supportedRegionAndCultureValues` sorted and localized; shown in Preferences > Localization when more than one value is supported | | `LanguageSelectionNode` | A single clickable `Rectangle` showing one locale's localized name; clicking it sets `localeProperty.value` to that locale and highlights the currently-selected one | | `LocalePanel` | A `Panel` laying out one `LanguageSelectionNode` per entry in `localeProperty.availableRuntimeLocales`, in a `GridBox` — the full language picker shown in Preferences > Localization | ```ts // Roughly what PreferencesModel builds internally when includeLocalePanel is true: const localePanel = new LocalePanel( localeProperty ); ``` ::: tip Preferences UI components are not PhET-iO instrumented `RegionAndCultureComboBox` and `LanguageSelectionNode`'s internal listeners deliberately use `Tandem.OPT_OUT` — Preferences controls are considered part of the platform chrome, not part of a sim's own instrumented API surface (see the `joist#744` discussion referenced in source comments). Don't expect to find these under a sim's PhET-iO tree; `localeProperty` and `regionAndCultureProperty` themselves *are* instrumented (under `Tandem.GENERAL_MODEL`), but the buttons that drive them are not. ::: ======================================================================== Page: Screen URL: https://veillette.github.io/Almanach/api/joist/screen Source: docs/api/joist/screen.md Category: API | Tags: joist, Screen | Status: verified ======================================================================== # Screen `Screen` is the largest organizational chunk inside a [`Sim`](/api/joist/sim): it pairs a model factory and a view factory into one selectable unit, shown as a button in the navigation bar (and, for multi-screen sims, on the home screen). `Screen` doesn't build the model or view itself — it holds *functions* that build them lazily, plus the name/icons/background color used to represent the screen in the UI. ::: warning `Screen` is exported from `scenerystack/sim`, not `scenerystack/joist` Same gotcha as [`Sim`](/api/joist/sim): despite living in the `joist` repository, the published package exports `Screen` from **`scenerystack/sim`**, alongside `Sim` and [`ScreenView`](/api/joist/screen-view). ::: ```ts import { Screen } from 'scenerystack/sim'; import { Tandem } from 'scenerystack/tandem'; import { Property } from 'scenerystack/axon'; ``` ## A minimal example ```ts const screenTandem = Tandem.ROOT.createTandem( 'myScreen' ); const myScreen = new Screen( () => new MyModel(), model => new MyScreenView( model, { tandem: screenTandem.createTandem( 'view' ) } ), { name: new Property( 'My Screen' ), backgroundColorProperty: new Property( 'white' ), tandem: screenTandem } ); ``` ## Constructor ```ts new Screen( createModel: () => M, createView: ( model: M ) => V, providedOptions: ScreenOptions ) ``` `createModel` and `createView` are called lazily by the owning `Sim` (only once the screen is actually needed), not eagerly at `Screen` construction time. `tandem` is the one option that's always required — everything else has a usable default for a single-screen sim. ## Options | Option | Default | Effect | | --- | --- | --- | | `name` | `null` | A `Property` shown as the screen's label; required for multi-screen sims, may be omitted for single-screen sims | | `backgroundColorProperty` | `Property( 'white' )` | Background color behind the screen's view | | `homeScreenIcon` | auto-generated rectangle | Icon shown on the home screen button for this screen | | `navigationBarIcon` | defaults to `homeScreenIcon`, scaled | Icon shown in the navigation bar; both icons must share the same aspect ratio | | `showUnselectedHomeScreenIconFrame` | `false` | Whether to draw a frame around this screen's home-screen icon when it isn't selected | | `showScreenIconFrameForNavigationBarFill` | `null` | Draw a frame around the navigation bar icon when the bar's fill is `'black'` or `'white'` (or `null` for no frame) | | `maxDT` | `0.5` | Caps the `dt` passed to the model/view per frame, to avoid large jumps after e.g. a background tab resumes | | `createKeyboardHelpNode` | `null` | `( tandem ) => Node` building the content shown in the keyboard-help dialog for this screen | | `screenButtonsHelpText` | derived from `name` | Accessible help text on the home screen and navigation bar buttons for this screen | | `instrumentNameProperty` | `true` | Whether `nameProperty` is instrumented for PhET-iO | ## Public API | Member | Description | | --- | --- | | `model` | The constructed model (getter) — asserts if accessed before `initializeModel()` has run | | `view` | The constructed [`ScreenView`](/api/joist/screen-view) (getter) — asserts if accessed before `initializeView()` has run | | `hasModel()` / `hasView()` | Whether the model/view have been constructed yet | | `activeProperty` | `BooleanProperty` — whether this screen is the one currently displayed | | `nameProperty` | `TReadOnlyProperty` — the screen's display name (empty string if `name` was omitted) | | `reset()` | Called by `ResetAllButton` handling; the base implementation is a no-op (background color is intentionally not reset here) | ::: tip Model and view aren't built until the screen is needed `createModel`/`createView` are only invoked once, lazily, when `Sim` initializes that screen — this is what lets multi-screen sims start up faster by deferring work for screens the user hasn't visited yet. Don't rely on side effects in the constructor of a not-yet-selected screen having already run. ::: ======================================================================== Page: ScreenIcon URL: https://veillette.github.io/Almanach/api/joist/screen-icon Source: docs/api/joist/screen-icon.md Category: API | Tags: joist, ScreenIcon, Screen | Status: verified ======================================================================== # ScreenIcon `ScreenIcon` is a `Node` that wraps an arbitrary icon `Node` in a fixed-size background rectangle, automatically scaling and centering the icon to fit — it's the standard way to build the `homeScreenIcon`/`navigationBarIcon` options a [`Screen`](/api/joist/screen) takes. Without it, you'd have to hand-compute scale factors and centering every time an icon's own bounds change (e.g. because it contains a string `Property` that changes with locale). ::: warning `ScreenIcon` is exported from `scenerystack/sim`, not `scenerystack/joist` Same gotcha as [`Screen`](/api/joist/screen), [`Sim`](/api/joist/sim), and [`ScreenView`](/api/joist/screen-view): despite living in the `joist` repository, it's exported from **`scenerystack/sim`**. ::: ```ts import { Screen, ScreenIcon } from 'scenerystack/sim'; import { Circle } from 'scenerystack/scenery'; const homeScreenIcon = new ScreenIcon( new Circle( 40, { fill: 'blue' } ), { fill: 'white' } ); const myScreen = new Screen( createModel, createView, { name: myScreenNameProperty, homeScreenIcon: homeScreenIcon, tandem: screenTandem } ); ``` ## Constructor ```ts new ScreenIcon( iconNode: Node, providedOptions?: ScreenIconOptions ) ``` `iconNode` is made `pickable: false` internally (icons shouldn't intercept input), and a listener on `iconNode.localBoundsProperty` keeps it centered and re-scaled if its bounds ever change after construction. ## Options | Option | Default | Effect | | --- | --- | --- | | `size` | `Screen.MINIMUM_HOME_SCREEN_ICON_SIZE` (`Dimension2( 548, 373 )`) | Size of the background rectangle. There's no `ScreenIcon.MINIMUM_HOME_SCREEN_ICON_SIZE` static — the default value is mirrored on `Screen` (and also importable as the standalone `MINIMUM_HOME_SCREEN_ICON_SIZE` export). Pass `Screen.MINIMUM_NAVBAR_ICON_SIZE` when building a `navigationBarIcon` specifically, so it matches the navigation bar's smaller aspect | | `maxIconWidthProportion` | `0.85` | Max proportion of the background's width the icon is scaled to fill | | `maxIconHeightProportion` | `0.85` | Max proportion of the background's height the icon is scaled to fill | | `fill` | `'white'` | Background rectangle fill | | `stroke` | `null` | Background rectangle stroke | `iconNode` is scaled uniformly (never stretched) by `Math.min` of the two proportion-based limits, then centered on the background — so a non-square icon never overflows either dimension. ::: tip `homeScreenIcon` and `navigationBarIcon` must share the same aspect ratio `Screen` asserts this at construction time, because the navigation bar's icon is effectively a scaled-down version of the home screen's — if you only build one `ScreenIcon`, `navigationBarIcon` defaults to a scaled copy of `homeScreenIcon`, which is usually what you want. Build `navigationBarIcon` separately (with `size: Screen.MINIMUM_NAVBAR_ICON_SIZE`) only if the two icons genuinely need different content, not just different sizes. ::: ## Related - [Screen](/api/joist/screen) — the `homeScreenIcon`/`navigationBarIcon` options that consume a `ScreenIcon`. - [HomeScreen](/api/joist/home-screen) — renders each sim screen's `homeScreenIcon` as a selectable button. - [NavigationBar](/api/joist/navigation-bar) — renders each sim screen's `navigationBarIcon`. ======================================================================== Page: ScreenView URL: https://veillette.github.io/Almanach/api/joist/screen-view Source: docs/api/joist/screen-view.md Category: API | Tags: joist, ScreenView | Status: verified ======================================================================== # ScreenView `ScreenView` is a `Node` subclass that serves as the root of a [`Screen`](/api/joist/screen)'s view. It defines `layoutBounds` — a fixed design-time coordinate frame you lay content out against — and `visibleBoundsProperty`, which tracks how much of that coordinate frame is actually visible after `Sim` scales/letterboxes it to fit the real browser window. ::: warning `ScreenView` is exported from `scenerystack/sim`, not `scenerystack/joist` Same gotcha as [`Sim`](/api/joist/sim) and [`Screen`](/api/joist/screen): despite living in the `joist` repository, it's exported from **`scenerystack/sim`**. ::: ```ts import { ScreenView, type ScreenViewOptions } from 'scenerystack/sim'; import { Text } from 'scenerystack/scenery'; ``` ## A minimal example ```ts class MyScreenView extends ScreenView { public constructor( model: MyModel, providedOptions: ScreenViewOptions ) { super( providedOptions ); const greetingText = new Text( 'Hello, SceneryStack!', { font: '24px sans-serif' } ); greetingText.center = this.layoutBounds.center; this.addChild( greetingText ); } } ``` ## Constructor ```ts new ScreenView( providedOptions?: ScreenViewOptions ) ``` `tandem` defaults to `Tandem.REQUIRED` and, when instrumented, is asserted to be named exactly `'view'` — pass `screenTandem.createTandem( 'view' )` from your `Screen`'s `createView` factory, not an arbitrary name. ## Options | Option | Default | Effect | | --- | --- | --- | | `layoutBounds` | `new Bounds2( 0, 0, 1024, 618 )` | The fixed design-time coordinate frame content is laid out against | | `includePDOMNodes` | `true` | Whether the screen-summary/play-area/control-area accessibility Nodes are added to the PDOM | | `screenSummaryContent` | `null` | A `ScreenSummaryContent` Node describing the screen for the Voicing/description features | ## Public API | Member | Description | | --- | --- | | `layoutBounds` | The fixed `Bounds2` this view's content is designed against — use `this.layoutBounds.center`, `.maxX`, etc. to position children | | `visibleBoundsProperty` | `Property`, in the same coordinate frame as `layoutBounds`; shrinks from the full `layoutBounds` when the actual browser window is a different aspect ratio, so UI meant to be reachable (buttons, controls) should stay inside it rather than assuming all of `layoutBounds` is always shown | | `step( dt )` | No-op by default — override in a subclass if the view itself needs per-frame updates (most simulations only need the model to step) | | `getVoicingOverviewContent()` / `getVoicingDetailsContent()` / `getVoicingHintContent()` | Supply the Voicing feature's "Overview"/"Details"/"Hint" responses; default implementations delegate to `screenSummaryContent` | ::: warning Don't set `pdomOrder` directly on a `ScreenView` `ScreenView` throws if you call `setPDOMOrder()` on it. It maintains its own PDOM structure via `pdomPlayAreaNode` and `pdomControlAreaNode` (protected members) so every screen in every sim gets the same heading structure for screen readers — order your accessible content under those Nodes instead, not the `ScreenView` itself. ::: ======================================================================== Page: Sim URL: https://veillette.github.io/Almanach/api/joist/sim Source: docs/api/joist/sim.md Category: API | Tags: joist, Sim | Status: verified ======================================================================== # Sim `Sim` is the single top-level object for a simulation: it owns the list of `Screen`s, builds the home screen (for multi-screen sims) and navigation bar, drives the `requestAnimationFrame` loop that steps the active screen, and manages global concerns like credits, PhET-iO state, and preferences. Every simulation constructs exactly one `Sim` and calls `start()` on it. ::: warning `Sim` is exported from `scenerystack/sim`, not `scenerystack/joist` The class lives in the `joist` repository internally, but the published package puts `Sim`, [`Screen`](/api/joist/screen), and [`ScreenView`](/api/joist/screen-view) on the **`scenerystack/sim`** subpath. `scenerystack/joist` still exists, but only exports supporting pieces (preferences panels, `CreditsNode`, locale utilities) — not these three classes. ::: ```ts import { Sim, onReadyToLaunch } from 'scenerystack/sim'; import { Property } from 'scenerystack/axon'; ``` ## A minimal example ```ts onReadyToLaunch( () => { const sim = new Sim( new Property( 'My Simulation' ), [ myScreen, anotherScreen ], { credits: { leadDesign: 'Ada Lovelace' } } ); sim.start(); } ); ``` Always construct and start the `Sim` from inside `onReadyToLaunch` — it waits for SceneryStack's asynchronous asset loader (fonts, images, translated strings) to finish before it's safe to build the scene graph. ## Constructor ```ts new Sim( simNameProperty: TReadOnlyProperty, allSimScreens: AnyScreen[], providedOptions?: SimOptions ) ``` `simNameProperty` is a string Property (not a plain string) so the sim's title can be translated; `allSimScreens` is the full ordered list of [`Screen`](/api/joist/screen) instances, in the order they should appear in the navigation bar. ## Options | Option | Default | Effect | | --- | --- | --- | | `credits` | `{}` | A `CreditsData` object rendered in the About dialog (`leadDesign`, `softwareDevelopment`, `team`, etc.) | | `homeScreenWarningNode` | `null` | A `Node` placed onto the home screen, e.g. for an experimental-build warning | | `preferencesModel` | auto-created | A `PreferencesModel` describing which Preferences dialog panels/features are available; omit it to get the default (no extra preferences) | | `webgl` | `SimDisplay.DEFAULT_WEBGL` | Whether the underlying `Display` may use WebGL rendering | | `detachInactiveScreenViews` | `false` | If `true`, only the active screen's `ScreenView` stays in the scene graph (saves memory/perf at the cost of slower screen switches); if `false`, all screens' views remain children and only one is visible | ## Public API | Member | Description | | --- | --- | | `screens` | All screens in the running sim, with the auto-created `HomeScreen` first if one exists | | `simScreens` | Just the sim-specific screens (excludes the `HomeScreen`) | | `selectedScreenProperty` | `Property` — which screen is currently showing | | `activeProperty` | `Property` — settable; whether the whole sim is running and processing user input. Setting it to `false` pauses the sim. Distinct from `browserTabVisibleProperty` (`TReadOnlyProperty`), which tracks the actual browser tab's visibility state | | `isConstructionCompleteProperty` | `TReadOnlyProperty` — becomes `true` once every screen's model and view have been constructed (useful for PhET-iO tooling) | | `dimensionProperty` | `TReadOnlyProperty` — current browser viewport size | | `lookAndFeel` | A `LookAndFeel` instance controlling navigation bar fill/text colors | | `start()` | Kicks off asynchronous screen initialization, then begins the animation-frame loop. Call this once, after construction | | `stepOneFrame()` | Advances model/view/display by a single frame — used by embedding contexts that need manual frame control | ::: tip A single-screen sim needs no home screen icons `Screen` options like `homeScreenIcon`/`navigationBarIcon` only matter once a sim has more than one entry in `allSimScreens` — with exactly one screen, there's no home screen or navigation bar screen button to show them in. See [Screen](/api/joist/screen) and [Your First Simulation](/getting-started/your-first-simulation) for the smallest possible wiring. ::: ======================================================================== Page: SimInfo URL: https://veillette.github.io/Almanach/api/joist/sim-info Source: docs/api/joist/sim-info.md Category: API | Tags: joist, SimInfo, Sim, phet-io | Status: verified ======================================================================== # SimInfo `SimInfo` is a diagnostic snapshot — sim name/version, screen names, browser user agent, viewport size, WebGL support, and (when running under PhET-iO) data-stream/wrapper metadata — collected once when a [`Sim`](/api/joist/sim) starts up. `Sim` constructs exactly one internally (`this.simInfo = new SimInfo( this )`, an internal/private field); as a simulation author you don't construct one yourself or read it off `sim` directly. It exists mostly so PhET-iO's data stream can emit a `simStarted` event describing the environment, and to back the diagnostic info shown in the sim's About dialog. ```ts import type { SimInfoState } from 'scenerystack/sim'; // SimInfo itself is constructed internally by Sim; simulation code doesn't create instances directly. ``` ::: tip This is a read-only diagnostic record, not a live/reactive object Every field in `SimInfoState` is captured once, synchronously, at construction time (`self.navigator.userAgent`, `window.innerWidth`, etc.) — there are no Properties here to `link()` to, and nothing updates after the fact (e.g. resizing the browser window afterward doesn't change the captured `window` field). If you need live values for something `SimInfo` also captures — like viewport size — use `Sim.dimensionProperty` instead. ::: ## Constructor ```ts new SimInfo( sim: Sim ) ``` Constructed once, internally, by `Sim`. ## `SimInfoState` fields | Field | Contents | | --- | --- | | `simName`, `simVersion`, `repoName` | The sim's display name, version string, and repository name | | `screens` | One `{ name, phetioID? }` entry per entry in `sim.screens` | | `url`, `userAgent`, `language`, `referrer`, `randomSeed` | Captured from `location`/`navigator`/`document`/the `randomSeed` query parameter at startup | | `window` | The browser viewport size at startup, as a `"widthxheight"` string | | `pixelRatio`, `isWebGLSupported`, `checkIE11StencilSupport`, `flags` | Rendering-capability diagnostics (device pixel ratio vs. backing-store ratio, WebGL availability, assorted browser input-capability flags) | | `screenPropertyValue`, `wrapperMetadata`, `dataStreamVersion`, `phetioCommandProcessorProtocol` | Populated only when running under PhET-iO | ## Related - [Sim](/api/joist/sim) — constructs the single `SimInfo` instance and is the source of most of its data (`simNameProperty`, `screens`, `version`). ======================================================================== Page: Arc URL: https://veillette.github.io/Almanach/api/kite/arc Source: docs/api/kite/arc.md Category: API | Tags: kite, Arc, segment, path | Status: verified ======================================================================== # Arc `Arc` (from `scenerystack/kite`) is a [`Segment`](/api/kite/shape) describing a piece of a circle — every point on it is equidistant (`radius`) from a `center`, swept from `startAngle` to `endAngle`. It's what `shape.arc( centerX, centerY, radius, startAngle, endAngle, anticlockwise )` appends to the current subpath, and it's also what `LineStyles` uses internally to build `'round'` line caps and joins (see [LineStyles](/api/kite/line-styles)). ```ts import { Arc, Shape } from 'scenerystack/kite'; import { Vector2 } from 'scenerystack/dot'; const halfCircle = new Arc( new Vector2( 0, 0 ), 50, 0, Math.PI, false ); halfCircle.start; // Vector2( 50, 0 ) halfCircle.end; // Vector2( -50, 0 ) (approximately) halfCircle.bounds; // Bounds2 tightly containing the arc // The usual way you'll get one: build it through Shape const shape = new Shape().arc( 0, 0, 50, 0, Math.PI, false ); ``` ::: tip Angles are in radians, and `anticlockwise` follows the Canvas 2D convention `startAngle`/`endAngle` are radians, not degrees, and `anticlockwise` matches the [HTML Canvas `arc()`](http://www.w3.org/TR/2dcontext/#dom-context-2d-arc) parameter of the same name — it's easy to get a swept direction backwards by assuming it means "counterclockwise in screen space," when scenery's y-axis points down, so `anticlockwise: true` sweeps what looks like *clockwise* on screen. If an arc looks like it's going the wrong way, flip this flag before touching the angles. ::: ## Constructor ```ts new Arc( center: Vector2, radius: number, startAngle: number, endAngle: number, anticlockwise: boolean ) ``` If `startAngle`/`endAngle` differ by (approximately) a full turn, the `Arc` represents a complete circle rather than a partial arc — see `isFullPerimeter` below. ## Public API | Member | Description | | --- | --- | | `center`, `radius`, `startAngle`, `endAngle`, `anticlockwise` | The five defining values, each with a getter and a setter (setters recompute cached bounds/tangents) | | `start` / `end` | The endpoint `Vector2`s, derived from `center` + `radius` at `startAngle`/`endAngle` | | `startTangent` / `endTangent` | Normalized tangent direction at each endpoint | | `actualEndAngle` | `endAngle` remapped relative to `startAngle` so it always represents the actual angular sweep direction | | `angleDifference` | The signed angular sweep of the arc, accounting for `anticlockwise` | | `isFullPerimeter` | `true` if the arc is (approximately) a complete circle rather than a partial sweep | | `bounds` | The tight `Bounds2` around the swept arc (not the full circle, unless `isFullPerimeter`) | | `positionAtAngle( angle )` / `tangentAtAngle( angle )` | Position/tangent at a raw angle, rather than a parametric `t` | | `transformed( matrix )` | Returns `Arc \| EllipticalArc` — a uniform-scale/rotation transform stays a circular `Arc`, but a non-uniform scale turns it into an [`EllipticalArc`](/api/kite/elliptical-arc), since circles don't stay circular under non-uniform scaling | | `reversed()` | A new `Arc` tracing the same points in the opposite direction | `Arc` also implements the full [`Segment`](/api/kite/shape) contract shared by every segment type — `positionAt( t )`, `tangentAt( t )`, `curvatureAt( t )`, `subdivided( t )`, and `getArcLength()` all work the same way they do on [`Line`](/api/kite/line-segment), [`Quadratic`](/api/kite/quadratic), and [`Cubic`](/api/kite/cubic). ## Related - [Shape](/api/kite/shape) — how `Arc` and other segments compose into subpaths and shapes; also has the fluent `arc()`/static `Shape.circle()` builders. - [EllipticalArc](/api/kite/elliptical-arc) — the more general segment `Arc.transformed()` can produce under non-uniform scaling. - [LineStyles](/api/kite/line-styles) — `'round'` caps/joins are built from `Arc` segments. ======================================================================== Page: Cubic URL: https://veillette.github.io/Almanach/api/kite/cubic Source: docs/api/kite/cubic.md Category: API | Tags: kite, Cubic, bezier, segment, path | Status: verified ======================================================================== # Cubic `Cubic` (from `scenerystack/kite`) is a [`Segment`](/api/kite/shape) describing a cubic Bézier curve — a `start` point, two control points (`control1`, `control2`) that shape the curve without the curve passing through them, and an `end` point. It's what `shape.cubicCurveTo( cp1x, cp1y, cp2x, cp2y, x, y )` appends to the current subpath, and it's the curve type behind most hand-drawn or design-tool-exported SVG path data (`C`/`c` commands). ```ts import { Cubic, Shape } from 'scenerystack/kite'; import { Vector2 } from 'scenerystack/dot'; const curve = new Cubic( new Vector2( 0, 0 ), // start new Vector2( 30, 100 ), // control1 new Vector2( 70, 100 ), // control2 new Vector2( 100, 0 ) // end ); curve.positionAt( 0.5 ); // the curve's midpoint // The usual way you'll get one: build it through Shape const shape = new Shape().moveTo( 0, 0 ).cubicCurveTo( 30, 100, 70, 100, 100, 0 ); ``` ::: tip Cubics can have cusps and inflection points — flatten before assuming smoothness Unlike `Quadratic`, a `Cubic` can have an S-shape, a cusp (a sharp point where the tangent direction reverses), or self-intersect. `Cubic` exposes `tCusp`, `tInflection1`/`tInflection2`, and `xExtremaT`/`yExtremaT` so algorithms that need monotonic pieces (hit-testing, stroking, tessellation) can `subdivided()` at those `t` values first. If you're writing code that assumes a curve bends only one way, check these before trusting that assumption. ::: ## Constructor ```ts new Cubic( start: Vector2, control1: Vector2, control2: Vector2, end: Vector2 ) ``` ## Public API | Member | Description | | --- | --- | | `start`, `control1`, `control2`, `end` | The four defining points, each with a getter and setter (setters recompute cached bounds/tangents) | | `startTangent` / `endTangent` | Normalized tangent direction at each endpoint — toward `control1` from `start`, and from `control2` toward `end` | | `tCusp` | Parametric `t` of a potential cusp (a point where the derivative vanishes and direction can flip) | | `tInflection1` / `tInflection2` | Parametric `t` values where curvature changes sign (`NaN` if not applicable) | | `xExtremaT` / `yExtremaT` | Arrays of `t` values where the x- or y-derivative is zero, used to compute tight bounds | | `bounds` | The tight `Bounds2` around the curve, accounting for the extrema above | | `subdivided( t )` | Splits into two `Cubic`s at parametric `t`, each an exact piece of the original curve | | `getArcLength()` | Approximate arc length, computed via recursive subdivision (see [Segment](/api/kite/shape)'s shared `getArcLength()`) | | `reversed()` | A new `Cubic` with `start`/`end` swapped and `control1`/`control2` swapped | | `transformed( matrix )` | A new `Cubic` with all four points transformed by a [`Matrix3`](/api/dot/matrix3) | `Cubic` also implements the shared [`Segment`](/api/kite/shape) contract — `positionAt( t )`, `tangentAt( t )`, `curvatureAt( t )` — the same as [`Line`](/api/kite/line-segment), [`Arc`](/api/kite/arc), and [`Quadratic`](/api/kite/quadratic). ## Related - [Shape](/api/kite/shape) — the fluent `cubicCurveTo()` builder. - [Quadratic](/api/kite/quadratic) — the simpler one-control-point curve. ======================================================================== Page: EllipticalArc URL: https://veillette.github.io/Almanach/api/kite/elliptical-arc Source: docs/api/kite/elliptical-arc.md Category: API | Tags: kite, EllipticalArc, segment, path | Status: verified ======================================================================== # EllipticalArc `EllipticalArc` (from `scenerystack/kite`) is a [`Segment`](/api/kite/shape) that generalizes [`Arc`](/api/kite/arc) to ellipses: instead of a single `radius`, it has independent `radiusX`/`radiusY`, plus a `rotation` for the ellipse's semi-major axis. It's what `shape.ellipticalArc( centerX, centerY, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise )` appends to the current subpath, and it's also what `Arc.transformed()` produces when a circular arc is transformed by a non-uniform scale (circles don't stay circular once x and y are scaled differently). ```ts import { EllipticalArc, Shape } from 'scenerystack/kite'; import { Vector2 } from 'scenerystack/dot'; const halfEllipse = new EllipticalArc( new Vector2( 0, 0 ), 80, 40, 0, 0, Math.PI, false ); halfEllipse.start; // Vector2( 80, 0 ) halfEllipse.bounds; // Bounds2 tightly containing the swept arc // The usual way you'll get one: build it through Shape const shape = new Shape().ellipticalArc( 0, 0, 80, 40, 0, 0, Math.PI, false ); ``` ::: tip A circular `Arc` is just an `EllipticalArc` with `radiusX === radiusY` and `rotation === 0` kite keeps them as separate classes for efficiency (circular math is cheaper), not because they're conceptually different — if you're writing code generic over "some kind of arc segment," `EllipticalArc` is the superset. Internally, `EllipticalArc` computes a `unitTransform` (a `Transform3` mapping the ellipse to a unit circle) and delegates position/tangent queries to the equivalent `Arc` on that unit circle, exposed as `unitArcSegment`. ::: ## Constructor ```ts new EllipticalArc( center: Vector2, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, anticlockwise: boolean ) ``` `rotation` (radians) rotates the whole ellipse — including which direction counts as `radiusX` vs `radiusY` — around `center`, applied before `startAngle`/`endAngle` are measured. ## Public API | Member | Description | | --- | --- | | `center`, `radiusX`, `radiusY`, `rotation`, `startAngle`, `endAngle`, `anticlockwise` | The defining values, each with a getter and setter (setters recompute cached bounds/tangents) | | `start` / `end` | The endpoint `Vector2`s | | `startTangent` / `endTangent` | Normalized tangent direction at each endpoint | | `actualEndAngle` | `endAngle` remapped relative to `startAngle` to reflect the true angular sweep | | `angleDifference` | The signed angular sweep, accounting for `anticlockwise` | | `isFullPerimeter` | `true` if the arc sweeps a complete ellipse | | `unitTransform` | The `Transform3` mapping this ellipse to/from a unit circle | | `unitArcSegment` | The equivalent circular [`Arc`](/api/kite/arc) on that unit circle | | `bounds` | The tight `Bounds2` around the swept arc | | `positionAtAngle( angle )` / `tangentAtAngle( angle )` | Position/tangent at a raw angle, rather than a parametric `t` | | `transformed( matrix )` | Always returns another `EllipticalArc` (unlike `Arc.transformed()`, which can stay circular) | | `reversed()` | A new `EllipticalArc` tracing the same points in the opposite direction | `EllipticalArc` also implements the shared [`Segment`](/api/kite/shape) contract — `positionAt( t )`, `tangentAt( t )`, `curvatureAt( t )`, `subdivided( t )`, `getArcLength()` — the same as [`Arc`](/api/kite/arc), [`Line`](/api/kite/line-segment), [`Quadratic`](/api/kite/quadratic), and [`Cubic`](/api/kite/cubic). ## Related - [Shape](/api/kite/shape) — the fluent `ellipticalArc()` builder and static `Shape.ellipse()` factory. - [Arc](/api/kite/arc) — the circular special case, and what a uniformly-scaled `EllipticalArc.transformed()` conceptually mirrors. ======================================================================== Page: Graph and Boolean Shape Operations URL: https://veillette.github.io/Almanach/api/kite/graph-and-boolean-operations Source: docs/api/kite/graph-and-boolean-operations.md Category: API | Tags: kite, Graph, Shape, Face, HalfEdge, Loop, Boundary, Edge, Vertex, boolean-operations | Status: complete ======================================================================== # Graph and Boolean Shape Operations `Graph` (from `scenerystack/kite`, exported alongside `Shape` from the same module) is the machinery behind [`Shape`](/api/kite/shape)'s boolean operations — `Shape.union()`, `Shape.intersection()`, `Shape.xor()`, and shape clipping. It builds a planar subdivision (a doubly-connected-edge-list-style structure of `Vertex`/`Edge`/`HalfEdge`/`Loop`/`Boundary`/`Face` objects) out of one or more shapes' segments, resolves every self-intersection and overlap between them, and then re-derives a clean output `Shape` from whichever faces satisfy a winding-number rule. In practice, you almost never touch `Graph` directly — you call the `Shape` static methods that use it internally. ```ts import { Shape } from 'scenerystack/kite'; const circle = Shape.circle( 50, 50, 40 ); const square = Shape.rectangle( 30, 30, 60, 60 ); const combined = Shape.union( [ circle, square ] ); // everything covered by either shape const overlap = Shape.intersection( [ circle, square ] ); // only the region covered by both const symmetricDifference = Shape.xor( [ circle, square ] ); // covered by exactly one, not both ``` ## The `Shape` entry points | Method | Result | | --- | --- | | `Shape.union( shapes )` | The region covered by at least one input shape | | `Shape.intersection( shapes )` | The region covered by every input shape | | `Shape.xor( shapes )` | The region covered by an odd number of input shapes (their symmetric difference) | All three (and `Graph.simplifyNonZero( shape )`, used to clean up self-intersections in a single shape) are implemented by constructing a `Graph`, adding each input shape with a distinct integer ID, computing the planar subdivision, then filtering faces by a `windingMap` predicate — a record of, for each shape ID, how many times that shape's boundary winds around a given face. `union` keeps any face where at least one shape's winding is non-zero; `intersection` keeps only faces where *every* shape's winding is non-zero; `xor` keeps faces where an odd number of shapes wind around it. ## Clipping one shape to another `Graph.clipShape( clipAreaShape, shape, options? )` returns the portion of `shape` that falls inside `clipAreaShape`, useful for anything that needs to restrict a drawn path to a clip region using real geometry (rather than a Canvas/SVG clip path) — for example, precomputing the clipped area to measure it, or to further combine it with other shapes. | Option | Default | Effect | | --- | --- | --- | | `includeExterior` | `false` | Keep the parts of `shape` outside `clipAreaShape` | | `includeBoundary` | `true` | Keep the parts of `shape` exactly on `clipAreaShape`'s boundary | | `includeInterior` | `true` | Keep the parts of `shape` inside `clipAreaShape` | ## What's inside a `Graph`, briefly If you do need to go one level deeper — say, to inspect exactly which faces a combination produced — a `Graph` is built from these pieces, in rough dependency order: `Vertex` (a point where segments meet), `Edge` (a `Segment` between two vertices, with a `forwardHalf`/`reversedHalf` pair of `HalfEdge`s for its two traversal directions), `Loop` (a cyclic sequence of half-edges from one original subpath), `Boundary` (a maximal cycle of half-edges bounding a region, possibly nested as holes via `childBoundaries`), and `Face` (a region bounded by a `Boundary`, with a `windingMap` recording each input shape's winding number there and a `filled` flag once `computeFaceInclusion()` has run). The typical pipeline a `Graph` runs through is: `addShape()` for each input, `computeSimplifiedFaces()` (resolves self-intersections/overlaps and builds faces), `computeFaceInclusion( windingMapFilter )` (marks `filled` per your predicate), `createFilledSubGraph()`, then `facesToShape()`/`Shape.fromGraph()` to get the resulting `Shape` back out. ::: tip Boolean operations are exact geometry, not a rendering trick Because `Graph` resolves the actual intersection points and rebuilds real segments, the `Shape` returned by `union`/`intersection`/`xor`/`clipShape` is genuine vector geometry — it has correct `bounds`, `containsPoint()`, and stroke behavior, unlike compositing two overlapping shapes with Canvas globalCompositeOperation tricks. The tradeoff is cost: computing intersections across many segments is the dominant expense in these operations, so avoid recomputing a boolean combination every frame for shapes that aren't actually changing. ::: ======================================================================== Page: Line URL: https://veillette.github.io/Almanach/api/kite/line-segment Source: docs/api/kite/line-segment.md Category: API | Tags: kite, Line, KiteLine, segment, path | Status: verified ======================================================================== # Line kite's `Line` is a [`Segment`](/api/kite/shape) — a straight line between two points, and one of the primitive pieces that a [`Shape`](/api/kite/shape)'s subpaths are built from. It is **not** a scene-graph node: it has no `fill`, `stroke`, or `addChild`, and rendering nothing on its own. For a drawable straight line, use scenery's [`Line`](/api/scenery/line) `Path` subclass instead — this page is about the geometry primitive underneath `Shape.lineTo()`, not the thing you'd add to a scene graph. You'll rarely construct a kite `Line` directly; `shape.lineTo( x, y )` and `Shape.lineSegment( p1, p2 )` create them for you. Where you do encounter the class directly is inspecting a built `Shape`'s internals, e.g. `shape.subpaths[ 0 ].segments[ 0 ]`. ::: warning Imported as `KiteLine`, not `Line` `scenerystack/kite`'s barrel re-exports this class as `KiteLine` specifically to avoid a naming collision with scenery's `Line` node: `import { KiteLine } from 'scenerystack/kite'`. There is no `Line` export from `scenerystack/kite` — only `scenerystack/scenery` exports a `Line` (the renderable one). ::: ```ts import { KiteLine, Shape } from 'scenerystack/kite'; import { Vector2 } from 'scenerystack/dot'; const segment = new KiteLine( new Vector2( 0, 0 ), new Vector2( 100, 50 ) ); segment.start; // Vector2( 0, 0 ) segment.end; // Vector2( 100, 50 ) segment.bounds; // Bounds2 tightly containing the segment // The usual way you'll actually get one: build it through Shape const shape = new Shape().moveTo( 0, 0 ).lineTo( 100, 50 ); shape.subpaths[ 0 ].segments[ 0 ] instanceof KiteLine; // true ``` ## Constructor ```ts new KiteLine( start: Vector2, end: Vector2 ) ``` ## Public API | Member | Description | | --- | --- | | `start` / `end` | The two endpoints (`Vector2`, settable via `setStart()`/`setEnd()` or the `start`/`end` setters — either recomputes bounds and tangents) | | `startTangent` / `endTangent` | Both equal the normalized direction from `start` to `end` — a straight line has one constant tangent along its whole length | | `bounds` | The tight axis-aligned `Bounds2` around the two endpoints | | `positionAt( t )` | Linear interpolation between `start` and `end`, `0 <= t <= 1` | | `tangentAt( t )` | Constant regardless of `t` — unlike other segment types, `Line.tangentAt()` actually returns the same *normalized* direction as `startTangent`/`endTangent` (it delegates straight to `getStartTangent()`), not a raw `end.minus( start )` | | `getArcLength()` | Equal to `start.distance( end )` — a line needs no subdivision to measure exactly | | `reversed()` | A new `KiteLine` with `start`/`end` swapped | | `transformed( matrix )` | A new `KiteLine` with both endpoints transformed by a [`Matrix3`](/api/dot/matrix3) | See [Shape](/api/kite/shape) for how segments compose into subpaths and shapes, [Subpath](/api/kite/subpath) for the ordered-run container these segments live in, and [LineStyles](/api/kite/line-styles) for how a stroke turns a run of segments into an outline (`Line`'s bevel/miter joins and butt/round/square caps are built out of `KiteLine` and [`Arc`](/api/kite/arc) segments). ======================================================================== Page: LineStyles URL: https://veillette.github.io/Almanach/api/kite/line-styles Source: docs/api/kite/line-styles.md Category: API | Tags: kite, LineStyles, stroke, path | Status: verified ======================================================================== # LineStyles `LineStyles` (from `scenerystack/kite`) is a small immutable-in-practice value object bundling every parameter that affects how a stroke is drawn: `lineWidth`, `lineCap`, `lineJoin`, `lineDash`, `lineDashOffset`, and `miterLimit`. It mirrors the [SVG stroke properties](https://svgwg.org/svg2-draft/painting.html) of the same names. You'll mostly encounter it as the argument to [`Shape.getStrokedShape()`](/api/kite/shape) — the method that computes the actual outline a stroke would draw, as a new fillable `Shape` — rather than constructing it to configure a scenery `Path`'s visual stroke directly (that's done with `Path`'s own `lineWidth`/`lineCap`/`lineJoin`/... options). ```ts import { LineStyles, Shape } from 'scenerystack/kite'; const styles = new LineStyles( { lineWidth: 4, lineCap: 'round', lineJoin: 'round' } ); const shape = new Shape().moveTo( 0, 0 ).lineTo( 100, 0 ).lineTo( 50, 80 ); const strokedOutline = shape.getStrokedShape( styles ); // a new Shape describing the stroke's outline ``` ::: tip `getStrokedShape()` is for hit-testing a stroke-only path, not for changing how it's drawn If you just want a `Path` to render with a particular `lineWidth`/`lineCap`/`lineJoin`, set those directly as `Path` options — scenery renders strokes natively via Canvas/SVG without needing a `LineStyles`. Construct a `LineStyles` and call `shape.getStrokedShape( styles )` only when you need the *outline geometry itself*, e.g. to hit-test clicks against a stroked-but-unfilled path, or to feed the stroke outline into another kite operation like `Shape.union()`. ::: ## Constructor ```ts new LineStyles( options?: LineStylesOptions ) ``` ## Options | Option | Default | Effect | | --- | --- | --- | | `lineWidth` | `1` | Total stroke width — the outline extends `lineWidth / 2` to each side of the path | | `lineCap` | `'butt'` | Appearance at the ends of an open subpath: `'butt'` (flat, at the endpoint), `'round'` (a semicircle, built from an [`Arc`](/api/kite/arc)), or `'square'` (flat, extended by `lineWidth / 2` past the endpoint) | | `lineJoin` | `'miter'` | Appearance where two segments meet: `'miter'` (sharp corner, extended until it meets or falls back to `'bevel'` past `miterLimit`), `'round'` (a circular fillet), or `'bevel'` (a single flat cut across the gap) | | `lineDash` | `[]` | Alternating dash/gap lengths (`[dash, gap, dash, gap, ...]`); empty means a solid line | | `lineDashOffset` | `0` | Offset into the `lineDash` pattern at which the dash starts, measured along the path | | `miterLimit` | `10` | How sharp a `'miter'` join is allowed to get (as `1 / sin(angle / 2)`) before falling back to a bevel — prevents extremely acute corners from producing enormous spikes | ## Public API | Member | Description | | --- | --- | | `equals( other )` | Value equality across all six properties (including element-wise `lineDash` comparison) | | `copy()` | A new `LineStyles` with the same values | | `leftJoin( center, fromTangent, toTangent )` / `rightJoin( ... )` | Build the array of segments forming a join on a given side, per `lineJoin` — used internally by `Subpath.stroked()` | | `cap( center, tangent )` | Build the array of segments forming an end cap at a subpath endpoint, per `lineCap` | ## Related - [Shape](/api/kite/shape) — `getStrokedShape( lineStyles )` is the main entry point that consumes a `LineStyles`. - [Subpath](/api/kite/subpath) — `subpath.stroked( lineStyles )` does the actual per-subpath outline construction that `Shape.getStrokedShape()` delegates to. - [Arc](/api/kite/arc) — `'round'` caps and joins are literally built from `Arc` segments. ======================================================================== Page: Quadratic URL: https://veillette.github.io/Almanach/api/kite/quadratic Source: docs/api/kite/quadratic.md Category: API | Tags: kite, Quadratic, bezier, segment, path | Status: verified ======================================================================== # Quadratic `Quadratic` (from `scenerystack/kite`) is a [`Segment`](/api/kite/shape) describing a quadratic Bézier curve — a `start` point, one `control` point the curve bends toward (but usually doesn't pass through), and an `end` point. It's what `shape.quadraticCurveTo( cpx, cpy, x, y )` appends to the current subpath. Quadratic curves are less common in hand-authored shapes than [`Cubic`](/api/kite/cubic) (SVG path data and most design tools default to cubic curves), but they're cheaper to evaluate and show up in font glyph outlines and some procedurally-generated shapes. ```ts import { Quadratic, Shape } from 'scenerystack/kite'; import { Vector2 } from 'scenerystack/dot'; const curve = new Quadratic( new Vector2( 0, 0 ), // start new Vector2( 50, 100 ), // control new Vector2( 100, 0 ) // end ); curve.positionAt( 0.5 ); // the curve's midpoint (not the control point) // The usual way you'll get one: build it through Shape const shape = new Shape().moveTo( 0, 0 ).quadraticCurveTo( 50, 100, 100, 0 ); ``` ::: tip The curve doesn't pass through its control point It's tempting to read `control` as a point the curve visits, but a Bézier control point only *pulls* the curve toward it — the curve touches `start` and `end` only. If you need a curve that passes through a specific interior point, either solve for the control point that produces it, or use several segments (`subdivided( t )`) to compose the shape you actually want. ::: ## Constructor ```ts new Quadratic( start: Vector2, control: Vector2, end: Vector2 ) ``` ## Public API | Member | Description | | --- | --- | | `start`, `control`, `end` | The three defining points, each with a getter and setter (setters recompute cached bounds/tangents) | | `startTangent` / `endTangent` | Normalized tangent direction at each endpoint — points from `start` toward `control`, and from `control` toward `end`, respectively | | `tCriticalX` / `tCriticalY` | The parametric `t` where the x- or y-derivative is zero (`NaN` if that extremum falls outside `[0, 1]`) — used internally to compute tight bounds | | `bounds` | The tight `Bounds2` around the curve (accounting for the extrema above, not just the three defining points) | | `subdivided( t )` | Splits into two `Quadratic`s at parametric `t`, each an exact piece of the original curve | | `getArcLength()` | Approximate arc length, computed via recursive subdivision (see [Segment](/api/kite/shape)'s shared `getArcLength()`) | | `reversed()` | A new `Quadratic` with `start`/`end` swapped and `control` unchanged | | `transformed( matrix )` | A new `Quadratic` with all three points transformed by a [`Matrix3`](/api/dot/matrix3) | `Quadratic` also implements the shared [`Segment`](/api/kite/shape) contract — `positionAt( t )`, `tangentAt( t )`, `curvatureAt( t )` — the same as [`Line`](/api/kite/line-segment), [`Arc`](/api/kite/arc), and [`Cubic`](/api/kite/cubic). ## Related - [Shape](/api/kite/shape) — the fluent `quadraticCurveTo()` builder. - [Cubic](/api/kite/cubic) — the two-control-point curve most SVG/design-tool path data actually uses. ======================================================================== Page: RayIntersection and SegmentIntersection URL: https://veillette.github.io/Almanach/api/kite/ray-and-segment-intersections Source: docs/api/kite/ray-and-segment-intersections.md Category: API | Tags: kite, RayIntersection, SegmentIntersection, Segment, Shape, intersection | Status: complete ======================================================================== # RayIntersection and SegmentIntersection `RayIntersection` and `SegmentIntersection` (both from `scenerystack/kite`) are small, immutable data classes describing the result of two different kinds of intersection query. `RayIntersection` describes where a [`Ray2`](/api/dot/ray2) crosses a [`Segment`](/api/kite/segment) or [`Shape`](/api/kite/shape) — the result of `segment.intersection( ray )` or `shape.intersection( ray )`. `SegmentIntersection` describes where two segments cross each other — the result of `Segment.intersect( a, b )`, used internally by shape simplification and boolean operations. ```ts import { Shape, Segment } from 'scenerystack/kite'; import { Ray2, Vector2 } from 'scenerystack/dot'; const shape = Shape.circle( 0, 0, 50 ); const ray = new Ray2( new Vector2( -100, 0 ), new Vector2( 1, 0 ) ); const hits = shape.intersection( ray ); // RayIntersection[] hits[ 0 ].point; // Vector2(-50, 0) - where the ray first crosses the circle hits[ 0 ].distance; // 50 - distance from the ray's origin to that point ``` ## `RayIntersection` One crossing point between a ray and a single segment (a `Shape`'s `intersection( ray )` collects these across every segment in every subpath). | Field | Meaning | | --- | --- | | `point` | The `Vector2` location of the intersection | | `distance` | Distance from the ray's origin to `point` (always `>= 0`) | | `normal` | Unit normal to the segment at the intersection, oriented so its dot product with the ray's direction is `<= 0` | | `wind` | `1` or `-1` — the winding contribution of this crossing, used by the non-zero fill rule that backs `Shape.containsPoint()` | | `t` | The parametric value (`0` to `1`) along the specific segment where the crossing occurs | `RayIntersection` is a plain constructed value (`new RayIntersection( distance, point, normal, wind, t )`) — you receive instances of it as return values; you won't typically construct one yourself. ## `SegmentIntersection` One crossing point between two arbitrary segments, as returned by the static `Segment.intersect( a, b )`. | Field | Meaning | | --- | --- | | `point` | The `Vector2` location of the intersection | | `aT` | Parametric value (`0` to `1`) along the *first* segment (`a`) where the crossing occurs | | `bT` | Parametric value (`0` to `1`) along the *second* segment (`b`) where the crossing occurs | | `getSwapped()` | Returns an equivalent `SegmentIntersection` with `aT`/`bT` swapped — useful if you called `Segment.intersect( a, b )` but want the result as if you'd called `Segment.intersect( b, a )` | ```ts import { Segment, Shape } from 'scenerystack/kite'; const line = Shape.lineSegment( 0, -50, 0, 50 ).subpaths[ 0 ].segments[ 0 ]; const circleArc = Shape.circle( 0, 0, 30 ).subpaths[ 0 ].segments[ 0 ]; const crossings = Segment.intersect( line, circleArc ); // SegmentIntersection[] ``` ::: tip These describe *where* two things cross — not whether a shape contains a point `shape.containsPoint( point )` and boolean shape operations (`Shape.union`/`intersection`/`xor`, see [Graph and Boolean Shape Operations](/api/kite/graph-and-boolean-operations)) are built out of exactly these intersection primitives internally (`RayIntersection.wind` for hit-testing, `SegmentIntersection` for splitting overlapping segments during simplification) — but as an API consumer you'd reach for `RayIntersection`/`SegmentIntersection` directly only for a custom geometry query (e.g. "where along this path does a laser beam first hit"), not for ordinary hit-testing or shape combination, which already have dedicated, higher-level methods. ::: ======================================================================== Page: Segment URL: https://veillette.github.io/Almanach/api/kite/segment Source: docs/api/kite/segment.md Category: API | Tags: kite, Segment, Arc, Cubic, Quadratic, Line, EllipticalArc, path | Status: complete ======================================================================== # Segment `Segment` (from `scenerystack/kite`) is the abstract base class behind every curve type kite offers: [`Arc`](/api/kite/arc), [`Cubic`](/api/kite/cubic), [`Quadratic`](/api/kite/quadratic), [`Line`](/api/kite/line-segment) (exported as `KiteLine`), and [`EllipticalArc`](/api/kite/elliptical-arc) all extend it. A [`Shape`](/api/kite/shape) is ultimately just an array of [`Subpath`](/api/kite/subpath)s, and each `Subpath` is an ordered array of `Segment`s — this page documents the contract they all share, so you can treat a mixed array of segment types uniformly (e.g. when walking `shape.subpaths[i].segments`) without caring which concrete subclass each one is. ```ts import { Shape, Segment } from 'scenerystack/kite'; const shape = new Shape().moveTo( 0, 0 ).lineTo( 100, 0 ).quadraticCurveTo( 150, 50, 100, 100 ); for ( const subpath of shape.subpaths ) { for ( const segment: Segment of subpath.segments ) { // Every segment, regardless of concrete type, supports this same API: console.log( segment.start.toString(), '->', segment.end.toString() ); console.log( 'arc length:', segment.getArcLength() ); console.log( 'midpoint:', segment.positionAt( 0.5 ).toString() ); } } ``` Every segment is parameterized over `t` from `0` (its `start`) to `1` (its `end`) — the same convention `Cubic` and `Quadratic`'s Bézier parameter uses, and `Arc`/`EllipticalArc` map linearly onto their angular sweep. ## The shared contract These members are declared `abstract` on `Segment` — every concrete subclass must implement them, each according to its own geometry: | Member | Effect | | --- | --- | | `start` / `end` (getters) | The segment's endpoints, at `t=0` and `t=1` | | `startTangent` / `endTangent` (getters) | Normalized tangent vectors at the endpoints, pointing in the direction of travel | | `bounds` (getter) / `getBounds()` | The segment's axis-aligned `Bounds2` | | `positionAt( t )` | The point at parametric value `t` (`0 <= t <= 1`) | | `tangentAt( t )` | The non-normalized tangent `(dx/dt, dy/dt)` at `t` | | `curvatureAt( t )` | Signed curvature at `t` (positive for a visually-clockwise turn) | | `subdivided( t )` | Splits the segment into up to 2 sub-segments at `t`, together tracing the same curve | | `getInteriorExtremaTs()` | The interior `t` values (`0 < t < 1`) where `dx/dt` or `dy/dt` is zero — subdividing at these produces monotone pieces | | `strokeLeft( lineWidth )` / `strokeRight( lineWidth )` | The offset-curve segments for the logical left/right side of a stroke | | `windingIntersection( ray )` | The winding-number contribution of a ray/segment intersection, used by `Shape.containsPoint()` | | `intersection( ray )` | The list of [`RayIntersection`](/api/kite/ray-and-segment-intersections)s between this segment and a `Ray2` | | `getOverlaps( segment, epsilon? )` | Whether (and how) this segment overlaps another of a compatible type, for shape-simplification purposes | | `getSignedAreaFragment()` | This segment's contribution to a subpath's signed area (Green's theorem) | | `getNondegenerateSegments()` | A list of equivalent, non-degenerate segments (e.g. an `Arc` with zero radius simplifies away) | | `transformed( matrix )` | A new segment of the same or an equivalent type, transformed by a `Matrix3` | | `reversed()` | A new segment tracing the same curve with `start`/`end` swapped | | `getSVGPathFragment()` | The SVG path-data fragment for this segment alone (assuming the pen is already at `start`) | | `writeToContext( context )` | Draws the segment to a `CanvasRenderingContext2D`, assuming the context is already at `start` | | `serialize()` / `Segment.deserialize()` | Round-trip to/from a plain serializable object | | `invalidate()` | Recomputes any cached derived state after mutating the segment in place | ## Shared concrete behavior `Segment` also implements a handful of concrete methods, in terms of the abstract ones above, so every subclass gets them for free: | Member | Effect | | --- | --- | | `getArcLength( distanceEpsilon?, curveEpsilon?, maxLevels? )` | Adaptive-subdivision arc length estimate — recursively `subdivided()` until each piece is "sufficiently flat," then sums straight-line distances | | `slice( t0, t1 )` | The portion of the segment between two parametric values, via `subdivided()` | | `subdivisions( tList )` | Splits at every `t` in a sorted list, in one pass | | `subdividedIntoMonotone()` | `subdivisions( this.getInteriorExtremaTs() )` | | `isSufficientlyFlat( distanceEpsilon, curveEpsilon )` | Whether the segment is close enough to its start-end chord to treat as flat, used internally by `getArcLength()` and piecewise-linear conversion | | `getDashValues( lineDash, lineDashOffset, ... )` | Parametric `t` values where dash boundaries fall, for rendering a dashed stroke | | `toPiecewiseLinearSegments( options )` | Approximates the curve as a series of `Line` segments | | `getClosestPoints( point )` / `Segment.closestToPoint( segments, point, threshold )` | Finds the closest point(s) on a segment (or set of segments) to an arbitrary point | | `Segment.intersect( a, b )` | Static: all [`SegmentIntersection`](/api/kite/ray-and-segment-intersections)s between two arbitrary segments | ::: tip Reach for the shared `Segment` API when writing type-agnostic geometry code Anything that processes `shape.subpaths[i].segments` generically — measuring total path length, hit-testing, generating a dashed outline, or converting to line-only approximations for export — should be written against this shared `Segment` contract rather than switching on `instanceof Arc`/`Cubic`/etc. The five concrete pages ([Arc](/api/kite/arc), [Cubic](/api/kite/cubic), [Quadratic](/api/kite/quadratic), [Line](/api/kite/line-segment), [EllipticalArc](/api/kite/elliptical-arc)) only need to document what's *specific* to each curve type — their constructors and shape-specific accessors (`center`/`radius` for `Arc`, control points for `Cubic`/`Quadratic`) — since everything on this page already applies uniformly. ::: ======================================================================== Page: Shape URL: https://veillette.github.io/Almanach/api/kite/shape Source: docs/api/kite/shape.md Category: API | Tags: kite, Shape, path | Status: verified ======================================================================== # Shape `Shape` (from `scenerystack/kite`) describes a 2D path — one or more subpaths built from line, quadratic, cubic, and arc segments — independent of how it's drawn. It's the geometry engine behind scenery's [`Path`](/api/scenery/path) (and `Path`'s subclasses like `Rectangle`, `Circle`, and `Line`): a `Path` node just takes a `Shape` and renders it with fill/stroke. ```ts import { Shape } from 'scenerystack/kite'; import { Vector2 } from 'scenerystack/dot'; const triangle = new Shape() .moveTo( 0, 0 ) .lineTo( 100, 0 ) .lineTo( 50, 80 ) .close(); triangle.bounds; // Bounds2 tightly containing the triangle triangle.containsPoint( new Vector2( 50, 40 ) ); // true ``` ## Building shapes with the fluent API The constructor `new Shape()` starts empty (or you can pass an SVG path-data string, e.g. `new Shape( 'M0,0 L100,0 L50,80 Z' )`). From there, chain path-drawing commands — each one returns `this`, so calls read like a pen tracing a line: | Method | Draws | | --- | --- | | `moveTo( x, y )` / `moveToPoint( vector )` | Starts a new subpath at a point (no line drawn) | | `lineTo( x, y )` / `lineToPoint( vector )` | A straight line from the current point | | `quadraticCurveTo( cpx, cpy, x, y )` | A quadratic Bézier curve | | `cubicCurveTo( cp1x, cp1y, cp2x, cp2y, x, y )` | A cubic Bézier curve | | `arc( centerX, centerY, radius, startAngle, endAngle, anticlockwise? )` | A circular arc | | `ellipticalArc( centerX, centerY, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise? )` | An elliptical arc | | `rect( x, y, width, height )` | An axis-aligned rectangle subpath | | `polygon( vertices )` | A closed polygon through the given `Vector2[]` points | | `close()` | Closes the current subpath back to its start point | Each of these also has a `...Relative` variant (`lineToRelative`, `moveToRelative`, ...) that interprets its arguments as a displacement from the current point rather than an absolute position. ## Static factories For common shapes, the static factories are usually more direct than the fluent builder: ```ts Shape.rectangle( 0, 0, 100, 50 ); // same as Shape.rect(...) Shape.circle( 50, 50, 20 ); // centerX, centerY, radius Shape.circle( new Vector2( 50, 50 ), 20 ); // center point, radius Shape.ellipse( 50, 50, 30, 20, 0 ); // centerX, centerY, radiusX, radiusY, rotation Shape.roundRect( 0, 0, 100, 50, 8, 8 ); // x, y, width, height, arc-width, arc-height Shape.polygon( [ new Vector2( 0, 0 ), new Vector2( 10, 0 ), new Vector2( 5, 10 ) ] ); Shape.lineSegment( 0, 0, 100, 100 ); // or Shape.lineSegment( p1, p2 ) Shape.regularPolygon( 6, 40 ); // sides, radius ``` `Shape.union( shapes )`, `Shape.intersection( shapes )`, and `Shape.xor( shapes )` perform boolean operations across an array of shapes, using kite's internal `Graph` machinery. ## Querying and transforming a shape | Method | Effect | | --- | --- | | `bounds` (getter, backed by `getBounds()`) | The tight-fitting axis-aligned [`Bounds2`](/api/dot/bounds2) | | `containsPoint( point )` | Hit-testing — whether the point is inside the shape (respecting winding rule) | | `copy()` | A deep copy of the shape | | `transformed( matrix )` | A new `Shape` with every segment transformed by a [`Matrix3`](/api/dot/matrix3) | | `getStrokedShape( lineStyles )` | A new `Shape` describing the *outline* a stroke of the given `LineStyles` would draw — useful for hit-testing a stroked-only path | ::: tip `Shape` is mutable while you build it, but treat finished shapes as immutable The fluent `moveTo`/`lineTo`/`arc`/... calls mutate the `Shape` instance in place (that's why they can be chained). Once a `Shape` is handed to a scenery `Path` (`new Path( shape )`), don't keep mutating that same instance to "animate" the path — `Path` doesn't automatically know the shape changed unless you call `path.shape = newShape` (or `path.invalidateShape()`), so prefer building a fresh `Shape` per frame, or wrap it in a `Property` if a shape needs to change reactively. Note also that `Shape.transformed()`, unlike the drawing methods, returns a **new** `Shape` rather than mutating the original. ::: ## Related - [Path](/api/scenery/path) — the scenery `Node` that renders a `Shape` with fill/stroke. - [Bounds2](/api/dot/bounds2) — the type returned by `shape.bounds`. - [Matrix3](/api/dot/matrix3) — the transform type accepted by `shape.transformed()`. - [Vector2](/api/dot/vector2) — the point type used throughout `Shape`'s construction methods. ======================================================================== Page: Subpath URL: https://veillette.github.io/Almanach/api/kite/subpath Source: docs/api/kite/subpath.md Category: API | Tags: kite, Subpath, path | Status: verified ======================================================================== # Subpath `Subpath` (from `scenerystack/kite`) holds one connected, ordered run of [`Segment`](/api/kite/shape)s — the layer between individual segments and a whole [`Shape`](/api/kite/shape). Every `Shape` is really just an array of these: `shape.subpaths` is a `Subpath[]`, and each call to `moveTo()` on a `Shape` starts a new one. A single `moveTo().lineTo().lineTo()` traces one `Subpath`; calling `moveTo()` again (or drawing a shape with multiple disconnected pieces, like the two loops of a letter "B") starts another. ```ts import { Shape } from 'scenerystack/kite'; const shape = new Shape() .moveTo( 0, 0 ).lineTo( 100, 0 ).lineTo( 50, 80 ).close() // subpath 0: a triangle .moveTo( 200, 0 ).lineTo( 300, 0 ); // subpath 1: an open line shape.subpaths.length; // 2 shape.subpaths[ 0 ].closed; // true - closed with .close() shape.subpaths[ 0 ].segments; // the 3 Line segments making up the triangle shape.subpaths[ 1 ].closed; // false - never closed ``` ::: tip A `Subpath` being `closed` isn't the same as its first and last points coinciding `closed` reflects whether `.close()` was called (which also appends a real closing segment connecting the last point back to the first, via `getClosingSegment()`) — it's not inferred from geometry. A subpath whose last `lineTo()` happens to land exactly on the start point, but which never called `.close()`, still reports `closed === false` and has no closing segment. Use `isClosed()` (equivalent to reading `.closed`) rather than comparing `getFirstPoint()`/`getLastPoint()` yourself. ::: ## Constructor ```ts new Subpath( segments?: Segment[], points?: Vector2[], closed?: boolean ) ``` You'll rarely construct a `Subpath` directly — build shapes through `Shape`'s fluent API (`moveTo`/`lineTo`/`arc`/...) and read the resulting `shape.subpaths` instead. The constructor mainly exists for `Shape`'s own internals (`copy()`, transform methods) and for advanced code assembling a `Shape` from pre-built segments. ## Public API | Member | Description | | --- | --- | | `segments` | The ordered `Segment[]` making up this subpath | | `points` | The `Vector2[]` of segment start points, plus the final segment's end point | | `closed` | Whether `.close()` was called on this subpath | | `bounds` | The `Bounds2` union of every segment's bounds | | `getFirstPoint()` / `getLastPoint()` | The subpath's first and last points | | `getFirstSegment()` / `getLastSegment()` | The subpath's first and last segments | | `isClosed()` | Same as reading `.closed` | | `hasClosingSegment()` / `getClosingSegment()` | A purely geometric check — whether (and what) synthetic `Line` segment would connect the last point back to the first. This is independent of the `closed` flag: it returns `true` whenever the first and last points differ by more than a tiny epsilon, even on a subpath that never called `.close()` | | `isDrawable()` | Whether there's at least one segment to draw | | `getArcLength()` | Sum of every contained segment's arc length | | `getFillSegments()` | Segments to use for filling — includes the closing segment even if `.close()` was never explicitly called, since fills always implicitly close | | `stroked( lineStyles )` | Returns `Subpath[]` describing the outline this subpath would have when stroked with the given [`LineStyles`](/api/kite/line-styles) — what `Shape.getStrokedShape()` delegates to per subpath | | `transformed( matrix )` | A new `Subpath` with every segment transformed by a [`Matrix3`](/api/dot/matrix3) | | `copy()` | A shallow-ish copy (new `Subpath`, same segment/point arrays sliced) | ## Related - [Shape](/api/kite/shape) — the `subpaths: Subpath[]` container, and the fluent builder that creates them. - [LineStyles](/api/kite/line-styles) — the stroke configuration `subpath.stroked()` consumes. - [Line](/api/kite/line-segment) — the segment type used for a subpath's synthetic closing segment. ======================================================================== Page: SVG Path Parsing and Serialization URL: https://veillette.github.io/Almanach/api/kite/svg-path-parsing-and-serialization Source: docs/api/kite/svg-path-parsing-and-serialization.md Category: API | Tags: kite, svgPath, svgNumber, Shape, SVG | Status: complete ======================================================================== # SVG Path Parsing and Serialization kite can both **read** an SVG path-data string into a [`Shape`](/api/kite/shape) and **write** a `Shape` back out as one. Reading goes through `svgPath`, a generated PEG.js parser (also exported directly from `scenerystack/kite`, for advanced use); writing goes through `Shape.getSVGPath()`, which stitches together each segment's own `getSVGPathFragment()` using `svgNumber` (also exported directly) to format numbers. ```ts import { Shape, svgPath, svgNumber } from 'scenerystack/kite'; // Reading: parse SVG path data directly in the Shape constructor const triangle = new Shape( 'M0,0 L100,0 L50,80 Z' ); // Writing: serialize any Shape back to an SVG path-data string triangle.getSVGPath(); // 'M 0.00... 0.00... L 100.00... 0.00... L 50.00... 80.00... Z ' svgNumber( 1 / 3 ); // '0.33333333333333331483' - toFixed(20), not scientific notation ``` ## Parsing: `new Shape( svgPathString )` `Shape`'s constructor accepts an SVG path-data string as its first argument (instead of, or in addition to, an array of pre-built `Subpath`s). Internally it runs the string through the `svgPath` parser (exported standalone as `svgPath` from `scenerystack/kite`, generated from a PEG.js grammar) to get a sequence of drawing commands, then replays those commands through the same fluent builder methods (`moveTo`, `lineTo`, `cubicCurveTo`, `arc`, ...) documented on [Shape](/api/kite/shape). This means anything valid in an SVG `d` attribute — absolute and relative commands, arcs, shorthand cubic/quadratic continuations — is supported, since it's the same grammar SVG itself uses. ```ts import { Shape } from 'scenerystack/kite'; // Arcs, relative commands, and shorthand curves all parse the same as in raw SVG: const path = new Shape( 'M10,10 h80 v80 h-80 Z' ); // a square, using relative h/v line commands ``` ## Serializing: `shape.getSVGPath()` `Shape.prototype.getSVGPath()` walks every subpath and every [`Segment`](/api/kite/segment) within it, calling each segment's own `getSVGPathFragment()` (part of the shared `Segment` contract — every concrete segment type implements it) and joining the fragments with an `M x y` at the start of each drawable subpath and a trailing `Z` for closed ones. The result is ready to assign directly to an SVG `` element's `d` attribute, or to hand back into `new Shape( ... )` to round-trip. ## `svgNumber`: why plain `toFixed` instead of dot's number formatting `svgNumber( n )` is a one-line function: `n.toFixed( 20 )`. Its doc comment explains why it deliberately bypasses dot's usual number-formatting utilities (like `toFixed` from `scenerystack/dot`): **SVG's path grammar doesn't support scientific notation** (e.g. `7e5`), so any formatter that might emit it (as `Number.prototype.toString()` can for very large/small numbers) would produce path data a browser can't parse. Using the built-in `toFixed` guarantees a plain fixed-point decimal string every time, at the cost of some (irrelevant, display-only) precision. ::: warning `svgPath` and `svgNumber` are low-level building blocks, not the typical entry point Most code should go through `new Shape( svgString )` and `shape.getSVGPath()` rather than calling `svgPath.parse()` or `svgNumber()` directly — those two exports exist mainly because `Shape` and `Segment` are built out of them internally, and are exposed for advanced cases (e.g. a tool that wants the raw parsed command list before it's turned into a `Shape`). For everyday reading/writing of SVG path data, the `Shape` methods above are simpler and keep bounds/segment invalidation consistent. ::: ======================================================================== Page: ThreeIsometricNode URL: https://veillette.github.io/Almanach/api/mobius/three-isometric-node Source: docs/api/mobius/three-isometric-node.md Category: API | Tags: mobius, ThreeIsometricNode, ThreeNode, three.js, 3d, isometric | Status: verified ======================================================================== # ThreeIsometricNode `ThreeIsometricNode` (from `scenerystack/mobius`) is the full-screen counterpart to [`ThreeNode`](/api/mobius/node-wrapping-conventions): instead of hosting a fixed-size three.js canvas as one child among other scenery Nodes, it takes a `ScreenView`'s `layoutBounds` and fills them with an isometric-scaled three.js viewport. `MobiusScreenView` and the [Three.js Integration](/examples/three-js-integration) example use this shape when 3D content is the primary backdrop for an entire screen. ```ts import { ThreeIsometricNode } from 'scenerystack/mobius'; import { Bounds2 } from 'scenerystack/dot'; import { THREE } from 'scenerystack/mobius'; const threeIsometricNode = new ThreeIsometricNode( layoutBounds, { cameraPosition: new THREE.Vector3( 0, 0, 5 ), fov: 50 } ); threeIsometricNode.stage.threeScene.add( myMesh ); this.addChild( threeIsometricNode ); ``` ## Constructor ```ts new ThreeIsometricNode( layoutBounds: Bounds2, providedOptions?: ThreeIsometricNodeOptions ) ``` `ThreeIsometricNodeOptions` combines `NodeOptions` with `ThreeStageOptions` plus mobius-specific fields: | Option | Default | Effect | | --- | --- | --- | | `fov` | `50` | Perspective camera field of view in degrees | | `parentMatrixProperty` | identity `Matrix3` | Optional parent transform applied when projecting between screen and 3D space | | `viewOffset` | `(0, 0)` | Pixel offset applied when mapping layout bounds to the embedded canvas | | `getPhetioMouseHit` | `null` | Optional `( point: Vector2 ) => PhetioObject | null | 'phetioNotSelectable'` callback for PhET-iO Studio autoselect on background clicks | Most `ThreeStageOptions` (`cameraPosition`, `backgroundColor`, `rendererParameters`, …) pass through to the internal [`ThreeStage`](/api/mobius/scene-and-camera-setup) the node constructs. ## Public members | Member | Description | | --- | --- | | `stage` | The internal `ThreeStage` — add meshes/lights to `stage.threeScene` and read `stage.threeCamera` | | `backgroundEventTarget` | A transparent `Rectangle` sized to `layoutBounds` that receives pointer events on empty background areas (useful for drag-to-rotate handlers) | ## When to use `ThreeIsometricNode` vs. `ThreeNode` | Shape | Use when | | --- | --- | | `ThreeIsometricNode` | 3D fills (or dominates) an entire screen's layout bounds — typical mobius sim screens | | `ThreeNode` | 3D is one fixed-size panel among other 2D UI — a small 3D preview, an inset viewport | Both embed the renderer canvas via a `DOM` node with `preventTransform: true` so scenery never CSS-transforms the WebGL surface directly. ======================================================================== Page: ThreeNode, ThreeInstrumentable, and ThreeObject3DPhetioObject URL: https://veillette.github.io/Almanach/api/mobius/node-wrapping-conventions Source: docs/api/mobius/node-wrapping-conventions.md Category: API | Tags: mobius, ThreeNode, ThreeInstrumentable, ThreeObject3DPhetioObject, three.js, phet-io | Status: verified ======================================================================== # ThreeNode, ThreeInstrumentable, and ThreeObject3DPhetioObject These three exports from `scenerystack/mobius` solve two related but distinct problems: getting a three.js viewport onto the scenery scene graph at all (`ThreeNode`), and giving individual `THREE.Object3D` instances placed inside that viewport a PhET-iO identity (`ThreeInstrumentable` + `ThreeObject3DPhetioObject`). ## `ThreeNode`: a fixed-size three.js viewport `ThreeNode` extends `scenerystack/scenery`'s `Node` and hosts one `ThreeStage` (see [ThreeStage](/api/mobius/scene-and-camera-setup)) at a fixed `width`/`height`, embedding the stage's renderer `` via a `DOM` node (`preventTransform: true`, `pickable: false`) so scenery never tries to CSS-transform the canvas itself. It's the "content sits at one place in the scene graph" counterpart to `ThreeIsometricNode`, which instead takes over an entire `ScreenView`'s layout bounds with isometric scaling (that's the Node `MobiusScreenView` sets up for you — see [Three.js Integration](/examples/three-js-integration)). Use `ThreeNode` directly when you want 3D content as one ordinary-sized child among other scenery Nodes, rather than a full-screen 3D backdrop. ```ts import { ThreeNode, THREE } from 'scenerystack/mobius'; import { Vector3 } from 'scenerystack/dot'; const threeNode = new ThreeNode( 400, 300, { cameraPosition: new Vector3( 0, 0, 3 ) } ); const geometry = new THREE.SphereGeometry( 0.5, 32, 16 ); const material = new THREE.MeshNormalMaterial(); threeNode.stage.threeScene.add( new THREE.Mesh( geometry, material ) ); threeNode.stage.threeScene.add( new THREE.DirectionalLight( 0xffffff, 1 ) ); // Call once the Node's transform/position in the global scene is settled... threeNode.layout(); // ...and on every animation frame: threeNode.render(); ``` ### `ThreeNode` constructor ```ts new ThreeNode( width: number, height: number, providedOptions?: ThreeNodeOptions ) ``` `ThreeNodeOptions` = `{ fov?: number }` (default `50`) plus every `ThreeStageOptions` (`cameraPosition`, `backgroundColorProperty`, `threeRendererOptions`, `threeRendererPixelRatio` — forwarded straight to the internal `ThreeStage`) plus ordinary `NodeOptions`. | Member | Description | | --- | --- | | `stage` | The `ThreeStage` this Node hosts — add `THREE.Object3D`s to `stage.threeScene` | | `backgroundEventTarget` | A `Rectangle` sized to `width`×`height`, added as the first child, for capturing drags/clicks that don't hit specific 3D content | | `projectPoint( Vector3 )` → `Vector2` | Delegates to `stage.projectPoint` | | `layout()` | Recomputes the stage's pixel dimensions from this Node's current global bounds and repositions the embedded canvas; call after any transform change | | `render( target? )` | Renders the stage (`autoClear: true`) | | `dispose()` | Disposes the `stage` | ## Instrumenting individual three.js objects `ThreeNode`/`ThreeIsometricNode` give the *viewport* a place in the scene graph, but a `THREE.Object3D` you add to `stage.threeScene` (a `THREE.Mesh`, `THREE.Group`, etc.) is not itself a scenery `Node` or a `PhetioObject` — three.js has no concept of PhET-iO. `ThreeObject3DPhetioObject` and the `ThreeInstrumentable` mixin exist to attach a PhET-iO identity to such objects by composition, so PhET-iO Studio can see and select them in the tree. ```ts import { ThreeInstrumentable, THREE } from 'scenerystack/mobius'; const InstrumentedMesh = ThreeInstrumentable( THREE.Mesh ); const sphere = new InstrumentedMesh( geometry, material, { tandem: tandem.createTandem( 'sphereMesh' ) } ); threeNode.stage.threeScene.add( sphere ); // sphere.phetioObject is the underlying ThreeObject3DPhetioObject. ``` ### `ThreeObject3DPhetioObject` ```ts new ThreeObject3DPhetioObject( providedOptions?: PhetioObjectOptions ) ``` A `PhetioObject` subclass with no extra state of its own: `phetioType` defaults to its own `ThreeObject3DIO`, and `tandem` defaults to `Tandem.REQUIRED` (you must supply one). It's meant to be held *by composition* inside a three.js object, not extended. ### `ThreeInstrumentable( type )` A memoized mixin function: `ThreeInstrumentable( SomeThreeClass )` returns a subclass of `SomeThreeClass` whose constructor takes the usual three.js constructor arguments plus a trailing `PhetioObjectOptions` object, and which exposes a `phetioObject: ThreeObject3DPhetioObject` (constructed from those options). Its overridden `dispose()` calls the wrapped type's own `dispose()` first, then disposes `phetioObject`. | Member | Description | | --- | --- | | `phetioObject` | The `ThreeObject3DPhetioObject` instrumented for this instance | | `dispose()` | Calls the wrapped type's own `dispose()` first (if present), then disposes `phetioObject` | ::: warning Instrumenting a three.js object does not capture its state `ThreeObject3DIO`'s `toStateObject`/`stateSchema` are both empty (`{}`) — wrapping a `THREE.Object3D` with `ThreeInstrumentable` only gives it an address in the PhET-iO tree for Studio to select and reference. It does **not** serialize or restore position, rotation, material, or any other three.js state on PhET-iO save/load. If your sim needs 3D object state to survive a state restore, you must model that state separately (e.g. as `Property`s on your own model) and use it to drive the three.js object, rather than relying on this instrumentation. ::: ======================================================================== Page: ThreeStage URL: https://veillette.github.io/Almanach/api/mobius/scene-and-camera-setup Source: docs/api/mobius/scene-and-camera-setup.md Category: API | Tags: mobius, ThreeStage, three.js, webgl, 3d | Status: verified ======================================================================== # ThreeStage `ThreeStage` (from `scenerystack/mobius`) encapsulates the three main three.js primitives needed to render a 3D scene: a `THREE.Scene`, a `THREE.PerspectiveCamera`, and (when WebGL is available) a `THREE.WebGLRenderer`. It is a plain class, not a scenery `Node` — the two Nodes that actually place mobius content in a scene graph, `ThreeNode` and `ThreeIsometricNode`, each construct one `ThreeStage` internally and embed its renderer's `` via a `DOM` node (see [ThreeNode and Object3D Instrumentation](/api/mobius/node-wrapping-conventions)). `ThreeStage` is where camera position, background color, dimensions, and screen-space/3D-space projection all live. ```ts import { ThreeStage, THREE } from 'scenerystack/mobius'; import { Vector3 } from 'scenerystack/dot'; import { Color } from 'scenerystack/scenery'; import { Property } from 'scenerystack/axon'; ``` ## A minimal example ```ts const stage = new ThreeStage( { cameraPosition: new Vector3( 0, 0, 5 ), backgroundColorProperty: new Property( new Color( 'black' ) ) } ); const geometry = new THREE.BoxGeometry( 1, 1, 1 ); const material = new THREE.MeshNormalMaterial(); const cube = new THREE.Mesh( geometry, material ); stage.threeScene.add( cube ); stage.threeScene.add( new THREE.DirectionalLight( 0xffffff, 1 ) ); stage.setDimensions( 400, 300 ); stage.render( undefined, true ); ``` In an actual scenery-based sim you would not construct a bare `ThreeStage` like this — you'd get one for free from `ThreeNode`/`ThreeIsometricNode` (`someThreeNode.stage`), as shown in [Three.js Integration](/examples/three-js-integration). This snippet shows the pieces `ThreeStage` itself owns. ## Constructor ```ts new ThreeStage( providedOptions?: ThreeStageOptions ) ``` ## Options | Option | Default | Effect | | --- | --- | --- | | `backgroundColorProperty` | `new Property( Color.BLACK )` | `TReadOnlyProperty` — sets `threeRenderer`'s clear color/alpha whenever it changes | | `cameraPosition` | `new Vector3( 0, 0, 10 )` | Initial position of `threeCamera` | | `threeRendererOptions` | `{ antialias, alpha: true, preserveDrawingBuffer }`, with `antialias`/`preserveDrawingBuffer` sourced from `MobiusQueryParameters` (`threeRendererAntialias`, `threeRendererPreserveDrawingBuffer`) — `alpha` is always hardcoded `true` | Forwarded to `new THREE.WebGLRenderer(...)` | | `threeRendererPixelRatio` | from `MobiusQueryParameters` | Forwarded to `threeRenderer.setPixelRatio(...)` | ## Properties and methods | Member | Description | | --- | --- | | `threeScene` | `THREE.Scene` — add your `THREE.Object3D`s (meshes, lights, groups) here | | `threeCamera` | `THREE.PerspectiveCamera` — `near`/`far` are fixed to `1`/`100`; `fov`/`aspect` are set by the hosting Node | | `threeRenderer` | `THREE.WebGLRenderer \| null` — `null` if WebGL isn't available (see `ThreeUtils.isWebGLEnabled()`) | | `canvasWidth` / `canvasHeight`, `width`/`height` getters | Current renderer dimensions | | `activeScale` | Scale factor applied to non-screen-coordinate interactions (e.g. rotation drags); maintained by the hosting Node's layout | | `dimensionsChangedEmitter` / `contextLostEmitter` / `contextRestoredEmitter` | `TEmitter`s for size changes and WebGL context loss/restore | | `setDimensions( width, height )` | Sets `canvasWidth`/`canvasHeight`, resizes the renderer, calls `threeCamera.updateProjectionMatrix()`, and fires `dimensionsChangedEmitter` — it does **not** itself change `threeCamera.aspect`; the hosting Node (or `adjustViewOffset`) is responsible for that | | `render( target, autoClear )` | Renders `threeScene`/`threeCamera` into `target` (or the canvas, if `target` is `undefined`) | | `renderToCanvas( supersampleMultiplier?, backingMultiplier?, scale? )` | Renders offscreen into a returned `HTMLCanvasElement` (used for screenshots) | | `projectPoint( Vector3 )` → `Vector2` | Projects a 3D world point to a 2D global (screen) point | | `unprojectPoint( Vector2, modelZ? )` → `Vector3` | Projects a 2D screen point back into the 3D scene at a given model z | | `getRayFromScreenPoint( Vector2 )` → `Ray3` | Returns the camera ray through a given screen point (useful for picking) | | `adjustViewOffset( cameraBounds: Bounds2 )` | Shifts the camera's projection view offset so its output lines up with `cameraBounds` — used internally for isometric layout/pan/zoom | | `ThreeStage.computeIsometricFOV( fov, canvasWidth, canvasHeight, layoutWidth, layoutHeight )` | Static helper computing the FOV needed to keep isometric content correctly scaled across aspect ratios | | `dispose()` | Disposes `threeRenderer` and `threeScene`, unlinks `backgroundColorProperty` | ::: warning `threeRenderer` can be `null` `ThreeStage` only creates a `THREE.WebGLRenderer` if `ThreeUtils.isWebGLEnabled()` returns true at construction time. Every renderer-touching call in `ThreeStage` itself already guards with `this.threeRenderer && ...` — if you call `threeRenderer` methods directly from your own code, guard the same way rather than assuming WebGL is always available. ::: ======================================================================== Page: ThreeUtils URL: https://veillette.github.io/Almanach/api/mobius/three-utils-helpers Source: docs/api/mobius/three-utils-helpers.md Category: API | Tags: mobius, ThreeUtils, three.js, webgl, 3d | Status: verified ======================================================================== # ThreeUtils `ThreeUtils` (from `scenerystack/mobius`) is a plain object of static helper functions (not a class you instantiate) that bridges SceneryStack's own `dot`/`scenery` types and three.js's equivalents, provides low-level vertex-buffer writers for building simple quad geometry, and exposes the WebGL-availability check that `ThreeStage` uses to decide whether to create a `WebGLRenderer` at all. ```ts import { ThreeUtils } from 'scenerystack/mobius'; import { Vector3 } from 'scenerystack/dot'; ``` ## A minimal example ```ts // Guard before doing any WebGL-dependent setup. if ( ThreeUtils.isWebGLEnabled() ) { const modelPosition = new Vector3( 1, 2, 3 ); mesh.position.copy( ThreeUtils.vectorToThree( modelPosition ) ); } // Build a single front-facing quad's position buffer, e.g. for a custom BufferGeometry. import { Bounds2 } from 'scenerystack/dot'; const positions = ThreeUtils.frontVertices( new Bounds2( -1, -1, 1, 1 ), 0 ); geometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) ); ``` ## Methods | Member | Description | | --- | --- | | `vectorToThree( vector: Vector3 )` → `THREE.Vector3` | Converts a dot `Vector3` to a three.js vector | | `threeToVector( vector: THREE.Vector3 )` → `Vector3` | The inverse conversion | | `colorToThree( color: Color )` → `THREE.Color` | Converts a scenery `Color` to a three.js color | | `frontVertices( bounds2: Bounds2, z )` → `Float32Array` | Vertices for a quad facing the camera (+z), spanning `bounds2` in x/y | | `topVertices( bounds2: Bounds2, y )` → `Float32Array` | Vertices for an up-facing quad, spanning `bounds2` in x/z | | `rightVertices( bounds2: Bounds2, x )` / `leftVertices( bounds2: Bounds2, x )` → `Float32Array` | Vertices for a right- or left-facing quad, spanning `bounds2` in z/y | | `writeTriangle( array, index, x0, y0, z0, ..., x2, y2, z2 )` → `number` | Writes 9 floats (one triangle, counterclockwise winding) into `array` at `index`, returns the next free index | | `writeQuad( array, index, ... 4 vertices ... )` → `number` | Writes 18 floats (two triangles) into `array`, returns the next free index | | `textureLoader` (getter) | Singleton `THREE.TextureLoader`, created lazily on first access | | `imageToTexture( image: HTMLImageElement, waitForLoad? )` → `THREE.Texture` | Loads a texture from an already-loaded ``; pass `waitForLoad: true` to register an `asyncLoader` lock until it decodes | | `isWebGLEnabled()` → `boolean` | `true` if the `webgl` query parameter allows it *and* `Utils.isWebGLSupported` — what `ThreeStage` checks before constructing a `WebGLRenderer` | ::: tip Check `isWebGLEnabled()` before assuming a renderer exists `ThreeStage.threeRenderer` is only non-null when `ThreeUtils.isWebGLEnabled()` was true at construction time. Any code that builds textures, geometry, or renderer-dependent state outside of `ThreeStage` itself should perform the same check first, rather than assuming WebGL is always available (some embedding contexts and the `?webgl=false` query parameter can disable it). ::: ======================================================================== Page: Atom, Element, and AtomNode URL: https://veillette.github.io/Almanach/api/nitroglycerin/atom-and-element Source: docs/api/nitroglycerin/atom-and-element.md Category: API | Tags: nitroglycerin, Atom, Element, AtomNode, chemistry | Status: verified ======================================================================== # Atom, Element, and AtomNode `Element`, `Atom`, and `AtomNode` (all from `scenerystack/nitroglycerin`) split the model/view concerns for a single atom in a chemistry sim. `Element` is a set of ~17 static, immutable instances holding real physical constants (covalent radius, electronegativity, atomic weight, display color) for specific elements (H, C, N, O, ...). `Atom` is a lightweight, constructible-many-times wrapper around one `Element` that adds per-instance identity (a unique `id`/`reference`) — you construct many `Atom`s referencing the same `Element.O` static. `AtomNode` is the `scenery` view: a shaded sphere sized and colored from the wrapped `Element`. ```ts import { Atom, Element, AtomNode } from 'scenerystack/nitroglycerin'; ``` ## A minimal example ```ts const oxygenAtom = new Atom( Element.O ); const oxygenNode = new AtomNode( Element.O ); // or, from a symbol string: const hydrogenAtom = Atom.createAtomFromSymbol( 'H' ); ``` ## `Element` `Element` is not meant to be constructed directly in client code — use one of its static instances or look one up by symbol. | Member | Description | | --- | --- | | `Element.H`, `Element.C`, `Element.N`, `Element.O`, ... | Static instances for the ~17 elements nitroglycerin models (`Ar, B, Be, Br, C, Cl, F, H, I, N, Ne, O, P, S, Si, Sn, Xe`) | | `Element.elements` | Array of all static instances | | `symbol` | e.g. `'O'` | | `covalentRadius` / `vanDerWaalsRadius` | In picometers | | `electronegativity` | Pauling units, `null` for noble gasses | | `atomicWeight` | Atomic mass units (u) | | `color` | `Color \| string` used by `AtomNode` | | `Element.getElementBySymbol( symbol )` | Static lookup; asserts if the symbol isn't one of the modeled elements | | `isSameElement( element )` / `isHydrogen()` / `isCarbon()` / `isOxygen()` | Identity/convenience predicates | ## `Atom` ```ts new Atom( element: Element ) ``` | Member | Description | | --- | --- | | `element` | The wrapped `Element` | | `symbol`, `covalentRadius`, `covalentDiameter`, `electronegativity`, `atomicWeight`, `color` | Unpacked from `element` for convenience (`covalentDiameter` is `2 * covalentRadius`, otherwise not present on `Element`) | | `id` / `reference` | Per-instance unique identifiers (`reference` is a hex counter, `id` is `` `${symbol}_${reference}` ``) — distinguishes two `Atom`s wrapping the same `Element` | | `Atom.createAtomFromSymbol( symbol )` | Static convenience combining `Element.getElementBySymbol` and `new Atom(...)` | | `hasSameElement( atom )` / `isHydrogen()` / `isCarbon()` / `isOxygen()` | Delegate to the wrapped `Element` | ## `AtomNode` ```ts new AtomNode( element: Element, providedOptions?: AtomNodeOptions ) ``` `AtomNode extends ShadedSphereNode` (from `scenerystack/scenery-phet`) — it takes an `Element`, not an `Atom`, and computes a display diameter from `element.covalentRadius` (compressed nonlinearly so the largest and smallest modeled atoms don't differ as dramatically on screen as their real radii would suggest). `mainColor` defaults to `element.color`; any other `ShadedSphereNodeOptions` may be passed through and override it. ```ts const bigOxygen = new AtomNode( Element.O ); const smallHydrogen = new AtomNode( Element.H, { mainColor: 'lightgray' } ); // override the default element color ``` ::: tip `AtomNode` takes an `Element`, not an `Atom` Since `AtomNode` only needs `covalentRadius` and `color` — both already on `Element` — it skips `Atom` entirely. Reach for `Atom` only when you need per-instance identity (e.g. tracking individual atoms in a reaction simulation); for pure rendering, construct `AtomNode` straight from the `Element` static you need. ::: ======================================================================== Page: ChemUtils URL: https://veillette.github.io/Almanach/api/nitroglycerin/chem-utils Source: docs/api/nitroglycerin/chem-utils.md Category: API | Tags: nitroglycerin, ChemUtils, Element, chemistry, formula | Status: verified ======================================================================== # ChemUtils `ChemUtils` (from `scenerystack/nitroglycerin`) is a plain object of static chemistry helpers — not a class you construct. Simulation code reaches for it when building molecular formula strings from [`Element`](/api/nitroglycerin/atom-and-element) lists, sorting atoms for Hill-system display order, or turning plain-text formulas like `C2H4` into HTML subscripts for `RichText`. ```ts import { ChemUtils, Element } from 'scenerystack/nitroglycerin'; const elements = [ Element.C, Element.C, Element.H, Element.H, Element.H, Element.H ]; ChemUtils.createSymbolWithoutSubscripts( elements ); // "C2H4" ChemUtils.createSymbol( elements ); // "C2H4" ChemUtils.toSubscript( 'C2H4' ); // same HTML subscript form ``` ## Formula helpers | Method | Description | | --- | --- | | `createSymbolWithoutSubscripts( elements )` | Builds a plain-text formula from an ordered `Element[]`, collapsing consecutive identical elements into counts (e.g. `[C,C,H,H,H,H]` → `"C2H4"`) | | `createSymbol( elements )` | Same as above, then runs `toSubscript()` for HTML display | | `toSubscript( inputString )` | Wraps every digit run in `` tags — `'C2H4'` → `'C2H4'` | The input `elements` array must list atoms **in the order they should appear in the symbol** — `ChemUtils` does not infer ordering from connectivity; use the Hill-system sorters below when you need standard ordering first. ## Hill-system sorting | Method | Description | | --- | --- | | `carbonHillSortValue( element )` | Sort key when the molecule contains carbon — carbon first, then hydrogen, then other elements alphabetically | | `nonCarbonHillSortValue( element )` | Sort key for molecules without carbon — alphabetical by element symbol, with two-letter symbols after one-letter symbols sharing the same first character | These return integers suitable for `Array.sort()` — lower values sort earlier. See the [Hill system](https://en.wikipedia.org/wiki/Hill_system) for the convention they implement. ::: tip Prefer prebuilt molecule Nodes for common formulas Nitroglycerin also exports dozens of ready-made `*Node` classes (`H2ONode`, `CO2Node`, …) and [`MoleculeNode`](/api/nitroglycerin/molecule-node) for drawing. Reach for `ChemUtils` when your sim builds formulas dynamically from a model's atom list rather than from a fixed chemical identity. ::: ======================================================================== Page: MoleculeNode URL: https://veillette.github.io/Almanach/api/nitroglycerin/molecule-node Source: docs/api/nitroglycerin/molecule-node.md Category: API | Tags: nitroglycerin, MoleculeNode, AtomNode, chemistry | Status: verified ======================================================================== # MoleculeNode `MoleculeNode` (from `scenerystack/nitroglycerin`) is the abstract base class every pre-built molecule `Node` extends — it takes a fixed array of already-positioned `AtomNode`s, wraps them in an inner `Node` re-centered on `Vector2.ZERO` (so the molecule's origin is its geometric center regardless of how lopsided the individual atoms' layout is), and exposes that as the molecule's contents. Its constructor is `protected`, so you never instantiate `MoleculeNode` directly — you use one of its ~40 concrete subclasses (`H2ONode`, `CO2Node`, `NH3Node`, and so on), each of which knows its own molecule's atom arrangement. ```ts import { H2ONode, CO2Node, NH3Node } from 'scenerystack/nitroglycerin'; ``` ## A minimal example ```ts const water = new H2ONode(); const carbonDioxide = new CO2Node( { atomNodeOptions: { opacity: 0.9 } } ); const ammonia = new NH3Node(); someLayerNode.children = [ water, carbonDioxide, ammonia ]; ``` ## Constructor (subclasses only) ```ts protected constructor( atomNodes: AtomNode[], providedOptions?: MoleculeNodeOptions ) ``` Each concrete subclass builds its own `AtomNode[]` (using real bond geometry — e.g. `H2ONode` places two hydrogens below-and-to-the-sides of a larger oxygen) and calls `super( atomNodes, providedOptions )`. `HorizontalMoleculeNode` is one such intermediate subclass that lays out a linear sequence of elements left-to-right for you (used by `CO2Node`, among others), which is why some molecule Node constructors take an `Element[]` instead of pre-built `AtomNode[]`. ## `MoleculeNodeOptions` | Option | Default | Effect | | --- | --- | --- | | `atomNodeOptions` | `undefined` | Forwarded to every `AtomNode` the concrete subclass constructs — the one option point common to all molecule Nodes, useful for e.g. dimming every atom uniformly | | ...rest of `NodeOptions` | — | Everything `scenery`'s `Node` accepts, except `children` (owned internally by `MoleculeNode`) | ## Pre-built molecule subclasses (examples) `scenerystack/nitroglycerin` exports around 40 concrete `MoleculeNode` subclasses, one per commonly-used molecule — each is a small, dedicated class rather than something you configure generically: | Class | Molecule | | --- | --- | | `H2ONode` | Water | | `CO2Node` | Carbon dioxide | | `NH3Node` | Ammonia | | `CH4Node` | Methane | | `N2ONode` | Nitrous oxide | (and ~35 more — `C2H4Node`, `SO2Node`, `PCl5Node`, `HClNode`, etc. — one per molecule nitroglycerin models.) Each accepts the same `atomNodeOptions` plus standard `NodeOptions`; check the specific subclass's `*NodeOptions` type for any molecule-specific additions. ::: tip Reach for a named subclass, not `MoleculeNode` itself `MoleculeNode`'s constructor is `protected` specifically to prevent constructing a generic, ad hoc molecule shape — every real molecule in nitroglycerin has its own class encoding correct bond angles and atom placement. If the molecule you need isn't among the ~40 pre-built subclasses, compose your own `AtomNode`s directly (as `H2ONode` does internally) rather than trying to instantiate `MoleculeNode`. ::: ======================================================================== Page: arrayRemove, arrayDifference, and cleanArray URL: https://veillette.github.io/Almanach/api/phet-core/collection-utilities Source: docs/api/phet-core/collection-utilities.md Category: API | Tags: phet-core, arrayRemove, arrayDifference, cleanArray, array, collections | Status: verified ======================================================================== # arrayRemove, arrayDifference, and cleanArray `scenerystack/phet-core` exports a handful of small, standalone array utilities used throughout SceneryStack instead of hand-rolled loops or a general utility-belt library. The three most broadly useful are `arrayRemove` (delete one matching element in place), `arrayDifference` (classify elements of two arrays as "only in A," "only in B," or "in both"), and `cleanArray` (empty an array in place, or create one if given a falsy value) — all plain functions with no class or options object involved. ```ts import { arrayRemove, arrayDifference, cleanArray } from 'scenerystack/phet-core'; ``` ## `arrayRemove` ```ts arrayRemove( array: T[], toRemove: T ): void ``` Removes the *first* matching element from `array` in place (via `splice`), asserting that the element is actually present — this is not a safe no-op removal, it's an assertion that the caller expected the element to be there. ```ts const listeners = [ listenerA, listenerB, listenerC ]; arrayRemove( listeners, listenerB ); // listeners is now [ listenerA, listenerC ] ``` ## `arrayDifference` ```ts arrayDifference( a: T[], b: T[], aOnly?: T[], bOnly?: T[], inBoth?: T[] ): T[] ``` Given two arrays of unique elements, sorts every element into "only in `a`," "only in `b`," or "in both," optionally writing into caller-supplied output arrays (each must start empty), and returns `aOnly` (the classic set-difference result). ```ts const aOnly: number[] = []; const bOnly: number[] = []; const inBoth: number[] = []; arrayDifference( [ 1, 2 ], [ 5, 2, 0 ], aOnly, bOnly, inBoth ); // aOnly: [ 1 ], bOnly: [ 5, 0 ], inBoth: [ 2 ] ``` Both input arrays must contain only unique values (`arrayDifference` asserts this) — it is not a general multiset diff. ## `cleanArray` ```ts cleanArray( arr?: T[] | null | undefined ): T[] ``` If given an array, empties it in place (via repeated `pop()`, chosen to avoid the garbage `length = 0` can generate) and returns the same reference; if given `null`/`undefined`, returns a fresh empty array instead. Useful for a field you want to "reset to empty" without deciding at each call site whether it's already been allocated. ```ts this.activeListeners = cleanArray( this.activeListeners ); ``` ## Summary | Function | Mutates input? | Returns | | --- | --- | --- | | `arrayRemove( array, toRemove )` | Yes | `void` | | `arrayDifference( a, b, aOnly?, bOnly?, inBoth? )` | Only the output arrays, if supplied | `aOnly` | | `cleanArray( arr? )` | Yes, if `arr` is truthy | The (now-empty) array | ::: tip `arrayRemove` asserts the element exists — it is not a safe "remove if present" Calling `arrayRemove` on a value that isn't actually in the array throws (`item not found in Array`) rather than silently doing nothing. If removal is conditional, check `array.includes( item )` first, or use `_.pull`/manual filtering when "not found" is an expected case rather than a bug. ::: ======================================================================== Page: assertHasProperties and assertMutuallyExclusiveOptions URL: https://veillette.github.io/Almanach/api/phet-core/assertion-helpers Source: docs/api/phet-core/assertion-helpers.md Category: API | Tags: phet-core, assertHasProperties, assertMutuallyExclusiveOptions, assertions, options | Status: verified ======================================================================== # assertHasProperties and assertMutuallyExclusiveOptions `assertHasProperties` and `assertMutuallyExclusiveOptions` (from `scenerystack/phet-core`) are development-time assertion helpers for validating an object's shape at a call site — the first checks that certain named members exist (anywhere in the prototype chain), the second checks that certain groups of option keys are never supplied together. Both are no-ops in a production build; like the rest of SceneryStack's `assert && assert(...)` convention, they only run when assertions are enabled, so they should guard invariants, not carry runtime logic. ```ts import { assertHasProperties, assertMutuallyExclusiveOptions } from 'scenerystack/phet-core'; ``` ## `assertHasProperties` ```ts assertHasProperties( object: unknown, properties: string[] ): void ``` Asserts that `object` has an own property *or* an inherited (prototype-chain) property for every name in `properties`. Useful for validating a duck-typed dependency (e.g. "this object needs to look like a `TColor`") without importing its concrete class. ```ts assertHasProperties( someNode, [ 'getOpacity', 'opacity', '_opacity' ] ); // no error if all three exist somewhere in the chain assertHasProperties( { flower: 2 }, [ 'tree' ] ); // throws: property not defined: tree ``` ## `assertMutuallyExclusiveOptions` ```ts assertMutuallyExclusiveOptions( options: object | null | undefined, ...sets: string[][] ): void ``` Asserts that at most one of the given key-sets has any of its keys present in `options` — pass one `string[]` per mutually-exclusive group. `options` may be `null`/`undefined` (treated as "nothing set," so it always passes). Only one of the provided sets may have any of its keys present at all — using multiple keys *within* the same set is fine: ```ts assertMutuallyExclusiveOptions( { tree: 1, flower: 2 }, [ 'tree' ], [ 'flower' ] ); // throws - one key used from each of the two sets assertMutuallyExclusiveOptions( { flower: 2 }, [ 'tree' ], [ 'flower' ] ); // fine - only the second set is used assertMutuallyExclusiveOptions( { tree: 1, mountain: 2 }, [ 'tree', 'mountain' ], [ 'flower' ] ); // fine - both used keys belong to the first set ``` ## Summary | Function | Checks | Throws when | | --- | --- | --- | | `assertHasProperties( object, properties )` | Named members exist (own or inherited) | Any name in `properties` is missing everywhere in the chain | | `assertMutuallyExclusiveOptions( options, ...sets )` | At most one option-key group is used | Keys from more than one of the provided `sets` are present in `options` | ::: tip Both are guarded by the global assertion flag, not a manual `if` Call these directly — don't wrap them in `assert && assertHasProperties(...)` yourself the way you would with `window.assert`; these two functions already check internally whether assertions are enabled and no-op if not, matching the convention used by `merge` and other `phet-core` runtime checks. ::: ======================================================================== Page: EnumerationValue URL: https://veillette.github.io/Almanach/api/phet-core/enumeration-value Source: docs/api/phet-core/enumeration-value.md Category: API | Tags: phet-core, EnumerationValue, Enumeration, enumeration | Status: verified ======================================================================== # EnumerationValue `EnumerationValue` (from `scenerystack/phet-core`) is the base class every value of a closed, PhET-style enumeration extends — see [the Enumeration Pattern](/patterns/enumeration-pattern) for the full "why" and a worked example. This page documents `EnumerationValue` itself: what it provides once its sibling class `Enumeration` has scanned a subclass's static instances and wired them up. ```ts import { EnumerationValue, Enumeration } from 'scenerystack/phet-core'; ``` ## Usage ```ts class Direction extends EnumerationValue { public static readonly NORTH = new Direction(); public static readonly SOUTH = new Direction(); // Must come last: scans the static instances declared above. public static readonly enumeration = new Enumeration( Direction ); } Direction.NORTH.name; // 'NORTH' - filled in by the Enumeration constructor Direction.NORTH.enumeration.values; // [ Direction.NORTH, Direction.SOUTH ] Direction.NORTH.toString(); // 'NORTH' ``` Before `new Enumeration( Direction )` runs, a freshly-constructed `EnumerationValue`'s `name` and `enumeration` are unset — reading them throws. `Enumeration`'s constructor walks `Object.keys` on the class (and its supertypes, to support augmenting an existing enumeration) looking for `instanceof` matches, and assigns `name`/`enumeration` on each one it finds, in declaration order. ## Members | Member | Description | | --- | --- | | `name` (getter) | The static property key the instance was assigned to (e.g. `'NORTH'`); throws if read before `Enumeration` has run | | `enumeration` (getter) | The owning `Enumeration` instance; throws if read before `Enumeration` has run | | `toString()` | Returns `name` | | `EnumerationValue.sealedCache` | A `Set` of constructors that already have an `Enumeration` built for them — `Enumeration`'s constructor adds to it, and the base `EnumerationValue` constructor throws if you try to `new` a sealed constructor directly (subclassing it further is still fine) | `name` and `enumeration` are set-once: assigning either a second time throws, which is what makes them safe to treat as immutable identity after construction. ::: warning You cannot construct new values after `Enumeration` has run Calling `new Enumeration( Direction )` seals `Direction` in `EnumerationValue.sealedCache` — any subsequent `new Direction()` throws. Declare every static instance *before* the `public static readonly enumeration = new Enumeration( Direction )` line, and add that line last, exactly once, per concrete enumeration class. ::: ======================================================================== Page: optionize and merge URL: https://veillette.github.io/Almanach/api/phet-core/optionize-and-merge Source: docs/api/phet-core/optionize-and-merge.md Category: API | Tags: phet-core, optionize, merge, combineOptions, options | Status: verified ======================================================================== # optionize and merge `optionize` (from `scenerystack/phet-core`) is the type-checked function almost every SceneryStack class constructor calls to combine its own default option values, its superclass's defaults, and a caller-provided options object into one final options object — it's what the [options pattern](/patterns/options-pattern) is built on. `merge` is the plain-JavaScript deep-merge function `optionize` delegates to at runtime; reach for `merge` directly only when you're combining plain option-literal objects outside a class constructor and don't need `optionize`'s compile-time "every optional field needs a default" check. ```ts import { optionize, merge, combineOptions } from 'scenerystack/phet-core'; ``` ## `optionize` ```ts optionize>() ( defaults: OptionizeDefaults, providedOptions: ProvidedOptions | undefined ) => OptionizeDefaults & ProvidedOptions ``` It's called with an empty type-argument list `<...>()` immediately followed by the actual `( defaults, providedOptions )` call — the split exists so TypeScript can infer `ProvidedOptions`/`SelfOptions`/`ParentOptions` from explicit type arguments while still inferring the return type from the runtime arguments. ```ts type SelfOptions = { headHeight?: number; doubleHead?: boolean; }; export type ArrowNodeOptions = SelfOptions & PathOptions; const options = optionize()( { headHeight: 10, doubleHead: false, fill: 'black' // a PathOptions default this class chooses to supply }, providedOptions ); ``` The type system enforces that every optional key declared in `SelfOptions` has a default in the first argument — omit one and the call fails to typecheck, not just at runtime. ## Related functions in the same file | Function | Signature | When to use | | --- | --- | --- | | `optionize3` | `optionize3()( {}, defaults, providedOptions )` | Same as `optionize`, for call sites (like `ResponsePacket`) that need an explicit empty-object first argument rather than mutating `defaults` in place | | `optionize4` | `optionize4()( {}, constantDefaults, classDefaults, providedOptions )` | Adds a middle layer — e.g. sim-wide constants — between class defaults and the caller's options | | `combineOptions( target, ...sources )` | Returns `Type` | Merges plain option-literal objects of the *same* type (no self/parent split, no default-completeness check) — for combining options outside a constructor | All four are thin typed wrappers that call `merge` underneath; the type layer is compile-time only; `merge` performs the actual runtime combination. ## `merge` ```ts merge( target: A, ...sources: B[] ): A & B ``` Recursively copies every own, non-`undefined` property from each source onto `target`, left to right (later sources win). Nested objects are merged recursively **only** when the key ends in `Options` (case-sensitive) and is not literally `'Options'` itself — e.g. `numberDisplayOptions: {...}` recurses, but arrays, functions, class instances, and any object with getters/setters throw an assertion error rather than being merged, since `merge` only supports plain object literals. ```ts const options = merge( {}, CommonConstants.SOME_DEFAULTS, { fill: 'red' }, providedOptions ); ``` ::: warning `merge`'s recursion is keyed on the literal suffix `Options` A field named `buttonOptions` merges deeply with a same-named field from an earlier source; a field named `button` (holding an object) is replaced wholesale instead. This is easy to get wrong when refactoring a nested options object — renaming a `*Options` field to drop that suffix silently changes merge behavior from "combine" to "overwrite." ::: ======================================================================== Page: Orientation URL: https://veillette.github.io/Almanach/api/phet-core/orientation Source: docs/api/phet-core/orientation.md Category: API | Tags: phet-core, Orientation, EnumerationValue, layout, enumeration | Status: complete ======================================================================== # Orientation `Orientation` (from `scenerystack/phet-core`) is a two-valued [`EnumerationValue`](/api/phet-core/enumeration-value) enumeration — `Orientation.HORIZONTAL` and `Orientation.VERTICAL` — used throughout layout-adjacent code (`FlowBox`, sliders, bamboo's [`GridLineSet`/`TickMarkSet`/`TickLabelSet`](/api/bamboo/gridlines-and-tick-marks) and [axis Nodes](/api/bamboo/axis-nodes)) so that logic can be written once and parameterized by orientation, instead of duplicated for the horizontal and vertical cases. Each value carries a bundle of orientation-specific string keys (`'centerX'` vs `'centerY'`, `'left'`/`'right'` vs `'top'`/`'bottom'`, `'preferredWidth'` vs `'preferredHeight'`, …) so you can index into a `Node` or `Bounds2` generically: `node[ orientation.centerCoordinate ] = value` works for either orientation without an `if`. ```ts import { Orientation } from 'scenerystack/phet-core'; ``` ## A minimal example ```ts function centerAlong( node: Node, orientation: Orientation, value: number ): void { // For HORIZONTAL this sets node.centerX; for VERTICAL it sets node.centerY. node[ orientation.centerCoordinate ] = value; } centerAlong( myNode, Orientation.HORIZONTAL, 100 ); // The opposite orientation, useful for "the other axis" logic. Orientation.HORIZONTAL.opposite === Orientation.VERTICAL; // true // Build a Vector-like value with the components in the right order for this orientation. const size = Orientation.VERTICAL.toVector( 10, 20, Vector2 ); // Vector2( 20, 10 ) -- (secondary, primary) for VERTICAL ``` ## Values | Value | Meaning | | --- | --- | | `Orientation.HORIZONTAL` | The x-axis / left-right orientation | | `Orientation.VERTICAL` | The y-axis / top-bottom orientation | ## Members Every `Orientation` value carries these fields (all set in the constructor, `readonly`): | Member | `HORIZONTAL` value | `VERTICAL` value | | --- | --- | --- | | `coordinate` | `'x'` | `'y'` | | `centerCoordinate` | `'centerX'` | `'centerY'` | | `minCoordinate` / `maxCoordinate` | `'minX'` / `'maxX'` | `'minY'` / `'maxY'` | | `minSide` / `maxSide` | `'left'` / `'right'` | `'top'` / `'bottom'` | | `minSize` / `maxSize` | `'minWidth'` / `'maxWidth'` | `'minHeight'` / `'maxHeight'` | | `rectCoordinate` / `rectSize` | `'rectX'` / `'rectWidth'` | `'rectY'` / `'rectHeight'` | | `flowBoxOrientation` / `ariaOrientation` | `'horizontal'` | `'vertical'` | | `size` | `'width'` | `'height'` | | `line` | `'column'` | `'row'` | | `preferredSize` / `localPreferredSize` | `'preferredWidth'` / `'localPreferredWidth'` | `'preferredHeight'` / `'localPreferredHeight'` | | `sizable` | `'widthSizable'` | `'heightSizable'` | | `opposite` | `Orientation.VERTICAL` | `Orientation.HORIZONTAL` | And these methods: | Member | Description | | --- | --- | | `modelToView( modelViewTransform, value )` / `viewToModel( modelViewTransform, value )` | Calls the transform's `modelToViewX`/`Y` (or `viewToModelX`/`Y`) method matching this orientation | | `toVector( primary, secondary, VectorType )` | Builds a `VectorType` instance with components in the right slot for this orientation — `(primary, secondary)` for `HORIZONTAL`, `(secondary, primary)` for `VERTICAL` — zero-padded to support up to a 4-component vector type | | `Orientation.fromLayoutOrientation( 'horizontal' \| 'vertical' )` | Static helper converting a `FlowBox`-style string literal to the matching `Orientation` value | ::: tip Prefer `orientation.opposite` over hardcoding a check Because `HORIZONTAL.opposite` and `VERTICAL.opposite` are wired up as circular references right after both values are constructed, code that needs "the other axis" (e.g. bamboo's `TickMarkSet`, which positions a tick's tail using `axisOrientation.opposite`) can just read `.opposite` instead of writing `orientation === Orientation.HORIZONTAL ? Orientation.VERTICAL : Orientation.HORIZONTAL`. It also composes correctly if a third orientation value were ever added, which a hardcoded ternary would not. ::: ======================================================================== Page: platform URL: https://veillette.github.io/Almanach/api/phet-core/platform-detection Source: docs/api/phet-core/platform-detection.md Category: API | Tags: phet-core, platform, browser, useragent | Status: complete ======================================================================== # platform `platform` (from `scenerystack/phet-core`, note the lowercase name — it's a plain object, not a class) is a set of boolean flags describing the current browser and OS, derived once from `navigator.userAgent` when the module is first imported. It exists for the rare cases where SceneryStack's normal feature/capability abstractions aren't enough and code genuinely needs to branch on *which* browser it's running in. ```ts import { platform } from 'scenerystack/phet-core'; ``` ## A minimal example ```ts if ( platform.mobileSafari ) { // Work around an iOS Safari-specific rendering quirk. node.renderer = 'canvas'; } if ( platform.firefox ) { // ... } ``` ## Flags | Flag | True when | | --- | --- | | `firefox` | User agent string contains `"firefox"` (case-insensitive) | | `mobileSafari` | Running as a PhET "phet-app" wrapper, or the UA matches iPod/iPhone/iPad, or `navigator.platform === 'MacIntel'` with multitouch (modern iPadOS reporting as Mac) — combined with an `AppleWebKit` match | | `safari` | `mobileSafari`, or the UA matches a `Version/…` + `Safari/…` + `AppleWebKit` pattern (desktop Safari) | | `safari5` / `safari6` / `safari7` / `safari9` / `safari10` / `safari11` | Desktop/iOS Safari matching that specific major version string | | `ie` | Detected as any version of Internet Explorer (via `getInternetExplorerVersion() !== -1`) | | `ie9` / `ie10` / `ie11` | Detected as that specific IE version | | `edge` | UA contains `Edge/` (legacy EdgeHTML Edge, not Chromium Edge) | | `chromium` | UA matches `chrom(e\|ium)` and is not legacy Edge | | `chromeOS` | UA contains `CrOS` | | `android` | UA contains `Android` | | `mac` | `navigator.platform` contains `Mac` | ## Methods `platform` has no methods — it is a plain object of booleans computed eagerly at import time, not re-evaluated if the environment somehow changes afterward. ::: warning "Use sparingly, if at all" That's the source file's own doc comment. Almost everything `platform` is used for in practice — layout quirks, input event differences, rendering fallbacks — has a better, more targeted abstraction elsewhere in SceneryStack (feature detection, capability checks, CSS). Reach for `platform` only as a last resort for a genuinely browser-specific workaround, and prefer the narrowest flag available (e.g. `mobileSafari` over `safari`) so the branch doesn't accidentally widen to browsers you didn't test. ::: ======================================================================== Page: Pool URL: https://veillette.github.io/Almanach/api/phet-core/pool Source: docs/api/phet-core/pool.md Category: API | Tags: phet-core, Pool, Poolable, performance, memory | Status: complete ======================================================================== # Pool `Pool` (from `scenerystack/phet-core`) manages a reusable set of instances of a single class, so that code creating and discarding many short-lived objects per frame (vectors for an intermediate calculation, drag-listener event objects, per-point plot helpers) can pull an existing instance back out of a pool instead of paying for a fresh allocation and, later, garbage collection. You construct one `Pool` per class, typically stored as a `static readonly pool` field on that class, and the pool's `create()`/`fetch()`/`freeToPool()` methods stand in for `new`. ```ts import { Pool } from 'scenerystack/phet-core'; ``` ## A minimal example ```ts class Vector2Scratch { public x = 0; public y = 0; public initialize( x: number, y: number ): this { this.x = x; this.y = y; return this; } public freeToPool(): void { Vector2Scratch.pool.freeToPool( this ); } // Must come after `initialize` is defined -- Pool reads it off the prototype. public static readonly pool = new Pool( Vector2Scratch ); } // Pulls a recycled instance and re-initializes it, or constructs a new one if the pool is empty. const scratch = Vector2Scratch.pool.create( 3, 4 ); // ...use scratch... // Release it back to the pool once you're done -- no other references should remain. scratch.freeToPool(); ``` ## Constructor ```ts new Pool( type: T, providedOptions?: PoolOptions ) ``` `type` is the class being pooled. If `type.prototype.initialize` exists with a compatible signature, `providedOptions` is optional; otherwise TypeScript requires you to pass an `initialize` option explicitly (`Pool` needs some function to call to "re-run the constructor" on a recycled instance). ## Options | Option | Default | Effect | | --- | --- | --- | | `initialize` | `type.prototype.initialize` | The function called on a recycled instance to reinitialize it with new constructor-like arguments; must return the object itself | | `defaultArguments` | `[]` | Arguments used to construct instances when the pool needs to grow without a caller-supplied argument list (e.g. filling `initialSize`) | | `maxSize` | `100` | Upper bound on how many instances `freeToPool()` will retain; excess instances are simply dropped (left for normal GC) | | `initialSize` | `0` | Number of instances eagerly created (via `defaultArguments`) when the `Pool` is constructed | | `useDefaultConstruction` | `false` | If `true`, a pool miss always constructs via `defaultArguments` first and then calls `initialize` with the real arguments, instead of constructing directly with the real arguments | ## Methods | Member | Description | | --- | --- | | `create( ...args )` | Returns an instance behaving as if constructed with `args`: pops one from the pool and calls `initialize( ...args )` on it if available, otherwise constructs a new one | | `fetch()` | Returns an instance with *arbitrary* leftover state — pops one from the pool if available, otherwise constructs one from `defaultArguments`. Use this only when you're about to overwrite all relevant state yourself | | `freeToPool( object )` | Returns `object` to the pool if `poolSize < maxPoolSize`; otherwise the object is silently dropped (left for the garbage collector) | | `forEach( callback )` | Iterates the objects currently sitting in the pool | | `poolSize` (getter) | Number of instances currently held in the pool | | `maxPoolSize` (getter/setter) | The cap `freeToPool()` checks against | ::: tip `Pool` supersedes the older `Poolable.mixInto()` mixin `scenerystack/phet-core` also exports `Poolable`, but its own source comment marks it `@deprecated - Please use Pool.ts instead as the new pooling pattern`. `Poolable.mixInto( MyType, options )` retrofits static `pool`/`createFromPool`/`dirtyFromPool` members onto an existing class via mutation, whereas `Pool` is a plain, explicit object you construct and assign yourself (`static readonly pool = new Pool( MyType )`) — prefer `Pool` in new code, and treat `Poolable` as something you might still encounter reading older call sites, not something to reach for. ::: ======================================================================== Page: Bucket URL: https://veillette.github.io/Almanach/api/phetcommon/bucket Source: docs/api/phetcommon/bucket.md Category: API | Tags: phetcommon, Bucket, SphereBucket, model | Status: verified ======================================================================== # Bucket `Bucket` (from `scenerystack/phetcommon`) is the model half of the "bucket" UI element seen across PhET sims — a container drawn with a 3D-ish tilted opening that other model objects (atoms, molecules, balls) can be dragged out of and back into. `Bucket` itself only computes shapes and stores presentation data (position, size, colors, caption); it defines no notion of "contents," which is deliberately left to a subclass. It extends `PhetioObject` (from `scenerystack/tandem`), so a `Bucket` can be instrumented for PhET-iO like any other model element. ```ts import { Bucket } from 'scenerystack/phetcommon'; import { Vector2, Dimension2 } from 'scenerystack/dot'; const bucket = new Bucket( { position: new Vector2( 0, 0 ), size: new Dimension2( 220, 60 ), baseColor: '#4d92c0', captionText: 'Atoms' } ); // Shapes are computed at construction time from position/size: bucket.holeShape; // the elliptical "opening" of the bucket bucket.containerShape; // the tilted 3D-ish body of the bucket ``` ## Options | Option | Default | Effect | | --- | --- | --- | | `position` | `Vector2.ZERO` | Model position of the **center of the bucket's opening** — not the bottom or the visual center, since that's the point other code most often needs to align against | | `size` | `Dimension2( 200, 50 )` | Overall dimensions used to derive `holeShape` and `containerShape` | | `baseColor` | `'#ff0000'` | Base fill color for the bucket body (a view reads this to style itself) | | `captionText` | `''` | Label text (plain string or `TReadOnlyProperty` for translated/dynamic captions) | | `captionColor` | `'white'` | Color for the caption text | | `invertY` | `false` | Flips the bucket's shape vertically, for use with a y-down (inverted) model coordinate convention | `Bucket` is a base class — the shapes it computes are for a view to render, but `Bucket` itself has no `addParticle`/`removeParticle`-style API for managing contents. ## Public state | Member | Effect | | --- | --- | | `position` | The model position (mutable) of the bucket's opening center | | `size` | The `Dimension2` used to compute the bucket's shapes | | `holeShape` | A `Shape` (from `scenerystack/kite`) for the elliptical opening | | `containerShape` | A `Shape` for the tilted body of the bucket | | `baseColor`, `captionText`, `captionColor` | Presentation data for a view to read | ::: tip SphereBucket adds the actual contents-management API `Bucket` only computes geometry — for a working bucket that spherical model objects can actually be added to, removed from, and stacked within (with automatic layout/relayout as items come and go), use `SphereBucket` (also exported from `scenerystack/phetcommon`), which extends `Bucket` and adds `addParticleFirstOpen()`, `addParticleNearestOpen()`, `removeParticle()`, `extractClosestParticle()`, and `reset()`. It expects each particle to expose `positionProperty`, `destinationProperty`, `isDraggingProperty`, and `containerProperty` — see the `Spherical` type it's generic over. ::: On the view side, `scenerystack/scenery-phet` exports matching `BucketFront` and `BucketHole` Nodes, which read a `Bucket`'s (or `SphereBucket`'s) `containerShape`/`holeShape`/colors/caption directly — `BucketHole` is drawn behind the draggable contents and `BucketFront` in front, so particles can appear to drop "into" the bucket. ======================================================================== Page: Fraction URL: https://veillette.github.io/Almanach/api/phetcommon/fraction Source: docs/api/phetcommon/fraction.md Category: API | Tags: phetcommon, Fraction, model, math | Status: complete ======================================================================== # Fraction `Fraction` (from `scenerystack/phetcommon`) represents a fraction as an integer `numerator` and integer `denominator`, with arithmetic operations that stay exact (no floating-point rounding) as long as the values involved stay below 2^53. It's the model class behind PhET's fraction- and ratio-based sims — every operation preserves the numerator/denominator pair rather than collapsing to a decimal, so `1/3 + 1/3` stays representable exactly instead of becoming `0.6666666666666666`. ```ts import { Fraction } from 'scenerystack/phetcommon'; ``` ## A minimal example ```ts const a = new Fraction( 1, 3 ); const b = new Fraction( 1, 6 ); const sum = a.plus( b ); // Fraction( 9, 18 ) -- lcm-based denominator, NOT auto-reduced sum.reduce(); // mutates sum in place to Fraction( 1, 2 ) a.getValue(); // 0.3333333333333333 -- the fraction still exposes a numeric value a.isReduced(); // true (gcd(1, 3) === 1) Fraction.fromDecimal( 0.25 ); // Fraction( 1, 4 ) ``` ## Constructor ```ts new Fraction( numerator: number, denominator: number ) ``` Both arguments must be integers — asserted at construction and on every subsequent `numerator`/`denominator` setter call. `Fraction` does not validate `denominator !== 0`; a zero denominator is allowed to be constructed (some operations, like `dividedInteger`, explicitly note that division by zero is permitted). ## Static members | Member | Value / Signature | | --- | --- | | `Fraction.ZERO` | `new Fraction( 0, 1 )` | | `Fraction.ONE` | `new Fraction( 1, 1 )` | | `Fraction.fromInteger( value )` | Builds `new Fraction( value, 1 )` | | `Fraction.fromDecimal( value )` | Converts a decimal `number` to an equivalent, reduced `Fraction` by counting decimal places | | `Fraction.fromStateObject( stateObject )` | Deserializes from `{ numerator, denominator }`, for PhET-iO | | `Fraction.FractionIO` | The `IOType` used to serialize/deserialize `Fraction` instances for PhET-iO state | ## Instance members | Member | Effect | | --- | --- | | `numerator` / `denominator` (getter/setter) | Read or replace either integer component directly | | `getValue()` / `value` (getter) | `numerator / denominator` as a floating-point number | | `isInteger()` | Whether the fraction reduces to a whole number (`numerator % denominator === 0`) | | `isReduced()` | Whether `gcd(numerator, denominator) === 1` | | `reduce()` | Divides both components by their GCD, **mutating this instance**; returns `this` | | `reduced()` | Returns a new, reduced `Fraction`, leaving this one untouched | | `copy()` | Returns a new `Fraction` with the same numerator/denominator | | `equals( fraction )` | True only if numerator **and** denominator match exactly — `1/2` and `2/4` are not `equals()`, even though they have the same value | | `isLessThan( fraction )` | Value comparison, computed via exact subtraction (not floating-point division) to avoid precision loss | | `sign` (getter) | `Math.sign( this.getValue() )` | | `abs()` | Returns a new `Fraction` with both components made non-negative | | `add( f )` / `subtract( f )` / `multiply( f )` / `divide( f )` | **Mutating** operations — modify `this` and return it. Sums/differences use the LCM of the two denominators; none of these reduce the result automatically | | `plus( f )` / `minus( f )` / `times( f )` / `divided( f )` | Non-mutating equivalents — each returns a new `Fraction`, leaving `this` unchanged | | `plusInteger( n )` / `minusInteger( n )` / `timesInteger( n )` / `dividedInteger( n )` | Convenience non-mutating operations against a plain integer, keeping this fraction's existing denominator (for `plusInteger`/`minusInteger`/`timesInteger`) | | `toString()` | Returns `"numerator/denominator"` | | `toStateObject()` | Serializes to `{ numerator, denominator }` | ::: warning Arithmetic results are not automatically reduced `plus()`, `minus()`, `times()`, `divided()` (and their mutating counterparts) deliberately leave the result unreduced — e.g. `new Fraction(1,3).plus(new Fraction(1,6))` gives `Fraction(9, 18)`, not `Fraction(1, 2)`. Call `.reduce()` (mutating) or `.reduced()` (non-mutating) explicitly whenever you need a fraction in lowest terms, such as before comparing two fractions with `equals()`, which checks the numerator/denominator pair exactly rather than the reduced value. ::: ======================================================================== Page: ModelViewTransform2 URL: https://veillette.github.io/Almanach/api/phetcommon/model-view-transform Source: docs/api/phetcommon/model-view-transform.md Category: API | Tags: phetcommon, dot, coordinates, transform, ModelViewTransform2 | Status: verified ======================================================================== # ModelViewTransform2 `ModelViewTransform2` (from `scenerystack/phetcommon`) converts between **model coordinates** — meters, moles, whatever your physics uses, typically with **y pointing up** — and **view coordinates** — scenery pixels, with **y pointing down**. Keeping the model in physical units and doing all conversion at the view boundary is a core SceneryStack convention; the model should never know about pixels. ```ts import { ModelViewTransform2 } from 'scenerystack/phetcommon'; import { Vector2 } from 'scenerystack/dot'; ``` ## Creating a transform The most common factory for physics-style simulations maps a single model point to a view point, applies a uniform scale, and inverts the y axis: ```ts // Model origin at the horizontal center, 100 px from the bottom of the layout // bounds; 1 model unit (e.g. 1 meter) = 50 view pixels; +y up in the model. const modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( Vector2.ZERO, // model point new Vector2( layoutBounds.centerX, layoutBounds.maxY - 100 ), // view point 50 // view units per model unit ); ``` Other factory methods: | Factory | Use case | | --- | --- | | `createIdentity()` | Model units are view units (still useful as a placeholder) | | `createOffsetScaleMapping( offset, scale )` | Translate + uniform scale, no y inversion | | `createSinglePointScaleMapping( modelPoint, viewPoint, scale )` | Like the inverted-y version, but y-down in the model too | | `createSinglePointScaleInvertedYMapping( modelPoint, viewPoint, scale )` | The standard choice for physics (y-up model) | | `createRectangleMapping( modelBounds, viewBounds )` | Fit a model rectangle onto a view rectangle | | `createRectangleInvertedYMapping( modelBounds, viewBounds )` | Same, with y inversion | ## Converting values ```ts // Positions (affected by translation, scale, and y inversion) const viewPosition = modelViewTransform.modelToViewPosition( body.positionProperty.value ); const modelPosition = modelViewTransform.viewToModelPosition( viewPoint ); // Deltas and distances (affected by scale only — no translation) const viewDx = modelViewTransform.modelToViewDeltaX( body.radius ); const viewDelta = modelViewTransform.modelToViewDelta( velocity ); // Bounds and shapes const viewBounds = modelViewTransform.modelToViewBounds( model.wallBounds ); const viewShape = modelViewTransform.modelToViewShape( orbitShape ); ``` ::: warning Positions vs. deltas `modelToViewPosition` applies the full transform including translation. For *sizes*, *radii*, and *velocities*, use the delta methods (`modelToViewDeltaX`, `modelToViewDelta`, …) — otherwise the offset of the model origin gets baked into your lengths, and with an inverted-y mapping `modelToViewDeltaY` of a positive height is correctly **negative**. ::: ## Typical wiring in a view component ```ts class BodyNode extends Circle { public constructor( body: Body, modelViewTransform: ModelViewTransform2 ) { super( modelViewTransform.modelToViewDeltaX( body.radius ), { fill: 'teal' } ); body.positionProperty.link( position => { this.translation = modelViewTransform.modelToViewPosition( position ); } ); } } ``` The same transform instance is passed to [drag listeners](/patterns/drag-listeners) via their `transform` option, so pointer motion in view coordinates updates the model in model coordinates. ======================================================================== Page: StringUtils URL: https://veillette.github.io/Almanach/api/phetcommon/string-utils Source: docs/api/phetcommon/string-utils.md Category: API | Tags: phetcommon, StringUtils, i18n, strings | Status: verified ======================================================================== # StringUtils ::: warning About this page's title This page was originally planned to cover `PhetcommonQueryParameters` — but no such export exists in the real `scenerystack` package. `phetcommon`'s barrel (`src/phetcommon.ts`) exports exactly five things: `AssertUtils`, [`Bucket`](/api/phetcommon/bucket), `Fraction`, `SphereBucket`, `StringUtils`, and [`ModelViewTransform2`](/api/phetcommon/model-view-transform); phetcommon defines no query parameters of its own (query parameters for a sim's screens/behavior are declared per-library, e.g. `sunQueryParameters`, and read via `scenerystack/query-string-machine` — see the [Query Parameters Pattern](/patterns/query-parameters-pattern)). This page documents `StringUtils` instead, a real, exported, and frequently-used phetcommon utility. ::: `StringUtils` (from `scenerystack/phetcommon`) is a small collection of static string-handling functions, used throughout SceneryStack for filling in translated pattern strings and for keeping bidirectional (LTR/RTL) text safe when numbers or substrings get spliced into a larger, possibly differently-directioned, string. It's a plain object of functions, not a class — there's nothing to construct. ```ts import { StringUtils } from 'scenerystack/phetcommon'; StringUtils.fillIn( '{{name}} is {{age}} years old', { name: 'Ada', age: 23 } ); // 'Ada is 23 years old' StringUtils.capitalize( 'hello there' ); // 'Hello there' ``` ## Methods | Method | Effect | | --- | --- | | `fillIn( template, values )` | Replaces `{{key}}` placeholders in `template` with `values[key]` — `template` may be a plain string or anything with a `.get()` (like a Property); unused keys in `values` are ignored, and unmatched placeholders are left as-is | | `format( pattern, ...args )` | **Deprecated** — the older positional `{0}`/`{1}` substitution; prefer `fillIn` for new code | | `capitalize( str )` | Capitalizes the first letter, skipping any leading whitespace/control characters — English-oriented, not locale-aware | | `wrapLTR( str )` / `wrapRTL( str )` | Wraps `str` in Unicode LTR/RTL embedding marks so it renders in a fixed direction regardless of surrounding text | | `wrapDirection( str, direction )` | Calls `wrapLTR`/`wrapRTL` based on a `'ltr' \| 'rtl'` argument | | `toFixedLTR( number, digits )` | `number.toFixed( digits )`, wrapped with `wrapLTR` so a negative sign always renders on the left even inside RTL text | | `toFixedNumberLTR( number, digits )` | Like `toFixedLTR`, but drops trailing zeros (uses `toFixedNumber` instead of `toFixed`) | | `embeddedSlice`, `embeddedSplit`, `embeddedDebugString`, `isEmbeddingMark` | Lower-level helpers for slicing/splitting strings that already contain LTR/RTL embedding marks without corrupting the marks | | `localeToLocalizedName( locale )` | Looks up a locale code's own-language display name (e.g. `'es'` → `'Español'`), correctly directioned | ## When you'd reach for this directly Most simulation code never calls `StringUtils.fillIn` by hand — [`PatternStringProperty`](/api/axon/pattern-string-property) wraps exactly this call (plus `toFixedLTR` for numeric decimal-place formatting) in a reactive, disposable `DerivedProperty`, and is the preferred way to build a *dynamic* pattern-filled string. Call `StringUtils` directly only for one-off, non-reactive string formatting — e.g. building a debug log line, or formatting a value at a point where wiring up a full derived Property would be overkill. ======================================================================== Page: Query parameter schema conventions (QSMSchemaObject) URL: https://veillette.github.io/Almanach/api/query-string-machine/query-parameter-schema-conventions Source: docs/api/query-string-machine/query-parameter-schema-conventions.md Category: API | Tags: query-string-machine, QSMSchemaObject, schema, validValues, elementSchema, query-parameters | Status: verified ======================================================================== # Query parameter schema conventions Each key passed to `QueryStringMachine.getAll` (see `QSMSchemaObject`, exported from `scenerystack/query-string-machine`) maps to one schema object describing how that parameter should be parsed and validated. Every schema variant shares a required `type` discriminant plus a fixed set of optional fields depending on that type; `QueryStringMachine` validates the schema itself (not just the parsed value) at parse time, so a malformed schema fails fast rather than silently misbehaving. ```ts import type { QSMSchemaObject } from 'scenerystack/query-string-machine'; ``` ## The five schema types | `type` | Result TS type | Fields | | --- | --- | --- | | `'flag'` | `boolean` | No `defaultValue` — presence in the URL (with no `=value`) means `true`, absence means `false` | | `'boolean'` | `boolean` | `defaultValue?: boolean` | | `'number'` | `number` | `defaultValue?: number` | | `'string'` | `string \| null` | `defaultValue?: string \| null` | | `'array'` | `T[]` | `elementSchema: Schema` (required), `separator?: string` (single character, default `','`), `defaultValue?: readonly T[] \| null` | | `'custom'` | Whatever `parse` returns | `parse: ( str: string ) => T` (required) | All types additionally accept `public` and `private` (below). `validValues`/`isValidValue` are supported only on `'number'`, `'string'`, `'array'`, and `'custom'` schemas — `'flag'` and `'boolean'` schemas do not accept either (their only two possible values, `true`/`false`, make an extra validity check redundant). ## Shared optional fields | Field | Effect | | --- | --- | | `validValues` | Restricts the parsed value to a fixed set — mutually exclusive with `isValidValue` (schemas providing both throw at parse time) | | `isValidValue` | A custom predicate the parsed value must satisfy — mutually exclusive with `validValues` | | `public` | If `true`, an invalid value logs to `QueryStringMachine.warnings` and falls back to `defaultValue` instead of throwing. Requires `defaultValue` to be present (except for `'flag'`, which has none by definition) | | `private` | The URL value is only honored for team members (gated on a `phetTeamMember` localStorage check); otherwise treated as absent | ## `'array'` schemas nest a full sub-schema ```ts const screensSchema: QSMSchemaObject = { screens: { type: 'array', elementSchema: { type: 'number', isValidValue: ( n: number ) => Number.isInteger( n ) && n >= 1 }, separator: ',', defaultValue: null } }; // ?screens=1,3,4 -> screens: [ 1, 3, 4 ] ``` `elementSchema` is itself a full `Schema` and is recursively validated the same way the outer schema is — but it may not declare `public` itself (that comes from the array schema as a whole, not per element). ## `'custom'` schemas hand parsing to you entirely ```ts const colorSchema: QSMSchemaObject = { backgroundColor: { type: 'custom', parse: ( str: string ) => new Color( str ), defaultValue: Color.WHITE } }; ``` `isValidValue` for a `'custom'` schema defaults to "always valid" — validation is the parse function's responsibility unless you also supply `isValidValue`/`validValues`. ## Validation rules enforced on the schema itself `QueryStringMachine` asserts all of the following before it will parse a value, throwing immediately if violated: - `type` is present and is one of the five known types. - `validValues` and `isValidValue` are not both present on the same schema. - `defaultValue`, if present, passes that type's own `isValidValue` check (and must be a member of `validValues` if both are given). - `public: true` requires `defaultValue` to be present, except for `'flag'` schemas. - No unsupported keys are present for the given `type` (e.g. a `'number'` schema with an `elementSchema` field throws). - For `'array'` schemas: `separator`, if given, is exactly one character, and `elementSchema` does not itself declare `public`. ::: warning `validValues` and `isValidValue` are mutually exclusive, not layered Supplying both on the same schema entry throws at parse time — `QueryStringMachine` doesn't combine them (e.g. "must be one of these, and also pass this extra check"). If you need both a fixed set and an extra predicate, encode the extra check as the *only* validation via `isValidValue`, checking membership yourself inside it. ::: ======================================================================== Page: QueryStringMachine.getAll URL: https://veillette.github.io/Almanach/api/query-string-machine/query-string-machine-getall Source: docs/api/query-string-machine/query-string-machine-getall.md Category: API | Tags: query-string-machine, QueryStringMachine, getAll, query-parameters | Status: verified ======================================================================== # QueryStringMachine.getAll `QueryStringMachine` (from `scenerystack/query-string-machine`) is a plain object — not a class — exposing `get`/`getAll` and their string-based variants for parsing `location.search` against a declared schema. `getAll` is the entry point almost every sim uses: pass a map of parameter name to schema, get back a fully-typed, defaulted, validated object in one call. See [the Query Parameters Pattern](/patterns/query-parameters-pattern) for *why* this convention exists and a worked schema; this page documents `QueryStringMachine`'s methods themselves. ```ts import { QueryStringMachine } from 'scenerystack/query-string-machine'; ``` ## `getAll` ```ts QueryStringMachine.getAll( schemaMap: SchemaMap ): QSMParsedParameters ``` ```ts const parameters = QueryStringMachine.getAll( { speed: { type: 'number', defaultValue: 1 }, showAnswers: { type: 'flag' } } ); parameters.speed; // number parameters.showAnswers; // boolean parameters.SCHEMA_MAP; // the schemaMap you passed in, attached for introspection ``` Internally, `getAll` reads `self.location.search` and delegates to `getAllForString`, which loops the schema map calling `getForString` per key — so `getAll`'s behavior is entirely explained by `get`'s single-key behavior, run once per declared parameter. ## The full method surface | Method | Signature | Use when | | --- | --- | --- | | `get( key, schema )` | `( key: string, schema: Schema ) => ParsedValue` | You only need one parameter's value | | `getAll( schemaMap )` | `( schemaMap: QSMSchemaObject ) => QSMParsedParameters` | The common case: parse every declared parameter for the sim at startup | | `getForString( key, schema, string )` | Same as `get`, plus an explicit query string | Testing, or parsing a query string that isn't `location.search` | | `getAllForString( schemaMap, string )` | Same as `getAll`, plus an explicit query string | Same, for the batch form | | `containsKey( key )` | `( key: string ) => boolean` | Check presence without parsing/validating a value | | `containsKeyForString( key, string )` | Same, with an explicit string | Testing variant | `string` arguments must be either the empty string or start with `?`; anything else throws. ## Other members on the same object | Member | Purpose | | --- | --- | | `warnings` | A read-only array of `{ key, value, message }` accumulated whenever a `public: true` schema silently falls back to its default because the URL supplied an invalid value | | `addWarning( key, value, message )` | Appends to `warnings` (de-duplicated); called internally, rarely by client code | | `removeKeyValuePair( queryString, key )` / `removeKeyValuePairs( queryString, keys )` | Returns a new query string with the given key(s) stripped — useful for constructing a "relaunch without this flag" URL | | `appendQueryString( url, queryParameters )` / `appendQueryStringArray( url, queryStringArray )` | Appends one or more `key=value` fragments to a URL, handling the `?`/`&` join correctly | | `getQueryString( url )` | Returns the `?...` tail of a URL, or `'?'` if there is none | ::: tip Invalid values only fall back silently when the schema is `public: true` If a URL supplies a value that fails validation (wrong type, not in `validValues`, fails `isValidValue`) and the schema does *not* set `public: true`, `QueryStringMachine` throws instead of warning-and-defaulting. Reserve `public: true` for parameters end users might plausibly mistype and where continuing with the default is safe; leave it off for internal/debug flags where a bad value should fail loudly. ::: ======================================================================== Page: AlignBox URL: https://veillette.github.io/Almanach/api/scenery/align-box Source: docs/api/scenery/align-box.md Category: API | Tags: scenery, AlignBox, layout | Status: verified ======================================================================== # AlignBox `AlignBox` (from `scenerystack/scenery`) wraps a single content [`Node`](/api/scenery/node) and positions it within a bounding box (`alignBounds`) according to horizontal/vertical alignment and margins — the scenery equivalent of centering or pinning one element inside a fixed-size slot. It's the standard way to reserve a consistent amount of space for content whose size can vary (e.g. a translated string) inside a [`FlowBox`](/api/scenery/flow-box) or [`GridBox`](/api/scenery/grid-box) cell, without the varying content shifting its neighbors. ```ts import { AlignBox, Circle } from 'scenerystack/scenery'; import { Bounds2 } from 'scenerystack/dot'; const icon = new Circle( 12, { fill: 'seagreen' } ); const centeredSlot = new AlignBox( icon, { alignBounds: new Bounds2( 0, 0, 60, 60 ), xAlign: 'center', yAlign: 'center' } ); ``` If `alignBounds` isn't provided, `AlignBox` sizes itself to fit the content plus margins, with a left-top corner at `(0, 0)` — in that case it behaves mostly as a margin wrapper rather than a fixed-size slot. Passing a preferred width/height (e.g. from a parent layout container) also drives `alignBounds` automatically. ## Options | Option | Default | Effect | | --- | --- | --- | | `alignBounds` | `null` (fits content) | The `Bounds2` the content is aligned within; see [`Bounds2`](/api/dot/bounds2) | | `alignBoundsProperty` | — | Drives `alignBounds` dynamically from a `TReadOnlyProperty`; mutually exclusive with `alignBounds` | | `align` | — | Shorthand setting both `xAlign` and `yAlign` (only `'center'` or `'stretch'` are valid for both axes at once) | | `xAlign` | `'center'` | `'left'`, `'center'`, `'right'`, or `'stretch'` | | `yAlign` | `'center'` | `'top'`, `'center'`, `'bottom'`, or `'stretch'` | | `margin` | `0` | Shorthand for all four margins | | `xMargin` / `yMargin` | `0` | Shorthand for left+right / top+bottom margins | | `leftMargin` / `rightMargin` / `topMargin` / `bottomMargin` | `0` | Per-side margin | | `group` | `null` | An `AlignGroup` that synchronizes this box's `alignBounds` with sibling boxes in the same group, so they all end up the same size | ## Methods | Method | Effect | | --- | --- | | `getContent()` / `.content` | Returns the wrapped content `Node` | | `setAlignBounds( bounds )` | Updates the bounds content is aligned within | | `invalidateAlignment()` | Forces an immediate re-layout — useful when content changed in a way that doesn't automatically trigger it | | `getContentBounds()` | Returns the content's current bounds within the alignment frame | | `dispose()` | Releases the box's internal listeners on the content and any `alignBoundsProperty`/`group` | ::: tip `sizable` defaults to `false` Unlike most layout-participating Nodes, `AlignBox` defaults to `sizable: false` — it's usually used to carve out a fixed amount of space, not to grow/shrink with its parent. Pass `sizable: true` (or set `xAlign`/`yAlign` to `'stretch'` with a resizable content Node) if you want it to participate in a parent's `grow`/`stretch` layout. ::: ::: warning Layout can lag by one frame `AlignBox` recomputes its layout when the content's bounds change, but per the source's own documentation, that update "may not happen immediately, and may be delayed until bounds of a alignBox's child occurs." Reading `alignBox.getBounds()` right after mutating content does not force a refresh and can return stale bounds — call `invalidateAlignment()` if you need the new layout synchronously, or rely on it settling before the next [`Display.updateDisplay()`](/api/scenery/display). ::: ======================================================================== Page: AlignGroup URL: https://veillette.github.io/Almanach/api/scenery/align-group Source: docs/api/scenery/align-group.md Category: API | Tags: scenery, AlignGroup, AlignBox, layout | Status: verified ======================================================================== # AlignGroup `AlignGroup` (from `scenerystack/scenery`) coordinates a set of [`AlignBox`](/api/scenery/align-box)es that don't otherwise share a parent, so they all end up sized to the same width/height — the largest of the group's minimum content sizes. Where a single `AlignBox` just centers/aligns one child in a bounding box you specify, `AlignGroup` computes that bounding box *for* you from whichever member currently needs the most space, and every box in the group is resized to match, guaranteeing every box ends up sized identically without measuring anything by hand. ```ts import { AlignGroup, AlignBox, Text } from 'scenerystack/scenery'; const group = new AlignGroup(); const shortLabel = group.createBox( new Text( 'Short' ), { xAlign: 'left' } ); const longLabel = group.createBox( new Text( 'A much longer label' ), { xAlign: 'left' } ); // Both boxes now report the same (largest) size to whatever lays them out — e.g. side-by-side panels. ``` `createBox()` is the usual way to add a member — it constructs an `AlignBox` with `group: this` already set. You can also build an `AlignBox` yourself and assign `group` on it directly. ## Constructor options | Option | Default | Effect | | --- | --- | --- | | `matchHorizontal` | `true` | Whether every box in the group is forced to the same width (the group's max content width) | | `matchVertical` | `true` | Whether every box in the group is forced to the same height (the group's max content height) | Also accepts `DisposableOptions` (e.g. `disposer`), inherited from `Disposable`. If only one of `matchHorizontal`/`matchVertical` is `false`, boxes use their own preferred size on that axis instead of the group's max — useful when you want boxes to share a common width but keep independent heights, for example. ## Methods | Method | Effect | | --- | --- | | `createBox( content, options? )` | Creates and registers a new `AlignBox` in this group, wrapping `content` | | `updateLayout()` | Forces an immediate recomputation of the shared size and reapplies it to every member box — normally you don't need this, since bounds changes on any member trigger it automatically | | `setMatchHorizontal( value )` / `.matchHorizontal` | Toggles horizontal size-matching after construction | | `setMatchVertical( value )` / `.matchVertical` | Toggles vertical size-matching after construction | | `getMaxWidth()` / `.maxWidth` | The group's current shared width | | `getMaxHeight()` / `.maxHeight` | The group's current shared height | | `getMaxWidthProperty()` / `.maxWidthProperty` | A `TProperty` tracking the shared width, for cases that need to observe it | | `getMaxHeightProperty()` / `.maxHeightProperty` | A `TProperty` tracking the shared height | | `dispose()` | Disposes every `AlignBox` the group created — disposing the group is enough to clean up its members too | ::: tip Populate largest-content boxes first Per source: since many `sun` UI components don't support resizing their contents dynamically after construction, populate an `AlignGroup` in order from largest expected content to smallest, so the group's shared size is established correctly from the start rather than needing every box to grow into a later-discovered maximum. ::: ======================================================================== Page: AnimatedPanZoomListener URL: https://veillette.github.io/Almanach/api/scenery/animated-pan-zoom-listener Source: docs/api/scenery/animated-pan-zoom-listener.md Category: API | Tags: scenery, AnimatedPanZoomListener, input, pan, zoom, accessibility | Status: verified ======================================================================== # AnimatedPanZoomListener `AnimatedPanZoomListener` (from `scenerystack/scenery`) transforms a target [`Node`](/api/scenery/node) — usually the whole rendered scene — in response to trackpad pinch gestures, mouse wheel scroll/zoom, middle-mouse-drag panning, and keyboard pan/zoom commands, animating toward the requested pan position and scale rather than jumping instantly. It extends `PanZoomListener` (itself a `MultiListener`), adding the animation and the input-gesture handling on top. Most applications don't construct it directly — they use the pre-built `animatedPanZoomSingleton` (also from `scenerystack/scenery`), which owns one shared instance for the whole document. ```ts import { Node, Display, animatedPanZoomSingleton } from 'scenerystack/scenery'; import { Bounds2 } from 'scenerystack/dot'; const rootNode = new Node(); const display = new Display( rootNode ); animatedPanZoomSingleton.initialize( rootNode, { panBounds: new Bounds2( 0, 0, display.width, display.height ) } ); display.addInputListener( animatedPanZoomSingleton.listener ); ``` Because the default `stepEmitter` option is the shared `axon` `stepTimer` (which PhET-iO sims already drive every frame), the animation advances automatically — you only need to call `.step( dt )` yourself if you're not using the standard sim time loop. ## Key options (`AnimatedPanZoomListenerOptions`) Inherited from `PanZoomListener` / `MultiListener`: | Option | Default | Effect | | --- | --- | --- | | `panBounds` | `Bounds2.NOTHING` | The bounds (global frame) that must stay fully filled with content at all times — effectively the viewport | | `targetBounds` | `null` (uses `targetNode.globalBounds`) | Overrides the bounds considered to belong to the target, e.g. when the Node has invisible content extending off-screen | | `targetScale` | `1` | A scale factor describing the target Node's own content scale, applied to translation during panning (for content that's uniformly scaled independent of this listener) | | `minScale` / `maxScale` | `1` / `4` | Clamps how far the listener can zoom the target in/out | | `allowScale` / `allowRotation` | `true` / `false` | Whether pinch/gesture input is allowed to scale / rotate the target (`AnimatedPanZoomListener` disables rotation by default via `PanZoomListener`) | Specific to `AnimatedPanZoomListener`: | Option | Default | Effect | | --- | --- | --- | | `stepEmitter` | `stepTimer` | The `TReadOnlyEmitter<[number]>` that drives animation frames; pass `null` to step it manually instead | ## Public state and methods | Member | Meaning | | --- | --- | | `animatingProperty` | `true` while the listener is actively animating toward its destination pan/scale | | `step( dt )` | Advances the pan/zoom animation by `dt` seconds; called automatically unless `stepEmitter: null` | | `panToNode( node, panToCenter, panDirection? )` | Pans so `node`'s bounds become visible within `panBounds` | | `panToTrail( trail )` (internal use pattern) | Keeps a given [`Trail`](/api/scenery/trail)'s bounds in view, panning if they fall outside `panBounds` | | `setPanBounds( bounds )` | Updates the viewport bounds that must stay filled, e.g. on window resize | | `setTargetScale( scale )` | Updates `targetScale` at runtime | | `cancel()` | Cancels any in-progress gesture/animation | | `dispose()` | Releases the listener's internal Properties and DOM gesture listeners | ::: warning It listens for keyboard zoom globally, so give it the whole Display `AnimatedPanZoomListener` attaches trackpad-gesture and keyboard-zoom handling at the `Display` level (via `display.addInputListener(...)`), and reacts to PDOM focus changes to keep the focused element in view. Initializing more than one, or attaching it to something other than the Display's root, produces competing pan/zoom behavior — use the shared `animatedPanZoomSingleton` unless you have a specific reason to manage your own instance. ::: ======================================================================== Page: Circle URL: https://veillette.github.io/Almanach/api/scenery/circle Source: docs/api/scenery/circle.md Category: API | Tags: scenery, Circle, Path | Status: verified ======================================================================== # Circle `Circle` (from `scenerystack/scenery`) is a [`Path`](/api/scenery/path) subclass that builds its own circular `Shape` from a single `radius` parameter, so you don't need to construct a kite `Shape` by hand for the common case of a plain circle. ```ts import { Circle } from 'scenerystack/scenery'; // new Circle( radius, options ) ... const ball = new Circle( 20, { fill: 'crimson', stroke: 'black', lineWidth: 2 } ); // ...or new Circle( { radius, ...options } ) const marker = new Circle( { radius: 5, fill: 'black' } ); ball.radius = 25; // radius is also a settable property ``` ## Options | Option | Effect | | --- | --- | | `radius` | The non-negative radius of the circle, in local coordinates | `Circle` accepts every [`Path` option](/api/scenery/path) (`fill`, `stroke`, `lineWidth`, `boundsMethod`, …) except `shape`/`shapeProperty`, which `Circle` computes itself, plus the full set of [`Node` options](/api/scenery/node). ::: tip Constructor overloads `Circle` supports both `new Circle( radius, options )` and `new Circle( { radius, ...options } )` — pick whichever reads more clearly at the call site; they're equivalent. ::: ======================================================================== Page: Color URL: https://veillette.github.io/Almanach/api/scenery/color Source: docs/api/scenery/color.md Category: API | Tags: scenery, Color, paint, fill, stroke | Status: verified ======================================================================== # Color `Color` (from `scenerystack/scenery`) represents an RGBA color in the sRGB space, and is one of the value types scenery's `fill`/`stroke` options accept directly (alongside a plain CSS string, `null`, or a `Property`). Unlike `Font`, `Color` is **mutable** — its `r`/`g`/`b`/`a` channels can be changed after construction, which lets you reuse one `Color` instance and get every Node using it to repaint automatically. ```ts import { Color, Circle } from 'scenerystack/scenery'; const bodyColor = new Color( 255, 100, 0 ); // r, g, b (0-255), alpha defaults to 1 const fromHex = new Color( 0xff6400 ); // hex number const fromKeyword = new Color( 'orange' ); // any CSS color keyword or string const transparent = new Color( null ); // fully transparent const body = new Circle( 20, { fill: bodyColor } ); bodyColor.red = 200; // mutating the instance repaints every Node using it as fill/stroke ``` ## Construction forms The constructor is overloaded — pass one of: | Form | Example | | --- | --- | | `r, g, b, a?` | `new Color( 255, 100, 0 )`, `new Color( 255, 100, 0, 0.5 )` | | A single hex `number`, `a?` | `new Color( 0xff6400 )` | | A CSS color `string` | `new Color( 'rgb(255,100,0)' )`, `new Color( 'orange' )`, `new Color( '#ff6400' )` | | Another `Color` | `new Color( existingColor )` (copies channel values) | | `null` | `new Color( null )` — fully transparent black | ## Mutating and deriving | Method | Effect | | --- | --- | | `setRGBA( r, g, b, a )` | Sets all four channels at once, returns `this` for chaining | | `red` / `green` / `blue` / `alpha` (get/set) | Individual channel accessors, e.g. `color.alpha = 0.5` | | `setAlpha( a )` | Sets just the alpha channel, returns `this` | | `withAlpha( a )` | Returns a **new** `Color` with a different alpha, leaving the original untouched | | `blend( otherColor, ratio )` | Returns a new `Color` linearly interpolated between this color and `otherColor` | | `brighterColor( factor? )` / `darkerColor( factor? )` | Returns a new, lighter/darker `Color`, useful for hover/pressed button states | | `toCSS()` | Returns the CSS `rgba(...)` string form, e.g. for use outside scenery (DOM styling) | | `equals( otherColor )` | Channel-wise equality check | ## Static helpers | Static member | Effect | | --- | --- | | `Color.toColor( colorSpec )` | Normalizes a `TColor` (string, `Color`, `null`, or `Property` of one) into a `Color` instance | | `Color.interpolateRGBA( c1, c2, distance )` | Interpolates between two colors at a given `distance` (0-1) | | `Color.isDarkColor( color, threshold? )` / `isLightColor( color, threshold? )` | Luminance-based checks, useful for choosing readable text color on top of a background | | `Color.getLuminance( color )` | Returns the color's perceptual luminance | | `Color.grayColor( rgb, a? )` | Shorthand for an equal-channel gray | | `Color.BLACK`, `Color.WHITE`, `Color.RED`, `Color.BLUE`, … | Named static `Color` constants for CSS basic color keywords (also available lowercase, e.g. `Color.black`) | ::: tip A shared, mutable `Color` beats an inline string when a color needs to change at runtime Passing `'orange'` directly as `fill` works, but scenery can't push runtime updates to it — you'd have to reassign `node.fill` yourself. A shared `Color` instance (or better, a `Property` via `ProfileColorProperty`, see [Color Profiles](/styling/color-profiles)) lets every Node using it repaint automatically when the color changes, which is how PhET's light/dark ("projector") theme switching works. ::: ======================================================================== Page: ColorProperty, ProfileColorProperty, and PaintColorProperty URL: https://veillette.github.io/Almanach/api/scenery/color-property-and-profile-color-property Source: docs/api/scenery/color-property-and-profile-color-property.md Category: API | Tags: scenery, ColorProperty, ProfileColorProperty, PaintColorProperty, Color, Property, theming | Status: complete ======================================================================== # ColorProperty, ProfileColorProperty, and PaintColorProperty All three of these classes (from `scenerystack/scenery`) are `Property` subclasses — they let a `fill`/`stroke` respond to a `Property` link like any other dynamic value, instead of being a fixed `Color`/string. They differ in *why* the color changes: - **`ColorProperty`** is the plain case: a `Property` you set directly, with a `valueType`/`phetioValueType` already configured for `Color` so you don't have to specify that yourself every time. - **`ProfileColorProperty`** changes its value automatically based on a global `colorProfileProperty` (e.g. switching between a `'default'` and `'projector'` color scheme) — you give it a map from profile name to color once, and it picks the right one and re-picks whenever the profile changes. - **`PaintColorProperty`** *derives* its value from another paint (a `Color`, a CSS string, a gradient, or even another `Property`) and stays in sync as that source paint changes — optionally brightening or darkening it via `luminanceFactor`. ```ts import { ColorProperty, ProfileColorProperty, PaintColorProperty, Color } from 'scenerystack/scenery'; import { Namespace } from 'scenerystack/phet-core'; // Plain, directly-set color Property const highlightColorProperty = new ColorProperty( new Color( 'yellow' ) ); highlightColorProperty.value = new Color( 'orange' ); // change it later // A namespace is required so ProfileColorProperty names stay globally unique const myNamespace = new Namespace( 'myModule' ); // Switches automatically with the global color profile const backgroundColorProperty = new ProfileColorProperty( myNamespace, 'background', { default: new Color( 'white' ), projector: new Color( '#f0f0f0' ) } ); // Always a slightly darker version of backgroundColorProperty, kept in sync automatically const shadowColorProperty = new PaintColorProperty( backgroundColorProperty, { luminanceFactor: -0.3 } ); ``` ## `ColorProperty` ```ts new ColorProperty( color: Color, providedOptions?: PropertyOptions ) ``` Just a `Property` with `valueType: Color` and `phetioValueType: Color.ColorIO` pre-set — you cannot pass either of those options yourself (it asserts if you try). Use this whenever a color needs to be observable/settable but doesn't need profile-switching or paint-derivation behavior. ## `ProfileColorProperty` ```ts new ProfileColorProperty( namespace: Namespace, colorName: string, colorProfileMap: Record, providedOptions?: PropertyOptions ) ``` `colorProfileMap` must include a `default` entry (asserted at construction) — other keys are additional profiles (PhET's convention is `projector` for a white-background, high-contrast alternative). All map values are eagerly coerced to immutable `Color` instances. Internally, a listener on the shared `colorProfileProperty` re-selects `this.value` from the map (falling back to `default` if the current profile has no entry) every time the profile changes — you never call anything to trigger the switch yourself. `namespace` + `colorName` together form a unique identifier (`${namespace.name}.${colorName}`) used to report color values to PhET's HTML color-editor tooling; constructing two `ProfileColorProperty`s with the same combination asserts. ## `PaintColorProperty` ```ts new PaintColorProperty( paint: TPaint, providedOptions?: PaintColorPropertyOptions ) ``` `paint` accepts anything scenery treats as a paint — a `Color`, a CSS color string, a gradient/pattern, or a `Property`/`TinyProperty` wrapping one of those — and `PaintColorProperty` resolves it to a concrete `Color` value, staying subscribed so `this.value` updates whenever the source paint (or a `Property` wrapping it) changes. | Option | Default | Effect | | --- | --- | --- | | `luminanceFactor` | `0` | `-1` (black) to `1` (white); adjusts the resolved color's brightness via `Color.colorUtilsBrightness()`. `0` applies no change | | Member | Effect | | --- | --- | | `paint` (getter/setter) | Swap to a different source paint at runtime; `this.value` updates immediately | | `luminanceFactor` (getter/setter) | Change the brightening/darkening amount at runtime | ::: tip Use `PaintColorProperty` to derive "lighter"/"darker" variants instead of hand-picking a second color A common pattern is a button's `baseColor` plus a slightly darker `PaintColorProperty` (via `luminanceFactor`) for its pressed/stroke state — if the base color is later changed (including by a `ProfileColorProperty` switching profiles), the derived color updates automatically instead of drifting out of sync. ::: ::: warning `ProfileColorProperty` is a JS-side profile switch, not a full re-theming system Only the colors you've explicitly wrapped in `ProfileColorProperty` respond to `colorProfileProperty` changes — it doesn't retroactively make every `fill`/`stroke` in your sim swappable. Sim-wide color profile support means consistently using `ProfileColorProperty` (often centralized in a single colors file) for every themeable color. ::: ======================================================================== Page: Display URL: https://veillette.github.io/Almanach/api/scenery/display Source: docs/api/scenery/display.md Category: API | Tags: scenery, Display, rendering, input | Status: verified ======================================================================== # Display `Display` (from `scenerystack/scenery`) is the bridge between a [`Node`](/api/scenery/node) scene graph and the actual DOM: it owns a root DOM element, tracks a root `Node`, and repaints that DOM element whenever you call `updateDisplay()`. Every scenery application needs exactly one (or occasionally more) `Display` — nothing is drawn to the page until one exists and is driven. ```ts import { Display, Node, Circle } from 'scenerystack/scenery'; const rootNode = new Node( { children: [ new Circle( 30, { fill: 'orange', x: 100, y: 100 } ) ] } ); const display = new Display( rootNode, { width: 400, height: 300, backgroundColor: 'white' } ); document.body.appendChild( display.domElement ); display.initializeEvents(); // wires up mouse/touch/keyboard input display.updateOnRequestAnimationFrame( dt => { // per-frame update logic (dt is elapsed seconds) goes here } ); ``` The typical lifecycle is: build a root `Node`, construct a `Display` around it, attach `display.domElement` to the page, call `initializeEvents()` if the scene needs input, then drive repaints with `updateOnRequestAnimationFrame()` (or call `updateDisplay()` yourself in a custom loop). ## Options | Option | Default | Effect | | --- | --- | --- | | `width` / `height` | `640` / `480` | Initial size of the Display's DOM element | | `backgroundColor` | `null` | A CSS color string or `Color` applied to the root DOM element | | `allowCSSHacks` | `true` | Applies CSS (disables text selection, touch callouts, drag) that makes the element behave like an interactive app surface rather than a document | | `allowSceneOverflow` | `false` | When `false`, content outside the Display's bounds is clipped | | `allowWebGL` | `true` | Whether WebGL-rendered drawables are allowed at all in this Display | | `accessibility` | `true` | Enables PDOM (accessibility tree) creation and support | | `interactive` | `true` | Whether input events reach listeners; setting `false` also interrupts in-progress input | | `assumeFullWindow` | `false` | If `true`, skips a `getBoundingClientRect()` call per event by assuming the Display fills the browser viewport | | `container` | — | An existing `HTMLElement` to repurpose as the Display's root element, instead of creating a new one | ## Frequently used methods | Method | Effect | | --- | --- | | `updateDisplay()` | Synchronously syncs the scene graph and repaints the DOM element — call this once per frame | | `updateOnRequestAnimationFrame( stepCallback? )` | Drives `updateDisplay()` from `requestAnimationFrame`, optionally calling `stepCallback( dt )` first each frame | | `initializeEvents( options? )` | Attaches browser input listeners so `Node` input listeners (e.g. [`FireListener`](/api/scenery/fire-listener)) start receiving events | | `detachEvents()` | Reverses `initializeEvents()` | | `addInputListener( listener )` / `removeInputListener( listener )` | Adds/removes a listener that hears every input event on the whole Display, not scoped to one Node | | `setWidthHeight( width, height )` | Resizes the DOM element on the next `updateDisplay()` | | `getRootNode()` / `.rootNode` | Returns the root `Node` this Display renders | | `dispose()` | Tears down the Display, detaching events and releasing DOM/listener resources | ## Reading `domElement` `display.domElement` (or `display.getDOMElement()`) is the plain `HTMLElement` you attach to the page yourself — `Display` does not insert itself into the DOM automatically: ```ts document.body.appendChild( display.domElement ); ``` ::: tip Input requires `initializeEvents()` Constructing a `Display` does not wire up mouse/touch/keyboard listeners by itself — attach a root `Node`'s [`inputListeners`](/api/scenery/node) (like a [`FireListener` or `PressListener`](/api/scenery/fire-listener)) and then call `display.initializeEvents()` once, or those listeners will never fire. Call `display.detachEvents()` if you need to reverse this later (e.g. tearing down the Display). ::: ::: warning Don't call `updateDisplay()` more than once per frame `updateOnRequestAnimationFrame()` already schedules a repaint every animation frame; calling `updateDisplay()` again yourself outside that loop (or from multiple independent loops) does redundant sync/repaint work. Prefer driving all per-frame work through the single `stepCallback` passed to `updateOnRequestAnimationFrame()`. ::: ======================================================================== Page: DragListener URL: https://veillette.github.io/Almanach/api/scenery/drag-listener Source: docs/api/scenery/drag-listener.md Category: API | Tags: scenery, DragListener, PressListener, input, drag | Status: complete ======================================================================== # DragListener `DragListener` (from `scenerystack/scenery`) is a `PressListener` subclass specialized for dragging: it tracks a pointer from press through release, converts the pointer's position into local/parent/model coordinate frames, and (optionally) writes the result straight into a `positionProperty`. As [`PressListener` and `FireListener`](/api/scenery/fire-listener) documents, `PressListener` is the shared base for both — `FireListener` adds a `fire()` callback for clicks, while `DragListener` adds coordinate tracking and repositioning for drags. If you also need keyboard support on the same draggable object, reach for [`RichDragListener`](/api/scenery/rich-drag-listener) instead, which composes a `DragListener` with a [`KeyboardDragListener`](/api/scenery/keyboard-drag-listener) rather than requiring you to wire up both yourself. ```ts import { Node, DragListener } from 'scenerystack/scenery'; import { Vector2 } from 'scenerystack/dot'; import { Property } from 'scenerystack/axon'; const positionProperty = new Property( new Vector2( 0, 0 ) ); const body = new Node( { cursor: 'pointer' } ); body.translation = positionProperty.value; body.addInputListener( new DragListener( { positionProperty: positionProperty, transform: modelViewTransform, // maps model <-> view (parent) coordinates dragBoundsProperty: model.dragBoundsProperty, start: () => body.moveToFront(), end: () => { /* drag finished */ } } ) ); positionProperty.link( position => { body.translation = modelViewTransform.modelToViewPosition( position ); } ); ``` This is the typical PhET pattern: give `DragListener` a `positionProperty` and a `transform`, and it keeps the model position in sync with the pointer without you writing any coordinate math. If you'd rather read the drag out manually, omit `positionProperty` and use the `drag` callback together with `listener.modelDelta`/`listener.modelPoint`. ## Options `DragListenerOptions` combines everything [`PressListener`](/api/scenery/fire-listener) accepts (`targetNode`, `attach`, `mouseButton`, `pressCursor`, `canStartPress`, `collapseDragEvents`, …) with: | Option | Default | Effect | | --- | --- | --- | | `positionProperty` | `null` | A `Property`-like object (needs only a settable `.value`) kept in sync with the drag, in the model coordinate frame | | `transform` | `null` | A `Transform3` or `TReadOnlyProperty` mapping the parent (view) frame to the model frame | | `dragBoundsProperty` | `null` | Constrains the model position to a `Bounds2` (via `closestPointTo`); combine with `mapPosition` for custom constraints | | `mapPosition` | `null` | `(point: Vector2) => Vector2` — custom mapping from desired to allowed model position, applied before `dragBoundsProperty` | | `translateNode` | `false` | If `true`, directly sets the drag target's `translation` during the drag, instead of (or in addition to) `positionProperty` | | `start` / `drag` / `end` | no-ops | Preferred over overriding `press`/`release` — these fire after the listener has already updated its internal state for the event | | `allowTouchSnag` | `true` | Whether an unattached touch that moves across the target will "snag" and start a press — helps with small draggable targets | | `applyOffset` | `true` | Whether the initial offset between the pointer and the target's local origin is preserved throughout the drag | | `useParentOffset` | `false` | Compute the offset in the parent coordinate frame from `positionProperty` instead of the target Node's actual transform — for cases where no single Node's transform represents the dragged thing | | `trackAncestors` | `false` | If `true`, an internal [`TransformTracker`](/api/scenery/transform-tracker) watches ancestor transforms and repositions automatically when they change | | `offsetPosition` | `null` | `(point, listener) => Vector2` added to the parent point before computation — useful to offset touch drags away from being directly under the finger | | `canClick` | `false` | `DragListener` normally can't be "clicked" (a single tap would pick up and immediately drop); set `true` to allow accessible single-activation in addition to dragging | ## Reading the drag state | Member | Meaning | | --- | --- | | `isPressedProperty` / `isUserControlledProperty` | Whether a drag is in progress (`isUserControlledProperty` is just a more readable alias for the same Property) | | `globalPoint` / `parentPoint` / `localPoint` / `modelPoint` | Defensive-copy getters for the current drag point in each coordinate frame | | `modelDelta` | The change in `modelPoint` since the previous reposition | | `dragBounds` | The current value of `dragBoundsProperty` | | `reposition( globalPoint )` | Recomputes all the coordinate-frame points and applies `positionProperty`/`translateNode`; called automatically on pointer move | | `interrupt()` | Cancels an in-progress drag without it looking like a normal release | | `dispose()` | Releases the listener's `Property`/`Action` resources | ::: tip DragListener is not itself keyboard-accessible `DragListener` only responds to pointer input (mouse/touch/pen). If a draggable object also needs to work with a keyboard, don't hand-wire a separate [`KeyboardDragListener`](/api/scenery/keyboard-drag-listener) alongside it — use [`RichDragListener`](/api/scenery/rich-drag-listener), which composes both and keeps their shared options (`positionProperty`, `transform`, `dragBoundsProperty`, `start`/`drag`/`end`) in sync automatically. ::: ::: warning `tandem` defaults to required, and drag actions are read-only for PhET-iO Like `PressListener`/`FireListener`, `DragListener` defaults `tandem` to `Tandem.REQUIRED`. Its internal `dragAction` is also `phetioReadOnly: true` by default — PhET-iO can observe drag events in the data stream, but can't trigger them, since simulating a drag requires more state than a single data-stream call can carry. ::: ======================================================================== Page: Filter and the Node.filters visual effects URL: https://veillette.github.io/Almanach/api/scenery/visual-filters Source: docs/api/scenery/visual-filters.md Category: API | Tags: scenery, Filter, Brightness, Contrast, Grayscale, Invert, Sepia, HueRotate, Saturate, ColorMatrixFilter, Node | Status: complete ======================================================================== # Filter and the Node.filters visual effects `Filter` (from `scenerystack/scenery`) is the abstract base class behind [`Node`](/api/scenery/node)'s `filters` option — an array of post-processing effects applied to that Node's rendered subtree, the scenery equivalent of the CSS `filter` property. You rarely construct a `Filter` directly; instead you construct one of its named subtypes (`Brightness`, `Contrast`, `Grayscale`, `Invert`, `Sepia`, `HueRotate`, `Saturate`, or a custom `ColorMatrixFilter`) and assign an array of them to `filters`. ```ts import { Node, Circle, Grayscale, Brightness, Contrast } from 'scenerystack/scenery'; const icon = new Node( { children: [ new Circle( 30, { fill: 'crimson' } ) ] } ); // Applied in array order: grayscale first, then brighten the result. icon.filters = [ new Grayscale( 1 ), new Brightness( 1.2 ) ]; // A disabled-looking control, built from two filters composed together: const disabledLookFilters = [ new Contrast( 0.6 ), new Brightness( 1.1 ) ]; ``` ## The shared pattern Every named filter is a small class that computes a fixed CSS filter string (e.g. `grayscale(1)`) and, where the effect can be expressed as a 4x5 color matrix (all of them except `Invert`), extends `ColorMatrixFilter` so the same math also works under SVG and Canvas rendering — not just DOM/CSS. `Node.filters` accepts an array so multiple effects can stack; they apply in array order. All of these filters live under `scenerystack/scenery`. Most take a single `amount` constructor argument that behaves like the equivalent CSS filter function: | Filter | Constructor | `amount` meaning | | --- | --- | --- | | `Brightness` | `new Brightness( amount )` | `0` = black, `1` = normal, greater = brighter. Non-negative, required | | `Contrast` | `new Contrast( amount )` | `0` = flat gray, `1` = normal, greater = higher contrast. Non-negative, required | | `Grayscale` | `new Grayscale( amount = 1 )` | `0` = unchanged, `1` = fully gray-scale. Must be in `[0, 1]` | | `Invert` | `new Invert( amount = 1 )` | `0` = unchanged, `1` = fully inverted. Must be in `[0, 1]` | | `Sepia` | `new Sepia( amount = 1 )` | `0` = unchanged, `1` = fully sepia-toned. Must be in `[0, 1]` | | `Saturate` | `new Saturate( amount )` | `0` = fully desaturated (grayscale), `1` = normal, greater = over-saturated. Non-negative, required | | `HueRotate` | `new HueRotate( amount )` | Hue rotation **in radians** (converted to degrees internally for the CSS string). Non-negative, required | `Brightness` and `Grayscale` each expose a convenience static: `Brightness.BLACKEN` (`new Brightness( 0 )`) and `Grayscale.FULL` (`new Grayscale( 1 )`); `Contrast.GRAY` (`new Contrast( 0 )`) similarly turns content flat gray. ## `ColorMatrixFilter` ```ts new ColorMatrixFilter( m00, m01, m02, m03, m04, m10, m11, m12, m13, m14, m20, m21, m22, m23, m24, m30, m31, m32, m33, m34 ) ``` Every named filter above (except `Invert`) is implemented purely by calling `super(...)` with 20 fixed matrix values — `ColorMatrixFilter` multiplies this 4x5 matrix against each pixel's `[r, g, b, a, 1]` column to produce a new `[r, g, b, a]`. Writing a custom `ColorMatrixFilter` is possible but PhET's own guidance is to prefer the named subtypes wherever they cover the need: a custom matrix filter is SVG/Canvas-compatible but is **not** compatible with WebGL or DOM content, and forces everything under it into a single SVG or Canvas block, which can reduce rendering performance. ## `Filter` (the abstract base) You won't subclass `Filter` directly in ordinary sim code, but its interface is why every filter can be mixed freely: each subclass implements `getCSSFilterString()` (for DOM/Canvas), `applySVGFilter( svgFilter, inName, resultName? )` (for SVG), and `applyCanvasFilter( wrapper )` (the ImageData fallback path), plus compatibility checks (`isDOMCompatible()`, `isSVGCompatible()`, `isCanvasCompatible()`, `isWebGLCompatible()`) that scenery uses to decide which rendering strategy can honor a given `filters` array. ::: tip Filters apply to the whole subtree, and can be animated like any other option Because `filters` is a plain `Node` option, it can be set/replaced at any time — e.g. swap in `[ new Grayscale( fraction ) ]` on every frame of a fade-to-grayscale transition by recomputing the array as `fraction` changes. There's no `Filter` `Property` API for interpolating an amount smoothly; you construct a fresh filter instance with the new amount. ::: ::: warning Mixed compatibility can force a slower rendering path Not every filter works under every renderer. If a Node's subtree can't be placed in a single SVG block or Canvas (e.g. because of a `layerSplit` or DOM content), some filters may fall back to a more expensive strategy, or in the case of custom `ColorMatrixFilter`s under WebGL/DOM content, may not be supported at all. Prefer the DOM-compatible named filters (all of the ones listed above are DOM-compatible) when performance matters and the content underneath isn't guaranteed to be single-block SVG or Canvas. ::: ======================================================================== Page: FlowBox URL: https://veillette.github.io/Almanach/api/scenery/flow-box Source: docs/api/scenery/flow-box.md Category: API | Tags: scenery, FlowBox, HBox, VBox, layout | Status: verified ======================================================================== # FlowBox `FlowBox` (from `scenerystack/scenery`) is a [`Node`](/api/scenery/node) that automatically positions its children along one axis (a row or a column), with configurable spacing, alignment, and wrapping — the scenery equivalent of a CSS flexbox with a single main axis. It re-runs layout whenever children are added/removed/reordered or resized, so you don't have to compute positions by hand. In practice, most code uses the fixed-orientation convenience subclasses `HBox` (`orientation: 'horizontal'`) and `VBox` (`orientation: 'vertical'`) instead of `FlowBox` directly. ```ts import { HBox, Circle, Rectangle } from 'scenerystack/scenery'; const row = new HBox( { spacing: 10, align: 'center', children: [ new Circle( 15, { fill: 'crimson' } ), new Rectangle( 0, 0, 40, 30, { fill: 'teal' } ), new Circle( 10, { fill: 'goldenrod' } ) ] } ); ``` Adding, removing, or reordering `row.children` afterward re-lays-out the remaining children automatically — no manual repositioning needed. ## Options | Option | Default | Effect | | --- | --- | --- | | `orientation` | `'horizontal'` | `'horizontal'` (row) or `'vertical'` (column) — fixed by `HBox`/`VBox` | | `spacing` | `0` | Gap between adjacent children along the main axis | | `lineSpacing` | `0` | Gap between wrapped lines, when `wrap: true` | | `align` | `'center'` | Cross-axis alignment for children — for a horizontal `FlowBox`: `'top'`, `'bottom'`, `'center'`, or `'origin'`; for a vertical one: `'left'`, `'right'`, `'center'`, or `'origin'` | | `stretch` | `false` | Whether children grow to fill the cross axis (requires the child to be resizable on that axis) | | `grow` | `0` | How much a child should grow to fill leftover main-axis space, relative to other children's `grow` | | `justify` | `'spaceBetween'` | How extra main-axis space is distributed, e.g. `'left'`/`'right'`/`'center'` (horizontal) or `'top'`/`'bottom'`/`'center'` (vertical), plus `'spaceBetween'`, `'spaceAround'`, `'spaceEvenly'` | | `wrap` | `false` | Whether children flow onto additional lines instead of overflowing | | `resize` | `true` | Whether the FlowBox keeps re-running layout after construction; set `false` and call `updateLayout()` manually for performance-sensitive cases | | `forward` | `true` | If `false`, children are laid out in reverse order (useful for RTL locales via `forwardProperty`) | | `margin` / `xMargin` / `yMargin` / `leftMargin` / `rightMargin` / `topMargin` / `bottomMargin` | `0` | Per-cell margins, settable box-wide or per-child via `layoutOptions` | | `minContentWidth` / `minContentHeight` / `maxContentWidth` / `maxContentHeight` | — | Clamp the FlowBox's own content size | Per-child overrides (`align`, `stretch`, `grow`, `margin`, …) can also be set on an individual child via that child's `layoutOptions`, taking precedence over the FlowBox-wide value: ```ts someChild.layoutOptions = { grow: 1, stretch: true }; ``` ## Methods | Method | Effect | | --- | --- | | `getCell( node )` | Returns the internal `FlowCell` tracking a given child, for advanced layout inspection | | `updateLayout()` (via `constraint`) | Forces an immediate re-layout, useful when `resize: false` | | `setForward( forward )` / `.forward` | Toggles child order, or bind `forwardProperty` to a locale/orientation `Property` | | `dispose()` | Releases layout listeners; call this when discarding the box for good | ::: tip Reach for `HBox`/`VBox` first `HBox` and `VBox` are `FlowBox` with `orientation` fixed to `'horizontal'`/`'vertical'` — use `FlowBox` directly only when the orientation itself needs to change at runtime. ::: ::: warning Per-child layout options live in `layoutOptions`, not on the child itself Options like `align`, `grow`, `stretch`, and margins are consumed by the parent `FlowBox` from each child's `layoutOptions` object, not from options passed directly to the child's own constructor. Setting `someChild.align = 'center'` does nothing — set `someChild.layoutOptions = { align: 'center' }` instead (or pass `align` on the `FlowBox` itself to apply it to every child). ::: ======================================================================== Page: FocusManager URL: https://veillette.github.io/Almanach/api/scenery/focus-manager Source: docs/api/scenery/focus-manager.md Category: API | Tags: scenery, FocusManager, focus, accessibility, pdomFocus | Status: verified ======================================================================== # FocusManager `FocusManager` (from `scenerystack/scenery`) is the class-level API behind scenery's focus system — it owns the Properties that track *where* focus currently is (DOM/keyboard focus, pointer hover focus for [Interactive Highlighting](/api/scenery/interactive-highlighting), and reading-block focus for Voicing) and a few Properties that control whether the corresponding highlights are visible. This page documents `FocusManager` itself; for what the Parallel DOM is and how to opt a `Node` into it, see [The Parallel DOM](/accessibility/pdom), and for customizing the highlight that's drawn, see [Focus Highlights](/accessibility/focus-highlights). Simulation code rarely constructs a `FocusManager` directly — a `Display` owns one — but reads and writes its static `pdomFocus` to inspect or move keyboard focus programmatically: ```ts import { FocusManager } from 'scenerystack/scenery'; // What Node currently has DOM/keyboard focus, if any const focusedNode = FocusManager.pdomFocus?.trail.lastNode() ?? null; // Move focus to null to defocus everything (also blurs the active element) FocusManager.pdomFocus = null; ``` ## Static members `pdomFocus` is a **static** accessor, because the DOM itself only allows one focused element at a time — it wouldn't make sense per-Display. | Member | Effect | | --- | --- | | `FocusManager.pdomFocus` | Static get/set `Focus \| null` — the `Focus` (a `Display` + `Trail` pair) whose Node currently has DOM/keyboard focus. Setting it to `null` blurs the active element | | `FocusManager.pdomFocusProperty` | Static `Property` mirroring `pdomFocus`, for listening to focus changes rather than polling | | `FocusManager.windowHasFocusProperty` | Static `TReadOnlyProperty` — whether the browser window itself currently has focus (useful for pausing global keyboard listeners when the tab is backgrounded) | | `FocusManager.attachToWindow()` / `detachFromWindow()` | Static — wires/unwires the window `focus`/`blur` listeners behind `windowHasFocusProperty`; called for you when a `Display` initializes events | ## Instance Properties Each `Display` holds one `FocusManager` instance with these Properties: | Property | Tracks | | --- | --- | | `pointerFocusProperty` | `Focus \| null` — the Node under the pointer that supports [Interactive Highlighting](/api/scenery/interactive-highlighting) | | `lockedPointerFocusProperty` | `Focus \| null` — set while a pointer is actively interacting with a Node, so the highlight doesn't jump to whatever the pointer happens to be over mid-drag | | `readingBlockFocusProperty` | `Focus \| null` — the Node whose Voicing "reading block" content is currently being spoken | | `pdomFocusHighlightsVisibleProperty` | `boolean` — whether highlights for DOM/keyboard focus should be drawn | | `interactiveHighlightsVisibleProperty` | `boolean` — whether pointer-hover highlights should be drawn (a user preference, off by default) | | `readingBlockHighlightsVisibleProperty` | `boolean` — whether reading-block highlights should be drawn | | `pointerHighlightsVisibleProperty` | Derived `boolean` — `true` if either interactive-highlight or reading-block highlights are enabled | ::: tip Setting `pdomFocus` does not scroll or validate focusability for you `FocusManager.pdomFocus = new Focus( display, trail )` moves scenery's notion of focus, but the underlying DOM element must actually be focusable (`focusable: true` and present in the PDOM, see [The Parallel DOM](/accessibility/pdom)) — assigning a `Focus` for a Node that isn't focusable is a no-op from the browser's perspective even though the Property changes. ::: ======================================================================== Page: Font URL: https://veillette.github.io/Almanach/api/scenery/font Source: docs/api/scenery/font.md Category: API | Tags: scenery, Font, typography, text | Status: verified ======================================================================== # Font `Font` (from `scenerystack/scenery`) is an immutable object describing a CSS-style font — style, variant, weight, stretch, size, line-height, and family — used by [`Text`](/api/scenery/text) and `RichText`'s `font` option. Simulation code almost always reaches for `PhetFont` (from `scenerystack/scenery-phet`) instead of constructing a plain `Font` directly; see [Typography and Fonts](/styling/typography-and-fonts) for that convention. This page documents the lower-level `Font` class itself. ```ts import { Font, Text } from 'scenerystack/scenery'; const font = new Font( { family: '"Times New Roman", serif', style: 'italic', size: 16, weight: 'bold' } ); const label = new Text( 'Hello', { font } ); font.font; // "italic bold 16px 'Times New Roman', serif" — the combined CSS shorthand font.family; // "'Times New Roman', serif" ``` `Font` is immutable: every property is set once at construction and there are no setters, only getters — to change a Text's font, construct a new `Font` (or use `Text`'s `fontSize`/`fontWeight`/etc. shorthand setters, which do this for you). ## Options | Option | Default | Effect | | --- | --- | --- | | `style` | `'normal'` | `'normal' \| 'italic' \| 'oblique'` | | `variant` | `'normal'` | `'normal' \| 'small-caps'` | | `weight` | `'normal'` | A CSS font-weight keyword (`'normal'`, `'bold'`, `'bolder'`, `'lighter'`) or a `'100'`–`'900'` string/number | | `stretch` | `'normal'` | A CSS font-stretch keyword, e.g. `'condensed'`, `'expanded'` | | `size` | `'10px'` | A number (treated as a pixel count) or any valid CSS font-size string | | `lineHeight` | `'normal'` | A CSS line-height value | | `family` | `'sans-serif'` | A comma-separated CSS font-family list | ## Reading values | Member | Returns | | --- | --- | | `font` | The combined CSS font shorthand string, e.g. `"bold 10px sans-serif"` | | `style`, `variant`, `weight`, `stretch`, `size`, `lineHeight`, `family` | The individual component, as given (or defaulted) at construction | ## Static helpers | Static member | Effect | | --- | --- | | `Font.DEFAULT` | The shared default `Font` instance (`10px sans-serif`) | | `Font.isFontStyle( s )` / `isFontVariant( s )` / `isFontWeight( s )` / `isFontStretch( s )` | Type-guard validators for each component's valid string values | | `Font.fromCSS( cssString )` | Parses a CSS font shorthand string (e.g. `"italic bold 16px serif"`) into a `Font` instance | ::: tip Prefer `PhetFont`, not raw `Font` `PhetFont` (from `scenerystack/scenery-phet`) wraps `Font` with the project's standard typeface and a guaranteed `sans-serif` fallback, so simulation code stays visually consistent. Reach for plain `Font` only when working outside a PhET-style project, or when building `PhetFont` itself. See [Typography and Fonts](/styling/typography-and-fonts) for the full convention. ::: ======================================================================== Page: GridBox URL: https://veillette.github.io/Almanach/api/scenery/grid-box Source: docs/api/scenery/grid-box.md Category: API | Tags: scenery, GridBox, layout | Status: verified ======================================================================== # GridBox `GridBox` (from `scenerystack/scenery`) is a [`Node`](/api/scenery/node) that arranges its children in a two-dimensional grid of rows and columns, similar to CSS grid. Where [`FlowBox`](/api/scenery/flow-box) only handles one axis at a time, `GridBox` positions each child at an explicit `(row, column)` cell (or lets it auto-place), independently controlling per-column width and per-row height so everything lines up. ```ts import { GridBox, Rectangle, Circle } from 'scenerystack/scenery'; const grid = new GridBox( { xSpacing: 10, ySpacing: 8, children: [ new Rectangle( 0, 0, 40, 40, { fill: 'teal', layoutOptions: { column: 0, row: 0 } } ), new Circle( 20, { fill: 'crimson', layoutOptions: { column: 1, row: 0 } } ), new Rectangle( 0, 0, 90, 20, { fill: 'goldenrod', layoutOptions: { column: 0, row: 1, horizontalSpan: 2 } } ) ] } ); ``` Each child's cell position and span are set through that child's `layoutOptions`, not through `GridBox` options — `GridBox` itself only configures grid-wide defaults (spacing, alignment, margins) and how auto-placement behaves. ## `GridBox`-level options | Option | Default | Effect | | --- | --- | --- | | `rows` | — | Sets children all at once from a 2D `(Node \| null)[][]` (`rows[row][column]`); mutually exclusive with `children`/`columns` | | `columns` | — | Same idea, transposed (`columns[column][row]`); mutually exclusive with `children`/`rows` | | `autoRows` | `null` | When set, children auto-place into 1-column-wide cells, wrapping to a new column after this many rows | | `autoColumns` | `null` | Same idea, wrapping to a new row after this many columns | | `resize` | `true` | Whether the GridBox keeps re-running layout after construction | | `spacing` | `0` | Shorthand for setting both `xSpacing` and `ySpacing` | | `xSpacing` / `ySpacing` | `0` | Gap between columns / between rows | | `xAlign` | `'center'` | Horizontal alignment within a cell: `'left'`, `'right'`, `'center'`, or `'origin'` | | `yAlign` | `'center'` | Vertical alignment within a cell: `'top'`, `'bottom'`, `'center'`, or `'origin'` | | `margin` / `xMargin` / `yMargin` / `leftMargin` / `rightMargin` / `topMargin` / `bottomMargin` | `0` | Per-cell margins, box-wide or overridable per child | | `minContentWidth` / `minContentHeight` / `maxContentWidth` / `maxContentHeight` | — | Clamp the GridBox's own content size | ## Per-child `layoutOptions` (grid position) These can **only** be set via a child's `layoutOptions`, not as `GridBox` constructor options: | `layoutOptions` key | Effect | | --- | --- | | `column` | Zero-based column index for this child (leftmost column is `0`) | | `row` | Zero-based row index for this child (topmost row is `0`) | | `horizontalSpan` | Number of columns this child occupies (default `1`) | | `verticalSpan` | Number of rows this child occupies (default `1`) | | `xAlign` / `yAlign` / `xStretch` / `yStretch` / `xGrow` / `yGrow` / margins | Per-child overrides of the corresponding `GridBox`-level option | ## Methods | Method | Effect | | --- | --- | | `setLines( orientation, lineArrays )` / `getLines( orientation )` | Programmatic equivalent of the `rows`/`columns` options, for reading or rewriting the whole grid at once | | `dispose()` | Releases layout listeners | ::: tip Auto-placement vs. explicit placement Use `autoRows`/`autoColumns` when children are added dynamically and you just want them to flow into a grid (like a tag list). Use explicit `layoutOptions.row`/`.column` (or the `rows`/`columns` constructor options) when the grid has a fixed, meaningful structure — the two approaches are mutually exclusive per the source's own `NOTE`s. ::: ::: warning `rows`/`columns` rewrites `layoutOptions` and replaces all children Passing `rows` or `columns` mutates every listed child's `layoutOptions` to bake in its `(row, column)` position, and replaces the `GridBox`'s entire `children` list. Don't mix it with manually calling `addChild()`/`removeChild()` afterward without accounting for that; prefer `autoRows`/`autoColumns` or manual `layoutOptions` if you need to add/remove children incrementally. ::: ======================================================================== Page: HBox URL: https://veillette.github.io/Almanach/api/scenery/h-box Source: docs/api/scenery/h-box.md Category: API | Tags: scenery, HBox, VBox, FlowBox, layout | Status: verified ======================================================================== # HBox `HBox` (from `scenerystack/scenery`) is [`FlowBox`](/api/scenery/flow-box) with `orientation` fixed to `'horizontal'` — it lays its children out left-to-right in a single row. Like [`VBox`](/api/scenery/v-box), it's the class most code reaches for instead of `FlowBox` directly, since orientation rarely changes at runtime; `HBox` accepts every `FlowBox` option (`spacing`, `align`, `stretch`, `grow`, `justify`, `wrap`, margins, …) except `orientation`, which the constructor sets for you and asserts you didn't also pass. ```ts import { HBox, Circle, Rectangle } from 'scenerystack/scenery'; const row = new HBox( { spacing: 10, align: 'center', children: [ new Circle( 15, { fill: 'crimson' } ), new Rectangle( 0, 0, 40, 30, { fill: 'teal' } ), new Circle( 10, { fill: 'goldenrod' } ) ] } ); ``` For a vertical (column) layout, use [`VBox`](/api/scenery/v-box) instead; for the full option table (spacing, alignment, growth, wrapping, margins) see [`FlowBox`](/api/scenery/flow-box), which documents everything `HBox` inherits. ## Options and methods `HBoxOptions` is `FlowBoxOptions` with `orientation` omitted — every option and method documented on [`FlowBox`](/api/scenery/flow-box) (`spacing`, `align`, `grow`, `justify`, `wrap`, `getCell()`, `dispose()`, …) applies here unchanged, just fixed to a horizontal main axis. For a row layout, `align` takes the vertical-alignment values (`'top'`, `'bottom'`, `'center'`, `'origin'`) since it controls cross-axis (vertical) placement of each child. ::: warning Use `VSeparator`, not `HSeparator`, to divide an `HBox` `HBox` asserts against inserting an `HSeparator` child — a *horizontal* dividing line makes no sense between columns laid out *horizontally*. Use `VSeparator` (a vertical rule, also from `scenerystack/scenery`) to visually divide sections of an `HBox`; `HSeparator` is for dividing rows inside a [`VBox`](/api/scenery/v-box) instead. ::: ======================================================================== Page: Highlight Rendering: HighlightPath and HighlightOverlay URL: https://veillette.github.io/Almanach/api/scenery/highlight-rendering Source: docs/api/scenery/highlight-rendering.md Category: API | Tags: scenery, HighlightPath, HighlightOverlay, HighlightFromNode, accessibility, focus | Status: complete ======================================================================== # Highlight Rendering: HighlightPath and HighlightOverlay [Focus Highlights](/accessibility/focus-highlights) explains *how to customize* the highlight a focusable Node shows — swapping in a `HighlightFromNode`, a raw `Shape`, or a Node of your own. This page covers the machinery underneath that actually draws those highlights: `HighlightPath` (from `scenerystack/scenery`), the `Path` subclass with the double-stroke "glow" look, and `HighlightOverlay`, the per-`Display` overlay that decides which highlight is currently active and renders it in its own child `Display`. ## HighlightPath: the double-stroke look A `HighlightPath` is a `Path` with a second, child `Path` (`innerHighlightPath`) drawn on top — an "outer" highlight (lighter, wider) and an "inner" highlight (darker, more opaque), giving the appearance of a highlight fading outward. `HighlightFromNode` (used throughout the [Focus Highlights guide](/accessibility/focus-highlights)) is a `HighlightPath` subclass that derives its shape from a Node's bounds and keeps it updated as those bounds change. ```ts import { HighlightPath } from 'scenerystack/scenery'; import { Shape } from 'scenerystack/kite'; const highlight = new HighlightPath( Shape.roundRect( 0, 0, 40, 40, 4, 4 ), { dashed: true // e.g. to indicate the target is currently being manipulated } ); ``` | Option | Default | Effect | | --- | --- | --- | | `outerStroke` / `innerStroke` | `HighlightPath.OUTER_FOCUS_COLOR` / `INNER_FOCUS_COLOR` | Colors of the two strokes | | `outerLineWidth` / `innerLineWidth` | `null` (computed from the local-to-global transform) | Fix the line widths instead of letting them scale automatically with the highlighted Node | | `lineDashOverride` | `null` | Fixes the dash pattern instead of letting it scale with the transform | | `dashed` | `false` | Whether the highlight uses a dashed stroke — PhET convention for "this is currently grabbed/being manipulated" | | `transformSourceNode` | `null` | The Node whose transform this highlight should track (set automatically by `HighlightFromNode`) | Static helpers worth knowing: `HighlightPath.getDilationCoefficient( matrix )` computes how far outside a Node's bounds the highlight should sit so there's visible whitespace between the Node and the inner edge of the highlight, and `HighlightPath.getDefaultHighlightLineWidth()` returns the untransformed default line width — useful if you need to reserve layout space for a highlight. ## HighlightOverlay: one per Display Every [`Display`](/api/scenery/display) that supports focus gets a `HighlightOverlay`, which owns a completely separate child `Display` layered on top of the main scene graph (`pointer-events: none`, so it never intercepts input). It listens to the global PDOM focus Property, the [`FocusManager`](/api/scenery/focus-manager)'s pointer/reading-block focus Properties, and picks one of four modes per activation: | Mode | When | | --- | --- | | `'bounds'` | Default — no custom `focusHighlight` was set; draws a `HighlightFromNode` around the focused Node's bounds | | `'shape'` | The Node's `focusHighlight`/`interactiveHighlight` is a kite `Shape` | | `'node'` | The Node's highlight is itself a `Node` (optionally `focusHighlightLayerable` to place it in the main scene graph instead of the overlay) | | `'invisible'` | The literal string `'invisible'` — suppresses the highlight entirely (mainly for automated testing) | `HighlightOverlay` also owns the group-focus-highlight and reading-block-highlight rendering, and exposes static setters for globally re-theming every highlight in an application: ```ts import { HighlightOverlay } from 'scenerystack/scenery'; import { Color } from 'scenerystack/scenery'; // Re-theme every focus highlight application-wide, e.g. for a dark background. HighlightOverlay.setInnerHighlightColor( new Color( 'yellow' ) ); ``` | Static method | Effect | | --- | --- | | `setInnerHighlightColor( color )` / `getInnerHighlightColor()` | Inner stroke color used by all subsequently drawn focus highlights | | `setOuterHilightColor( color )` / `getOuterHighlightColor()` | Outer stroke color (note the source's method name is `setOuterHilightColor`, missing a "g") | | `setInnerGroupHighlightColor` / `setOuterGroupHighlightColor` | Same, for group focus highlights | ::: tip Line width and dash scale automatically with pan/zoom and Node scale `HighlightOverlay` recomputes each highlight's line width and dash pattern every time its [`TransformTracker`](/api/scenery/transform-tracker) reports a transform change, combining the Node's own scale with the inverse of the current pan/zoom matrix (`HighlightPath.getCorrectiveScalingMatrix()`). This is why a focus highlight looks the same relative thickness whether the Node is scaled up, scaled down, or the user has zoomed in with [`AnimatedPanZoomListener`](/api/scenery/animated-pan-zoom-listener) — you don't need to correct for either case yourself. ::: ======================================================================== Page: Hit-Testing and Picking URL: https://veillette.github.io/Almanach/api/scenery/hit-testing-and-picking Source: docs/api/scenery/hit-testing-and-picking.md Category: API | Tags: scenery, Picker, pickable, mouse-area, touch-area, cursor, input | Status: complete ======================================================================== # Hit-Testing and Picking Every `Node` internally owns a `Picker` (`scenerystack/scenery`, but never constructed directly — it's an implementation detail of `Node`) that decides whether a given point hits that Node's subtree, and caches enough bounds information to prune most of the scene graph before doing expensive per-shape containment checks. You won't call into `Picker` yourself, but its behavior is controlled entirely through Node options you *do* set directly: `pickable`, `mouseArea`, `touchArea`, and `cursor`. ```ts import { Circle } from 'scenerystack/scenery'; import { Shape } from 'scenerystack/kite'; const smallTarget = new Circle( 6, { fill: 'red', cursor: 'pointer', // Expand the *touch* hit region well beyond the drawn 6px radius, without changing // how it looks or how mouse hit-testing behaves. touchArea: Shape.circle( 0, 0, 22 ) } ); ``` ## `pickable`: the tri-state that controls pruning | Value | Meaning | | --- | --- | | `null` (default) | Pass-through: this subtree is only hit-tested if some ancestor or descendant is `pickable: true` or has an input listener attached | | `false` | This Node and its entire subtree are pruned — nothing in it will ever be hit, receive events, or be picked, regardless of listeners | | `true` | This subtree is never pruned for lacking a listener (it's always searched), except where a descendant sets `pickable: false` | The practical rule: **you almost never need to set `pickable` explicitly.** Attaching an input listener (a [`FireListener`](/api/scenery/fire-listener), [`DragListener`](/api/scenery/drag-listener), etc.) already makes a Node's subtree includable in hit-testing. `pickable: false` is for the opposite case — a decorative overlay drawn on top of interactive content that should be visible but never intercept input, letting hits fall through to whatever is beneath it. ## `mouseArea` and `touchArea` Both accept a `Shape` (see [kite `Shape`](/api/kite/shape)) or a `Bounds2`, in the Node's local coordinate frame, and completely replace the drawn shape for the purposes of hit-testing that input type — a mouse/touch position only counts as "over" the Node if it falls inside the given area, not the Node's actual painted region. This is the standard way to make small controls touch-friendly without changing how they look: dilate the touch area well past the visual bounds while leaving `mouseArea` unset (so precise mouse pointing still behaves normally). ## `cursor` A CSS cursor string (or `null` to inherit from whatever's shown by default). `Node.getEffectiveCursor()` walks: this Node's own `cursor`, then any attached input listener's cursor (see `useInputListenerCursor` on [`PressListener`](/api/scenery/fire-listener)), falling through to ancestors if none is set — this is how a listener's `pressCursor` can force a specific cursor for the duration of a press even as the pointer moves off the original target. ::: tip `pickable: false` is not the same as `inputEnabled: false` If you want a Node to still be "pickable" — showing a cursor, appearing as hovered/focused, participating in [interactive highlighting](/api/scenery/interactive-highlighting) — but not actually respond to input, use `inputEnabled: false` on the Node instead of `pickable: false`. `pickable: false` removes the subtree from hit-testing entirely (so nothing under it looks interactive either); `inputEnabled: false` only suppresses the *events* while leaving picking behavior alone. ::: ======================================================================== Page: Hotkey, HotkeyData, and globalHotkeyRegistry URL: https://veillette.github.io/Almanach/api/scenery/hotkeys Source: docs/api/scenery/hotkeys.md Category: API | Tags: scenery, Hotkey, HotkeyData, globalHotkeyRegistry, input, keyboard, accessibility | Status: complete ======================================================================== # Hotkey, HotkeyData, and globalHotkeyRegistry `Hotkey` (from `scenerystack/scenery`) is the low-level object that represents a single keyboard shortcut — one main key plus optional modifier keys, with press/release/fire callbacks. [`KeyboardListener`](/api/scenery/keyboard-listener) is built on top of `Hotkey` (its `keys` option internally creates one `Hotkey` per entry, exposed via `keyboardListener.hotkeys`), and most application code should reach for `KeyboardListener` first. Use `Hotkey` directly when you need a shortcut that lives *outside* the usual "attach to a focusable Node" model — either globally regardless of focus (via `globalHotkeyRegistry`), or as a light-weight `TInputListener` entry without the rest of `KeyboardListener`'s API. ```ts import { Hotkey, globalHotkeyRegistry } from 'scenerystack/scenery'; import { Property } from 'scenerystack/axon'; // Available anywhere in the document, regardless of what has focus. globalHotkeyRegistry.add( new Hotkey( { keyStringProperty: new Property( 'alt+shift+d' ), fire: () => console.log( 'debug overlay toggled' ) } ) ); // Or, scoped to a Node's own focus, added directly as an inputListeners entry: someNode.addInputListener( { hotkeys: [ new Hotkey( { keyStringProperty: new Property( 'escape' ), fire: () => console.log( 'closed' ) } ) ] } ); ``` ## `Hotkey` options | Option | Default | Effect | | --- | --- | --- | | `keyStringProperty` | *(required)* | A `TReadOnlyProperty` describing the key + modifiers, e.g. `'shift+tab'` — a Property (not a plain string) so hotkeys can support i18n/remapping later | | `fire` | no-op | `( event: KeyboardEvent \| null ) => void` — the event is `null` when fired via fire-on-hold repetition | | `press` / `release` | no-op | Called on press/release; `press` is not called for fire-on-hold repeats | | `fireOnDown` | `true` | Fire when the combination is first pressed; `false` fires on release instead | | `fireOnHold` | `false` | Whether holding the combination repeats `fire` | | `fireOnHoldTiming` | `'browser'` | `'browser'` uses the OS/browser's own key-repeat timing; `'custom'` uses the two options below | | `fireOnHoldCustomDelay` / `fireOnHoldCustomInterval` | `400` / `100` | Timing in ms when `fireOnHoldTiming: 'custom'` | | `overlapBehavior` | `'handle'` | How this hotkey behaves when another active hotkey shares one of its keys — see below | `isPressedProperty` reports whether the hotkey is currently active, `interrupted` records whether the most recent release was due to an interruption rather than a key-up, and `interrupt()` cancels an in-progress press. ### `overlapBehavior` | Value | Meaning | | --- | --- | | `'handle'` | Default for node-scoped hotkeys — if two active hotkeys share keys, only the one closest to the focused Node in the scene graph fires | | `'prevent'` | Default for `globalHotkeyRegistry` entries — overlapping global hotkeys are treated as a programming error and assert | | `'allow'` | Both overlapping hotkeys fire; takes precedence over `'prevent'` if the two disagree | ## `HotkeyData` `HotkeyData` bundles a set of keystrokes with the documentation metadata scenery needs for the keyboard-help dialog and PhET's internal "binder" documentation generator — it doesn't itself listen for anything. It's the shape [`KeyboardDragListener`](/api/scenery/keyboard-drag-listener) and other built-in listeners use internally to describe *which* keys they respond to, separately from the listener that acts on them. | Member | Meaning | | --- | --- | | `keys` (constructor option) | `Array>` — the keystrokes this data describes | | `keyStringProperties` | Normalized array of `TReadOnlyProperty`, one per key in `keys` | | `hasKeyStroke( keyStroke )` | Whether any of this data's keys match the given stroke | | `HotkeyData.combineKeyStringProperties( array )` | Static — flattens several `HotkeyData`'s key Properties into one array, handy for feeding a single `KeyboardListener` | | `HotkeyData.anyHaveKeyStroke( array, keyStroke )` | Static — checks a keystroke against a whole array of `HotkeyData` at once | ## `globalHotkeyRegistry` A module-level singleton (not a class you construct) with one member: `hotkeysProperty`, a `TProperty>` of every currently-registered global hotkey. Call `globalHotkeyRegistry.add( hotkey )` / `.remove( hotkey )` to make a `Hotkey` active regardless of what has document focus — this is what `KeyboardListener.createGlobal()` uses internally. ::: warning Modifier keys must be listed explicitly, exactly as with KeyboardListener `Hotkey` (and therefore `KeyboardListener`) only fires when *exactly* the described keys are down. A hotkey for `'tab'` will not fire while shift is also held — add `'shift+tab'` as a second `Hotkey`, or mark the modifier ignorable in the key string with `'shift?+tab'`, exactly as documented on [`KeyboardListener`](/api/scenery/keyboard-listener). ::: ======================================================================== Page: HSeparator, VSeparator, HStrut, and VStrut URL: https://veillette.github.io/Almanach/api/scenery/separators-and-struts Source: docs/api/scenery/separators-and-struts.md Category: API | Tags: scenery, HSeparator, VSeparator, HStrut, VStrut, Separator, FlowBox, layout | Status: complete ======================================================================== # HSeparator, VSeparator, HStrut, and VStrut These four small `Node` subclasses (all from `scenerystack/scenery`) exist to be dropped directly into a layout container's `children` array as spacing helpers, rather than being positioned by hand: `HSeparator`/`VSeparator` draw a thin dividing line that automatically stretches to fill the container's cross-axis width/height, while `HStrut`/`VStrut` draw nothing and simply reserve a fixed amount of space along one axis. Both pairs are meant to read clearly at the call site — "put a divider here" or "put a fixed gap here" — inside a [`FlowBox`](/api/scenery/flow-box), [`VBox`](/api/scenery/v-box), or [`HBox`](/api/scenery/h-box). ```ts import { VBox, HSeparator, Text, HBox, HStrut, Circle } from 'scenerystack/scenery'; const menu = new VBox( { align: 'left', spacing: 4, children: [ new Text( 'Option A' ), new Text( 'Option B' ), new HSeparator(), // a full-width horizontal rule between the groups new Text( 'Option C' ) ] } ); const row = new HBox( { spacing: 4, children: [ new Circle( 10, { fill: 'crimson' } ), new HStrut( 40 ), // a fixed 40px gap, wider than `spacing` alone new Circle( 10, { fill: 'teal' } ) ] } ); ``` ## `HSeparator` and `VSeparator` `new HSeparator( options? )` and `new VSeparator( options? )` are both `Line` subclasses (via a shared internal `Separator` base) that default to a dark gray `stroke` (`'rgb(100,100,100)'`) and set `layoutOptions: { stretch: true, isSeparator: true }`. `HSeparator` draws a horizontal line and grows to fill the *width* offered by its parent's layout constraint (for use inside a vertical container like `VBox`); `VSeparator` draws a vertical line and grows to fill *height* (for use inside a horizontal container like `HBox`). `SeparatorOptions` is `LineOptions` minus `tandem` — separators are purely presentational and are not PhET-iO instrumented. The `isSeparator: true` layout flag is what makes separators special inside `FlowBox`-based containers (`VBox`/`HBox`): a separator that would end up first, last, or immediately adjacent to another separator in the visible layout order is automatically hidden, so toggling the visibility of surrounding items never leaves a dangling or doubled-up divider line. ## `HStrut` and `VStrut` `HStrut` and `VStrut` are thin [`Spacer`](/api/scenery/spacer) subclasses — `new HStrut( width, options? )` is exactly `new Spacer( width, 0, options )`, and `new VStrut( height, options? )` is exactly `new Spacer( 0, height, options )`. Like `Spacer`, they draw nothing and can never have children; they exist purely so a layout container sees reserved space of the given size. `HStrutOptions`/`VStrutOptions` are just `SpacerOptions` (in turn just `NodeOptions`) — there's no strut-specific option beyond the constructor's single dimension argument. See [Spacer](/api/scenery/spacer) for the full details these two thin wrappers build on. ## Choosing between them | Need | Use | | --- | --- | | A visible dividing line, full-width/height, that auto-hides at the edges | `HSeparator` / `VSeparator` | | An invisible, fixed-size gap along one axis, larger or smaller than the container's uniform `spacing` | `HStrut` / `VStrut` | | An invisible, fixed-size rectangular block (both dimensions) | [`Spacer`](/api/scenery/spacer) directly | ::: tip Separators only auto-hide inside FlowBox-based containers The "hide at the edges / hide when adjacent to another separator" behavior comes from `isSeparator` being read by `FlowBox`'s layout constraint (which `VBox`/`HBox` both use). An `HSeparator`/`VSeparator` placed outside of a `FlowBox`-family container is just a `Line` with default styling — it stretches via `localPreferredWidthProperty`/`localPreferredHeightProperty` only when a layout container actually provides a preferred size. ::: ======================================================================== Page: Image URL: https://veillette.github.io/Almanach/api/scenery/image Source: docs/api/scenery/image.md Category: API | Tags: scenery, Image, mipmap | Status: verified ======================================================================== # Image `Image` (from `scenerystack/scenery`) is a [`Node`](/api/scenery/node) that displays a single raster image, accepting a URL string, an `HTMLImageElement`, an `HTMLCanvasElement`, or a mipmap data structure (`ImageableImage`). ```ts import { Image } from 'scenerystack/scenery'; const icon = new Image( '/assets/icon.png', { scale: 0.5, imageOpacity: 0.9 } ); icon.image = '/assets/icon-hover.png'; // swap the displayed image later ``` ## Swapping the image via a Property Pass a `TReadOnlyProperty` directly as the constructor argument (or as the `imageProperty` option) to have the displayed image track a `Property`, e.g. for theme or state-driven artwork: ```ts import { Image } from 'scenerystack/scenery'; import { Property } from 'scenerystack/axon'; const sourceProperty = new Property( '/assets/switch-off.png' ); const switchImage = new Image( sourceProperty ); sourceProperty.value = '/assets/switch-on.png'; // switchImage updates automatically ``` ## Options | Option | Effect | | --- | --- | | `image` | The image source: a URL string, `HTMLImageElement`, `HTMLCanvasElement`, or mipmap array | | `imageProperty` | Sets forwarding of the image source from a `TReadOnlyProperty` | | `imageOpacity` | Opacity applied to just this image (not its children), in `[0, 1]` | | `imageBounds` | Overrides what region of the image is considered "inside" for bounds/hit-testing purposes | | `initialWidth`, `initialHeight` | Placeholder dimensions to use for layout before the image has finished loading | | `mipmap` | Whether mipmapped rendering is enabled, trading memory for smoother minification | | `mipmapBias`, `mipmapInitialLevel`, `mipmapMaxLevel` | Fine-tune mipmap generation and level selection | | `hitTestPixels` | Whether hit-testing should respect per-pixel transparency instead of the bounding rectangle | `Image` also accepts the full set of [`Node` options](/api/scenery/node). ::: tip Give not-yet-loaded images a size If an image URL hasn't finished loading when a layout container ([`FlowBox`](/api/scenery/flow-box), [`GridBox`](/api/scenery/grid-box)) measures it, its bounds will be empty and layout will shift once it loads. Set `initialWidth`/`initialHeight` to the image's known final dimensions to reserve the right amount of space up front. ::: ======================================================================== Page: InteractiveHighlighting URL: https://veillette.github.io/Almanach/api/scenery/interactive-highlighting Source: docs/api/scenery/interactive-highlighting.md Category: API | Tags: scenery, InteractiveHighlighting, highlight, accessibility, pointer | Status: verified ======================================================================== # InteractiveHighlighting `InteractiveHighlighting` (from `scenerystack/scenery`) is a trait that gives a Node a highlight on **pointer** hover (mouse/touch), the same visual language as the keyboard-focus highlights covered in [Focus Highlights](/accessibility/focus-highlights) — it exists so mouse and touch users get the same "this is interactive" affordance that keyboard users get for free from focus. Highlight visibility is gated behind a user preference (`FocusManager.interactiveHighlightsVisibleProperty`, see [`FocusManager`](/api/scenery/focus-manager)), so it does nothing until that preference is enabled. `InteractiveHighlighting` is a mixin function, applied the same way as `Voicing`: ```ts import { Node, InteractiveHighlighting, Rectangle } from 'scenerystack/scenery'; const iconNode = new Rectangle( 0, 0, 40, 40, { fill: 'teal' } ); class HoverableIcon extends InteractiveHighlighting( Node ) { public constructor() { super( { children: [ iconNode ], cursor: 'pointer', tagName: 'button', accessibleName: 'Play' } ); } } ``` For cases where composing a trait is more friction than it's worth, `InteractiveHighlightingNode` (from `scenerystack/scenery`) is a ready-made `InteractiveHighlighting( Node )` subclass: ```ts import { InteractiveHighlightingNode } from 'scenerystack/scenery'; const hoverable = new InteractiveHighlightingNode( { children: [ iconNode ], cursor: 'pointer' } ); ``` ## Options These are `InteractiveHighlighting`'s mutator keys (`INTERACTIVE_HIGHLIGHTING_OPTIONS` in source): | Option | Effect | | --- | --- | | `interactiveHighlight` | A `Shape \| Node \| 'invisible' \| null` highlight shown on pointer hover, analogous to `focusHighlight` from `ParallelDOM`. `null`/omitted falls back to the Node's bounds-based default | | `interactiveHighlightLayerable` | If `true`, you take responsibility for placing the highlight Node in the scene graph yourself, rather than scenery's overlay drawing it on top | | `interactiveHighlightEnabled` | `boolean` — turns the highlight-on-hover behavior on or off for this specific Node without removing the trait | ## Reading highlight-active state | Member | Effect | | --- | --- | | `isInteractiveHighlightActiveProperty` | `TReadOnlyProperty` — `true` while this Node's interactive highlight is currently displayed | | `isInteractiveHighlighting( node )` | Exported standalone function (from `scenerystack/scenery`) — a type guard checking whether an arbitrary `Node` has composed this trait, preferred over `instanceof` checks against the mixin | ::: tip `Voicing` already includes `InteractiveHighlighting` If a Node composes [`Voicing`](/api/scenery/voicing), it is already `InteractiveHighlighting`-capable — `Voicing`'s implementation extends `InteractiveHighlighting( Type )` internally. Reach for `InteractiveHighlighting` on its own only for Nodes that need a hover highlight but don't need Voicing's spoken responses. ::: ======================================================================== Page: KeyboardDragListener URL: https://veillette.github.io/Almanach/api/scenery/keyboard-drag-listener Source: docs/api/scenery/keyboard-drag-listener.md Category: API | Tags: scenery, KeyboardDragListener, input, keyboard, accessibility, drag | Status: verified ======================================================================== # KeyboardDragListener `KeyboardDragListener` (from `scenerystack/scenery`) is a `KeyboardListener` specialized for dragging: it moves an object with the arrow keys or W/A/S/D, writing to the same kind of model `positionProperty` a pointer [`DragListener`](/patterns/drag-listeners) would. Pointer dragging alone is never accessible, so any draggable object that needs to work for keyboard/switch users needs this listener (or [`RichDragListener`](/api/scenery/rich-drag-listener), which bundles both) in addition to — or instead of — pointer dragging. ```ts import { Node, KeyboardDragListener } from 'scenerystack/scenery'; const bodyNode = new Node( { tagName: 'div', focusable: true } ); bodyNode.addInputListener( new KeyboardDragListener( { positionProperty: body.positionProperty, transform: modelViewTransform, dragBoundsProperty: model.dragBoundsProperty, dragSpeed: 150, // model units/second while a key is held shiftDragSpeed: 50 // finer motion with shift held } ) ); ``` ## Options `KeyboardDragListenerOptions` accepts everything `DragListener` shares via `AllDragListenerOptions` (`positionProperty`, `transform`, `dragBoundsProperty`, `mapPosition`, `translateNode`, `start`/`drag`/`end`), plus: | Option | Default | Effect | | --- | --- | --- | | `dragDelta` | `10` | Discrete step (in parent/view coordinates) moved per `moveOnHoldInterval` tick — a "typical application" feel; mutually exclusive with `dragSpeed`/`shiftDragSpeed` | | `shiftDragDelta` | `5` | Finer `dragDelta` while shift is held | | `dragSpeed` | `0` | Continuous units/second moved while a direction key is held — smoother, game-like motion; mutually exclusive with `dragDelta`/`shiftDragDelta` | | `shiftDragSpeed` | `0` | Finer `dragSpeed` while shift is held | | `keyboardDragDirection` | `'both'` | Constrains motion: `'both'` (2D), `'leftRight'`, or `'upDown'` | | `moveOnHoldDelay` | `500` | Milliseconds a key must be held before repeat movement begins (delta mode only) | | `moveOnHoldInterval` | `400` | Milliseconds between discrete steps once repeating (delta mode only; must be > 0) | `dragSpeed`/`shiftDragSpeed` and `dragDelta`/`shiftDragDelta` are mutually exclusive — pick one motion model per listener. If you pass `dragBoundsProperty`, you must also provide `positionProperty` or `translateNode: true`, since the listener otherwise has no way to know the current position to constrain. ## Public state | Member | Meaning | | --- | --- | | `modelDelta` | The `Vector2` delta (in model coordinates) applied during the current drag step | | `modelPoint` | The current drag point in model coordinates | | `isPressedProperty` (inherited from `KeyboardListener`) | Whether a drag is currently active | ## Methods Shares `interrupt()` and `dispose()` with [`KeyboardListener`](/api/scenery/keyboard-listener), since `KeyboardDragListener` extends it directly. ::: tip Requires a focusable target — pair it with the PDOM `KeyboardDragListener` only receives key events when its Node has keyboard focus. Set `tagName: 'div'` and `focusable: true` (and a meaningful `accessibleName`) on the target Node, or the listener will never fire. See [Drag Listeners](/patterns/drag-listeners) for the full checklist, including combining this with pointer dragging via `RichDragListener`. ::: ======================================================================== Page: KeyboardListener URL: https://veillette.github.io/Almanach/api/scenery/keyboard-listener Source: docs/api/scenery/keyboard-listener.md Category: API | Tags: scenery, KeyboardListener, input, keyboard, accessibility, hotkey | Status: verified ======================================================================== # KeyboardListener `KeyboardListener` (from `scenerystack/scenery`) declares one or more key combinations ("hotkeys") as strings and fires a callback when they're pressed, without you having to track `keydown`/`keyup` state by hand. It's the keyboard counterpart to [`FireListener`](/api/scenery/fire-listener): attach it via [`inputListeners`](/api/scenery/node) on a focusable `Node`, and it fires while that Node has focus (or, via `KeyboardListener.createGlobal`, anywhere in the document regardless of focus). ```ts import { Node, KeyboardListener } from 'scenerystack/scenery'; const panel = new Node( { tagName: 'div', focusable: true } ); panel.addInputListener( new KeyboardListener( { keys: [ 'escape', 'shift+t' ], fire: ( event, keysPressed, listener ) => { if ( keysPressed === 'escape' ) { console.log( 'closed' ); } else if ( keysPressed === 'shift+t' ) { console.log( 'shift+t pressed' ); } } } ) ); ``` Each entry in `keys` is a combination like `'alt+shift+r'` — keys before the last one are modifiers, and modifier order doesn't matter. A `'?'` marks a modifier as ignorable: `'shift?+y'` fires on `y` whether or not shift is held. ## Options | Option | Default | Effect | | --- | --- | --- | | `keys` | `null` | Array of key combination strings, e.g. `[ 'a+b', 'shift+arrowLeft' ]`; mutually exclusive with `keyStringProperties` | | `keyStringProperties` | `null` | Array of `TReadOnlyProperty` instead of static strings — for hotkeys that change with i18n or remapping | | `fire` | no-op | `( event, keysPressed, listener ) => void`, called when the combination fires | | `press` | no-op | Called on the initial press of a combination, before fire-on-hold repetition | | `release` | no-op | Called on release (or `null` `keysPressed` on interruption) | | `focus` / `blur` | no-op | Called when the listener's target Node gains/loses focus | | `fireOnDown` | `true` | Fire when the combination is pressed; set `false` to fire on release instead | | `fireOnHold` | `false` | Whether holding the combination repeats `fire` | | `fireOnHoldTiming` | `'browser'` | `'browser'` (use the OS/browser's own key-repeat timing) or `'custom'` (use the two delay/interval options below) | | `fireOnHoldCustomDelay` | `400` | Milliseconds held before repeat starts, when `fireOnHoldTiming: 'custom'` | | `fireOnHoldCustomInterval` | `100` | Milliseconds between repeats, when `fireOnHoldTiming: 'custom'` | | `overlapBehavior` | `'handle'` | How this listener behaves when another active `KeyboardListener` shares overlapping keys | ## Public state and methods | Member | Meaning | | --- | --- | | `hotkeys` | The underlying `Hotkey[]` this listener created from `keys`/`keyStringProperties` | | `isPressedProperty` | `true` while any of the listener's hotkeys is active | | `interrupted` | Whether the most recent release was due to an interruption | | `interrupt()` | Cancels any in-progress press without firing `release` normally | | `dispose()` | Disposes every underlying `Hotkey` and this listener's Properties | | `KeyboardListener.createGlobal( target, options )` | Static factory for a listener registered in the global hotkey registry — fires regardless of document focus, as long as `target` can receive input (visible, enabled, input-enabled) | ::: warning Modifier keys must be listed explicitly, or they suppress firing The `fire` callback only runs when *exactly* the keys in a combination are down. Listing `'tab'` alone means it will **not** fire while shift is also held — you must add `'shift+tab'` as a separate entry in `keys` to handle that case too, or mark the modifier as ignorable with `'shift?+tab'`. This also applies to `KeyboardDragListener`'s underlying arrow/WASD keys, which is why the drag keys are declared with `?` internally. ::: ======================================================================== Page: Line URL: https://veillette.github.io/Almanach/api/scenery/line Source: docs/api/scenery/line.md Category: API | Tags: scenery, Line, Path | Status: verified ======================================================================== # Line `Line` (from `scenerystack/scenery`) is a [`Path`](/api/scenery/path) subclass that draws a single straight segment between two points, built from `p1`/`p2` (or `x1`/`y1`/`x2`/`y2`) parameters instead of a hand-built kite `Shape`. Since a `Line` has no interior area, only `stroke` (not `fill`) has a visible effect. ```ts import { Line } from 'scenerystack/scenery'; import { Vector2 } from 'scenerystack/dot'; // new Line( x1, y1, x2, y2, options ) const axis = new Line( 0, 0, 100, 0, { stroke: 'black', lineWidth: 2 } ); // new Line( p1, p2, options ) const diagonal = new Line( new Vector2( 0, 0 ), new Vector2( 50, 50 ), { stroke: 'gray' } ); axis.x2 = 150; // update an endpoint after construction ``` ## Constructor overloads | Signature | Use case | | --- | --- | | `new Line( options )` | Fully options-driven | | `new Line( p1, p2, options? )` | Endpoints as `Vector2`s | | `new Line( x1, y1, x2, y2, options? )` | Endpoints as separate numbers | ## Options | Option | Effect | | --- | --- | | `p1` | Start point as a `Vector2` | | `p2` | End point as a `Vector2` | | `x1`, `y1` | Start point coordinates individually | | `x2`, `y2` | End point coordinates individually | `Line` accepts every [`Path` option](/api/scenery/path) except `shape`/`shapeProperty` — `stroke`, `lineWidth`, `lineCap`, `lineDash`, etc. all apply — plus the full set of [`Node` options](/api/scenery/node). ::: tip `fill` has no effect A `Line` has zero area, so setting `fill` does nothing visible; use `stroke` and `lineWidth` to control its appearance. ::: ======================================================================== Page: LinearGradient URL: https://veillette.github.io/Almanach/api/scenery/linear-gradient Source: docs/api/scenery/linear-gradient.md Category: API | Tags: scenery, LinearGradient, gradient, paint, fill, stroke | Status: verified ======================================================================== # LinearGradient `LinearGradient` (from `scenerystack/scenery`) is a paint type — like [`Color`](/api/scenery/color) — that can be assigned directly to a Node's `fill` or `stroke` option to render a color gradient along a straight line between two points, in the Node's local coordinate frame. It extends the abstract `Gradient` base class it shares with [`RadialGradient`](/api/scenery/radial-gradient), which supplies the shared `addColorStop` API. ```ts import { Rectangle, LinearGradient } from 'scenerystack/scenery'; // Gradient line from (0, 0) to (100, 0) — a horizontal sweep across the Rectangle's width const gradient = new LinearGradient( 0, 0, 100, 0 ) .addColorStop( 0, 'red' ) .addColorStop( 0.5, 'yellow' ) .addColorStop( 1, 'green' ); const bar = new Rectangle( 0, 0, 100, 20, { fill: gradient } ); ``` ## Constructor `new LinearGradient( x0, y0, x1, y1 )` — the four coordinates describe the gradient line's start (ratio 0) and end (ratio 1) points, in the local coordinate frame of whatever Node it's used on. The gradient extends perpendicular to this line and repeats the end colors beyond the line's extent. ## Color stops `addColorStop( ratio, color )` (inherited from `Gradient`) appends one stop and returns `this`, so calls chain naturally: | Parameter | Meaning | | --- | --- | | `ratio` | A number from `0` to `1`, position along the gradient line | | `color` | `null`, a CSS string, a [`Color`](/api/scenery/color) instance, or a `Property` resolving to one of those | Stops must be added in non-decreasing `ratio` order — calling `addColorStop` with a smaller ratio than the previous call throws, since browsers handle out-of-order stops inconsistently. ::: warning Color stops must be added in increasing order `addColorStop( 0.5, 'yellow' )` followed by `addColorStop( 0.2, 'red' )` throws `'Color stops not specified in the order of increasing ratios'`. If stop positions are computed dynamically, sort them before calling `addColorStop` in a loop. ::: ======================================================================== Page: ManualConstraint URL: https://veillette.github.io/Almanach/api/scenery/manual-constraint Source: docs/api/scenery/manual-constraint.md Category: API | Tags: scenery, ManualConstraint, layout, constraint | Status: verified ======================================================================== # ManualConstraint `ManualConstraint` (from `scenerystack/scenery`) exists for the cases [`FlowBox`](/api/scenery/flow-box)/[`GridBox`](/api/scenery/grid-box) don't cover: one-off imperative positioning code like `node.left = otherNode.right + 5` that needs to automatically rerun whenever any of the involved Nodes' bounds change. Unlike writing that assignment once in application code, `ManualConstraint` also handles Nodes that don't share a coordinate frame directly — only a common ancestor — by giving the callback `LayoutProxy` objects (bounds-based positional getters/setters, like a Node's own `left`/`right`/`centerY`, but transformed into the ancestor's coordinate frame). ```ts import { ManualConstraint, Node, Circle, Rectangle } from 'scenerystack/scenery'; const ancestor = new Node(); const label = new Rectangle( 0, 0, 100, 30, { fill: 'white' } ); const icon = new Circle( 8, { fill: 'crimson' } ); ancestor.children = [ label, icon ]; new ManualConstraint( ancestor, [ label, icon ], ( labelProxy, iconProxy ) => { iconProxy.left = labelProxy.right + 5; iconProxy.centerY = labelProxy.centerY; } ); ``` The constraint reruns the callback automatically whenever `label` or `icon`'s bounds change (or they're reparented) — you never call the assignment again yourself. ## Constructor `new ManualConstraint( ancestorNode, nodes, layoutCallback )` | Parameter | Meaning | | --- | --- | | `ancestorNode` | The common ancestor `Node` whose coordinate frame the layout is computed relative to; must be an ancestor of every entry in `nodes` | | `nodes` | A tuple of `Node`s the callback needs to position — each becomes one `LayoutProxy` argument, in the same order | | `layoutCallback` | `( ...proxies: LayoutProxy[] ) => void` — read/write positional properties on the proxies to perform layout | A static `ManualConstraint.create( ancestorNode, nodes, layoutCallback )` is equivalent to calling the constructor directly, useful when you want a factory function reference instead of `new`. ## Behavior notes - The callback only runs once every involved Node currently has a connected `Trail` back to `ancestorNode` — if a Node is temporarily removed from the tree, the constraint simply skips re-running until it's reconnected. - Each `LayoutProxy` exposes the same bounds-based getters/setters as [`Node`](/api/scenery/node) (`left`, `right`, `top`, `bottom`, `centerX`, `centerY`, `center`, `width`, `height`, …), but reads/writes them in `ancestorNode`'s coordinate frame rather than the proxied Node's own parent frame. - `dispose()` releases the constraint's internal `LayoutCell`s and stops listening to the constrained Nodes. ::: tip Reach for `FlowBox`/`GridBox`/`AlignBox` first `ManualConstraint` is intentionally low-level — it doesn't know about spacing, alignment, or growth, it just reruns a callback you wrote. Prefer [`FlowBox`](/api/scenery/flow-box) or [`GridBox`](/api/scenery/grid-box) for anything expressible as a row/column/grid; reach for `ManualConstraint` only for relationships those containers can't express, such as positioning one Node relative to another that lives in a completely different part of the tree. ::: ======================================================================== Page: Node URL: https://veillette.github.io/Almanach/api/scenery/node Source: docs/api/scenery/node.md Category: API | Tags: scenery, Node, scene-graph, transform, bounds | Status: verified ======================================================================== # Node `Node` (from `scenerystack/scenery`) is the base class for every visual element in a scenery scene graph. On its own a `Node` draws nothing — it's a container that holds `children` and applies a transform (translation/rotation/scale), visibility, opacity, input behavior, and layout options to that subtree. Every other page in this section — [`Path`](/api/scenery/path), [`Text`](/api/scenery/text), [`Image`](/api/scenery/image), the layout containers — is a `Node` subclass, so the options and methods documented here apply everywhere. ```ts import { Node, Circle, Rectangle } from 'scenerystack/scenery'; import { Vector2 } from 'scenerystack/dot'; const group = new Node( { children: [ new Circle( 20, { fill: 'orange' } ), new Rectangle( 30, -10, 40, 20, { fill: 'teal' } ) ], x: 100, y: 50, scale: 1.5, visible: true } ); group.addChild( new Circle( 5, { fill: 'black', x: 60 } ) ); ``` ## Building the scene graph | Method | Effect | | --- | --- | | `addChild( node )` | Appends `node` as the last (topmost) child | | `insertChild( index, node )` | Inserts `node` at a specific position in the children order | | `removeChild( node )` | Removes a specific child | | `removeChildAt( index )` | Removes the child at a given index | | `removeAllChildren()` | Clears all children | | `mutate( options )` | Applies a `NodeOptions` object all at once, in a fixed, documented order | Children later in the list are drawn on top of earlier ones — order matters for visual stacking, not just structure. ## Frequently used options | Option | Effect | | --- | --- | | `children` | Initial list of child `Node`s, added in order | | `visible` | Whether this Node (and its subtree) is displayed; invisible subtrees are also excluded from picking by default | | `pickable` | `true`/`false`/`null` — overrides whether this subtree can receive input events | | `enabled` | Application-level enabled state (paired with `enabledProperty`) | | `inputEnabled` | Whether input events are allowed to reach this subtree | | `inputListeners` | Array of input listeners attached at construction, e.g. a [`FireListener`](/api/scenery/fire-listener) | | `opacity` | 0 (transparent) to 1 (opaque) applied to the whole subtree | | `cursor` | CSS cursor shown when the pointer is over this Node | | `x`, `y`, `translation` | Translation of this Node relative to its parent | | `rotation` | Rotation in radians | | `scale` | Uniform or `Vector2` scale | | `left`, `right`, `top`, `bottom`, `centerX`, `centerY`, `center`, `leftTop`, `rightBottom`, … | Positioning shortcuts based on this Node's (parent-coordinate-frame) bounds — applied *after* other transform options | | `maxWidth` / `maxHeight` | Automatically scales the Node down (never up) to fit within the given local-bounds dimensions | | `clipArea` | A `Shape` (see [kite `Shape`](/api/kite/shape)) outside of which content is hidden | | `mouseArea` / `touchArea` | Overrides the hit-testing region, independent of the drawn shape | | `layoutOptions` | Options consumed by a parent layout container, e.g. [`FlowBox`](/api/scenery/flow-box) or [`GridBox`](/api/scenery/grid-box) | ## Reading bounds and position ```ts const b = group.bounds; // Bounds2 in the parent coordinate frame const local = group.localBounds; // Bounds2 in this Node's own coordinate frame group.center = new Vector2( 0, 0 ); // reposition using the bounds-based setter ``` `Node` also exposes coordinate-conversion helpers: `localToGlobalPoint( point )` and `globalToLocalPoint( point )` convert between this Node's local frame and the `Display`'s root frame — useful when translating pointer coordinates. ::: tip Order of options in `mutate()` Node options are applied in a fixed, documented order (transform options like `x`/`scale` before bounds-based options like `left`/`center`). This is why you can safely pass both a `scale` and a `center` in the same options object and get the expected result — the centering happens after scaling. ::: ::: warning Dispose subtrees you remove permanently Removing a `Node` from its parent does not automatically dispose it. If a subtree is being discarded for good (not just hidden), call `dispose()` (or `disposeSubtree()`) to release its listeners, `Property` links, and other resources — otherwise removed simulation elements can leak memory. ::: ======================================================================== Page: ParallelDOM URL: https://veillette.github.io/Almanach/api/scenery/parallel-dom-deep-dive Source: docs/api/scenery/parallel-dom-deep-dive.md Category: API | Tags: scenery, ParallelDOM, pdom, accessibility, tagName, accessibleName | Status: verified ======================================================================== # ParallelDOM `ParallelDOM` (from `scenerystack/scenery`) is the trait mixed into `Node` itself — every `Node`, not just some subset, has the options documented here available, though most are no-ops until you set `tagName` to place the Node in the Parallel DOM. This page documents the trait's actual options and accessors; for what the Parallel DOM is, why it exists, and worked examples of building an accessible scene graph, read the narrative guide [The Parallel DOM](/accessibility/pdom) first. ```ts import { Node, Rectangle } from 'scenerystack/scenery'; const resetButton = new Rectangle( 0, 0, 40, 40, { tagName: 'button', accessibleName: 'Reset Masses', accessibleHelpText: 'Return both masses to their starting values.' } ); const infoSection = new Node( { tagName: 'div', labelTagName: 'h3', labelContent: 'Mass Controls', descriptionContent: 'Adjust the mass of each body, in kilograms.' } ); ``` ## Options These are `ParallelDOM`'s actual mutator keys (`ACCESSIBILITY_OPTION_KEYS` in source), grouped as the source groups them: | Option | Effect | | --- | --- | | `tagName` | HTML element name for this Node's primary sibling (`'div'`, `'button'`, `'ul'`, …). Setting it is what places the Node in the PDOM; `null` removes it | | `focusable` | `boolean \| null` — whether the element can receive keyboard focus | | `accessibleName` | Higher-level API: the accessible name announced by screen readers. Prefer this over the lower-level `innerContent`/`ariaLabel` options below | | `accessibleHelpText` | Higher-level API: supplementary guidance text associated with the element | | `accessibleParagraph` | Higher-level API: a standalone paragraph of accessible content, for a Node that is purely descriptive rather than interactive | | `accessibleNameBehavior` / `accessibleHelpTextBehavior` / `accessibleParagraphBehavior` | Lower-level: functions controlling *how* the corresponding higher-level API value is rendered into the PDOM (which sibling, which attribute) — most code never needs to override these; reusable UI components (`sun` controls) do | | `accessibleHeading` | Sets heading text for this Node using scenery's automatic heading-level management | | `accessibleHeadingIncrement` | Adjusts how much this subtree increments the heading level counter | | `containerTagName` / `containerAriaRole` | Tag name and ARIA role for an optional parent element wrapping this Node's own siblings | | `innerContent` | Lower-level: text content placed inside the primary sibling element | | `inputType` / `inputValue` / `pdomChecked` | Lower-level: for `tagName: 'input'` elements — the `type` attribute, `value`, and checked state | | `ariaLabel` / `ariaRole` | Lower-level: raw `aria-label` and `role` attributes | | `labelTagName` / `labelContent` / `appendLabel` | A sibling label element, its content, and whether it's ordered after (rather than before) the primary sibling | | `descriptionTagName` / `descriptionContent` / `appendDescription` | A sibling description element, its content, and ordering | | `focusHighlight` / `focusHighlightLayerable` / `groupFocusHighlight` | The Node's focus highlight — see [Focus Highlights](/accessibility/focus-highlights) for the full picture | | `pdomVisible` / `pdomVisibleProperty` | Whether this Node's PDOM content is present at all, independent of its visual `visible` | | `pdomOrder` | Explicit traversal order for this Node's PDOM children, overriding scene-graph order | | `pdomAttributes` | Arbitrary additional PDOM element attributes | | `ariaLabelledbyAssociations` / `ariaDescribedbyAssociations` / `activeDescendantAssociations` | Cross-Node ARIA relationships (`aria-labelledby`, `aria-describedby`, `aria-activedescendant`) pointing at other Nodes' PDOM elements | | `focusPanTargetBoundsProperty` / `limitPanDirection` | Controls for auto-panning the viewport to keep this Node visible when it receives focus | | `positionInPDOM` | Whether this Node's DOM element should also be moved to visually overlap its rendered position (needed for some native form controls) | | `pdomTransformSourceNode` | An alternate Node whose transform should be used to position this Node's PDOM element | ## Reading values back Every option above has a matching getter (e.g. `getAccessibleName()` / `accessibleName`, `getTagName()` / `tagName`), following the same `set`/`get` + ES5 accessor pattern as the rest of `Node`. ::: warning `accessibleName` supersedes the lower-level options for most use cases `ParallelDOM` exposes both a higher-level API (`accessibleName`, `accessibleHelpText`, `accessibleParagraph`) and the lower-level primitives it's built from (`innerContent`, `ariaLabel`, `labelContent`, behavior functions). Mixing both on the same Node — e.g. setting `accessibleName` and also hand-writing `labelContent` — can conflict, since `accessibleName` may itself be implemented via one of these lower-level options through its behavior function. Reach for the higher-level options first; only touch the lower-level ones (or a custom `accessibleNameBehavior`) when a reusable component needs to render its name into a non-default sibling or attribute. ::: ======================================================================== Page: Path URL: https://veillette.github.io/Almanach/api/scenery/path Source: docs/api/scenery/path.md Category: API | Tags: scenery, Path, Shape, fill, stroke | Status: verified ======================================================================== # Path `Path` (from `scenerystack/scenery`) is a [`Node`](/api/scenery/node) that draws an arbitrary [kite `Shape`](/api/kite/shape) — the general-purpose way to render vector graphics that aren't a plain circle, rectangle, or line. `Circle`, `Rectangle`, and `Line` are all `Path` subclasses that build their `shape` for you from simpler parameters; reach for `Path` directly when you need a custom outline. ```ts import { Path } from 'scenerystack/scenery'; import { Shape } from 'scenerystack/kite'; const triangle = new Shape() .moveTo( 0, 0 ) .lineTo( 40, 0 ) .lineTo( 20, -30 ) .close(); const triangleNode = new Path( triangle, { fill: 'rebeccapurple', stroke: 'black', lineWidth: 2 } ); ``` ## Options | Option | Effect | | --- | --- | | `shape` | The `Shape` (or SVG path-data string, or `null` for nothing drawn) to render — see `InputShape` | | `shapeProperty` | Sets the shape via a `TReadOnlyProperty` instead of a plain value | | `boundsMethod` | How self bounds are computed: `'accurate'` (default, exact stroked bounds), `'unstroked'`, `'tightPadding'`, `'safePadding'`, or `'none'` | | `fill` | Fill paint — a CSS color string, `Color`, `LinearGradient`, `RadialGradient`, or `Pattern` (from `Paintable`, shared with `Text`) | | `stroke` | Stroke paint, same paint types as `fill` | | `lineWidth` | Width of the stroked outline | | `lineCap` / `lineJoin` / `miterLimit` | Stroke cap/join rendering, matching the Canvas 2D API | | `lineDash` / `lineDashOffset` | Dash pattern for the stroke | All `Path` subclasses also accept the full set of [`Node` options](/api/scenery/node) (`x`, `scale`, `visible`, `inputListeners`, `layoutOptions`, …). ::: warning Don't mutate a shared `Shape` in place If a `Shape` isn't marked immutable (`shape.makeImmutable()`), `Path` attaches a listener so it re-renders when the shape mutates — which also means the `Path` keeps a reference to the `Shape` (and vice versa) for as long as that link is alive. Reusing one mutable `Shape` instance across many `Path`s can create surprising update coupling and memory retention; prefer building each `Path`'s shape independently, or call `path.dispose()` / set `path.shape = null` when finished with it. ::: ======================================================================== Page: Pattern URL: https://veillette.github.io/Almanach/api/scenery/pattern-paint Source: docs/api/scenery/pattern-paint.md Category: API | Tags: scenery, Pattern, paint, fill, stroke, image | Status: verified ======================================================================== # Pattern (paint) `Pattern` (from `scenerystack/scenery`) is a paint type — assignable to a `fill` or `stroke` alongside [`Color`](/api/scenery/color) and the gradient paints — that repeats an image tile in both directions to fill a shape, matching the CSS/Canvas notion of a repeating image pattern. This is unrelated to the "design pattern" write-ups in Almanach's [`patterns/`](/patterns/options-pattern) section (options pattern, enumeration pattern, etc.) — this `Pattern` is a rendering primitive, not a coding convention. ```ts import { Pattern, Rectangle } from 'scenerystack/scenery'; const image = document.createElement( 'img' ); image.src = '/assets/checker-tile.png'; const pattern = new Pattern( image ); const tiledBackground = new Rectangle( 0, 0, 200, 100, { fill: pattern } ); ``` ## Constructor `new Pattern( image )` takes a single `HTMLImageElement` that repeats (tiled, `'repeat'` in both x and y) to fill whatever shape it's used as `fill`/`stroke` on. Unlike [`Image`](/api/scenery/image), `Pattern` does not accept a URL string directly or manage loading — the `HTMLImageElement` should already have a `src` set (and typically already be loaded) before constructing the `Pattern`. ## Deriving a pattern from a Node instead of an image file `NodePattern` (from `scenerystack/scenery`) is a `Pattern` subclass that rasterizes an arbitrary `Node` subtree into the backing image, for tiling a pattern built from scenery content (shapes, gradients, text) rather than a static asset: ```ts import { NodePattern, Circle } from 'scenerystack/scenery'; const dot = new Circle( 4, { fill: 'black' } ); const dotPattern = new NodePattern( dot, 2, -8, -8, 16, 16 ); // node, resolution, x, y, width, height ``` ::: warning `Pattern` does not load the image for you Passing an `HTMLImageElement` whose `src` hasn't finished loading yet produces a blank or stale pattern — `Pattern`'s constructor synchronously builds a `CanvasPattern` from whatever image data is available at that moment. Wait for the image's `load` event (or use an already-loaded/cached image) before constructing the `Pattern`, rather than relying on it to update once loading completes. ::: ======================================================================== Page: Pointer, Mouse, Touch, and Pen URL: https://veillette.github.io/Almanach/api/scenery/pointer-mouse-touch-pen Source: docs/api/scenery/pointer-mouse-touch-pen.md Category: API | Tags: scenery, Pointer, Mouse, Touch, Pen, input | Status: complete ======================================================================== # Pointer, Mouse, Touch, and Pen `Pointer` (from `scenerystack/scenery`) is scenery's abstraction for a single input device: the one system mouse, one instance per active touch contact, or one instance per stylus. Every scenery input event carries a reference to the `Pointer` that triggered it via [`SceneryEvent.pointer`](/api/scenery/scenery-event), and listeners like [`FireListener`](/api/scenery/fire-listener) and [`DragListener`](/api/scenery/drag-listener) track pointers internally to know when a press is still "over" their target. `Pointer` itself is abstract — what you actually get is always a `Mouse`, `Touch`, or `Pen` instance (or the internal PDOM pointer used for keyboard/assistive-technology activation). ```ts import { FireListener, Mouse } from 'scenerystack/scenery'; button.addInputListener( new FireListener( { press: ( event, listener ) => { // Special-case mouse-only behavior, e.g. restrict to the right button if ( event.pointer instanceof Mouse ) { console.log( 'pressed with the mouse' ); } } } ) ); ``` ## The `Pointer` base class | Member | Meaning | | --- | --- | | `point` | Current position in the global (Display root) coordinate frame | | `type` | `'mouse'` \| `'touch'` \| `'pen'` \| `'pdom'` — cheaper than an `instanceof` check when you just need the category | | `trail` | The `Trail` this pointer is currently over, updated as it moves | | `isDownProperty` | Whether the pointer is currently pressed | | `attachedProperty` / `isAttached()` | Whether a "primary" listener has claimed this pointer (see `attach` on [`PressListener`](/api/scenery/fire-listener)) — used to prevent two listeners from responding to the same physical drag | | `cursor` | An override cursor that takes precedence over the trail's CSS cursor, typically set for the duration of a drag | | `isTouchLike()` | `true` for `Touch` and `Pen`, `false` for `Mouse` — useful when code should treat touch and pen the same way | | `addInputListener( listener, attach? )` / `removeInputListener( listener )` | Attaches a listener directly to the pointer (rather than to a Node) — this is how `PressListener` tracks moves/releases after a press starts | | `interruptAttached()` | Interrupts whichever listener is currently attached, if any | | `interruptAll()` | Interrupts every listener on this pointer | | `reserveForDrag()` / `reserveForKeyboardDrag()` | Marks the pointer with an `Intent` (`Intent.DRAG` or `Intent.KEYBOARD_DRAG`) so other listeners in the same dispatch can change behavior — `DragListener` calls `reserveForDrag()` automatically on a successful press | | `hasIntent( intent )` | Checks whether a given `Intent` is currently set | ## `Mouse`, `Touch`, and `Pen` | Class | `type` | Notes | | --- | --- | --- | | `Mouse` | `'mouse'` | A singleton per `Display` — there's only ever one. Adds `leftDown`/`middleDown`/`rightDown` (deprecated in favor of DOM button tracking), plus `wheelDelta`/`wheelDeltaMode` for wheel events | | `Touch` | `'touch'` | One instance per active touch contact, identified by `id`. `isTouchLike()` returns `true` | | `Pen` | `'pen'` | One instance per active stylus contact, identified by `id`. Also carries tilt/pressure information from the underlying `PointerEvent`. `isTouchLike()` returns `true` | Because touches come and go as fingers land and lift, code that needs to distinguish "the mouse" from "some touch" should check `pointer.type === 'mouse'` or `pointer instanceof Mouse`, not assume there's exactly one pointer active at a time — a multi-touch interaction can have several `Touch` pointers live simultaneously. ::: tip Prefer `mouseButton` and `pressCursor` over checking `Mouse` yourself Most pointer-type-specific behavior is already exposed as options on the listeners themselves — [`PressListener`](/api/scenery/fire-listener)'s `mouseButton` restricts which mouse button starts a press (any touch/pen still works), and `pressCursor` only visibly applies to `Mouse` anyway (touch/pen have no persistent cursor). Reach for `instanceof Mouse` only when you need behavior no listener option already covers. ::: ======================================================================== Page: PressListener (direct usage) URL: https://veillette.github.io/Almanach/api/scenery/press-listener Source: docs/api/scenery/press-listener.md Category: API | Tags: scenery, PressListener, DragListener, FireListener, input, accessibility, PDOM | Status: complete ======================================================================== # PressListener (direct usage) `PressListener` (from `scenerystack/scenery`) is the shared base class documented alongside its most common subclass in [PressListener and FireListener](/api/scenery/fire-listener) — that page covers the options and read-only Properties both classes share. This page is for the case [PressListener and FireListener](/api/scenery/fire-listener) calls out explicitly: reaching for `PressListener` **directly**, without `FireListener`'s fire-on-release semantics or `DragListener`'s coordinate tracking, because the interaction is something else — a press-and-hold tool, a custom multi-pointer gesture, or a Node whose "pressed" visual state needs to track PDOM (keyboard/switch-device) focus as well as pointer input. ```ts import { Node, Circle, PressListener } from 'scenerystack/scenery'; import { Tandem } from 'scenerystack/tandem'; const magnifier = new Node( { children: [ new Circle( 20, { fill: 'lightblue' } ) ], cursor: 'pointer' } ); const magnifyListener = new PressListener( { press: () => magnifier.setScaleMagnitude( 1.5 ), release: () => magnifier.setScaleMagnitude( 1 ), canStartPress: () => !magnifier.isDisposed, tandem: Tandem.OPT_OUT } ); magnifier.addInputListener( magnifyListener ); // Drive a highlight off state that also accounts for keyboard focus, not just pointer hover: magnifyListener.isOverOrFocusedProperty.link( overOrFocused => { magnifier.opacity = overOrFocused ? 1 : 0.8; }); ``` `PressListener` extends axon's `EnabledComponent`, so every instance also has a standard `enabledProperty`/`enabled` — disabling the listener prevents new presses from starting without needing a separate guard in `canStartPress`. ## PDOM- and focus-aware state Beyond the `isPressedProperty`/`isOverProperty`/`isHoveringProperty`/`isHighlightedProperty` covered in [PressListener and FireListener](/api/scenery/fire-listener), `PressListener` tracks additional state specifically so a Node's "looks interactive" visuals stay correct for keyboard and switch-device users, not just mouse/touch: | Property | Meaning | | --- | --- | | `isFocusedProperty` | Whether this listener's Node currently has PDOM focus | | `isOverOrFocusedProperty` | `isOverProperty \|\| ` (focused **and** the Display is currently showing focus highlights) — the right signal for "should this look highlighted," including keyboard users | | `pdomClickingProperty` | `true` while a press is being processed from a PDOM `click` event (as opposed to real pointer down/up) — needed because some assistive devices send a single `click` rather than separate down/up | | `looksPressedProperty` | `pdomClickingProperty \|\| isPressedProperty` — whether the button should render as pressed, accounting for the fact that a PDOM click fires the press/release callbacks immediately but should still *look* pressed briefly | | `overPointers` | An `ObservableArray` of every pointer currently over the listener's Node | `a11yLooksPressedInterval` (default `100` ms) controls how long `looksPressedProperty` stays `true` after a PDOM click, since the press and release callbacks both fire essentially at once for that input source. ## Methods beyond press/release/drag | Method | Effect | | --- | --- | | `canPress( event )` | Whether a press could currently start for the given event (checks button/attachment/`canStartPress`) | | `canClick()` | Whether a programmatic `click()` could currently succeed | | `click( event, callback? )` | Simulates a full press-then-release from a single PDOM click event; used internally by [`FireListener`](/api/scenery/fire-listener) and accessible-input handling | | `focus( event )` / `blur()` | Called by scenery's focus system to update `isFocusedProperty`; not something you typically call directly | | `step()` | Advances any pending collapsed drag event (see `collapseDragEvents`); called automatically if the listener is registered appropriately | | `setCreatePanTargetBounds( fn )` | Supplies a callback returning the `Bounds2` that [`AnimatedPanZoomListener`](/api/scenery/animated-pan-zoom-listener) should keep in view while this listener is pressed — useful when the pressed/dragged region differs from the listener's Node bounds | ## `useInputListenerCursor` `useInputListenerCursor: true` makes any Node this listener is attached to adopt `pressCursor` as its effective cursor whenever that Node's own `cursor` is `null` — a small convenience so you don't have to set `cursor: 'pointer'` on the Node separately from configuring the listener's `pressCursor`. ::: tip Reach for PressListener directly when the interaction isn't "fire" or "drag" If the shape of the interaction is genuinely custom — press-and-hold-to-charge, multi-touch gesture recognition, or anything that needs `isOverOrFocusedProperty`/`overPointers` — build on `PressListener` directly rather than working around `FireListener`'s fire semantics or `DragListener`'s coordinate machinery. Both of those subclasses exist specifically to save you from reimplementing this state for the two most common cases. ::: ::: warning `tandem` still defaults to required Like `FireListener` and `DragListener`, a directly-constructed `PressListener` defaults `tandem` to `Tandem.REQUIRED` and defaults `phetioReadOnly: true` on its internal press/release `PhetioAction`s. Pass `Tandem.OPT_OUT` for non-instrumented sim code, as shown above. ::: ======================================================================== Page: PressListener and FireListener URL: https://veillette.github.io/Almanach/api/scenery/fire-listener Source: docs/api/scenery/fire-listener.md Category: API | Tags: scenery, FireListener, PressListener, input | Status: verified ======================================================================== # PressListener and FireListener `PressListener` and `FireListener` (both from `scenerystack/scenery`) are the input listeners that give a `Node` press/click behavior. `PressListener` is the general-purpose base — it tracks whether a pointer is pressed/hovering/focused and exposes `press`/`release`/`drag` callbacks — and is also the shared base class for `DragListener`. `FireListener` adds one thing on top: a `fire()` callback invoked on a complete press-then-release (or on down, if `fireOnDown` is set), which is exactly the "button" interaction pattern. Attach either as an [`inputListeners`](/api/scenery/node) entry on a `Node`, and make sure the owning [`Display`](/api/scenery/display) has called `initializeEvents()`. ```ts import { Circle, FireListener } from 'scenerystack/scenery'; import { Tandem } from 'scenerystack/tandem'; const button = new Circle( 25, { fill: 'dodgerblue', cursor: 'pointer' } ); button.addInputListener( new FireListener( { fire: () => console.log( 'fired!' ), tandem: Tandem.REQUIRED } ) ); ``` ## `FireListener` options `FireListenerOptions` adds these to everything `PressListener` accepts: | Option | Default | Effect | | --- | --- | --- | | `fire` | no-op | Called as `fire( event )` when the button fires | | `fireOnDown` | `false` | If `true`, fires on press instead of on release-while-hovering | | `fireOnHold` | `false` | Enables fire-on-hold (repeated firing while held) | | `fireOnHoldDelay` | `400` | Milliseconds held before fire-on-hold starts repeating | | `fireOnHoldInterval` | `100` | Milliseconds between repeated fires once fire-on-hold is active | ## `PressListener` options | Option | Default | Effect | | --- | --- | --- | | `press` | no-op | Called as `press( event, listener )` when a press starts | | `release` | no-op | Called as `release( event \| null, listener )` when released, interrupted, or canceled | | `drag` | no-op | Called as `drag( event, listener )` on pointer move while pressed | | `targetNode` | `null` | Overrides the Node used to compute the pressed `Trail`/coordinate frame, when it differs from the Node the listener is attached to | | `attach` | `true` | Whether this listener attaches itself to the pointer (blocking other `attach: true` listeners while pressed) | | `mouseButton` | `0` (left) | Restricts which mouse button starts a press; any touch/pen still works | | `pressCursor` | `'pointer'` | Cursor to force while pressed | | `canStartPress` | always `true` | Predicate checked before allowing a press to start | | `collapseDragEvents` | `false` | Coalesces multiple drag events between frames into one `drag()` call | ## Useful read-only properties | Property | Meaning | | --- | --- | | `isPressedProperty` | Whether the listener is currently pressed | | `isOverProperty` | Whether a pointer is currently over the associated Node | | `isHoveringProperty` | `true` while a pointer that could fire this listener is over it (pressed-and-over, or unpressed-and-over) | | `isHighlightedProperty` | `isPressed || isHovering` — the usual signal for "should this look highlighted" | | `pointer` / `pressedTrail` | The active `Pointer` and press `Trail`, or `null` when not pressed | | `interrupted` | Whether the most recent release was due to an interruption rather than a normal up/click | ## Methods shared by both | Method | Effect | | --- | --- | | `press( event, targetNode?, callback? )` | Programmatically attempts to start a press; returns whether it succeeded | | `release( event?, callback? )` | Programmatically ends the press | | `interrupt()` | Cancels an in-progress press without treating it as a normal release (no `fire()` on `FireListener`) | | `dispose()` | Releases the listener's `Property`/`Emitter` resources | `FireListener` additionally exposes `fire( event )` (invoke the fire callback directly) and `click( event, callback? )` (used by PDOM/accessibility interactions to press-and-release in one step). ::: tip Use `FireListener` for buttons, `PressListener`/`DragListener` for everything else If you just need "clicked," reach for `FireListener` — it already integrates with keyboard/PDOM activation. If you need to track pointer movement while pressed (dragging a `Node` around), use `PressListener` directly or [`DragListener`](/guides/scenery-input), which builds on the same `press`/`release`/`drag` shape. ::: ::: warning `tandem` defaults to required Both listeners default `tandem` to `Tandem.REQUIRED` (for PhET-iO instrumentation of user actions). If your project isn't PhET-iO-instrumented, pass `Tandem.OPT_OUT`, or supply a real tandem as shown above — leaving the default in place will throw if PhET-iO validation is active. ::: ======================================================================== Page: RadialGradient URL: https://veillette.github.io/Almanach/api/scenery/radial-gradient Source: docs/api/scenery/radial-gradient.md Category: API | Tags: scenery, RadialGradient, gradient, paint, fill, stroke | Status: verified ======================================================================== # RadialGradient `RadialGradient` (from `scenerystack/scenery`) is a paint type — assignable to a Node's `fill` or `stroke` just like [`Color`](/api/scenery/color) or [`LinearGradient`](/api/scenery/linear-gradient) — that renders a gradient between two circles (a start circle at ratio 0 and an end circle at ratio 1), matching the CSS/SVG/Canvas notion of a radial gradient. It extends the same abstract `Gradient` base class `LinearGradient` does, so color stops work identically. ```ts import { Circle, RadialGradient } from 'scenerystack/scenery'; // Start circle: center (0,0), radius 0 (a point). End circle: center (0,0), radius 20. const gradient = new RadialGradient( 0, 0, 0, 0, 0, 20 ) .addColorStop( 0, 'white' ) .addColorStop( 1, 'steelblue' ); const sphere = new Circle( 20, { fill: gradient } ); ``` ## Constructor `new RadialGradient( x0, y0, r0, x1, y1, r1 )` — the start circle's center and radius (ratio 0), then the end circle's center and radius (ratio 1), all in the Node's local coordinate frame. A common "spherical" look uses the same center for both circles with `r0` near `0` and `r1` matching the shape's radius, as above; offsetting the start circle's center instead produces a highlight-style radial gradient. ## Color stops `addColorStop( ratio, color )` is inherited from the same `Gradient` base class [`LinearGradient`](/api/scenery/linear-gradient) uses — stops must be added in non-decreasing `ratio` order, and `color` accepts `null`, a CSS string, a [`Color`](/api/scenery/color), or a `Property` resolving to one of those. ::: tip Safari gets a workaround for coincident-center radial gradients Scenery detects Safari and nudges both circles' centers to their midpoint internally to work around a browser gradient-rendering bug — you don't need to do anything for this, but it's worth knowing if a `RadialGradient`'s rendered center looks *slightly* off from the coordinates you passed specifically on Safari; the visual difference is designed to be imperceptible. ::: ======================================================================== Page: Rectangle URL: https://veillette.github.io/Almanach/api/scenery/rectangle Source: docs/api/scenery/rectangle.md Category: API | Tags: scenery, Rectangle, Path | Status: verified ======================================================================== # Rectangle `Rectangle` (from `scenerystack/scenery`) is a [`Path`](/api/scenery/path) subclass that builds its own rectangular (optionally rounded) `Shape` from `x`/`y`/`width`/`height` parameters, so you don't need to construct a kite `Shape` by hand for plain or rounded rectangles. ```ts import { Rectangle } from 'scenerystack/scenery'; import { Bounds2 } from 'scenerystack/dot'; // new Rectangle( x, y, width, height, options ) const panel = new Rectangle( 0, 0, 200, 100, { fill: '#eee', stroke: 'black', lineWidth: 1 } ); // new Rectangle( x, y, width, height, cornerXRadius, cornerYRadius, options ) const roundedPanel = new Rectangle( 0, 0, 200, 100, 8, 8, { fill: '#eee' } ); // new Rectangle( bounds, options ) const fromBounds = new Rectangle( new Bounds2( 0, 0, 200, 100 ), { fill: 'white' } ); panel.rectWidth = 220; // resize after construction ``` ## Constructor overloads | Signature | Use case | | --- | --- | | `new Rectangle( options )` | Fully options-driven | | `new Rectangle( bounds, options? )` | From a `Bounds2` | | `new Rectangle( bounds, cornerXRadius, cornerYRadius, options? )` | From a `Bounds2` with rounded corners | | `new Rectangle( x, y, width, height, options? )` | Explicit geometry | | `new Rectangle( x, y, width, height, cornerXRadius, cornerYRadius, options? )` | Explicit geometry, rounded corners | ## Options | Option | Effect | | --- | --- | | `rectBounds` | Sets `x`/`y`/`width`/`height` at once from a `Bounds2` | | `rectSize` | Sets `width`/`height` at once from a `Dimension2` | | `rectX`, `rectY` | Top-left corner position | | `rectWidth`, `rectHeight` | Dimensions | | `cornerRadius` | Sets both `cornerXRadius` and `cornerYRadius` together | | `cornerXRadius`, `cornerYRadius` | Independent horizontal/vertical radii for elliptical rounded corners | `Rectangle` also accepts every [`Path` option](/api/scenery/path) (`fill`, `stroke`, `lineWidth`, …) except `shape`/`shapeProperty`, plus the full set of [`Node` options](/api/scenery/node) — including layout sizing, since `Rectangle` mixes in `Sizable` and can be given a `preferredWidth`/`preferredHeight` by a parent layout container. ::: tip Resizing without rebuilding the shape Prefer `rectWidth`/`rectHeight`/`rectX`/`rectY`/`rectBounds` over recreating the `Rectangle` or setting `.shape` directly — these setters update the internal shape efficiently and are what layout containers use when they resize a `Rectangle` child. ::: ======================================================================== Page: RichDragListener URL: https://veillette.github.io/Almanach/api/scenery/rich-drag-listener Source: docs/api/scenery/rich-drag-listener.md Category: API | Tags: scenery, RichDragListener, input, drag, keyboard, accessibility | Status: verified ======================================================================== # RichDragListener `RichDragListener` (from `scenerystack/scenery`) is not a subclass of `DragListener` — it *composes* one internal `DragListener` and one internal `KeyboardDragListener`, forwarding every scenery input-listener callback (`down`, `up`, `keydown`, `focus`, …) to whichever of the two applies, and implements `TInputListener` so it can be attached to a Node exactly like either listener alone. It exists so a draggable object gets pointer *and* keyboard support from one declaration instead of wiring up both listeners and their shared options by hand — see [Drag Listeners](/patterns/drag-listeners) for why it's the recommended default for new draggable code, with plain `DragListener`/`KeyboardDragListener` reserved for cases needing one modality only or finer control. ```ts import { Node, RichDragListener } from 'scenerystack/scenery'; const bodyNode = new Node( { tagName: 'div', focusable: true, cursor: 'pointer' } ); bodyNode.addInputListener( new RichDragListener( { positionProperty: body.positionProperty, transform: modelViewTransform, dragBoundsProperty: model.dragBoundsProperty, start: () => body.userControlledProperty.set( true ), end: () => body.userControlledProperty.set( false ), keyboardDragListenerOptions: { dragSpeed: 150 } } ) ); ``` ## Options `RichDragListenerOptions` shares the common drag options (`positionProperty`, `transform`, `dragBoundsProperty`, `mapPosition`, `translateNode`, `start`/`drag`/`end`) applied to *both* internal listeners, plus: | Option | Default | Effect | | --- | --- | --- | | `dragListenerOptions` | `{}` | Additional/overriding options passed only to the internal `DragListener` (e.g. `allowTouchSnag`, `mouseButton`) | | `keyboardDragListenerOptions` | `{}` | Additional/overriding options passed only to the internal `KeyboardDragListener` (e.g. `dragSpeed`, `keyboardDragDirection`) | | `tandem` | `Tandem.REQUIRED` | Split internally into `tandem.createTandem('dragListener')` / `'keyboardDragListener'` for PhET-iO | `start`/`drag`/`end` given directly on `RichDragListener` fire for *either* input type; listener-specific callbacks in `dragListenerOptions`/`keyboardDragListenerOptions` fire in addition to (not instead of) the shared ones. ## Public state and methods | Member | Meaning | | --- | --- | | `dragListener` | The internal `DragListener` instance — public, so you can inspect/reuse it directly | | `keyboardDragListener` | The internal `KeyboardDragListener` instance | | `isPressedProperty` | `true` if either internal listener is pressed | | `interrupt()` | Interrupts both internal listeners | | `dispose()` | Disposes both internal listeners and this listener's own Properties | ::: tip Starting one input type interrupts the other When the pointer `DragListener` starts, it interrupts the `KeyboardDragListener` (and vice versa) — the two are mutually exclusive at any instant, so a drag can't be simultaneously driven by a mouse and the keyboard. This is handled internally; you don't need to manage it in your `start`/`end` callbacks. ::: ======================================================================== Page: RichText URL: https://veillette.github.io/Almanach/api/scenery/rich-text Source: docs/api/scenery/rich-text.md Category: API | Tags: scenery, RichText, markup, links | Status: verified ======================================================================== # RichText `RichText` (from `scenerystack/scenery`) renders a string containing a constrained, security-conscious subset of HTML-like markup, splitting it internally into multiple [`Text`](/api/scenery/text) children. Use it when a single string needs mixed styling — bold/italic runs, sub/superscripts, links, line breaks — that plain `Text` can't express. Malformed markup is not accepted, and HTML entities in the content must be escaped. ```ts import { RichText } from 'scenerystack/scenery'; import { PhetFont } from 'scenerystack/scenery-phet'; const description = new RichText( 'RichText supports bold, italic, and H2O-style subscripts.', { font: new PhetFont( 16 ), fill: 'black' } ); const withLink = new RichText( 'See our website for more.', { links: { phetWebsite: 'https://phet.colorado.edu' } } ); ``` ## Supported markup ``/``, ``/``, ``, ``, ``, ``, `
`, ``, ``, `` links, and `` for embedding an arbitrary `Node` inline. ## Options | Option | Effect | | --- | --- | | `string` / `stringProperty` | The markup string to render, same as `Text` | | `font`, `fill`, `stroke`, `lineWidth` | Base styling applied to unmarked text (per-tag colors override `fill` locally) | | `links` | Either `true` (embed `href`s directly from the markup) or a `Record void)>` mapping placeholder names to real URLs/callbacks — the default (safer) way to allow links | | `linkFill` | Fill color used specifically for link text | | `linkEventsHandled` | Whether clicking a link calls `event.handle()`, stopping propagation | | `nodes` | A `Record` of Nodes to splice in for `` tags | | `tags` | A `Record Node>` for custom wrapping tags, e.g. `...` | | `align` | `'left'`, `'center'`, or `'right'` alignment across wrapped lines | | `lineWrap` | Maximum line width (in local coordinates) before wrapping; `null` disables wrapping | | `leading` | Extra vertical spacing between wrapped lines | | `subScale`, `subXSpacing`, `subYOffset`, `supScale`, `supXSpacing`, `supYOffset` | Fine-tune subscript/superscript sizing and placement | | `underlineLineWidth`, `underlineHeightScale`, `strikethroughLineWidth`, `strikethroughHeightScale` | Fine-tune ``/`` rendering | `RichText` also accepts the full set of [`Node` options](/api/scenery/node). ::: warning Links default to being ignored, not embedded Passing a raw `` without a matching `links` map entry causes that link to be silently ignored — this is a deliberate XSS guard, since `RichText` content often comes from translated strings. To use literal `href`s straight from the markup, pass `links: true` explicitly; to remap placeholders to safe destinations, use the `links: { placeholder: url }` form shown above. ::: ======================================================================== Page: SceneryEvent URL: https://veillette.github.io/Almanach/api/scenery/scenery-event Source: docs/api/scenery/scenery-event.md Category: API | Tags: scenery, SceneryEvent, input, Trail | Status: complete ======================================================================== # SceneryEvent `SceneryEvent` (from `scenerystack/scenery`) is the object every scenery input listener callback receives — `press`, `release`, `drag`, `fire`, and the raw listener methods (`down`, `up`, `move`, …) documented on [`PressListener`/`FireListener`](/api/scenery/fire-listener) and [`DragListener`](/api/scenery/drag-listener) all take one. It wraps the native browser event together with scenery-specific context: which [`Pointer`](/api/scenery/pointer-mouse-touch-pen) triggered it, and the [`Trail`](/api/scenery/trail) of Nodes it was dispatched through. A single DOM `TouchEvent` can carry multiple touch points, so don't assume a `SceneryEvent`'s `domEvent` is exclusive to that event — the same DOM event object can back multiple `SceneryEvent`s. ```ts import { FireListener } from 'scenerystack/scenery'; button.addInputListener( new FireListener( { fire: event => { console.log( 'fired by', event.pointer.type ); // 'mouse' | 'touch' | 'pen' | 'pdom' console.log( 'hit trail', event.trail.toString() ); console.log( 'leaf node', event.target === event.trail.lastNode() ); // true } } ) ); ``` ## Properties | Property | Meaning | | --- | --- | | `trail` | Root-to-leaf `Trail` of Nodes hit by this event | | `type` | The scenery event type string that was fired, e.g. `'down'`, `'move'` | | `pointer` | The [`Pointer`](/api/scenery/pointer-mouse-touch-pen) (`Mouse`/`Touch`/`Pen`/PDOM pointer) that triggered this event | | `domEvent` | The raw underlying DOM event (`MouseEvent`, `TouchEvent`, `PointerEvent`, `KeyboardEvent`, …), or `null` | | `context` | The `EventContext` scenery captured around the DOM event (environment info at dispatch time) | | `activeElement` | `document.activeElement` at the time the event fired | | `currentTarget` | The Node the listener was attached to; `null` when the event is being fired directly on a `Pointer` rather than dispatched through the scene graph | | `target` | The leaf-most Node in `trail` — the actual Node that was hit | | `isPrimary` | `true` for touches, and for the mouse only when it's the primary (left) button | | `handled` / `aborted` | Set by `handle()`/`abort()`, see below | ## Methods | Method | Effect | | --- | --- | | `handle()` | Marks the event `handled`, signaling it shouldn't bubble further — analogous to DOM `stopPropagation()`, but named differently because it does **not** call the underlying DOM method | | `abort()` | Marks the event `aborted`, so no further listeners see it at all (stronger than `handle()`) | | `isFromPDOM()` | `true` if the event originated from the Parallel DOM (keyboard/assistive-technology activation) rather than a physical pointer | | `canStartPress()` | `true` if a fresh, unattached `PressListener` could start a press with this event — ignores non-left mouse buttons and pointers already attached to another listener | ::: tip `handle()` doesn't touch the native DOM event Calling `event.handle()` stops *scenery's* internal dispatch from continuing to bubble the `SceneryEvent` up the trail — it deliberately does not call `stopPropagation()` on the underlying DOM event, since other (non-scenery) listeners on the page may still need to see it. If you need to prevent default browser behavior too, call `event.domEvent?.preventDefault()` yourself. ::: ======================================================================== Page: Sizable, WidthSizable, and HeightSizable URL: https://veillette.github.io/Almanach/api/scenery/sizable-mixins Source: docs/api/scenery/sizable-mixins.md Category: API | Tags: scenery, Sizable, WidthSizable, HeightSizable, layout | Status: complete ======================================================================== # Sizable, WidthSizable, and HeightSizable By default, a `Node`'s size is whatever its content makes it — layout containers like [`FlowBox`](/api/scenery/flow-box) and [`GridBox`](/api/scenery/grid-box) can position such a Node, but can't make it *fill* available space, because it has no way to be told "be this wide." `WidthSizable`, `HeightSizable`, and `Sizable` (all from `scenerystack/scenery`) are mixin traits that add exactly that capability: a settable `preferredWidth`/`preferredHeight` the layout container writes to, and a `minimumWidth`/`minimumHeight` the component reports back so the container knows how small it can be shrunk. `Sizable` is just `WidthSizable` and `HeightSizable` combined, with `Dimension2`-based convenience accessors on top. ```ts import { Node, Rectangle, Sizable, HBox } from 'scenerystack/scenery'; class FillBar extends Sizable( Node ) { private readonly background: Rectangle; public constructor() { super(); this.background = new Rectangle( 0, 0, 0, 20, { fill: 'teal' } ); this.addChild( this.background ); // Mutate sizable options AFTER the super() call, in a later mutate() — see the warning below. this.mutate( { minimumWidth: 20 } ); this.preferredWidthProperty.link( preferredWidth => { this.background.rectWidth = preferredWidth ?? this.minimumWidth ?? 0; } ); } } const bar = new FillBar(); bar.layoutOptions = { stretch: true, grow: 1 }; const row = new HBox( { children: [ bar ], stretch: true } ); ``` Once `bar` is `WidthSizable`, `HBox`/`FlowBox` recognizes it as resizable and — combined with `stretch`/`grow` [layout options](/api/scenery/flow-box) — will set its `preferredWidth` to fill the row, instead of leaving it at its natural (zero, in this example) content width. ## Options (per trait) | Option | Default | Effect | | --- | --- | --- | | `preferredWidth` / `preferredHeight` | `null` | Set by the parent layout container (in the parent coordinate frame); the component should try to make `node.width`/`node.height` match this | | `minimumWidth` / `minimumHeight` | `null` | Set by the component itself, to report the smallest size it can render acceptably; usually derived from `localMinimumWidth`/`localMinimumHeight` | | `localPreferredWidth` / `localPreferredHeight` | `null` | The same preferred size, in the Node's own local coordinate frame — kept in sync with the parent-frame version automatically as the transform changes | | `localMinimumWidth` / `localMinimumHeight` | `null` | The local-frame minimum size — this is usually the one a resizable component sets directly | | `widthSizable` / `heightSizable` | `true` | Whether this Node currently accepts a preferred size from its layout container at all; set `false` to opt a specific instance out | `Sizable` adds `preferredSize`/`minimumSize`/`localPreferredSize`/`localMinimumSize` as `Dimension2`-valued get/set pairs over both dimensions at once, plus a single `sizable` boolean covering both `widthSizable` and `heightSizable`. ## Checking whether an arbitrary Node participates `scenerystack/scenery` also exports type-check helpers for code that receives an arbitrary `Node` and needs to know whether it supports sizing before touching these properties: | Helper | Checks | | --- | --- | | `isWidthSizable( node )` / `isHeightSizable( node )` / `isSizable( node )` | Whether a Node's *current* `widthSizable`/`heightSizable`/`sizable` value is `true` | | `extendsWidthSizable( node )` / `extendsHeightSizable( node )` / `extendsSizable( node )` | Whether a Node was constructed *with* the trait at all, regardless of its current boolean value | ::: warning Set sizable options from a later `mutate()`, not the constructor options passed to `super()` The properties these mixins add (`preferredWidthProperty`, etc.) aren't initialized until partway through the mixed-in constructor. If you pass `widthSizable`-related options directly into the `Node` options object handed to `super()` in your subclass constructor, they'll fail — assertions catch this, but it's easy to trip over. Apply them via a follow-up `this.mutate( { ... } )` call after `super()` returns, as in the example above. ::: ======================================================================== Page: Spacer URL: https://veillette.github.io/Almanach/api/scenery/spacer Source: docs/api/scenery/spacer.md Category: API | Tags: scenery, Spacer, layout, bounds | Status: verified ======================================================================== # Spacer `Spacer` (from `scenerystack/scenery`) is a [`Node`](/api/scenery/node) that draws nothing and can never have children — it exists purely to occupy a fixed rectangular region of `[0, width] x [0, height]` in its own local coordinate frame, so a layout container that sees its `bounds` treats that region as reserved space. It's the scenery equivalent of a CSS empty spacer `
`: use it inside a [`FlowBox`](/api/scenery/flow-box)/[`GridBox`](/api/scenery/grid-box) to force a gap that isn't uniform `spacing`, or standalone to pad out a fixed layout. ```ts import { Spacer, HBox, Circle } from 'scenerystack/scenery'; const row = new HBox( { spacing: 4, children: [ new Circle( 10, { fill: 'crimson' } ), new Spacer( 40, 1 ), // a 40-wide gap wider than `spacing` alone would give new Circle( 10, { fill: 'teal' } ) ] } ); ``` ## Constructor `new Spacer( width, height, options? )` — `width` and `height` must both be finite numbers; they set `localBounds` to `Bounds2( 0, 0, width, height )` before any `options` (standard [`Node`](/api/scenery/node) options like `x`/`y`/`layoutOptions`) are applied via `mutate()`. `SpacerOptions` is just `NodeOptions` — `Spacer` adds no options of its own beyond the `width`/`height` constructor arguments. ## `HStrut` and `VStrut` `HStrut` and `VStrut` (also from `scenerystack/scenery`) are thin `Spacer` subclasses for the common one-dimensional case: `new HStrut( width, options? )` is exactly `new Spacer( width, 0, options )`, and `new VStrut( height, options? )` is exactly `new Spacer( 0, height, options )`. Reach for these when you only need to reserve space along one axis — e.g. a horizontal gap inside a [`VBox`](/api/scenery/v-box) — since the intent reads more clearly than a `Spacer` with an explicit `0`. ::: warning A Spacer can never have children `Spacer` is built with scenery's `Leaf` mixin, which throws if you try to `addChild()` to it — it is always and only a leaf in the scene graph. If you need an invisible *container* (something that can hold other Nodes but isn't itself drawn), use a plain [`Node`](/api/scenery/node) with no visible content instead of `Spacer`. ::: ======================================================================== Page: Sprites, Sprite, SpriteImage, and SpriteSheet URL: https://veillette.github.io/Almanach/api/scenery/sprites Source: docs/api/scenery/sprites.md Category: API | Tags: scenery, Sprites, Sprite, SpriteImage, SpriteSheet, SpriteInstance, performance, particles, webgl, canvas | Status: complete ======================================================================== # Sprites, Sprite, SpriteImage, and SpriteSheet `Sprites` (from `scenerystack/scenery`) is a single `Node` that draws a large number of image instances — hundreds or thousands of particles, molecules, coins, whatever — with far less overhead than giving each instance its own [`Image`](/api/scenery/image) `Node`. Where an ordinary scene graph pays a per-Node cost for transforms, bounds, and picking, `Sprites` collapses an entire population of same-looking-but-differently-placed images into one `Node` and one WebGL/Canvas draw call, driven by a plain array you mutate directly rather than a tree of children. The pieces work together like this: a `Sprite` wraps a `SpriteImage` (the actual pixel content plus a center offset); a `SpriteInstance` is a lightweight, poolable record of "this sprite, at this matrix, with this alpha"; and `Sprites` is the `Node` that takes an array of `Sprite`s and an array of `SpriteInstance`s and paints them all. `SpriteSheet` is a lower-level utility (a single Canvas/WebGL texture packing multiple source images together) that most sim code won't touch directly — it exists so custom WebGL drawables can batch several distinct images into one texture. ```ts import { Sprites, Sprite, SpriteImage, SpriteInstance, SpriteInstanceTransformType } from 'scenerystack/scenery'; import { Vector2 } from 'scenerystack/dot'; import { Bounds2 } from 'scenerystack/dot'; // Build one Sprite from a loaded image, offset so (0,0) is the image's center. const particleImage: HTMLImageElement = document.createElement( 'img' ); // in practice, an already-loaded image const particleSpriteImage = new SpriteImage( particleImage, new Vector2( particleImage.width / 2, particleImage.height / 2 ) ); const particleSprite = new Sprite( particleSpriteImage ); // Create (or pool-fetch) one SpriteInstance per on-screen particle. const instances: SpriteInstance[] = []; for ( let i = 0; i < 500; i++ ) { const instance = SpriteInstance.pool.fetch(); instance.sprite = particleSprite; instance.transformType = SpriteInstanceTransformType.TRANSLATION; instance.matrix.setToTranslation( Math.random() * 400, Math.random() * 400 ); instances.push( instance ); } const spritesNode = new Sprites( { sprites: [ particleSprite ], spriteInstances: instances, canvasBounds: new Bounds2( 0, 0, 400, 400 ) } ); // After mutating positions in the model's step function: instances.forEach( instance => instance.matrix.setToTranslation( /* new x */ 0, /* new y */ 0 ) ); spritesNode.invalidatePaint(); // tell Sprites to repaint with the updated matrices ``` ## `Sprite` `new Sprite( spriteImage: SpriteImage )` wraps a `SpriteImage` in a mutable `imageProperty`, so the same `Sprite` object referenced by many `SpriteInstance`s can have its underlying image swapped (e.g. regenerated at a different resolution) without touching every instance. `Sprite` exposes `getShape()` and `containsPoint( point )`, both delegating to the current `SpriteImage`. ## `SpriteImage` `new SpriteImage( image, offset: Vector2, providedOptions? )` — `image` is an `HTMLImageElement` or `HTMLCanvasElement` (or a `TReadOnlyProperty` of one), and `offset` is the 2D point (in image pixels, top-left origin) that should be treated as the sprite's local "center" — this is what a `SpriteInstance`'s matrix positions. | Option | Default | Effect | | --- | --- | --- | | `hitTestPixels` | `false` | If `true`, hit-testing uses the actual non-transparent pixels instead of the full rectangular bounds | | `pickable` | `true` | Whether this image participates in hit-testing at all | ## `SpriteInstance` `SpriteInstance` is deliberately a bare data container, not something you subclass or configure via options — instances are meant to be created via `SpriteInstance.pool.fetch()` and mutated directly for performance, then returned with `freeToPool()` when no longer needed. | Field | Meaning | | --- | --- | | `sprite` | The `Sprite` to display for this instance (`null` means nothing is drawn) | | `matrix` | A `Matrix3` positioning this instance; mutate it directly rather than replacing it | | `transformType` | A `SpriteInstanceTransformType` telling `Sprites` how much of `matrix` to trust — see below | | `alpha` | Per-instance opacity, `0` to `1` | `SpriteInstanceTransformType` has four values, each a performance/flexibility tradeoff: `TRANSLATION` (fastest — only `matrix`'s translation is used), `TRANSLATION_AND_SCALE`, `TRANSLATION_AND_ROTATION`, and `AFFINE` (slowest — the full matrix is respected). Pick the cheapest one that describes how your instances actually move. ## `Sprites` options | Option | Default | Effect | | --- | --- | --- | | `sprites` | `[]` | The fixed set of `Sprite`s this Node can draw; cannot be changed after construction | | `spriteInstances` | `[]` | The array of `SpriteInstance`s to paint, in order (later entries draw on top); mutate this array in place | | `canvasBounds` | — | The `Bounds2` scenery uses for layout/repainting/hit-testing fallback — must cover everywhere the sprites actually draw, or content can be clipped | | `hitTestSprites` | `false` | If `true`, picking calls each instance's `containsPoint()`; if `false`, any point inside `canvasBounds` counts as a hit | | `renderer` | `'webgl'` | `Sprites` defaults to WebGL rendering (unlike most Nodes), since that's normally why you'd reach for it; Canvas is also supported | ## Updating what's drawn Because `spriteInstances` is a plain array you mutate directly (add, remove, or change `sprite`/`matrix`/`alpha` on existing entries), `Sprites` has no automatic dirty-tracking — call `invalidatePaint()` after any change you want reflected on the next frame. ::: tip Sprites exists for populations too large for one Node each If you have a handful of images, ordinary [`Image`](/api/scenery/image) Nodes in the scene graph are simpler and get you free input handling, layout participation, and per-Node transforms. Reach for `Sprites` specifically when the population is large enough (hundreds-plus, changing every frame) that per-Node overhead becomes the bottleneck — it trades scene-graph convenience for one big, hand-managed draw call. ::: ::: warning `canvasBounds` is not computed automatically Unlike most Nodes, `Sprites` has no way to infer its own bounds from its content — you must set `canvasBounds` yourself (initially via the option, or later via `setCanvasBounds()`) to a region that covers every instance's drawn extent, and keep it updated as instances move. ::: ======================================================================== Page: Text URL: https://veillette.github.io/Almanach/api/scenery/text Source: docs/api/scenery/text.md Category: API | Tags: scenery, Text, PhetFont, font | Status: verified ======================================================================== # Text `Text` (from `scenerystack/scenery`) is a [`Node`](/api/scenery/node) that displays a single string with a chosen `font`, `fill`, and (optionally) `stroke`. Use it for plain, single-style strings; reach for [`RichText`](/api/scenery/rich-text) instead when a string needs mixed styling (bold, sub/superscript, links) within itself. ```ts import { Text, Font } from 'scenerystack/scenery'; import { PhetFont } from 'scenerystack/scenery-phet'; const label = new Text( 'Hello, world!', { font: new PhetFont( 24 ), fill: 'black' } ); // Shorthand font properties instead of a Font instance: const caption = new Text( 'caption text', { fontSize: 14, fontWeight: 'bold' } ); ``` Simulations conventionally use [`PhetFont`](/api/scenery-phet/phet-font) (from `scenerystack/scenery-phet`) rather than the browser default or a raw `Font` so that text renders consistently across platforms; `Font` (from `scenerystack/scenery`) is the lower-level class `PhetFont` builds on. ## Options | Option | Effect | | --- | --- | | `string` | The text to display (also accepts a `number`, which is stringified) | | `stringProperty` | Sets forwarding of the string from a `TReadOnlyProperty` — the standard way to make displayed text respond to changing/translated content | | `font` | A `Font` (or `PhetFont`) instance, or a CSS font shorthand string | | `fontWeight`, `fontFamily`, `fontStretch`, `fontStyle`, `fontSize` | Shorthand setters that construct/replace the underlying `Font` for you | | `boundsMethod` | How bounds are computed: `'fast'`, `'fastCanvas'`, `'accurate'`, or `'hybrid'` (default) | `Text` also mixes in `Paintable` (`fill`, `stroke`, `lineWidth`, …) and accepts the full set of [`Node` options](/api/scenery/node). The default `fill` is `'#000000'` (black), unlike a plain `Node`. ## Reacting to changing text ```ts import { Text } from 'scenerystack/scenery'; import { StringProperty } from 'scenerystack/axon'; const messageProperty = new StringProperty( 'Score: 0' ); const scoreText = new Text( messageProperty, { fontSize: 18 } ); messageProperty.value = 'Score: 10'; // scoreText updates automatically ``` ::: tip Pass a Property directly as the first constructor argument `new Text( stringProperty, options )` is equivalent to `new Text( '', { stringProperty, ...options } )` — either form links the displayed string to a `TReadOnlyProperty` (see [`StringProperty`](/api/axon/string-property)) so translated or dynamically-generated strings update the display without manual re-rendering. ::: ======================================================================== Page: Trail URL: https://veillette.github.io/Almanach/api/scenery/trail Source: docs/api/scenery/trail.md Category: API | Tags: scenery, Trail, scene-graph, hit-testing, coordinates | Status: verified ======================================================================== # Trail `Trail` (from `scenerystack/scenery`) represents one path through the scene graph — an ordered array of [`Node`](/api/scenery/node)s from some root down to a descendant, plus the child index at each step. Because a `Node` can appear under more than one parent (a DAG, not just a tree), a single `Node` can be reached by multiple distinct trails; `Trail` is what disambiguates *which* instance of a Node is meant when computing bounds, transforms, or picking. Scenery hands you a `Trail` on every input event (`event.trail`) and uses trails internally to walk the [`Display`](/api/scenery/display)'s rendered tree. ```ts import { Node, Circle } from 'scenerystack/scenery'; import { Vector2 } from 'scenerystack/dot'; const root = new Node(); const group = new Node(); const dot = new Circle( 5, { fill: 'crimson' } ); root.addChild( group ); group.addChild( dot ); const trail = new Trail( [ root, group, dot ] ); trail.lastNode(); // dot trail.rootNode(); // root trail.getMatrix(); // Matrix3: dot's full transform relative to root trail.localToGlobalPoint( new Vector2( 0, 0 ) ); // dot's local origin, in root's frame ``` ## Building and inspecting a Trail | Method | Effect | | --- | --- | | `new Trail( nodes )` | Builds a trail from a `Node[]` (root-to-leaf order), a single `Node`, or copies another `Trail` | | `addAncestor( node, index? )` / `addDescendant( node, index? )` | Extends the trail at the root end / leaf end | | `removeAncestor()` / `removeDescendant()` | Shortens the trail from either end | | `slice( start, end? )` / `subtrailTo( node, excludeNode? )` | Returns a new `Trail` covering part of this one | | `copy()` | Independent copy that can be mutated without affecting the original | | `reindex()` | Recomputes `indices` after the scene graph has changed, since indices can go stale | | `lastNode()` / `rootNode()` | The leaf Node / the root Node of the trail | | `nodes` / `indices` / `length` | The raw `Node[]`, per-step child indices, and trail length | ## Queries used for hit-testing and visibility | Method | Meaning | | --- | --- | | `isValid()` | Whether every node in the trail is still actually connected root-to-leaf (reindexes first) | | `isVisible()` | `true` only if every Node along the trail has `visible: true` | | `isPickable()` | Whether this trail would actually be hit-tested, given `pickable`/`visible`/attached listeners along the way | | `getOpacity()` | Combined (multiplied) opacity of every Node on the trail | | `containsNode( node )` | Whether `node` appears anywhere on this trail | | `isExtensionOf( other, allowSameTrail? )` | Whether this trail continues on from `other` (i.e. `other` is a prefix) | | `equals( other )` / `compare( other )` | Structural equality, or a stable ordering comparison (for z-order-like sorting) | ## Coordinate transforms | Method | Effect | | --- | --- | | `getMatrix()` | `Matrix3` transforming the leaf Node's local coordinates into the root's coordinate frame | | `getTransform()` | Same, wrapped as a `Transform3` | | `localToGlobalPoint( point )` / `globalToLocalPoint( point )` | Converts a `Vector2` between the leaf's local frame and the trail's root frame | | `localToGlobalBounds( bounds )` / `globalToLocalBounds( bounds )` | Same, for a `Bounds2` | | `getTransformTo( otherTrail )` / `getMatrixTo( otherTrail )` | Transform between two trails that share a common ancestor | ::: tip A Node's `Trail` isn't unique — request it when you need it Because Nodes can have multiple parents, there's no single "the trail" for a Node in general; that's why `Trail` objects are handed to you contextually (an input event's `event.trail`, or `node.getUniqueTrail()` when a Node is only used once) rather than stored permanently on the `Node` itself. Don't cache a `Trail` across scene graph edits without calling `reindex()` or rechecking `isValid()` first — added/removed/reordered children can make its `indices` stale. ::: ======================================================================== Page: TransformTracker URL: https://veillette.github.io/Almanach/api/scenery/transform-tracker Source: docs/api/scenery/transform-tracker.md Category: API | Tags: scenery, TransformTracker, Trail, transform | Status: complete ======================================================================== # TransformTracker `TransformTracker` (from `scenerystack/scenery`) watches every Node along a [`Trail`](/api/scenery/trail) and notifies you when the trail's *cumulative* transform (the composed local-to-global matrix) changes — without recomputing the whole matrix chain from scratch each time. It caches the composed matrix and only recomputes the portion downstream of whichever Node actually changed, using a dirty index. Scenery uses it internally for exactly this reason: [`DragListener`](/api/scenery/drag-listener)'s `trackAncestors` option creates one to reposition a dragged Node when an ancestor moves, and the [highlight-rendering overlay](/api/scenery/highlight-rendering) uses one per active focus highlight to keep it aligned with the Node it surrounds. ```ts import { TransformTracker } from 'scenerystack/scenery'; const trail = someNode.getUniqueTrail(); const tracker = new TransformTracker( trail ); tracker.addListener( () => { console.log( 'cumulative transform changed:', tracker.matrix ); } ); // ... later, when this tracker is no longer needed: tracker.dispose(); ``` ## Constructor and options `new TransformTracker( trail, providedOptions? )` takes the `Trail` to watch and: | Option | Default | Effect | | --- | --- | --- | | `isStatic` | `false` | If `true`, listeners are notified synchronously without defensively copying the listener array first — a minor performance win when you're certain listeners won't add/remove other listeners during notification | ## Methods | Method | Effect | | --- | --- | | `addListener( listener )` | Registers a no-argument callback fired synchronously whenever any Node in the trail (except the root) changes its transform | | `removeListener( listener )` | Removes a previously added listener | | `getMatrix()` / `.matrix` | Returns the current local-to-global `Matrix3` for the trail's leaf node — recomputed lazily, only as far up the dirty chain as needed | | `dispose()` | Removes all the internal per-Node listeners this tracker installed; call this when you're done, or the tracked Nodes will hold references indefinitely | ::: tip Exclude the leaf node's own transform with `trail.copy().removeDescendant()` A `TransformTracker` includes every Node in the given trail. If you only care about *ancestor* transform changes (not the dragged/highlighted Node's own transform, which you're presumably setting yourself), pass a trail with the leaf removed — this is exactly the pattern `DragListener` uses internally: `new TransformTracker( pressedTrail.copy().removeDescendant() )`. ::: ======================================================================== Page: VBox URL: https://veillette.github.io/Almanach/api/scenery/v-box Source: docs/api/scenery/v-box.md Category: API | Tags: scenery, VBox, HBox, FlowBox, layout | Status: verified ======================================================================== # VBox `VBox` (from `scenerystack/scenery`) is [`FlowBox`](/api/scenery/flow-box) with `orientation` fixed to `'vertical'` — it stacks its children top-to-bottom in a single column. It's the class most code reaches for instead of `FlowBox` directly, since orientation rarely needs to change at runtime; `VBox` accepts every `FlowBox` option (`spacing`, `align`, `stretch`, `grow`, `justify`, `wrap`, margins, …) except `orientation`, which the constructor sets for you and asserts you didn't also pass. ```ts import { VBox, Circle, Rectangle } from 'scenerystack/scenery'; const column = new VBox( { spacing: 8, align: 'center', children: [ new Circle( 15, { fill: 'crimson' } ), new Rectangle( 0, 0, 40, 30, { fill: 'teal' } ), new Circle( 10, { fill: 'goldenrod' } ) ] } ); ``` For a horizontal (row) layout, use [`HBox`](/api/scenery/h-box) instead; for the full option table (spacing, alignment, growth, wrapping, margins) see [`FlowBox`](/api/scenery/flow-box), which documents everything `VBox` inherits. ## Options and methods `VBoxOptions` is `FlowBoxOptions` with `orientation` omitted — every option and method documented on [`FlowBox`](/api/scenery/flow-box) (`spacing`, `align`, `grow`, `justify`, `wrap`, `getCell()`, `dispose()`, …) applies here unchanged, just fixed to a vertical main axis. For a vertical stack, `align` takes the horizontal-alignment values (`'left'`, `'right'`, `'center'`, `'origin'`) since it controls cross-axis (horizontal) placement of each row. ::: warning Use `HSeparator`, not `VSeparator`, to divide a `VBox` `VBox` asserts against inserting a `VSeparator` child — a *vertical* dividing line makes no sense between rows stacked *vertically*. Use `HSeparator` (a horizontal rule, also from `scenerystack/scenery`) to visually divide sections of a `VBox`; `VSeparator` is for dividing columns inside an [`HBox`](/api/scenery/h-box) instead. ::: ======================================================================== Page: Voicing URL: https://veillette.github.io/Almanach/api/scenery/voicing Source: docs/api/scenery/voicing.md Category: API | Tags: scenery, Voicing, voicing, speech, accessibility | Status: verified ======================================================================== # Voicing This page is the **class-level API reference** for the `Voicing` trait (from `scenerystack/scenery`) — its options, Properties, and methods as defined in source. For what Voicing is, why it's layered on top of the PDOM rather than replacing it, and worked examples, read the narrative guide at [Voicing](/accessibility/voicing) first; this page assumes that context and only documents the surface itself. `Voicing` is a mixin function: `Voicing( Node )` (or `Voicing( SomeNodeSubclass )`) returns a class with the response Properties and speak methods below, layered on top of [`InteractiveHighlighting`](/api/scenery/interactive-highlighting) (so every `Voicing` Node is also automatically interactive-highlighting-capable). ```ts import { Node, Voicing } from 'scenerystack/scenery'; class LaunchButtonNode extends Voicing( Node ) { public constructor() { super( { tagName: 'button', accessibleName: 'Launch', voicingNameResponse: 'Launch', voicingHintResponse: 'Launches the ball at the current angle and speed.' } ); } } ``` ## Options / response Properties These are `Voicing`'s mutator keys (`VOICING_OPTION_KEYS` in source): | Option | Effect | | --- | --- | | `voicingNameResponse` | The "what is it" response — a `VoicingResponse` (string, `null`, or a function/Property resolving to one) | | `voicingObjectResponse` | The "current state" response | | `voicingContextResponse` | The "what changed elsewhere" response | | `voicingHintResponse` | The "how to interact" response | | `voicingResponsePatternCollection` | A `ResponsePatternCollection` controlling how the four responses above are combined into spoken sentences | | `voicingIgnoreVoicingManagerProperties` | If `true`, this Node's speech ignores the global response-category preferences (`responseCollector`) that normally let users silence hints, etc. | | `voicingUtterance` | A specific `Utterance` instance to funnel all of this Node's speech through, instead of an internally-created one | | `voicingFocusListener` | The listener invoked on DOM focus; defaults to speaking the full response. Pass `null` to suppress automatic speech-on-focus | | `voicingPressable` | If `true`, attaches a `VoicingActivationResponseListener` so responses are also spoken on press, not just focus | Each option has a matching getter/setter pair (`setVoicingNameResponse()` / `voicingNameResponse`, etc.), consistent with the rest of scenery's mutator pattern. ## Speak-on-demand methods | Method | Speaks | | --- | --- | | `voicingSpeakFullResponse( options? )` | All four response categories currently assigned to the Node | | `voicingSpeakResponse( options? )` | Only whatever response strings are passed in `options` | | `voicingSpeakNameResponse( options? )` | The name response | | `voicingSpeakObjectResponse( options? )` | The object response | | `voicingSpeakContextResponse( options? )` | The context response | | `voicingSpeakHintResponse( options? )` | The hint response | `SpeakingOptions` accepts an `utterance` override alongside the response text, for one-off calls that shouldn't use `voicingUtterance`. ::: tip `Voicing` composes `InteractiveHighlighting` automatically `VoicingOptions` extends `InteractiveHighlightingOptions`, and the trait's implementation class extends `InteractiveHighlighting( Type )` internally — so a `Voicing`-enabled Node is already pointer-highlightable without separately mixing in [`InteractiveHighlighting`](/api/scenery/interactive-highlighting). Mixing both traits directly on the same class is redundant and unnecessary. ::: ======================================================================== Page: WebGLNode, CanvasNode, and DOM URL: https://veillette.github.io/Almanach/api/scenery/rendering-backends Source: docs/api/scenery/rendering-backends.md Category: API | Tags: scenery, WebGLNode, CanvasNode, DOM, rendering, renderer | Status: verified ======================================================================== # WebGLNode, CanvasNode, and DOM Most scenery content is built from `Path`/`Text`/`Image`-style Nodes and rendered through scenery's own SVG or Canvas layers, chosen automatically per-Node. `WebGLNode`, `CanvasNode`, and `DOM` (all from `scenerystack/scenery`) are the three escape hatches for content scenery's normal drawing pipeline can't produce — each locks a subtree to one specific rendering backend and hands you direct control of that backend's drawing surface (or, for `DOM`, an actual live DOM element). All three are `Node` subclasses, so they take part in the scene graph, transforms, and bounds like any other Node. ## When to reach for each | Class | Use it for | | --- | --- | | `CanvasNode` | Custom 2D drawing that's cheaper or only expressible as imperative Canvas calls — particle systems, heatmaps, per-pixel effects — where per-Node SVG elements would be too slow or don't have an equivalent primitive | | `WebGLNode` | Custom GPU-accelerated drawing — large numbers of instances, shader effects — where even Canvas 2D isn't fast enough | | `DOM` | Embedding an actual pre-existing HTML element (an `