@solidrt/2d 0.0.55 → 0.0.57
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/AGENTS.md +417 -70
- package/README.md +3 -3
- package/examples/README.md +49 -7
- package/examples/anim.tsx +84 -0
- package/examples/camera-probe.tsx +202 -0
- package/examples/camera.tsx +196 -0
- package/examples/parity.tsx +15 -13
- package/examples/pick.tsx +89 -36
- package/examples/split-screen.tsx +111 -0
- package/examples/springs.tsx +6 -9
- package/examples/sprites.tsx +9 -12
- package/examples/tiles.tsx +42 -18
- package/examples/views.tsx +177 -0
- package/package.json +3 -3
- package/src/animation.ts +178 -0
- package/src/atlas.ts +35 -33
- package/src/camera.ts +111 -0
- package/src/camera2d.ts +713 -0
- package/src/components/auto-oversample.ts +27 -0
- package/src/components/camera2d.tsx +82 -0
- package/src/components/context.ts +48 -0
- package/src/components/group.tsx +68 -0
- package/src/components/index.ts +28 -0
- package/src/components/sprite-layer.tsx +312 -0
- package/src/components/sprite.tsx +72 -0
- package/src/components/tile-layer.tsx +160 -0
- package/src/components/view2d.tsx +151 -0
- package/src/dispatch.ts +231 -0
- package/src/frames.ts +63 -14
- package/src/index.ts +53 -12
- package/src/input.ts +50 -0
- package/src/layer.ts +716 -309
- package/src/oversample-math.ts +73 -0
- package/src/oversample.ts +39 -11
- package/src/records.ts +109 -109
- package/src/shaders.ts +93 -14
- package/src/tiles.ts +256 -48
- package/src/views.ts +294 -0
- package/src/components.tsx +0 -357
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/2d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.57",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"AGENTS.md"
|
|
16
16
|
],
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
19
|
-
"@solidrt/core": "0.0.
|
|
18
|
+
"@solidjs/signals": "2.0.0-rc.6",
|
|
19
|
+
"@solidrt/core": "0.0.57"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/src/animation.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Sprite frame animation: a clip (frames + fps) with a shared clock that
|
|
2
|
+
// steps every attached sprite's frame through setSprite. One timer per
|
|
3
|
+
// playing clip and one frame write per attached sprite per STEP (not per
|
|
4
|
+
// display frame), so an 8fps walk cycle generates 8 publishes a second and
|
|
5
|
+
// a paused clip costs nothing - the demand-gate story unchanged. The clip
|
|
6
|
+
// does not own its sprites (removeSprite prunes lazily on the next step),
|
|
7
|
+
// and a sprite belongs to at most one animation. Plain JS over setSprite,
|
|
8
|
+
// so it works on both layer kinds and composes with <Sprite> via ref (leave
|
|
9
|
+
// the frame prop off - the clip owns that field).
|
|
10
|
+
import { getOwner, onCleanup } from "@solidrt/core"
|
|
11
|
+
import type { Frame } from "./frames.ts"
|
|
12
|
+
import type { Sprite } from "./layer.ts"
|
|
13
|
+
import { setSprite } from "./layer.ts"
|
|
14
|
+
|
|
15
|
+
// Timer period as a fraction of the clip's frame duration: the step index
|
|
16
|
+
// is computed from the wall clock (drift-free), so the timer only decides
|
|
17
|
+
// how late after a frame boundary the step lands - half a frame bounds
|
|
18
|
+
// that lateness at half a frame, where a full-frame period could show a
|
|
19
|
+
// frame almost a whole frame late.
|
|
20
|
+
const TICK_RATIO = 0.5
|
|
21
|
+
|
|
22
|
+
export type AnimationOptions = {
|
|
23
|
+
/** Wrap around (default) or play once and hold the last frame. */
|
|
24
|
+
loop?: boolean
|
|
25
|
+
/** Skip the owner-scoped auto-dispose (the core resource contract). */
|
|
26
|
+
autoFree?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type SpriteAnimation = {
|
|
30
|
+
/** Current frame index into the clip. */
|
|
31
|
+
readonly frame: number
|
|
32
|
+
/** True while the clock advances: born playing, false after pause() and
|
|
33
|
+
* after a one-shot ends. */
|
|
34
|
+
readonly playing: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Attach a sprite: it shows the clip's current frame immediately and
|
|
37
|
+
* steps with the shared clock (every attached sprite shows the SAME
|
|
38
|
+
* index). A sprite belongs to at most one animation - attaching detaches
|
|
39
|
+
* it from its previous one.
|
|
40
|
+
*/
|
|
41
|
+
add(sprite: Sprite): void
|
|
42
|
+
/** Detach; the sprite keeps the frame it is showing. */
|
|
43
|
+
remove(sprite: Sprite): void
|
|
44
|
+
/** Resume a paused clip (phase kept), or replay a finished one-shot
|
|
45
|
+
* from frame 0. */
|
|
46
|
+
play(): void
|
|
47
|
+
/** Freeze the clock; attached sprites hold the current frame. */
|
|
48
|
+
pause(): void
|
|
49
|
+
/** One-shot clips (loop: false): the last frame finished its display
|
|
50
|
+
* time. Fires once per play-through, never on pause or dispose. */
|
|
51
|
+
onFinish?: () => void
|
|
52
|
+
/** Stop the clock and detach every sprite. Owner-scoped like a layer
|
|
53
|
+
* (opt out with autoFree: false). */
|
|
54
|
+
dispose(): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A sprite's current animation - add() moves the sprite here, so two clips
|
|
58
|
+
// never fight over one sprite's frame field.
|
|
59
|
+
let attachedTo = new WeakMap<Sprite, SpriteAnimation>()
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A frame animation clip: `frames` (usually a slice of a `grid()` result)
|
|
63
|
+
* played at `fps`. Sprites attach with `add`; the clip steps their `frame`
|
|
64
|
+
* on its own wall-clock timer, independent of display rate.
|
|
65
|
+
*/
|
|
66
|
+
export function createAnimation(frames: Frame[], fps: number, opts?: AnimationOptions): SpriteAnimation {
|
|
67
|
+
if (frames.length === 0) throw new Error("createAnimation: frames is empty")
|
|
68
|
+
if (!(Number.isFinite(fps) && fps > 0)) {
|
|
69
|
+
throw new Error(`createAnimation: fps must be positive, got ${fps}`)
|
|
70
|
+
}
|
|
71
|
+
let loop = opts?.loop !== false
|
|
72
|
+
let sprites = new Set<Sprite>()
|
|
73
|
+
let current = 0
|
|
74
|
+
let playing = true
|
|
75
|
+
let ended = false
|
|
76
|
+
let disposed = false
|
|
77
|
+
let timer: number | null = null
|
|
78
|
+
let elapsedBase = 0 // ms of clip time accumulated before the running stretch
|
|
79
|
+
let runStart = 0 // performance.now() when the clock last started
|
|
80
|
+
|
|
81
|
+
function startClock(): void {
|
|
82
|
+
if (timer !== null || !playing || sprites.size === 0) return
|
|
83
|
+
// A looping single-frame clip never changes; a one-shot still needs
|
|
84
|
+
// the clock once, to end.
|
|
85
|
+
if (loop && frames.length === 1) return
|
|
86
|
+
runStart = performance.now()
|
|
87
|
+
timer = setInterval(tick, (1000 / fps) * TICK_RATIO)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function stopClock(): void {
|
|
91
|
+
if (timer === null) return
|
|
92
|
+
elapsedBase += performance.now() - runStart
|
|
93
|
+
clearInterval(timer)
|
|
94
|
+
timer = null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeAll(): void {
|
|
98
|
+
let frame = frames[current]!
|
|
99
|
+
for (let sprite of sprites) {
|
|
100
|
+
if (sprite.layer === null) {
|
|
101
|
+
// Removed sprite: prune (deleting during for..of is safe on a Set).
|
|
102
|
+
sprites.delete(sprite)
|
|
103
|
+
attachedTo.delete(sprite)
|
|
104
|
+
} else setSprite(sprite, { frame })
|
|
105
|
+
}
|
|
106
|
+
if (sprites.size === 0) stopClock()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function setIndex(idx: number): void {
|
|
110
|
+
if (idx === current) return
|
|
111
|
+
current = idx
|
|
112
|
+
writeAll()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function tick(): void {
|
|
116
|
+
let raw = Math.floor(((elapsedBase + performance.now() - runStart) * fps) / 1000)
|
|
117
|
+
if (!loop && raw >= frames.length) {
|
|
118
|
+
// The last frame has shown for its full duration: hold it and stop.
|
|
119
|
+
setIndex(frames.length - 1)
|
|
120
|
+
stopClock()
|
|
121
|
+
playing = false
|
|
122
|
+
ended = true
|
|
123
|
+
handle.onFinish?.()
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
setIndex(loop ? raw % frames.length : Math.min(raw, frames.length - 1))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let handle: SpriteAnimation = {
|
|
130
|
+
get frame() {
|
|
131
|
+
return current
|
|
132
|
+
},
|
|
133
|
+
get playing() {
|
|
134
|
+
return playing
|
|
135
|
+
},
|
|
136
|
+
onFinish: undefined,
|
|
137
|
+
add(sprite) {
|
|
138
|
+
if (disposed) throw new Error("createAnimation: add on a disposed animation")
|
|
139
|
+
attachedTo.get(sprite)?.remove(sprite)
|
|
140
|
+
attachedTo.set(sprite, handle)
|
|
141
|
+
sprites.add(sprite)
|
|
142
|
+
setSprite(sprite, { frame: frames[current]! })
|
|
143
|
+
startClock()
|
|
144
|
+
},
|
|
145
|
+
remove(sprite) {
|
|
146
|
+
if (!sprites.delete(sprite)) return
|
|
147
|
+
attachedTo.delete(sprite)
|
|
148
|
+
if (sprites.size === 0) stopClock()
|
|
149
|
+
},
|
|
150
|
+
play() {
|
|
151
|
+
if (disposed || playing) return
|
|
152
|
+
if (ended) {
|
|
153
|
+
ended = false
|
|
154
|
+
elapsedBase = 0
|
|
155
|
+
current = 0
|
|
156
|
+
writeAll()
|
|
157
|
+
}
|
|
158
|
+
playing = true
|
|
159
|
+
startClock()
|
|
160
|
+
},
|
|
161
|
+
pause() {
|
|
162
|
+
if (!playing) return
|
|
163
|
+
stopClock()
|
|
164
|
+
playing = false
|
|
165
|
+
},
|
|
166
|
+
dispose() {
|
|
167
|
+
if (disposed) return
|
|
168
|
+
stopClock()
|
|
169
|
+
playing = false
|
|
170
|
+
disposed = true
|
|
171
|
+
for (let sprite of sprites) attachedTo.delete(sprite)
|
|
172
|
+
sprites.clear()
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (opts?.autoFree !== false && getOwner()) onCleanup(() => handle.dispose())
|
|
177
|
+
return handle
|
|
178
|
+
}
|
package/src/atlas.ts
CHANGED
|
@@ -1,52 +1,54 @@
|
|
|
1
|
-
// Atlas creation:
|
|
2
|
-
// Thin by design - the frame math lives in frames.ts
|
|
3
|
-
// the texture is an ordinary core texture. createImage
|
|
4
|
-
// never forwards sampler options, and pixel-art atlases
|
|
5
|
-
// `filter: "nearest"` (render small, display big with hard pixels).
|
|
6
|
-
import { createTexture
|
|
7
|
-
import type {
|
|
8
|
-
import type {
|
|
9
|
-
import { grid } from "./frames.ts"
|
|
10
|
-
import type { GridOptions } from "./frames.ts"
|
|
1
|
+
// Atlas creation: decoded pixels to a GPU texture plus the pixel size the
|
|
2
|
+
// slicers measure in. Thin by design - the frame math lives in frames.ts
|
|
3
|
+
// (pure, checkable) and the texture is an ordinary core texture. createImage
|
|
4
|
+
// is NOT used here: it never forwards sampler options, and pixel-art atlases
|
|
5
|
+
// want `filter: "nearest"` (render small, display big with hard pixels).
|
|
6
|
+
import { createTexture } from "@solidrt/core"
|
|
7
|
+
import type { DecodedImage } from "@solidrt/core"
|
|
8
|
+
import type { SamplerOptions, TextureId } from "@solidrt/core/gpu"
|
|
11
9
|
|
|
10
|
+
/**
|
|
11
|
+
* A texture with its pixel size: what `grid` and `namedFrames` slice, and
|
|
12
|
+
* whose `texture` a layer samples. A plain record, so a texture that did not
|
|
13
|
+
* come through `createAtlas` - a render target, a camera frame - is an atlas
|
|
14
|
+
* as a literal: `{ texture, width, height }`.
|
|
15
|
+
*/
|
|
12
16
|
export type Atlas = {
|
|
13
17
|
/** The atlas texture, sampled by every sprite in layers created over it. */
|
|
14
18
|
texture: TextureId
|
|
19
|
+
/** Pixel size, the space the slicers measure rects and gaps in. */
|
|
15
20
|
width: number
|
|
16
21
|
height: number
|
|
17
|
-
/** Frames in cell order when created via the grid option, else empty. */
|
|
18
|
-
frames: Frame[]
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/** Slice a uniform sheet at creation: cols x rows of equal cells. */
|
|
29
|
-
grid?: { cols: number; rows: number } & Omit<GridOptions, "width" | "height">
|
|
24
|
+
/**
|
|
25
|
+
* Sampling is core's `SamplerOptions`, fixed at creation like every core
|
|
26
|
+
* texture: `filter` ("nearest" is the pixel-art path with hard pixels at any
|
|
27
|
+
* scale, "linear" the default photographic one), `wrap`, `mipmap` (a chain
|
|
28
|
+
* for atlases drawn far below their texel size) and `anisotropy`.
|
|
29
|
+
*/
|
|
30
|
+
export type AtlasOptions = SamplerOptions & {
|
|
30
31
|
label?: string
|
|
31
32
|
/** Skip the owner-scoped auto-free (the core createTexture contract). */
|
|
32
33
|
autoFree?: boolean
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Upload decoded pixels as an atlas texture. `image` is a core
|
|
38
|
+
* `DecodedImage` (`{ data, width, height }`, premultiplied RGBA8): an
|
|
39
|
+
* encoded sheet goes through `decodeImage` first -
|
|
40
|
+
* `createAtlas(decodeImage(sheet), { filter: "nearest" })`, with `sheet`
|
|
41
|
+
* from `import sheet from "./sheet.png" with { type: "binary" }` or
|
|
42
|
+
* `await file("assets/sheet.png").bytes()` - and pixels built in code pass
|
|
43
|
+
* as they are. Freed with the owning reactive scope like any core texture
|
|
44
|
+
* (opt out with `{ autoFree: false }`).
|
|
40
45
|
*/
|
|
41
|
-
export function createAtlas(
|
|
42
|
-
let
|
|
43
|
-
let texture = createTexture(
|
|
46
|
+
export function createAtlas(image: DecodedImage, opts?: AtlasOptions): Atlas {
|
|
47
|
+
let { data, width, height } = image
|
|
48
|
+
let texture = createTexture(data, width, height, {
|
|
49
|
+
...opts,
|
|
44
50
|
filter: opts?.filter ?? "linear",
|
|
45
51
|
label: opts?.label ?? "atlas",
|
|
46
|
-
autoFree: opts?.autoFree,
|
|
47
52
|
})
|
|
48
|
-
|
|
49
|
-
? grid(opts.grid.cols, opts.grid.rows, { ...opts.grid, width: decoded.width, height: decoded.height })
|
|
50
|
-
: []
|
|
51
|
-
return { texture, width: decoded.width, height: decoded.height, frames }
|
|
53
|
+
return { texture, width, height }
|
|
52
54
|
}
|
package/src/camera.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The 2d camera vocabulary, pure: the one camera type both layers share
|
|
2
|
+
// and its world <-> screen mapping as plain functions - no GPU or GUI
|
|
3
|
+
// imports, so the checks rig (checks/camera-check.ts) exercises this
|
|
4
|
+
// module headless on the flux binary. Three consumers implement or undo
|
|
5
|
+
// exactly this mapping and must agree with it: the vertex stages
|
|
6
|
+
// (shaders.ts, uCamera + uCameraRot), the tile layer's composite
|
|
7
|
+
// transform (the <view> props in components/tile-layer.tsx) and the pointer inverse
|
|
8
|
+
// (spriteDispatch in layer.ts, which calls unprojectCamera directly). The
|
|
9
|
+
// check keeps the spellings from drifting.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A camera over a 2d world - the sprite layer's `setCamera`/`camera` prop
|
|
13
|
+
* and the tile layer's `TileCamera` alike. The world point (`x`, `y`) is
|
|
14
|
+
* shown at the viewport point (`pivotX`, `pivotY`), the world scaled by
|
|
15
|
+
* `zoom` and rotated by `rotation` ABOUT that pivot. Rotation turns the
|
|
16
|
+
* world clockwise (y-down) about the pivot; to render a heading `h`
|
|
17
|
+
* upward use `rotation = -h - Math.PI / 2`. The pivot defaults to (0, 0),
|
|
18
|
+
* which makes `{ x, y, zoom }` mean "world x/y at the viewport top-left" -
|
|
19
|
+
* the plain scrolling camera.
|
|
20
|
+
*
|
|
21
|
+
* As a `setCamera` update, absent keys keep their values (the params
|
|
22
|
+
* rule); as an argument to projectCamera/unprojectCamera, absent keys
|
|
23
|
+
* read as the defaults (x/y/rotation 0, zoom 1, pivot (0, 0)).
|
|
24
|
+
*/
|
|
25
|
+
export type CameraUpdate = {
|
|
26
|
+
/** World pixel shown at the pivot (the viewport top-left by default). */
|
|
27
|
+
x?: number
|
|
28
|
+
y?: number
|
|
29
|
+
/** World-to-screen scale; 1 is pixel-for-pixel. */
|
|
30
|
+
zoom?: number
|
|
31
|
+
/** Rotation, radians, clockwise (y-down), about the pivot. */
|
|
32
|
+
rotation?: number
|
|
33
|
+
/** Viewport point the camera's world point pins to; default (0, 0). */
|
|
34
|
+
pivotX?: number
|
|
35
|
+
pivotY?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A camera snapshot (a layer's `camera()`): CameraUpdate's fields, all
|
|
39
|
+
* present - the argument for projectCamera/unprojectCamera. */
|
|
40
|
+
export type CameraState = Required<CameraUpdate>
|
|
41
|
+
|
|
42
|
+
const CAMERA_FIELDS = ["x", "y", "zoom", "rotation", "pivotX", "pivotY"] as const
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate a setCamera update (throws - the dev validation policy): every
|
|
46
|
+
* present field must be finite, zoom additionally positive. A NaN rotation
|
|
47
|
+
* or pivot would otherwise poison the uniforms and blank the layer with no
|
|
48
|
+
* error. Internal - both layer kinds call it at the top of setCamera.
|
|
49
|
+
*/
|
|
50
|
+
export function checkCamera(update: CameraUpdate): void {
|
|
51
|
+
for (let key of CAMERA_FIELDS) {
|
|
52
|
+
let value = update[key]
|
|
53
|
+
if (value !== undefined && !Number.isFinite(value)) {
|
|
54
|
+
throw new Error(`setCamera: ${key} must be a finite number, got ${value}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (update.zoom !== undefined && update.zoom <= 0) {
|
|
58
|
+
throw new Error(`setCamera: zoom must be positive, got ${update.zoom}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* World -> screen under a camera:
|
|
64
|
+
* `pivot + R(rotation) * zoom * (world - cam)`. Screen means the layer's
|
|
65
|
+
* viewport pixels (the sprite layer's output, the tile world's container).
|
|
66
|
+
*/
|
|
67
|
+
export function projectCamera(camera: CameraUpdate, worldX: number, worldY: number): [number, number] {
|
|
68
|
+
let zoom = camera.zoom ?? 1
|
|
69
|
+
let rotation = camera.rotation ?? 0
|
|
70
|
+
let c = Math.cos(rotation)
|
|
71
|
+
let s = Math.sin(rotation)
|
|
72
|
+
let dx = (worldX - (camera.x ?? 0)) * zoom
|
|
73
|
+
let dy = (worldY - (camera.y ?? 0)) * zoom
|
|
74
|
+
return [(camera.pivotX ?? 0) + dx * c - dy * s, (camera.pivotY ?? 0) + dx * s + dy * c]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Screen -> world: the exact inverse of projectCamera. */
|
|
78
|
+
export function unprojectCamera(camera: CameraUpdate, screenX: number, screenY: number): [number, number] {
|
|
79
|
+
let zoom = camera.zoom ?? 1
|
|
80
|
+
let rotation = camera.rotation ?? 0
|
|
81
|
+
let c = Math.cos(rotation)
|
|
82
|
+
let s = Math.sin(rotation)
|
|
83
|
+
let dx = screenX - (camera.pivotX ?? 0)
|
|
84
|
+
let dy = screenY - (camera.pivotY ?? 0)
|
|
85
|
+
return [(camera.x ?? 0) + (dx * c + dy * s) / zoom, (camera.y ?? 0) + (dy * c - dx * s) / zoom]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A camera at the defaults: what a layer or view starts with. */
|
|
89
|
+
export function defaultCamera(): CameraState {
|
|
90
|
+
return { x: 0, y: 0, zoom: 1, rotation: 0, pivotX: 0, pivotY: 0 }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Apply a setCamera update in place: absent keys keep their values (the
|
|
94
|
+
* params rule). Validate with checkCamera first. */
|
|
95
|
+
export function applyCamera(camera: CameraState, update: CameraUpdate): void {
|
|
96
|
+
if (update.x !== undefined) camera.x = update.x
|
|
97
|
+
if (update.y !== undefined) camera.y = update.y
|
|
98
|
+
if (update.zoom !== undefined) camera.zoom = update.zoom
|
|
99
|
+
if (update.rotation !== undefined) camera.rotation = update.rotation
|
|
100
|
+
if (update.pivotX !== undefined) camera.pivotX = update.pivotX
|
|
101
|
+
if (update.pivotY !== undefined) camera.pivotY = update.pivotY
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The shared params the vertex stages (shaders.ts) read for a camera:
|
|
105
|
+
* uCamera [x, y, zoom, zoom] and uCameraRot [cos, sin, pivotX, pivotY]. */
|
|
106
|
+
export function cameraParams(camera: CameraState): { uCamera: [number, number, number, number]; uCameraRot: [number, number, number, number] } {
|
|
107
|
+
return {
|
|
108
|
+
uCamera: [camera.x, camera.y, camera.zoom, camera.zoom],
|
|
109
|
+
uCameraRot: [Math.cos(camera.rotation), Math.sin(camera.rotation), camera.pivotX, camera.pivotY],
|
|
110
|
+
}
|
|
111
|
+
}
|