@effect-motion/three 0.5.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.
@@ -0,0 +1,16 @@
1
+ import { Data } from "effect";
2
+ /**
3
+ * A three.js operation failed.
4
+ *
5
+ * @remarks
6
+ * Every fallible call in this package reports through this one error type,
7
+ * so a caller catches by tag rather than guarding each three API
8
+ * separately. `operation` names which call failed — `"WebGPURenderer.init"`,
9
+ * `"readRenderTargetPixelsAsync"` — and `cause` carries whatever three
10
+ * threw or rejected with.
11
+ *
12
+ * Typical causes are environmental rather than logical: no WebGPU adapter,
13
+ * a lost device, a shader that failed to compile.
14
+ */
15
+ export class ThreeException extends Data.TaggedError("ThreeException") {
16
+ }
package/dist/Tsl.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * TSL — three's shading language, for building node materials and shader
3
+ * graphs.
4
+ *
5
+ * @remarks
6
+ * Re-exported so downstream code has a single import root for the subset
7
+ * this project uses, rather than reaching into `three/tsl` from a dozen
8
+ * places.
9
+ *
10
+ * These are pure description: building a node graph runs no GPU work and
11
+ * cannot fail, so nothing here is an Effect. Effect enters when a graph is
12
+ * eventually rendered.
13
+ *
14
+ * Note that node types are deliberately not re-exported — three's published
15
+ * node types expand into unions large enough to stall a type check, so
16
+ * consumers declare the minimal shape they use. See `PostProcessing`.
17
+ */
18
+ export { attribute, float, fwidth, interleavedGradientNoise, mix, perspectiveDepthToViewZ, positionGeometry, screenCoordinate, screenUV, smoothstep, texture, uniform, vec2, vec3, vec4, } from "three/tsl";
package/dist/Tsl.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * TSL — three's shading language, for building node materials and shader
3
+ * graphs.
4
+ *
5
+ * @remarks
6
+ * Re-exported so downstream code has a single import root for the subset
7
+ * this project uses, rather than reaching into `three/tsl` from a dozen
8
+ * places.
9
+ *
10
+ * These are pure description: building a node graph runs no GPU work and
11
+ * cannot fail, so nothing here is an Effect. Effect enters when a graph is
12
+ * eventually rendered.
13
+ *
14
+ * Note that node types are deliberately not re-exported — three's published
15
+ * node types expand into unions large enough to stall a type check, so
16
+ * consumers declare the minimal shape they use. See `PostProcessing`.
17
+ */
18
+ export { attribute, float, fwidth, interleavedGradientNoise, mix, perspectiveDepthToViewZ, positionGeometry, screenCoordinate, screenUV, smoothstep, texture, uniform, vec2, vec3, vec4, } from "three/tsl";
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `@effect-motion/three` — an Effect wrapper over three.js.
3
+ *
4
+ * @remarks
5
+ * Bindings only. This package knows nothing about effect-motion — no
6
+ * frames, no entities, no projection — and exists to give three.js the two
7
+ * things Effect code needs from it: resources that clean themselves up, and
8
+ * failures in a typed channel.
9
+ *
10
+ * The organizing rule is **Effect at the seams, raw three in between**:
11
+ *
12
+ * - **Construction and teardown are Effects.** {@link Renderer.make},
13
+ * {@link Scene.make}, and {@link RenderTarget.make} are scoped, so a GPU
14
+ * device, a scene's children, and a render target's memory are released
15
+ * when the scope closes rather than by hand.
16
+ * - **Anything that can fail or is async is an Effect** — rendering,
17
+ * readback, shader compilation — typed as {@link ThreeException}.
18
+ * - **Infallible mutation stays synchronous and chains.** Adding objects,
19
+ * setting a background, resizing: these cannot fail, so wrapping them in
20
+ * Effects would buy ceremony and nothing else. They return their handle
21
+ * and compose with `.pipe`.
22
+ *
23
+ * Handles are branded wrappers around the three object, which stays
24
+ * reachable — `ThreeRaw` re-exports three itself for the leaf value types
25
+ * (geometries, materials, `Vector3`) and the per-frame mutation in a hot
26
+ * path. That escape hatch is deliberate: reaching past a wrapper should be
27
+ * a visible, greppable import rather than the path of least resistance.
28
+ *
29
+ * For headless rendering on a real GPU, import
30
+ * `@effect-motion/three/node` — it lives behind its own subpath so
31
+ * Node-only code never reaches a browser bundle.
32
+ *
33
+ * @example
34
+ * Scoped construction, sync chaining, Effects only where GPU work happens.
35
+ * ```typescript
36
+ * import { Renderer, RenderTarget, Scene } from "@effect-motion/three";
37
+ * import { Mesh, PerspectiveCamera } from "three/webgpu";
38
+ * import { Effect } from "effect";
39
+ *
40
+ * const program = Effect.gen(function* () {
41
+ * const scene = yield* Scene.make();
42
+ * const renderer = yield* Renderer.make({ width: 640, height: 360 });
43
+ * const target = yield* RenderTarget.make(640, 360);
44
+ *
45
+ * scene.pipe(Scene.add([new Mesh()]), Scene.setBackground(null));
46
+ *
47
+ * const camera = new PerspectiveCamera(50, 16 / 9, 1, 1000);
48
+ * Renderer.setRenderTarget(renderer, target);
49
+ * yield* Renderer.render(renderer, scene, camera);
50
+ * return yield* Renderer.readRenderTarget(renderer, target, 640, 360);
51
+ * }).pipe(Effect.scoped);
52
+ * ```
53
+ *
54
+ * @packageDocumentation
55
+ */
56
+ export * as ThreeRaw from "three/webgpu";
57
+ export * as Interop from "./Interop.js";
58
+ export * as Line2 from "./Line2.js";
59
+ export * as Object3D from "./Object3D.js";
60
+ export * as PostProcessing from "./PostProcessing.js";
61
+ export * as Renderer from "./Renderer.js";
62
+ export * as RenderTarget from "./RenderTarget.js";
63
+ export * as Scene from "./Scene.js";
64
+ export { ThreeException } from "./ThreeException.js";
65
+ export * as Tsl from "./Tsl.js";
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `@effect-motion/three` — an Effect wrapper over three.js.
3
+ *
4
+ * @remarks
5
+ * Bindings only. This package knows nothing about effect-motion — no
6
+ * frames, no entities, no projection — and exists to give three.js the two
7
+ * things Effect code needs from it: resources that clean themselves up, and
8
+ * failures in a typed channel.
9
+ *
10
+ * The organizing rule is **Effect at the seams, raw three in between**:
11
+ *
12
+ * - **Construction and teardown are Effects.** {@link Renderer.make},
13
+ * {@link Scene.make}, and {@link RenderTarget.make} are scoped, so a GPU
14
+ * device, a scene's children, and a render target's memory are released
15
+ * when the scope closes rather than by hand.
16
+ * - **Anything that can fail or is async is an Effect** — rendering,
17
+ * readback, shader compilation — typed as {@link ThreeException}.
18
+ * - **Infallible mutation stays synchronous and chains.** Adding objects,
19
+ * setting a background, resizing: these cannot fail, so wrapping them in
20
+ * Effects would buy ceremony and nothing else. They return their handle
21
+ * and compose with `.pipe`.
22
+ *
23
+ * Handles are branded wrappers around the three object, which stays
24
+ * reachable — `ThreeRaw` re-exports three itself for the leaf value types
25
+ * (geometries, materials, `Vector3`) and the per-frame mutation in a hot
26
+ * path. That escape hatch is deliberate: reaching past a wrapper should be
27
+ * a visible, greppable import rather than the path of least resistance.
28
+ *
29
+ * For headless rendering on a real GPU, import
30
+ * `@effect-motion/three/node` — it lives behind its own subpath so
31
+ * Node-only code never reaches a browser bundle.
32
+ *
33
+ * @example
34
+ * Scoped construction, sync chaining, Effects only where GPU work happens.
35
+ * ```typescript
36
+ * import { Renderer, RenderTarget, Scene } from "@effect-motion/three";
37
+ * import { Mesh, PerspectiveCamera } from "three/webgpu";
38
+ * import { Effect } from "effect";
39
+ *
40
+ * const program = Effect.gen(function* () {
41
+ * const scene = yield* Scene.make();
42
+ * const renderer = yield* Renderer.make({ width: 640, height: 360 });
43
+ * const target = yield* RenderTarget.make(640, 360);
44
+ *
45
+ * scene.pipe(Scene.add([new Mesh()]), Scene.setBackground(null));
46
+ *
47
+ * const camera = new PerspectiveCamera(50, 16 / 9, 1, 1000);
48
+ * Renderer.setRenderTarget(renderer, target);
49
+ * yield* Renderer.render(renderer, scene, camera);
50
+ * return yield* Renderer.readRenderTarget(renderer, target, 640, 360);
51
+ * }).pipe(Effect.scoped);
52
+ * ```
53
+ *
54
+ * @packageDocumentation
55
+ */
56
+ // Browser-safe, bindings-only surface over three.js: branded handles with
57
+ // Effect at the seams (lifecycle, async boundaries, failures), sync
58
+ // chaining for infallible mutation. Knows nothing about effect-motion —
59
+ // no frames, entities, or projection. The Node entry (Dawn device +
60
+ // environment shims) lives at "@effect-motion/three/node" so node-only
61
+ // code never reaches a browser bundle.
62
+ //
63
+ // `ThreeRaw` is the deliberate escape hatch, not the front door: reaching
64
+ // past a wrapper (three's leaf value types, the per-frame object mutation
65
+ // in the renderer's hot path) is a visible, greppable import rather than
66
+ // the path of least resistance. New code should prefer the actors below.
67
+ export * as ThreeRaw from "three/webgpu";
68
+ export * as Interop from "./Interop.js";
69
+ export * as Line2 from "./Line2.js";
70
+ export * as Object3D from "./Object3D.js";
71
+ export * as PostProcessing from "./PostProcessing.js";
72
+ export * as Renderer from "./Renderer.js";
73
+ export * as RenderTarget from "./RenderTarget.js";
74
+ export * as Scene from "./Scene.js";
75
+ export { ThreeException } from "./ThreeException.js";
76
+ export * as Tsl from "./Tsl.js";
package/dist/node.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { Effect } from "effect";
2
+ import type { ThreeException } from "./ThreeException.js";
3
+ /**
4
+ * Acquire a GPU device from Dawn.
5
+ *
6
+ * @remarks
7
+ * Pass the result to `Renderer.make` rather than letting three request its
8
+ * own. three asks for a "compatibility" feature level, which Chrome quietly
9
+ * upgrades but Dawn honors literally — costing MSAA and other core features,
10
+ * so headless output would be visibly worse than the browser's for the same
11
+ * scene. This requests a core device with every feature the adapter offers.
12
+ *
13
+ * Fails with a `ThreeException` when no adapter is available.
14
+ */
15
+ export declare const makeDevice: () => Effect.Effect<GPUDevice, ThreeException>;
16
+ /**
17
+ * A canvas-shaped stand-in for headless rendering.
18
+ *
19
+ * @remarks
20
+ * three's renderer requires a canvas and a context even when nothing is
21
+ * displayed. These satisfy that without a DOM. The renderer must draw only
22
+ * into render targets: asking this context for the default framebuffer
23
+ * throws, deliberately, rather than silently rendering nowhere.
24
+ *
25
+ * @param width - Buffer width in device pixels.
26
+ * @param height - Buffer height in device pixels.
27
+ */
28
+ export declare const stubCanvas: (width: number, height: number) => {
29
+ canvas: {
30
+ width: number;
31
+ height: number;
32
+ style: {};
33
+ addEventListener(): void;
34
+ removeEventListener(): void;
35
+ dispatchEvent(): void;
36
+ getContext: () => {
37
+ configure(): void;
38
+ unconfigure(): void;
39
+ getCurrentTexture(): never;
40
+ };
41
+ };
42
+ context: {
43
+ configure(): void;
44
+ unconfigure(): void;
45
+ getCurrentTexture(): never;
46
+ };
47
+ };
package/dist/node.js ADDED
@@ -0,0 +1,157 @@
1
+ import { create, globals } from "webgpu";
2
+ import { wrapPromise } from "./Interop.js";
3
+ /**
4
+ * Running three.js on a real GPU in Node, with no browser.
5
+ *
6
+ * @remarks
7
+ * WebGPU comes from Dawn (Chrome's implementation, via the `webgpu` npm
8
+ * bindings). Importing this module installs the browser environment three
9
+ * expects as a TOP-LEVEL SIDE EFFECT: `navigator.gpu`, the WebGPU
10
+ * constants, `self`, a `requestAnimationFrame` shim, and a minimal
11
+ * `XMLHttpRequest` that font loaders need. Import it before creating a
12
+ * renderer.
13
+ *
14
+ * Node-only. Never import it from the browser-safe root entry.
15
+ *
16
+ * Two gotchas worth knowing before your first script:
17
+ *
18
+ * - **Your script will hang on exit.** three's animation loop keeps
19
+ * rescheduling the rAF shim, so Node's event loop never drains. End a
20
+ * standalone script with `process.exit(0)`, or run it under
21
+ * `NodeRuntime.runMain`, which exits explicitly. Test runners and the
22
+ * export pipeline already handle this.
23
+ * - **There is no canvas**, so render into a `RenderTarget` and read it
24
+ * back. {@link stubCanvas} supplies the canvas-shaped object the renderer
25
+ * constructor insists on.
26
+ *
27
+ * @example
28
+ * The headless setup, start to finish.
29
+ * ```typescript
30
+ * import * as NodeThree from "@effect-motion/three/node";
31
+ * import { Renderer, RenderTarget, Scene } from "@effect-motion/three";
32
+ * import { Effect } from "effect";
33
+ *
34
+ * const program = Effect.gen(function* () {
35
+ * const device = yield* NodeThree.makeDevice();
36
+ * const { canvas, context } = NodeThree.stubCanvas(640, 360);
37
+ * const renderer = yield* Renderer.make({
38
+ * canvas: canvas as unknown as HTMLCanvasElement,
39
+ * context: context as never,
40
+ * device,
41
+ * width: 640,
42
+ * height: 360,
43
+ * });
44
+ * const target = yield* RenderTarget.make(640, 360);
45
+ * Renderer.setRenderTarget(renderer, target);
46
+ * yield* Renderer.render(renderer, scene, camera);
47
+ * return yield* Renderer.readRenderTarget(renderer, target, 640, 360);
48
+ * }).pipe(Effect.scoped);
49
+ * ```
50
+ */
51
+ // WebGPU constants (GPUBufferUsage etc.) that browser code assumes global
52
+ Object.assign(globalThis, globals);
53
+ // Node ≥ 24 defines `navigator` as a getter-only global — defineProperty
54
+ // over it. Idempotent: skip when a gpu-bearing navigator already exists.
55
+ if (typeof navigator === "undefined" ||
56
+ navigator.gpu === undefined) {
57
+ Object.defineProperty(globalThis, "navigator", {
58
+ value: { gpu: create([]) },
59
+ configurable: true,
60
+ });
61
+ }
62
+ // three's internal Animation loop wants requestAnimationFrame on `self`
63
+ const g = globalThis;
64
+ if (g.self === undefined) {
65
+ g.self = globalThis;
66
+ }
67
+ if (g.requestAnimationFrame === undefined) {
68
+ g.requestAnimationFrame = (cb) => setTimeout(() => cb(performance.now()), 16);
69
+ g.cancelAnimationFrame = (id) => clearTimeout(id);
70
+ }
71
+ // Minimal XMLHttpRequest over fetch: arraybuffer GETs only — what font
72
+ // loaders (troika's Typr path) need. Supports data:/http(s) URIs.
73
+ if (g.XMLHttpRequest === undefined) {
74
+ class NodeXHR {
75
+ responseType = "";
76
+ response = null;
77
+ status = 0;
78
+ statusText = "";
79
+ onload = null;
80
+ onerror = null;
81
+ url = "";
82
+ open(_method, url) {
83
+ this.url = url;
84
+ }
85
+ send() {
86
+ fetch(this.url)
87
+ .then(async (res) => {
88
+ this.status = res.status;
89
+ this.statusText = res.statusText;
90
+ this.response =
91
+ this.responseType === "arraybuffer"
92
+ ? await res.arrayBuffer()
93
+ : await res.text();
94
+ this.onload?.();
95
+ })
96
+ .catch((err) => {
97
+ this.onerror?.(err);
98
+ });
99
+ }
100
+ }
101
+ g.XMLHttpRequest = NodeXHR;
102
+ }
103
+ /**
104
+ * Acquire a GPU device from Dawn.
105
+ *
106
+ * @remarks
107
+ * Pass the result to `Renderer.make` rather than letting three request its
108
+ * own. three asks for a "compatibility" feature level, which Chrome quietly
109
+ * upgrades but Dawn honors literally — costing MSAA and other core features,
110
+ * so headless output would be visibly worse than the browser's for the same
111
+ * scene. This requests a core device with every feature the adapter offers.
112
+ *
113
+ * Fails with a `ThreeException` when no adapter is available.
114
+ */
115
+ export const makeDevice = () => wrapPromise("requestAdapter", async () => {
116
+ const gpu = navigator.gpu;
117
+ const adapter = await gpu.requestAdapter({
118
+ featureLevel: "core",
119
+ });
120
+ if (adapter === null) {
121
+ throw new Error("no WebGPU adapter available (Dawn)");
122
+ }
123
+ return adapter.requestDevice({
124
+ requiredFeatures: [...adapter.features],
125
+ });
126
+ });
127
+ /**
128
+ * A canvas-shaped stand-in for headless rendering.
129
+ *
130
+ * @remarks
131
+ * three's renderer requires a canvas and a context even when nothing is
132
+ * displayed. These satisfy that without a DOM. The renderer must draw only
133
+ * into render targets: asking this context for the default framebuffer
134
+ * throws, deliberately, rather than silently rendering nowhere.
135
+ *
136
+ * @param width - Buffer width in device pixels.
137
+ * @param height - Buffer height in device pixels.
138
+ */
139
+ export const stubCanvas = (width, height) => {
140
+ const context = {
141
+ configure() { },
142
+ unconfigure() { },
143
+ getCurrentTexture() {
144
+ throw new Error("default framebuffer used in headless mode");
145
+ },
146
+ };
147
+ const canvas = {
148
+ width,
149
+ height,
150
+ style: {},
151
+ addEventListener() { },
152
+ removeEventListener() { },
153
+ dispatchEvent() { },
154
+ getContext: () => context,
155
+ };
156
+ return { canvas, context };
157
+ };
@@ -0,0 +1,16 @@
1
+ import { Effect } from "effect";
2
+ /**
3
+ * A worked example of this package's conventions, kept compiling so it
4
+ * cannot drift.
5
+ *
6
+ * @remarks
7
+ * Demonstrates the three rules the wrapper is built on: scoped
8
+ * construction that cleans itself up, synchronous chaining for mutation
9
+ * that cannot fail, and Effects only where a call is fallible or async.
10
+ *
11
+ * Internal — a reference to read, not part of the public surface. It is
12
+ * written for the browser and kept compiling rather than kept running:
13
+ * executing it in Node fails at renderer init, since it takes no canvas or
14
+ * device. See `@effect-motion/three/node` for the headless equivalent.
15
+ */
16
+ export declare const program: Effect.Effect<number, import("./ThreeException.js").ThreeException, never>;
@@ -0,0 +1,35 @@
1
+ import { Effect } from "effect";
2
+ import { Mesh, PerspectiveCamera } from "three/webgpu";
3
+ import * as Renderer from "./Renderer.js";
4
+ import * as RenderTarget from "./RenderTarget.js";
5
+ import * as Scene from "./Scene.js";
6
+ /**
7
+ * A worked example of this package's conventions, kept compiling so it
8
+ * cannot drift.
9
+ *
10
+ * @remarks
11
+ * Demonstrates the three rules the wrapper is built on: scoped
12
+ * construction that cleans itself up, synchronous chaining for mutation
13
+ * that cannot fail, and Effects only where a call is fallible or async.
14
+ *
15
+ * Internal — a reference to read, not part of the public surface. It is
16
+ * written for the browser and kept compiling rather than kept running:
17
+ * executing it in Node fails at renderer init, since it takes no canvas or
18
+ * device. See `@effect-motion/three/node` for the headless equivalent.
19
+ */
20
+ export const program = Effect.gen(function* () {
21
+ // scoped construction: the scene detaches its children on close, the
22
+ // renderer drains and disposes, the target frees its GPU allocation
23
+ const scene = yield* Scene.make();
24
+ const renderer = yield* Renderer.make({ width: 640, height: 360 });
25
+ const target = yield* RenderTarget.make(640, 360);
26
+ // infallible mutation: sync, chains through pipe, allocates no Effect
27
+ scene.pipe(Scene.add([new Mesh()]), Scene.setBackground(null));
28
+ Renderer.setPixelRatio(renderer, 2);
29
+ // GPU work can fail: Effects, typed as ThreeException
30
+ const camera = new PerspectiveCamera(50, 16 / 9, 1, 1000);
31
+ Renderer.setRenderTarget(renderer, target);
32
+ yield* Renderer.render(renderer, scene, camera);
33
+ const pixels = yield* Renderer.readRenderTarget(renderer, target, 640, 360);
34
+ return pixels.byteLength;
35
+ }).pipe(Effect.scoped);
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@effect-motion/three",
3
+ "version": "0.5.0",
4
+ "description": "Bindings-only Effect wrapper over three.js for effect-motion",
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/three"
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
+ "three",
18
+ "webgpu",
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
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@types/three": "^0.185.1",
39
+ "three": "^0.185.1",
40
+ "webgpu": "^0.4.0"
41
+ },
42
+ "peerDependencies": {
43
+ "effect": ">=4.0.0-beta.98"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^26.1.1",
47
+ "effect": "4.0.0-beta.98",
48
+ "typescript": "^7.0.2",
49
+ "vitest": "^4.1.10"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc -p tsconfig.build.json",
53
+ "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
54
+ "test": "vitest run --passWithNoTests",
55
+ "check": "tsc --noEmit"
56
+ }
57
+ }