@effect-motion/react 0.5.0 → 0.6.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 +1 -1
- package/dist/Player.js +83 -18
- package/package.json +62 -62
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Frames stream in as the scene runs rather than being rendered up front, so playb
|
|
|
9
9
|
`effect` and `effect-motion` are peer dependencies — install them alongside:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
|
|
12
|
+
bun add @effect-motion/react effect-motion effect
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Play a scene
|
package/dist/Player.js
CHANGED
|
@@ -20,18 +20,32 @@ class PlayerError extends Data.TaggedError("PlayerError") {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* The
|
|
24
|
-
*
|
|
23
|
+
* The viewport for a `width` × `height` frame shown in `canvas`: logical size
|
|
24
|
+
* is the frame's, pixel density follows how big the canvas is displayed.
|
|
25
25
|
*
|
|
26
26
|
* @remarks
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Scene coordinates, camera and HUD stay in frame units; only the pixel ratio
|
|
28
|
+
* changes, set so the backing store matches the displayed CSS size at device
|
|
29
|
+
* density — a 1920-wide scene shown 700 CSS px wide on a 2× display renders
|
|
30
|
+
* 1400 px wide, not 3840. Capped at the device ratio, so never more detail
|
|
31
|
+
* than the display can show. Before layout (width 0) it falls back to the
|
|
32
|
+
* device ratio.
|
|
30
33
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
34
|
+
* `pw`/`ph` are the backing-store size three will set (it floors), used to
|
|
35
|
+
* skip resizes that would not change a pixel.
|
|
33
36
|
*/
|
|
34
|
-
const
|
|
37
|
+
const viewportFor = (canvas, width, height) => {
|
|
38
|
+
const dpr = window.devicePixelRatio;
|
|
39
|
+
const displayed = canvas.clientWidth;
|
|
40
|
+
const ratio = displayed > 0 ? Math.min(dpr, (displayed * dpr) / width) : dpr;
|
|
41
|
+
return {
|
|
42
|
+
width,
|
|
43
|
+
height,
|
|
44
|
+
ratio,
|
|
45
|
+
pw: Math.floor(width * ratio),
|
|
46
|
+
ph: Math.floor(height * ratio),
|
|
47
|
+
};
|
|
48
|
+
};
|
|
35
49
|
const PlayerScene = Context.Service("PlayerScene");
|
|
36
50
|
/**
|
|
37
51
|
* The frame buffer: keeps the most recent `capacity` frames, keyed by
|
|
@@ -147,8 +161,9 @@ const useScene = (sceneProp, options) => {
|
|
|
147
161
|
width: 1,
|
|
148
162
|
height: 1,
|
|
149
163
|
}).pipe(Effect.mapError(PlayerError.of("Error acquiring the renderer")));
|
|
150
|
-
// viewport tracking: sized from frame metadata
|
|
151
|
-
|
|
164
|
+
// viewport tracking: sized from frame metadata + displayed size
|
|
165
|
+
// on first render, then on every resize / DPR change
|
|
166
|
+
let sized = { width: 0, height: 0, ratio: 0, pw: 0, ph: 0 };
|
|
152
167
|
// pipeline pre-warm happens once, on the first rendered frame,
|
|
153
168
|
// before playback reveals motion — no first-frame compile jank
|
|
154
169
|
let prewarmed = false;
|
|
@@ -203,7 +218,8 @@ const useScene = (sceneProp, options) => {
|
|
|
203
218
|
};
|
|
204
219
|
}).pipe(Effect.mapError(PlayerError.of("Error getting frame buffer")));
|
|
205
220
|
const renderSemaphore = yield* Semaphore.make(1);
|
|
206
|
-
|
|
221
|
+
// unlocked: callers hold renderSemaphore
|
|
222
|
+
const renderFrame = (frameIndex) => Effect.gen(function* () {
|
|
207
223
|
if (!canvasRef.current) {
|
|
208
224
|
return;
|
|
209
225
|
}
|
|
@@ -216,12 +232,13 @@ const useScene = (sceneProp, options) => {
|
|
|
216
232
|
// loadFrameBuffer), and the playhead must reflect what's shown
|
|
217
233
|
updateCurrentFrame(framebuffer.index);
|
|
218
234
|
const frame = framebuffer.frame;
|
|
219
|
-
const
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
235
|
+
const next = viewportFor(canvas, frame.width, frame.height);
|
|
236
|
+
if (next.width !== sized.width ||
|
|
237
|
+
next.height !== sized.height ||
|
|
238
|
+
next.pw !== sized.pw ||
|
|
239
|
+
next.ph !== sized.ph) {
|
|
240
|
+
sized = next;
|
|
241
|
+
FrameRenderer.setViewport(sink, next.width, next.height, next.ratio);
|
|
225
242
|
}
|
|
226
243
|
// font loaders resolve from this runtime's context (the
|
|
227
244
|
// renderLayers merge); missing loaders defect loudly
|
|
@@ -232,7 +249,30 @@ const useScene = (sceneProp, options) => {
|
|
|
232
249
|
yield* FrameRenderer.prewarm(sink);
|
|
233
250
|
}
|
|
234
251
|
yield* FrameRenderer.render(sink);
|
|
235
|
-
}).pipe(Effect.mapError(PlayerError.of("Error rendering frame")))
|
|
252
|
+
}).pipe(Effect.mapError(PlayerError.of("Error rendering frame")));
|
|
253
|
+
const render = (frameIndex) => renderSemaphore.withPermitsIfAvailable(1)(renderFrame(frameIndex));
|
|
254
|
+
// resize / DPR change: resizing clears the canvas, so re-render
|
|
255
|
+
// the shown frame (a paused player would otherwise stay blank or
|
|
256
|
+
// scaled). Waits for the permit rather than dropping, so the
|
|
257
|
+
// final size of a drag is never lost; at most one waits.
|
|
258
|
+
let redrawQueued = false;
|
|
259
|
+
const redraw = Effect.suspend(() => {
|
|
260
|
+
if (redrawQueued) {
|
|
261
|
+
return Effect.void;
|
|
262
|
+
}
|
|
263
|
+
redrawQueued = true;
|
|
264
|
+
return renderSemaphore.withPermits(1)(Effect.suspend(() => {
|
|
265
|
+
redrawQueued = false;
|
|
266
|
+
// unsized: the first render has not run yet and will size
|
|
267
|
+
if (sized.width === 0) {
|
|
268
|
+
return Effect.void;
|
|
269
|
+
}
|
|
270
|
+
const next = viewportFor(canvas, sized.width, sized.height);
|
|
271
|
+
return next.pw === sized.pw && next.ph === sized.ph
|
|
272
|
+
? Effect.void
|
|
273
|
+
: renderFrame(currentFrame);
|
|
274
|
+
}));
|
|
275
|
+
});
|
|
236
276
|
const play = Effect.suspend(() => {
|
|
237
277
|
if (isPlaying)
|
|
238
278
|
return Effect.void;
|
|
@@ -312,6 +352,7 @@ const useScene = (sceneProp, options) => {
|
|
|
312
352
|
return Context.make(PlayerScene, {
|
|
313
353
|
play,
|
|
314
354
|
pause,
|
|
355
|
+
redraw,
|
|
315
356
|
frameIndex: Effect.sync(() => currentFrame),
|
|
316
357
|
render,
|
|
317
358
|
load: (frameIndex) => loadFrameBuffer(frameIndex),
|
|
@@ -378,6 +419,30 @@ const useScene = (sceneProp, options) => {
|
|
|
378
419
|
play();
|
|
379
420
|
}
|
|
380
421
|
}, []);
|
|
422
|
+
const redraw = useEffectEvent(() => runReported(Effect.service(PlayerScene).pipe(Effect.flatMap((e) => e.redraw), Effect.scoped)));
|
|
423
|
+
// keep the backing store at displayed size × device pixel ratio: the
|
|
424
|
+
// observer covers container resize and fullscreen; the resolution query
|
|
425
|
+
// covers DPR changes that leave the CSS size alone (monitor move, zoom)
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
const canvas = canvasRef.current;
|
|
428
|
+
if (canvas === null) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const observer = new ResizeObserver(() => redraw());
|
|
432
|
+
observer.observe(canvas);
|
|
433
|
+
let media = null;
|
|
434
|
+
const onDprChange = () => {
|
|
435
|
+
media?.removeEventListener("change", onDprChange);
|
|
436
|
+
media = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
437
|
+
media.addEventListener("change", onDprChange);
|
|
438
|
+
redraw();
|
|
439
|
+
};
|
|
440
|
+
onDprChange();
|
|
441
|
+
return () => {
|
|
442
|
+
observer.disconnect();
|
|
443
|
+
media?.removeEventListener("change", onDprChange);
|
|
444
|
+
};
|
|
445
|
+
}, []);
|
|
381
446
|
// scene time in seconds of the current frame, and total when known
|
|
382
447
|
const currentTime = Time.frameToMillis(currentFrame, fps) / 1000;
|
|
383
448
|
const totalTime = totalFrames !== null
|
package/package.json
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
2
|
+
"name": "@effect-motion/react",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "React bindings for effect-motion: usePlayer hook and Player component",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/julia-script/effect-motion.git",
|
|
10
|
+
"directory": "packages/react"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/julia-script/effect-motion#readme",
|
|
13
|
+
"bugs": "https://github.com/julia-script/effect-motion/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"effect",
|
|
16
|
+
"motion",
|
|
17
|
+
"react",
|
|
18
|
+
"animation",
|
|
19
|
+
"motion-graphics"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./*": {
|
|
27
|
+
"types": "./dist/*.d.ts",
|
|
28
|
+
"default": "./dist/*.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
37
|
+
"test": "vitest run --passWithNoTests",
|
|
38
|
+
"check": "tsc --noEmit"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@effect-motion/renderer": "workspace:^",
|
|
45
|
+
"effect-motion": "workspace:^"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"effect": ">=4.0.0-rc.115",
|
|
49
|
+
"react": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@testing-library/react": "^16.3.0",
|
|
53
|
+
"@types/node": "^26.1.1",
|
|
54
|
+
"@types/react": "^19.2.0",
|
|
55
|
+
"@types/react-dom": "^19.2.0",
|
|
56
|
+
"effect": "4.0.0-rc.115",
|
|
57
|
+
"happy-dom": "^20.10.6",
|
|
58
|
+
"react": "^19.2.0",
|
|
59
|
+
"react-dom": "^19.2.0",
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
|
+
"vitest": "^4.1.10"
|
|
62
|
+
}
|
|
63
|
+
}
|