@weasel-js/parallax 1.9.4 → 1.10.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 +4 -5
- package/dist/index.d.ts +4 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -20,8 +20,7 @@ package holds the layer wrapper (`createParallaxLayer`), the live plane
|
|
|
20
20
|
## `deriveParallaxView(outer, opts)`
|
|
21
21
|
|
|
22
22
|
Pure. Takes the camera `View` and per-plane factors, returns the plane's inner
|
|
23
|
-
`View`.
|
|
24
|
-
you need "a view that tracks the camera at a fraction."
|
|
23
|
+
`View`.
|
|
25
24
|
|
|
26
25
|
| Option | Meaning |
|
|
27
26
|
| --- | --- |
|
|
@@ -30,10 +29,10 @@ you need "a view that tracks the camera at a fraction."
|
|
|
30
29
|
| `anchor` | The camera position at which every plane lines up. Defaults to the origin. |
|
|
31
30
|
|
|
32
31
|
Both `pan` and `zoom` take a scalar or `{x, y}`, so you can parallax one axis
|
|
33
|
-
only
|
|
32
|
+
only.
|
|
34
33
|
|
|
35
|
-
**Identity holds:** `pan: 1, zoom: 1` returns a view equal to `outer`.
|
|
36
|
-
|
|
34
|
+
**Identity holds:** `pan: 1, zoom: 1` returns a view equal to `outer`. Preserve
|
|
35
|
+
that if you touch the math: a plane at defaults must be
|
|
37
36
|
pixel-identical to no parallax at all.
|
|
38
37
|
|
|
39
38
|
## `planeMap(outer, opts)`
|
package/dist/index.d.ts
CHANGED
|
@@ -49,18 +49,9 @@ interface CreateParallaxLayerOpts<TData> {
|
|
|
49
49
|
declare function createParallaxLayer<TData>(opts: CreateParallaxLayerOpts<TData>): RenderLayer<TData>;
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Which copies of a periodic lattice a visible range touches
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* lattice repeats it at every multiple of `period`. This answers the only
|
|
56
|
-
* question drawing it needs: the integer copies `k` whose cell overlaps
|
|
57
|
-
* `[visStart, visEnd]`. Copy `k` draws the authored content shifted by
|
|
58
|
-
* `k * period`.
|
|
59
|
-
*
|
|
60
|
-
* `bleed` is how far the content reaches outside its own cell, in world
|
|
61
|
-
* units — the printing term, and the same idea. Content that overhangs by
|
|
62
|
-
* more than it declares pops in at the edge of the view instead of scrolling
|
|
63
|
-
* in.
|
|
52
|
+
* Which copies of a periodic lattice a visible range touches: the integer
|
|
53
|
+
* copies `k` whose cell overlaps `[visStart, visEnd]`. Copy `k` draws the
|
|
54
|
+
* authored content shifted by `k * period`.
|
|
64
55
|
*/
|
|
65
56
|
interface TileRange {
|
|
66
57
|
/** First copy index, inclusive. */
|
|
@@ -77,9 +68,6 @@ declare function tiledProject(visStart: number, visEnd: number, period: number,
|
|
|
77
68
|
* How far apart the copies sit, in world units. A bare number wraps the x
|
|
78
69
|
* axis only — the side-scrolling backdrop this is usually reached for.
|
|
79
70
|
* `{ x, y }` wraps both; naming one key wraps that axis alone.
|
|
80
|
-
*
|
|
81
|
-
* The period names the axes, so there is no separate `axis` option: two ways
|
|
82
|
-
* to say which axes wrap is one way for them to disagree.
|
|
83
71
|
*/
|
|
84
72
|
type TilePeriod = number | {
|
|
85
73
|
x?: number;
|
|
@@ -120,9 +108,7 @@ interface CreateTiledLayerOpts<TData> {
|
|
|
120
108
|
* `+k * period` in world space.
|
|
121
109
|
*
|
|
122
110
|
* The lattice is per layer, not per shape. Content on two different periods
|
|
123
|
-
* is two tiled layers stacked
|
|
124
|
-
* — a per-shape period would move the lattice into the source and make the
|
|
125
|
-
* wrapper unable to cull.
|
|
111
|
+
* is two tiled layers stacked.
|
|
126
112
|
*
|
|
127
113
|
* The plane is `space: 'world'`, which is what lets it be a `source` of
|
|
128
114
|
* `createParallaxLayer`: the parallax plane hands it the derived inner view,
|
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.\n *\n * A tiled layer's content is authored once, in the cell `[0, period)`, and the\n * lattice repeats it at every multiple of `period`. This answers the only\n * question drawing it needs: the integer copies `k` whose cell overlaps\n * `[visStart, visEnd]`. Copy `k` draws the authored content shifted by\n * `k * period`.\n *\n * `bleed` is how far the content reaches outside its own cell, in world\n * units — the printing term, and the same idea. Content that overhangs by\n * more than it declares pops in at the edge of the view instead of scrolling\n * in.\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 *\n * The period names the axes, so there is no separate `axis` option: two ways\n * to say which axes wrap is one way for them to disagree.\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, which keeps each source ignorant of the other\n * — a per-shape period would move the lattice into the source and make the\n * wrapper unable to cull.\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 `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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/parallax",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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.
|
|
40
|
+
"@weasel-js/registry": "1.10.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@weasel-js/core": "1.
|
|
43
|
+
"@weasel-js/core": "1.10.0"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public",
|