@weasel-js/parallax 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@ background planes move less than the camera and foreground planes move more.
5
5
 
6
6
  Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
7
7
  scene-graph canvas kit for React. See the
8
- [API reference](https://orochi235.github.io/weasel/api/).
8
+ [API reference](https://orochi235.github.io/weasel/#pkg/parallax).
9
9
 
10
10
  ```sh
11
11
  npm install @weasel-js/parallax
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/createParallaxPlane.ts","../src/createParallaxLayer.ts","../src/tiledProject.ts","../src/createTiledLayer.ts"],"names":["subscribeToSources"],"mappings":"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","file":"index.js","sourcesContent":["import { createEmitter } from '@weasel-js/registry';\nimport type { RedrawSource } from '@weasel-js/core';\nimport type { ParallaxOpts } from '@weasel-js/core';\n\n/** Parallax opts that can change after a layer is built. `get` is read on\n * every draw, and a notification repaints whatever draws through it. */\nexport interface ParallaxSource extends RedrawSource {\n get(): ParallaxOpts;\n}\n\n/** A {@link ParallaxSource} you write to — what an animator's `onTick` drives\n * to tween a plane's `pan`, `zoom` or `anchor`. */\nexport interface ParallaxPlane extends ParallaxSource {\n /** Merge `patch` over the current opts and notify. */\n set(patch: Partial<ParallaxOpts>): void;\n}\n\n/** A {@link ParallaxPlane} starting at `initial`. Pass it as a parallax\n * layer's `parallax` and write to it to move the plane. */\nexport function createParallaxPlane(initial: ParallaxOpts): ParallaxPlane {\n let current: ParallaxOpts = { ...initial };\n const listeners = createEmitter();\n return {\n get: () => current,\n set(patch) {\n current = { ...current, ...patch };\n listeners.emit();\n },\n subscribe: listeners.subscribe,\n };\n}\n\n/** Whether `p` is a live source rather than a fixed set of opts. */\nexport function isParallaxSource(p: ParallaxOpts | ParallaxSource): p is ParallaxSource {\n return typeof (p as ParallaxSource).get === 'function';\n}\n","import { drawOneLayer, subscribeToSources, type RenderLayer } from '@weasel-js/core';\nimport type { View } from '@weasel-js/core';\nimport { deriveParallaxView, type ParallaxOpts } from '@weasel-js/core';\nimport { isParallaxSource, type ParallaxSource } from './createParallaxPlane';\n\n/** Options for `createParallaxLayer`. */\nexport interface CreateParallaxLayerOpts<TData> {\n id: string;\n label: string;\n /** Layers re-rendered through the derived inner view. */\n source: RenderLayer<TData>[];\n /** How the plane tracks the camera. Pass a `ParallaxSource` (such as\n * `createParallaxPlane`) to change it after the layer is built — an\n * animator tweening `pan` or `zoom` writes the plane, not the layer. */\n parallax: ParallaxOpts | ParallaxSource;\n /** Where the camera view comes from. Defaults to the view the Canvas hands\n * the layer — the `view` prop. A consumer running a 60 Hz camera through\n * refs pins that prop to identity, and would otherwise get identity back\n * for every `pan` value and a backdrop that never moves. */\n getOuterView?: () => View;\n}\n\n/**\n * Wrap source RenderLayers in a parallax plane. The plane's inner view is\n * derived from the camera view per {@link deriveParallaxView}; each source\n * layer is drawn through it by `drawOneLayer`, so a `space: 'world'` source\n * gets the inner view's transform and a `space: 'screen'` one does not. The\n * plane itself is emitted as `space: 'screen'` — its children already carry\n * whatever transform they need, and the outer Canvas must add none.\n *\n * What this draws is paint: pointer events target the camera's view. Content\n * that should be clickable belongs on a scene layer carrying `parallax`\n * instead, which `<SceneCanvas>` renders and picks through the same view.\n *\n * **Screen-space source layers don't compose meaningfully** — they ignore\n * the derived view by definition. Same constraint as `createViewportLayer`.\n */\nexport function createParallaxLayer<TData>(\n opts: CreateParallaxLayerOpts<TData>,\n): RenderLayer<TData> {\n const { id, label, source, parallax, getOuterView } = opts;\n const read = isParallaxSource(parallax) ? () => parallax.get() : () => parallax;\n const fromSources = subscribeToSources(source);\n return {\n id,\n label,\n space: 'screen',\n subscribe: isParallaxSource(parallax)\n ? (listener) => {\n const offSources = fromSources(listener);\n const offPlane = parallax.subscribe(listener);\n return () => { offSources(); offPlane(); };\n }\n : fromSources,\n draw: (data, outer, dims) => {\n const inner = deriveParallaxView(getOuterView?.() ?? outer, read());\n return source.flatMap((layer) => drawOneLayer(layer, data, inner, dims));\n },\n };\n}\n","/**\n * Which copies of a periodic lattice a visible range touches: the integer\n * copies `k` whose cell overlaps `[visStart, visEnd]`. Copy `k` draws the\n * authored content shifted by `k * period`.\n */\nexport interface TileRange {\n /** First copy index, inclusive. */\n from: number;\n /** Last copy index, inclusive. `to < from` means nothing is visible. */\n to: number;\n}\n\n/** The copies of a lattice with cell size `period` that `[visStart, visEnd]`\n * touches, widened by `bleed`. A non-positive or non-finite `period` answers\n * copy 0 alone. */\nexport function tiledProject(\n visStart: number,\n visEnd: number,\n period: number,\n bleed = 0,\n): TileRange {\n if (!Number.isFinite(period) || period <= 0) return { from: 0, to: 0 };\n return {\n from: Math.floor((visStart - bleed) / period),\n to: Math.floor((visEnd + bleed) / period),\n };\n}\n","import type { DrawCommand } from '@weasel-js/core';\nimport { subscribeToSources, type Dims, type RenderLayer } from '@weasel-js/core';\nimport type { View } from '@weasel-js/core';\nimport { tiledProject } from './tiledProject';\n\n/**\n * How far apart the copies sit, in world units. A bare number wraps the x\n * axis only — the side-scrolling backdrop this is usually reached for.\n * `{ x, y }` wraps both; naming one key wraps that axis alone.\n */\nexport type TilePeriod = number | { x?: number; y?: number };\n\n/** How far the content reaches outside its own cell, in world units. */\nexport type TileBleed = number | { x?: number; y?: number };\n\n/** Options for {@link createTiledLayer}. */\nexport interface CreateTiledLayerOpts<TData> {\n id: string;\n label: string;\n /** Layers redrawn once per visible copy. */\n source: RenderLayer<TData>[];\n /**\n * The lattice. A function is read per draw, so the period can follow the\n * camera or the canvas — \"one screen wide\" is a period, not a constant.\n */\n period: TilePeriod | ((view: View, dims: Dims) => TilePeriod);\n /**\n * How far the source's content reaches outside the cell `[0, period)`, in\n * world units. Content that overhangs by more than it declares pops in at\n * the edge of the view instead of scrolling in. Default 0.\n */\n bleed?: TileBleed;\n}\n\ninterface Axes { x: number; y: number }\n\n/** Copies one draw will make. Past this the view is zoomed so far out that a\n * cell covers a pixel or two, and each copy still costs a full source draw. */\nconst MAX_COPIES = 4096;\n\nfunction axes(v: TilePeriod | TileBleed | undefined, fallback: number): Axes {\n if (v === undefined) return { x: fallback, y: fallback };\n if (typeof v === 'number') return { x: v, y: fallback };\n return { x: v.x ?? fallback, y: v.y ?? fallback };\n}\n\n/** Column-major, the layout `Mat3` documents. */\nfunction translate(tx: number, ty: number): Float64Array {\n return new Float64Array([1, 0, 0, 0, 1, 0, tx, ty, 1]);\n}\n\n/**\n * Wrap source RenderLayers in a periodic lattice: the content is authored\n * once, in the cell `[0, period)`, and every copy of it the view can see is\n * drawn. Panning in either direction keeps producing copies, so the content\n * loops seamlessly with no state and no wrap-around bookkeeping.\n *\n * Each copy draws the source through a view shifted by `-k * period`, so a\n * source that culls against the view it is handed culls per copy and never\n * learns it is being tiled. The copy's commands are then translated by\n * `+k * period` in world space.\n *\n * The lattice is per layer, not per shape. Content on two different periods\n * is two tiled layers stacked.\n *\n * The plane is `space: 'world'`, which is what lets it be a `source` of\n * `createParallaxLayer`: the parallax plane hands it the derived inner view,\n * the lattice resolves against that, and the copies ride the plane.\n *\n * **Zoomed out until a draw would take more than a few thousand copies, the\n * lattice draws none.** A cell is then a pixel or two across.\n *\n * **A `space: 'screen'` source is drawn once, untiled** — it ignores the view\n * by definition, so every copy would land on top of the last. Same constraint\n * `createParallaxLayer` carries.\n */\nexport function createTiledLayer<TData>(\n opts: CreateTiledLayerOpts<TData>,\n): RenderLayer<TData> {\n const { id, label, source, period, bleed } = opts;\n return {\n id,\n label,\n space: 'world',\n subscribe: subscribeToSources(source),\n draw: (data, view, dims) => {\n const p = axes(typeof period === 'function' ? period(view, dims) : period, Infinity);\n const b = axes(bleed, 0);\n const spanX = dims.width / view.scale.x;\n const spanY = dims.height / view.scale.y;\n const kx = tiledProject(view.x, view.x + spanX, p.x, b.x);\n const ky = tiledProject(view.y, view.y + spanY, p.y, b.y);\n\n const out: DrawCommand[] = [];\n for (const layer of source) {\n if ((layer.space ?? 'world') === 'screen') out.push(...layer.draw(data, view, dims));\n }\n const tiles = source.filter((layer) => (layer.space ?? 'world') !== 'screen');\n // Written so a non-finite span, which a zero view scale gives, fails it too.\n if (!((kx.to - kx.from + 1) * (ky.to - ky.from + 1) <= MAX_COPIES)) return out;\n\n for (let i = kx.from; i <= kx.to; i++) {\n for (let j = ky.from; j <= ky.to; j++) {\n const tx = Number.isFinite(p.x) ? i * p.x : 0;\n const ty = Number.isFinite(p.y) ? j * p.y : 0;\n const shifted: View = { ...view, x: view.x - tx, y: view.y - ty };\n const children: DrawCommand[] = [];\n for (const layer of tiles) children.push(...layer.draw(data, shifted, dims));\n if (children.length === 0) continue;\n out.push(tx === 0 && ty === 0\n ? { kind: 'group', children }\n : { kind: 'group', transform: translate(tx, ty), children });\n }\n }\n return out;\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/createParallaxPlane.ts","../src/createParallaxLayer.ts","../src/tiledProject.ts","../src/createTiledLayer.ts"],"names":["subscribeToSources"],"mappings":"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","file":"index.js","sourcesContent":["import { createEmitter } from '@weasel-js/registry';\nimport type { RedrawSource } from '@weasel-js/core';\nimport type { ParallaxOpts } from '@weasel-js/core';\n\n/** Parallax opts that can change after a layer is built. `get` is read on\n * every draw, and a notification repaints whatever draws through it. */\nexport interface ParallaxSource extends RedrawSource {\n get(): ParallaxOpts;\n}\n\n/** A {@link ParallaxSource} you write to — what an animator's `onTick` drives\n * to tween a plane's `pan`, `zoom` or `anchor`. */\nexport interface ParallaxPlane extends ParallaxSource {\n /** Merge `patch` over the current opts and notify. */\n set(patch: Partial<ParallaxOpts>): void;\n}\n\n/** A {@link ParallaxPlane} starting at `initial`. Pass it as a parallax\n * layer's `parallax` and write to it to move the plane. */\nexport function createParallaxPlane(initial: ParallaxOpts): ParallaxPlane {\n let current: ParallaxOpts = { ...initial };\n const listeners = createEmitter();\n return {\n get: () => current,\n set(patch) {\n current = { ...current, ...patch };\n listeners.emit();\n },\n subscribe: listeners.subscribe,\n };\n}\n\n/** Whether `p` is a live source rather than a fixed set of opts. */\nexport function isParallaxSource(p: ParallaxOpts | ParallaxSource): p is ParallaxSource {\n return typeof (p as ParallaxSource).get === 'function';\n}\n","import { drawOneLayer, subscribeToSources, type RenderLayer } from '@weasel-js/core';\nimport type { View } from '@weasel-js/core';\nimport { deriveParallaxView, type ParallaxOpts } from '@weasel-js/core';\nimport { isParallaxSource, type ParallaxSource } from './createParallaxPlane';\n\n/** Options for `createParallaxLayer`. */\nexport interface CreateParallaxLayerOpts<TData> {\n id: string;\n label: string;\n /** Layers re-rendered through the derived inner view. */\n source: RenderLayer<TData>[];\n /** How the plane tracks the camera. Pass a `ParallaxSource` (such as\n * `createParallaxPlane`) to change it after the layer is built — an\n * animator tweening `pan` or `zoom` writes the plane, not the layer. */\n parallax: ParallaxOpts | ParallaxSource;\n /** Where the camera view comes from. Defaults to the view the Canvas hands\n * the layer — the `view` prop. A consumer running a 60 Hz camera through\n * refs pins that prop to identity, and would otherwise get identity back\n * for every `pan` value and a backdrop that never moves. */\n getOuterView?: () => View;\n}\n\n/**\n * Wrap source RenderLayers in a parallax plane. The plane's inner view is\n * derived from the camera view per {@link deriveParallaxView}; each source\n * layer is drawn through it by `drawOneLayer`, so a `space: 'world'` source\n * gets the inner view's transform and a `space: 'screen'` one does not. The\n * plane itself is emitted as `space: 'screen'` — its children already carry\n * whatever transform they need, and the outer Canvas must add none.\n *\n * What this draws is paint: pointer events target the camera's view. Content\n * that should be clickable belongs on a scene layer carrying `parallax`\n * instead, which `<SceneCanvas>` renders and picks through the same view.\n *\n * **Screen-space source layers don't compose meaningfully** — they ignore\n * the derived view by definition. Same constraint as `createViewportLayer`.\n */\nexport function createParallaxLayer<TData>(\n opts: CreateParallaxLayerOpts<TData>,\n): RenderLayer<TData> {\n const { id, label, source, parallax, getOuterView } = opts;\n const read = isParallaxSource(parallax) ? () => parallax.get() : () => parallax;\n const fromSources = subscribeToSources(source);\n return {\n id,\n label,\n space: 'screen',\n subscribe: isParallaxSource(parallax)\n ? (listener) => {\n const offSources = fromSources(listener);\n const offPlane = parallax.subscribe(listener);\n return () => { offSources(); offPlane(); };\n }\n : fromSources,\n draw: (data, outer, dims) => {\n const inner = deriveParallaxView(getOuterView?.() ?? outer, read());\n return source.flatMap((layer) => drawOneLayer(layer, data, inner, dims));\n },\n };\n}\n","/**\n * Which copies of a periodic lattice a visible range touches: the integer\n * copies `k` whose cell overlaps `[visStart, visEnd]`. Copy `k` draws the\n * authored content shifted by `k * period`.\n */\nexport interface TileRange {\n /** First copy index, inclusive. */\n from: number;\n /** Last copy index, inclusive. `to < from` means nothing is visible. */\n to: number;\n}\n\n/** The copies of a lattice with cell size `period` that `[visStart, visEnd]`\n * touches, widened by `bleed`. A non-positive or non-finite `period` answers\n * copy 0 alone. */\nexport function tiledProject(\n visStart: number,\n visEnd: number,\n period: number,\n bleed = 0,\n): TileRange {\n if (!Number.isFinite(period) || period <= 0) return { from: 0, to: 0 };\n return {\n from: Math.floor((visStart - bleed) / period),\n to: Math.floor((visEnd + bleed) / period),\n };\n}\n","import type { DrawCommand } from '@weasel-js/core';\nimport { subscribeToSources, type Dims, type RenderLayer } from '@weasel-js/core';\nimport type { View } from '@weasel-js/core';\nimport { tiledProject } from './tiledProject';\n\n/**\n * How far apart the copies sit, in world units. A bare number wraps the x\n * axis only — the side-scrolling backdrop this is usually reached for.\n * `{ x, y }` wraps both; naming one key wraps that axis alone.\n */\nexport type TilePeriod = number | { x?: number; y?: number };\n\n/** How far the content reaches outside its own cell, in world units. */\nexport type TileBleed = number | { x?: number; y?: number };\n\n/** Options for {@link createTiledLayer}. */\nexport interface CreateTiledLayerOpts<TData> {\n id: string;\n label: string;\n /** Layers redrawn once per visible copy. */\n source: RenderLayer<TData>[];\n /**\n * The lattice. A function is read per draw, so the period can follow the\n * camera or the canvas — \"one screen wide\" is a period, not a constant.\n */\n period: TilePeriod | ((view: View, dims: Dims) => TilePeriod);\n /**\n * How far the source's content reaches outside the cell `[0, period)`, in\n * world units. Content that overhangs by more than it declares pops in at\n * the edge of the view instead of scrolling in. Default 0.\n */\n bleed?: TileBleed;\n}\n\ninterface Axes { x: number; y: number }\n\n/** Copies one draw will make. Past this the view is zoomed so far out that a\n * cell covers a pixel or two, and each copy still costs a full source draw. */\nconst MAX_COPIES = 4096;\n\nfunction axes(v: TilePeriod | TileBleed | undefined, fallback: number): Axes {\n if (v === undefined) return { x: fallback, y: fallback };\n if (typeof v === 'number') return { x: v, y: fallback };\n return { x: v.x ?? fallback, y: v.y ?? fallback };\n}\n\n/** Column-major, the layout `GlMat3` documents. */\nfunction translate(tx: number, ty: number): Float64Array {\n return new Float64Array([1, 0, 0, 0, 1, 0, tx, ty, 1]);\n}\n\n/**\n * Wrap source RenderLayers in a periodic lattice: the content is authored\n * once, in the cell `[0, period)`, and every copy of it the view can see is\n * drawn. Panning in either direction keeps producing copies, so the content\n * loops seamlessly with no state and no wrap-around bookkeeping.\n *\n * Each copy draws the source through a view shifted by `-k * period`, so a\n * source that culls against the view it is handed culls per copy and never\n * learns it is being tiled. The copy's commands are then translated by\n * `+k * period` in world space.\n *\n * The lattice is per layer, not per shape. Content on two different periods\n * is two tiled layers stacked.\n *\n * The plane is `space: 'world'`, which is what lets it be a `source` of\n * `createParallaxLayer`: the parallax plane hands it the derived inner view,\n * the lattice resolves against that, and the copies ride the plane.\n *\n * **Zoomed out until a draw would take more than a few thousand copies, the\n * lattice draws none.** A cell is then a pixel or two across.\n *\n * **A `space: 'screen'` source is drawn once, untiled** — it ignores the view\n * by definition, so every copy would land on top of the last. Same constraint\n * `createParallaxLayer` carries.\n */\nexport function createTiledLayer<TData>(\n opts: CreateTiledLayerOpts<TData>,\n): RenderLayer<TData> {\n const { id, label, source, period, bleed } = opts;\n return {\n id,\n label,\n space: 'world',\n subscribe: subscribeToSources(source),\n draw: (data, view, dims) => {\n const p = axes(typeof period === 'function' ? period(view, dims) : period, Infinity);\n const b = axes(bleed, 0);\n const spanX = dims.width / view.scale.x;\n const spanY = dims.height / view.scale.y;\n const kx = tiledProject(view.x, view.x + spanX, p.x, b.x);\n const ky = tiledProject(view.y, view.y + spanY, p.y, b.y);\n\n const out: DrawCommand[] = [];\n for (const layer of source) {\n if ((layer.space ?? 'world') === 'screen') out.push(...layer.draw(data, view, dims));\n }\n const tiles = source.filter((layer) => (layer.space ?? 'world') !== 'screen');\n // Written so a non-finite span, which a zero view scale gives, fails it too.\n if (!((kx.to - kx.from + 1) * (ky.to - ky.from + 1) <= MAX_COPIES)) return out;\n\n for (let i = kx.from; i <= kx.to; i++) {\n for (let j = ky.from; j <= ky.to; j++) {\n const tx = Number.isFinite(p.x) ? i * p.x : 0;\n const ty = Number.isFinite(p.y) ? j * p.y : 0;\n const shifted: View = { ...view, x: view.x - tx, y: view.y - ty };\n const children: DrawCommand[] = [];\n for (const layer of tiles) children.push(...layer.draw(data, shifted, dims));\n if (children.length === 0) continue;\n out.push(tx === 0 && ty === 0\n ? { kind: 'group', children }\n : { kind: 'group', transform: translate(tx, ty), children });\n }\n }\n return out;\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weasel-js/parallax",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Parallax planes and tiled surfaces for weasel: layers that draw other layers through a derived view, and a live plane a camera drives. No React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,10 +37,10 @@
37
37
  "build": "tsup"
38
38
  },
39
39
  "dependencies": {
40
- "@weasel-js/registry": "1.10.0"
40
+ "@weasel-js/registry": "1.11.0"
41
41
  },
42
42
  "peerDependencies": {
43
- "@weasel-js/core": "1.10.0"
43
+ "@weasel-js/core": "1.11.0"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public",