effect-motion 0.4.0 → 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.
- package/README.md +2 -2
- package/dist/Camera.d.ts +186 -49
- package/dist/Camera.js +343 -76
- package/dist/Color.d.ts +101 -1
- package/dist/Color.js +101 -1
- package/dist/EffectMotionError.d.ts +17 -0
- package/dist/EffectMotionError.js +17 -0
- package/dist/Entity.d.ts +682 -38
- package/dist/Entity.js +271 -27
- package/dist/Font.d.ts +108 -0
- package/dist/Font.js +95 -0
- package/dist/Image.d.ts +71 -0
- package/dist/Image.js +50 -0
- package/dist/Instance.d.ts +73 -11
- package/dist/Instance.js +44 -11
- package/dist/Motion.d.ts +328 -53
- package/dist/Motion.js +278 -46
- package/dist/Physics.d.ts +154 -20
- package/dist/Physics.js +92 -11
- package/dist/Projection.d.ts +37 -132
- package/dist/Projection.js +33 -292
- package/dist/Resource.d.ts +26 -0
- package/dist/Resource.js +41 -0
- package/dist/Runner.d.ts +639 -350
- package/dist/Runner.js +197 -150
- package/dist/Scene.d.ts +603 -175
- package/dist/Scene.js +596 -112
- package/dist/Timing.d.ts +170 -13
- package/dist/Timing.js +124 -6
- package/dist/Tree.d.ts +39 -0
- package/dist/Tree.js +127 -0
- package/dist/index.d.ts +54 -5
- package/dist/index.js +56 -5
- package/dist/particles/Particle.d.ts +2 -2
- package/dist/particles/ParticleField.d.ts +8 -6
- package/dist/particles/ParticleField.js +8 -9
- package/dist/particles/constructors.d.ts +5 -6
- package/dist/particles/constructors.js +8 -3
- package/dist/particles/legacy.d.ts +58 -0
- package/dist/particles/legacy.js +46 -0
- package/dist/particles/simulate.js +11 -3
- package/dist/particles/step.js +14 -10
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/package.json +2 -4
- package/dist/CameraHelpers.d.ts +0 -70
- package/dist/CameraHelpers.js +0 -239
- package/dist/CanvasExporter.d.ts +0 -12
- package/dist/CanvasExporter.js +0 -40
- package/dist/Fonts.d.ts +0 -41
- package/dist/Fonts.js +0 -27
- package/dist/Images.d.ts +0 -33
- package/dist/Images.js +0 -24
- package/dist/PngExporter.d.ts +0 -6
- package/dist/PngExporter.js +0 -85
- package/dist/Renderer.d.ts +0 -118
- package/dist/Renderer.js +0 -508
- package/dist/Shapes.d.ts +0 -11
- package/dist/Shapes.js +0 -11
- package/dist/demo.d.ts +0 -3
- package/dist/demo.js +0 -71
- package/dist/render/dof.d.ts +0 -27
- package/dist/render/dof.js +0 -37
- package/dist/render/paint.d.ts +0 -30
- package/dist/render/paint.js +0 -36
- package/dist/render/shapes.d.ts +0 -42
- package/dist/render/shapes.js +0 -310
- package/dist/shapes/Circle.d.ts +0 -32
- package/dist/shapes/Circle.js +0 -9
- package/dist/shapes/Ellipse.d.ts +0 -35
- package/dist/shapes/Ellipse.js +0 -10
- package/dist/shapes/Group.d.ts +0 -126
- package/dist/shapes/Group.js +0 -89
- package/dist/shapes/Hud.d.ts +0 -38
- package/dist/shapes/Hud.js +0 -35
- package/dist/shapes/Image.d.ts +0 -45
- package/dist/shapes/Image.js +0 -28
- package/dist/shapes/Line.d.ts +0 -47
- package/dist/shapes/Line.js +0 -35
- package/dist/shapes/Path.d.ts +0 -107
- package/dist/shapes/Path.js +0 -32
- package/dist/shapes/Rect.d.ts +0 -51
- package/dist/shapes/Rect.js +0 -22
- package/dist/shapes/Shape2D.d.ts +0 -49
- package/dist/shapes/Shape2D.js +0 -52
- package/dist/shapes/Shapes.d.ts +0 -11
- package/dist/shapes/Shapes.js +0 -11
- package/dist/shapes/Square.d.ts +0 -32
- package/dist/shapes/Square.js +0 -11
- package/dist/shapes/Text.d.ts +0 -51
- package/dist/shapes/Text.js +0 -24
package/dist/Entity.js
CHANGED
|
@@ -1,29 +1,273 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
1
2
|
import * as Schema from "effect/Schema";
|
|
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
|
-
|
|
3
|
+
import * as Color from "./Color.js";
|
|
4
|
+
import * as Font from "./Font.js";
|
|
5
|
+
import * as ImageResource from "./Image.js";
|
|
6
|
+
/**
|
|
7
|
+
* The closed entity world.
|
|
8
|
+
*
|
|
9
|
+
* Every entity the library knows is a member of one tagged union, declared
|
|
10
|
+
* here. There is deliberately no way to define an entity outside this module:
|
|
11
|
+
* an entity's complete field set is derivable from its `_tag` alone, which
|
|
12
|
+
* is what lets the runner, the animators, and the renderer narrow instead
|
|
13
|
+
* of casting.
|
|
14
|
+
*
|
|
15
|
+
* Shared fields come from mixins, never from per-entity declarations, so no
|
|
16
|
+
* entity can quietly omit or rename one. An `Instance` is a REFERENCE to a
|
|
17
|
+
* live entity in the runner tree — see `Instance.ts`.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* A point or offset in 3D space.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* Scene coordinates with depth: `x` right, `y` UP (the mathematical
|
|
24
|
+
* convention), `z` toward the viewer, origin at the viewport center — a
|
|
25
|
+
* right-handed frame. Every axis defaults to 0, so `vec3({ x: 100 })` is a
|
|
26
|
+
* valid horizontal offset.
|
|
27
|
+
*
|
|
28
|
+
* At `z = 0` content renders exactly as flat 2D. Non-zero `z` is only
|
|
29
|
+
* meaningful under a perspective camera, where it changes apparent size and
|
|
30
|
+
* parallax.
|
|
31
|
+
*/
|
|
32
|
+
export const Vec3 = Schema.TaggedStruct("Vec3", {
|
|
33
|
+
x: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(0))),
|
|
34
|
+
y: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(0))),
|
|
35
|
+
z: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(0))),
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Build a {@link Vec3}; omitted axes are 0.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* Entity.vec3({ x: 100, y: 50 })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const vec3 = Vec3.make;
|
|
46
|
+
const vec3Default = (x, y, z) => Vec3.pipe(Schema.withConstructorDefault(Effect.sync(() => Vec3.make({ x, y, z }))));
|
|
47
|
+
const defaultedNumber = (value) => Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(value)));
|
|
48
|
+
/**
|
|
49
|
+
* Position and orientation. Carried by EVERY entity, the camera included:
|
|
50
|
+
* a viewpoint has a location and an orientation like anything else.
|
|
51
|
+
*/
|
|
52
|
+
const transformMixin = {
|
|
53
|
+
position: vec3Default(0, 0, 0),
|
|
54
|
+
rotation: vec3Default(0, 0, 0),
|
|
29
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Presentation state for things that PAINT. The camera is the one entity
|
|
58
|
+
* that does not (it is view state, omitted from the frame's instance map),
|
|
59
|
+
* so it takes the transform mixin and none of this.
|
|
60
|
+
*/
|
|
61
|
+
const appearanceMixin = {
|
|
62
|
+
scale: vec3Default(1, 1, 1),
|
|
63
|
+
opacity: defaultedNumber(1),
|
|
64
|
+
visible: Schema.Boolean.pipe(Schema.withConstructorDefault(Effect.succeed(true))),
|
|
65
|
+
};
|
|
66
|
+
/** transform + appearance: the base every paintable entity shares */
|
|
67
|
+
const paintableMixin = {
|
|
68
|
+
...transformMixin,
|
|
69
|
+
...appearanceMixin,
|
|
70
|
+
};
|
|
71
|
+
const strokeMixin = {
|
|
72
|
+
strokeWidth: defaultedNumber(1),
|
|
73
|
+
strokeColor: Color.Color.pipe(Schema.withConstructorDefault(Effect.succeed(Color.black))),
|
|
74
|
+
};
|
|
75
|
+
const fillMixin = {
|
|
76
|
+
fillColor: Color.Color.pipe(Schema.withConstructorDefault(Effect.succeed(Color.white))),
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Children are stored as instance ids, in paint order. The AUTHORING input
|
|
80
|
+
* is more permissive (a string becomes a Text, an Instance contributes its
|
|
81
|
+
* id, an un-yielded `instantiate` effect is resolved) — the runner
|
|
82
|
+
* normalizes it here, so stored data stays plain ids.
|
|
83
|
+
*/
|
|
84
|
+
const childrenMixin = {
|
|
85
|
+
children: Schema.Array(Schema.String).pipe(Schema.withConstructorDefault(Effect.sync(() => []))),
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* A path command point, relative to the Path's own `position` — the same
|
|
89
|
+
* relative rule Line's endpoints follow, so translating a Path is rigid
|
|
90
|
+
* without rewriting the command array.
|
|
91
|
+
*/
|
|
92
|
+
const point = {
|
|
93
|
+
x: Schema.Number,
|
|
94
|
+
y: Schema.Number,
|
|
95
|
+
z: Schema.optionalKey(Schema.Number),
|
|
96
|
+
};
|
|
97
|
+
export const PathCommand = Schema.TaggedUnion({
|
|
98
|
+
M: point,
|
|
99
|
+
L: point,
|
|
100
|
+
Z: {},
|
|
101
|
+
});
|
|
102
|
+
// ── the entities ─────────────────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* A segment. `start`/`end` are offsets from `position`, so moving the line
|
|
105
|
+
* translates it rigidly — no endpoint compensation anywhere in the system.
|
|
106
|
+
* Skeletal: parametrized by its endpoints, never by an anchor plus size.
|
|
107
|
+
*/
|
|
108
|
+
export const Line = Schema.TaggedStruct("Line", {
|
|
109
|
+
...paintableMixin,
|
|
110
|
+
...strokeMixin,
|
|
111
|
+
start: vec3Default(0, 0, 0),
|
|
112
|
+
end: vec3Default(0, 0, 0),
|
|
113
|
+
});
|
|
114
|
+
/**
|
|
115
|
+
* A polyline/polygon. Command points are relative to `position` (see
|
|
116
|
+
* `point`). The first command must be `M` — an open vocabulary of M/L/Z;
|
|
117
|
+
* curves and arcs arrive later via deterministic flattening.
|
|
118
|
+
*/
|
|
119
|
+
export const Path = Schema.TaggedStruct("Path", {
|
|
120
|
+
...paintableMixin,
|
|
121
|
+
...fillMixin,
|
|
122
|
+
...strokeMixin,
|
|
123
|
+
commands: Schema.NonEmptyArray(PathCommand).check(Schema.makeFilter((commands) => commands[0]._tag === "M"
|
|
124
|
+
? undefined
|
|
125
|
+
: { path: [0], issue: "the first path command must be M" })),
|
|
126
|
+
});
|
|
127
|
+
/**
|
|
128
|
+
* The canonical 2.5D plane. All-zero rotation is a camera-facing billboard;
|
|
129
|
+
* non-zero tilts it as a real plane in 3D.
|
|
130
|
+
*/
|
|
131
|
+
export const Rect = Schema.TaggedStruct("Rect", {
|
|
132
|
+
...paintableMixin,
|
|
133
|
+
...fillMixin,
|
|
134
|
+
...strokeMixin,
|
|
135
|
+
width: defaultedNumber(100),
|
|
136
|
+
height: defaultedNumber(100),
|
|
137
|
+
});
|
|
138
|
+
/** A filled circle of a given `radius`, centered on its `position`. */
|
|
139
|
+
export const Circle = Schema.TaggedStruct("Circle", {
|
|
140
|
+
...paintableMixin,
|
|
141
|
+
...fillMixin,
|
|
142
|
+
...strokeMixin,
|
|
143
|
+
radius: defaultedNumber(10),
|
|
144
|
+
});
|
|
145
|
+
/** An ellipse with independent `radiusX` and `radiusY`. */
|
|
146
|
+
export const Ellipse = Schema.TaggedStruct("Ellipse", {
|
|
147
|
+
...paintableMixin,
|
|
148
|
+
...fillMixin,
|
|
149
|
+
...strokeMixin,
|
|
150
|
+
radiusX: defaultedNumber(20),
|
|
151
|
+
radiusY: defaultedNumber(10),
|
|
152
|
+
});
|
|
153
|
+
/**
|
|
154
|
+
* A plain-string text leaf. The engine cannot measure text, so `text` has
|
|
155
|
+
* no dimensional fields — layout is the renderer's business. Inline
|
|
156
|
+
* formatting is expressed by composing several Texts, not by a rich tree.
|
|
157
|
+
*/
|
|
158
|
+
export const Text = Schema.TaggedStruct("Text", {
|
|
159
|
+
...paintableMixin,
|
|
160
|
+
...fillMixin,
|
|
161
|
+
text: Schema.String,
|
|
162
|
+
// numeric, therefore tweenable
|
|
163
|
+
fontSize: defaultedNumber(16),
|
|
164
|
+
// a Font resource reference ({_tag, id}), never a bare string. Defaults
|
|
165
|
+
// to the built-in font, so bare Text stays zero-ceremony and the default
|
|
166
|
+
// never enters the scene's loader requirements.
|
|
167
|
+
fontFamily: Font.schema.pipe(Schema.withConstructorDefault(Effect.sync(() => Font.schema.make({ id: Font.defaultFont.id })))),
|
|
168
|
+
textAnchor: Schema.optionalKey(Schema.Literals(["start", "middle", "end"])),
|
|
169
|
+
baseline: Schema.optionalKey(Schema.Literals(["auto", "middle", "hanging"])),
|
|
170
|
+
});
|
|
171
|
+
/**
|
|
172
|
+
* A container: positions and structures its children, paints nothing
|
|
173
|
+
* itself. Its transform composes down the subtree like any entity's.
|
|
174
|
+
*
|
|
175
|
+
* It carries no composition bounds. `width`/`height`/`backgroundColor` used
|
|
176
|
+
* to live here so `Scene.play` could copy a nested scene's dimensions onto
|
|
177
|
+
* the mount group; a Scene owns those itself, and a render-to-texture
|
|
178
|
+
* boundary is `Scene.play`'s business, not a field on every group.
|
|
179
|
+
*/
|
|
180
|
+
export const Group = Schema.TaggedStruct("Group", {
|
|
181
|
+
...paintableMixin,
|
|
182
|
+
...childrenMixin,
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* A screen-space container: its subtree is projected through the identity
|
|
186
|
+
* camera rather than the active one, so HUD content ignores camera
|
|
187
|
+
* movement, zoom, and shake, and always paints on top.
|
|
188
|
+
*
|
|
189
|
+
* `position.z` is depth WITHIN the HUD tier — z consistently means depth in
|
|
190
|
+
* the entity's own coordinate space, world for world content and screen for
|
|
191
|
+
* HUD content. A Hud must be a top-level child of the root (or of another
|
|
192
|
+
* Hud); nesting one inside world content is a loud defect.
|
|
193
|
+
*/
|
|
194
|
+
export const Hud = Schema.TaggedStruct("Hud", {
|
|
195
|
+
...paintableMixin,
|
|
196
|
+
...childrenMixin,
|
|
197
|
+
});
|
|
198
|
+
/**
|
|
199
|
+
* A raster/vector image leaf. `image` is an Image resource reference; the
|
|
200
|
+
* bytes live in the scene's requirements, never in frame data.
|
|
201
|
+
*
|
|
202
|
+
* `width`/`height` are optional and undefaulted: set BOTH to draw at that
|
|
203
|
+
* size (numeric, so they tween), leave both absent for the source's natural
|
|
204
|
+
* size. A lone dimension is ignored — aspect math needs the natural size,
|
|
205
|
+
* which frame data never sees.
|
|
206
|
+
*/
|
|
207
|
+
export const Image = Schema.TaggedStruct("Image", {
|
|
208
|
+
...paintableMixin,
|
|
209
|
+
image: ImageResource.schema,
|
|
210
|
+
width: Schema.optionalKey(Schema.Number),
|
|
211
|
+
height: Schema.optionalKey(Schema.Number),
|
|
212
|
+
});
|
|
213
|
+
/**
|
|
214
|
+
* The viewpoint — the one entity that is never drawn.
|
|
215
|
+
*
|
|
216
|
+
* @remarks
|
|
217
|
+
* A camera is view state rather than content, so it carries `position` and
|
|
218
|
+
* `rotation` but no `scale`, `opacity`, or `visible`, and is absent from the
|
|
219
|
+
* frame's instance map. Every scene has one already; reach for it with
|
|
220
|
+
* `Scene.camera`.
|
|
221
|
+
*
|
|
222
|
+
* `focalLength` sets the perspective strength — a longer lens flattens
|
|
223
|
+
* depth, a shorter one exaggerates it. It and the resting `z` are
|
|
224
|
+
* width-relative and filled in by the runner at instantiate time (only the
|
|
225
|
+
* runner knows the composition width), so the zero defaults here are
|
|
226
|
+
* placeholders that are always overwritten. The resting values are chosen so
|
|
227
|
+
* content at `z = 0` renders exactly as flat 2D.
|
|
228
|
+
*
|
|
229
|
+
* `poi` is an optional point of interest: when set, the camera auto-aims at
|
|
230
|
+
* that world point and any explicit `rotation` composes on top of the aim.
|
|
231
|
+
* The `Camera` module's helpers are the ergonomic way to drive it.
|
|
232
|
+
*
|
|
233
|
+
* `aperture` and `focusDistance` describe depth of field. They are carried
|
|
234
|
+
* on the frame but currently have NO visual effect — depth-of-field
|
|
235
|
+
* rendering is not implemented, and every frame renders sharp.
|
|
236
|
+
*/
|
|
237
|
+
export const Camera = Schema.TaggedStruct("Camera", {
|
|
238
|
+
...transformMixin,
|
|
239
|
+
focalLength: defaultedNumber(0),
|
|
240
|
+
focusDistance: defaultedNumber(0),
|
|
241
|
+
aperture: defaultedNumber(0),
|
|
242
|
+
poi: Vec3.pipe(Schema.NullOr, Schema.withConstructorDefault(Effect.succeed(null))),
|
|
243
|
+
});
|
|
244
|
+
// ── the union ────────────────────────────────────────────────────────────
|
|
245
|
+
export const EntityMap = {
|
|
246
|
+
Line,
|
|
247
|
+
Path,
|
|
248
|
+
Rect,
|
|
249
|
+
Circle,
|
|
250
|
+
Ellipse,
|
|
251
|
+
Text,
|
|
252
|
+
Group,
|
|
253
|
+
Hud,
|
|
254
|
+
Image,
|
|
255
|
+
Camera,
|
|
256
|
+
};
|
|
257
|
+
export const getEntityDefinitionByTag = (tag) => EntityMap[tag];
|
|
258
|
+
/**
|
|
259
|
+
* Every container tag, exhaustively. `Record<ContainerTag, true>` (not
|
|
260
|
+
* `satisfies Array<ContainerTag>`) is deliberate: an array only checks its
|
|
261
|
+
* members are valid tags, so a newly-added container would silently miss
|
|
262
|
+
* this set. A Record demands every key, so omitting one fails the build.
|
|
263
|
+
*/
|
|
264
|
+
const containerTags = {
|
|
265
|
+
Group: true,
|
|
266
|
+
Hud: true,
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Whether an entity can hold children. Previously this was "does its data
|
|
270
|
+
* have a `children` field", answerable only at runtime because entity shapes
|
|
271
|
+
* were unknowable; now it is a property of the tag.
|
|
272
|
+
*/
|
|
273
|
+
export const isContainer = (entity) => entity._tag in containerTags;
|
package/dist/Font.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import * as Resource from "./Resource.js";
|
|
6
|
+
import type { EnsureLiteral } from "./types.js";
|
|
7
|
+
export declare const tag: "effect-motion/Resources/Font";
|
|
8
|
+
/**
|
|
9
|
+
* A font reference as entity data stores it: `{ _tag, id }`. The id is the
|
|
10
|
+
* family name text paints register and select by. Obtain one by yielding a
|
|
11
|
+
* {@link Font} constant inside a scene — that is what puts the matching
|
|
12
|
+
* `FontLoader<ID>` into the scene's requirements.
|
|
13
|
+
*/
|
|
14
|
+
export interface Font<ID extends string = string> {
|
|
15
|
+
readonly _tag: typeof tag;
|
|
16
|
+
readonly id: ID;
|
|
17
|
+
}
|
|
18
|
+
/** The stored-data schema for font references (see `Text.fontFamily`). */
|
|
19
|
+
export declare const schema: Schema.TaggedStruct<"effect-motion/Resources/Font", {
|
|
20
|
+
readonly id: Schema.String;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* A loaded font, provided as a context service: bytes are already in memory
|
|
24
|
+
* by the time any consumer reads this (loads run eagerly at layer
|
|
25
|
+
* construction — see {@link layer}). `format` is optional; consumers sniff
|
|
26
|
+
* magic bytes when absent.
|
|
27
|
+
*/
|
|
28
|
+
export interface FontLoader<ID extends string = string> extends Resource.LoaderBrand {
|
|
29
|
+
readonly id: ID;
|
|
30
|
+
readonly bytes: Uint8Array;
|
|
31
|
+
readonly format?: "ttf" | "otf";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The context key for a font's loader, derived from the id string alone —
|
|
35
|
+
* the bridge over literal erasure: frame data carries only `{ id: string }`,
|
|
36
|
+
* and rebuilding the key from that string resolves the same service the
|
|
37
|
+
* authored constant provides.
|
|
38
|
+
*/
|
|
39
|
+
export declare const Loader: <ID extends string>(id: ID) => Context.Service<FontLoader<ID>, FontLoader<ID>>;
|
|
40
|
+
/**
|
|
41
|
+
* One font constant, two faces:
|
|
42
|
+
*
|
|
43
|
+
* - **author side**: `yield*` it in a scene to get the {@link Font} value for
|
|
44
|
+
* `fontFamily`, adding `FontLoader<ID>` to the scene's requirements. The
|
|
45
|
+
* requirement is PHANTOM — the effect succeeds without touching context,
|
|
46
|
+
* so `Scene.run`/`stream` stay loader-free and only the render path
|
|
47
|
+
* (the first actual consumer of bytes) demands the loader.
|
|
48
|
+
* - **provider side**: `.Loader` is the context key; pair it with
|
|
49
|
+
* {@link layer} to provide bytes.
|
|
50
|
+
*/
|
|
51
|
+
export interface FontResource<ID extends string = string> extends Effect.Effect<Font<ID>, never, FontLoader<ID>> {
|
|
52
|
+
readonly id: ID;
|
|
53
|
+
readonly Loader: Context.Service<FontLoader<ID>, FontLoader<ID>>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Declare a font your scene uses.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* Call it once at module scope to get a constant with two faces: `yield*` it
|
|
60
|
+
* inside a scene to obtain the value for a Text's `fontFamily`, and pair it
|
|
61
|
+
* with {@link layer} to supply the actual bytes when rendering.
|
|
62
|
+
*
|
|
63
|
+
* Yielding the constant is what records the font in the scene's
|
|
64
|
+
* requirements, so a scene that uses a font it was never given fails to
|
|
65
|
+
* typecheck instead of rendering in the wrong face.
|
|
66
|
+
*
|
|
67
|
+
* The id must be a literal string — that literal is what ties the
|
|
68
|
+
* requirement to the provider.
|
|
69
|
+
*
|
|
70
|
+
* @param id - The family name, as a literal string.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* const Inter = Font.Font("Inter");
|
|
75
|
+
*
|
|
76
|
+
* const scene = Scene.make(function* () {
|
|
77
|
+
* const inter = yield* Inter;
|
|
78
|
+
* yield* Scene.instantiate("Text", { text: "hello", fontFamily: inter });
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare const Font: <const ID extends string>(id: ID & EnsureLiteral<ID, "Font id must be a literal string">) => FontResource<ID>;
|
|
83
|
+
/**
|
|
84
|
+
* Provide a font's bytes. The load effect runs ONCE, at layer construction
|
|
85
|
+
* (runtime build) — never at frame time; compose retries/timeouts on the
|
|
86
|
+
* load effect itself. Every provided font loads regardless of whether the
|
|
87
|
+
* scene ends up using it (preload-all-provided policy).
|
|
88
|
+
*/
|
|
89
|
+
export declare const layer: <ID extends string, E, R>(font: FontResource<ID>, load: Effect.Effect<Uint8Array, E, R>, options?: {
|
|
90
|
+
readonly format?: "ttf" | "otf";
|
|
91
|
+
}) => Layer.Layer<FontLoader<ID>, E, R>;
|
|
92
|
+
/**
|
|
93
|
+
* The built-in default font under the RESERVED id `"sans-serif"` — the
|
|
94
|
+
* `Text.fontFamily` constructor default. It never appears in a scene's
|
|
95
|
+
* requirements (no `yield*` happens for a schema default); the render path
|
|
96
|
+
* auto-provides its loader beneath caller context, so providing your own
|
|
97
|
+
* loader under the `"sans-serif"` id overrides the built-in bytes.
|
|
98
|
+
*/
|
|
99
|
+
export declare const defaultFont: FontResource<"sans-serif">;
|
|
100
|
+
export { defaultFont as default };
|
|
101
|
+
/**
|
|
102
|
+
* A CORS-open, static-weight TrueType mapped to the default family. Lived
|
|
103
|
+
* on the render engine before this refactor; now the render path fetches it
|
|
104
|
+
* (module-cached) only when a frame actually uses the default font and no
|
|
105
|
+
* caller-provided `"sans-serif"` loader is in context.
|
|
106
|
+
*/
|
|
107
|
+
export declare const DEFAULT_FONT_URL = "https://cdn.jsdelivr.net/npm/@expo-google-fonts/inter@0.2.3/Inter_400Regular.ttf";
|
|
108
|
+
export declare const loadDefaultBytes: Effect.Effect<Uint8Array>;
|
package/dist/Font.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import * as Resource from "./Resource.js";
|
|
6
|
+
export const tag = "effect-motion/Resources/Font";
|
|
7
|
+
/** The stored-data schema for font references (see `Text.fontFamily`). */
|
|
8
|
+
export const schema = Schema.TaggedStruct(tag, {
|
|
9
|
+
id: Schema.String,
|
|
10
|
+
});
|
|
11
|
+
const loaderKeyPrefix = "effect-motion/Resources/FontLoader/";
|
|
12
|
+
/**
|
|
13
|
+
* The context key for a font's loader, derived from the id string alone —
|
|
14
|
+
* the bridge over literal erasure: frame data carries only `{ id: string }`,
|
|
15
|
+
* and rebuilding the key from that string resolves the same service the
|
|
16
|
+
* authored constant provides.
|
|
17
|
+
*/
|
|
18
|
+
export const Loader = (id) => Context.Service(`${loaderKeyPrefix}${id}`);
|
|
19
|
+
/**
|
|
20
|
+
* Declare a font your scene uses.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* Call it once at module scope to get a constant with two faces: `yield*` it
|
|
24
|
+
* inside a scene to obtain the value for a Text's `fontFamily`, and pair it
|
|
25
|
+
* with {@link layer} to supply the actual bytes when rendering.
|
|
26
|
+
*
|
|
27
|
+
* Yielding the constant is what records the font in the scene's
|
|
28
|
+
* requirements, so a scene that uses a font it was never given fails to
|
|
29
|
+
* typecheck instead of rendering in the wrong face.
|
|
30
|
+
*
|
|
31
|
+
* The id must be a literal string — that literal is what ties the
|
|
32
|
+
* requirement to the provider.
|
|
33
|
+
*
|
|
34
|
+
* @param id - The family name, as a literal string.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const Inter = Font.Font("Inter");
|
|
39
|
+
*
|
|
40
|
+
* const scene = Scene.make(function* () {
|
|
41
|
+
* const inter = yield* Inter;
|
|
42
|
+
* yield* Scene.instantiate("Text", { text: "hello", fontFamily: inter });
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export const Font = (id) => {
|
|
47
|
+
const value = { _tag: tag, id };
|
|
48
|
+
// R is covariant, so the loader-free succeed widens to the phantom
|
|
49
|
+
// FontLoader<ID> requirement without a cast
|
|
50
|
+
return Object.assign(Effect.succeed(value), {
|
|
51
|
+
id: id,
|
|
52
|
+
Loader: Loader(id),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Provide a font's bytes. The load effect runs ONCE, at layer construction
|
|
57
|
+
* (runtime build) — never at frame time; compose retries/timeouts on the
|
|
58
|
+
* load effect itself. Every provided font loads regardless of whether the
|
|
59
|
+
* scene ends up using it (preload-all-provided policy).
|
|
60
|
+
*/
|
|
61
|
+
export const layer = (font, load, options) => Layer.effect(font.Loader, load.pipe(Effect.map((bytes) => ({
|
|
62
|
+
[Resource.LoaderTypeId]: Resource.LoaderTypeId,
|
|
63
|
+
id: font.id,
|
|
64
|
+
bytes,
|
|
65
|
+
...(options?.format !== undefined ? { format: options.format } : {}),
|
|
66
|
+
}))));
|
|
67
|
+
/**
|
|
68
|
+
* The built-in default font under the RESERVED id `"sans-serif"` — the
|
|
69
|
+
* `Text.fontFamily` constructor default. It never appears in a scene's
|
|
70
|
+
* requirements (no `yield*` happens for a schema default); the render path
|
|
71
|
+
* auto-provides its loader beneath caller context, so providing your own
|
|
72
|
+
* loader under the `"sans-serif"` id overrides the built-in bytes.
|
|
73
|
+
*/
|
|
74
|
+
export const defaultFont = Font("sans-serif");
|
|
75
|
+
export { defaultFont as default };
|
|
76
|
+
/**
|
|
77
|
+
* A CORS-open, static-weight TrueType mapped to the default family. Lived
|
|
78
|
+
* on the render engine before this refactor; now the render path fetches it
|
|
79
|
+
* (module-cached) only when a frame actually uses the default font and no
|
|
80
|
+
* caller-provided `"sans-serif"` loader is in context.
|
|
81
|
+
*/
|
|
82
|
+
export const DEFAULT_FONT_URL = "https://cdn.jsdelivr.net/npm/@expo-google-fonts/inter@0.2.3/Inter_400Regular.ttf";
|
|
83
|
+
// immutable bytes, fetched at most once per process — the default font must
|
|
84
|
+
// not refetch per render call
|
|
85
|
+
let defaultBytesCache;
|
|
86
|
+
export const loadDefaultBytes = Effect.promise(() => {
|
|
87
|
+
defaultBytesCache ??= fetch(DEFAULT_FONT_URL).then(async (response) => {
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
defaultBytesCache = undefined;
|
|
90
|
+
throw new Error(`default font fetch failed (HTTP ${response.status}): ${DEFAULT_FONT_URL}`);
|
|
91
|
+
}
|
|
92
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
93
|
+
});
|
|
94
|
+
return defaultBytesCache;
|
|
95
|
+
});
|
package/dist/Image.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import * as Resource from "./Resource.js";
|
|
6
|
+
import type { EnsureLiteral } from "./types.js";
|
|
7
|
+
export declare const tag: "effect-motion/Resources/Image";
|
|
8
|
+
/**
|
|
9
|
+
* An image reference as entity data stores it: `{ _tag, id }`. Obtain one
|
|
10
|
+
* by yielding an {@link Image} constant inside a scene — that is what puts
|
|
11
|
+
* the matching `ImageLoader<ID>` into the scene's requirements.
|
|
12
|
+
*/
|
|
13
|
+
export interface Image<ID extends string = string> {
|
|
14
|
+
readonly _tag: typeof tag;
|
|
15
|
+
readonly id: ID;
|
|
16
|
+
}
|
|
17
|
+
/** The stored-data schema for image references (see `Shapes.Image`). */
|
|
18
|
+
export declare const schema: Schema.TaggedStruct<"effect-motion/Resources/Image", {
|
|
19
|
+
readonly id: Schema.String;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* A loaded image (encoded bytes — png/jpg/webp per the render session's
|
|
23
|
+
* decoder), provided as a context service: bytes are already in memory by
|
|
24
|
+
* the time any consumer reads this (loads run eagerly at layer
|
|
25
|
+
* construction — see {@link layer}). A separate shape from FontLoader on
|
|
26
|
+
* purpose (kind-specific metadata will diverge); only the loader brand is
|
|
27
|
+
* shared.
|
|
28
|
+
*/
|
|
29
|
+
export interface ImageLoader<ID extends string = string> extends Resource.LoaderBrand {
|
|
30
|
+
readonly id: ID;
|
|
31
|
+
readonly bytes: Uint8Array;
|
|
32
|
+
}
|
|
33
|
+
/** The context key for an image's loader, derived from the id string alone. */
|
|
34
|
+
export declare const Loader: <ID extends string>(id: ID) => Context.Service<ImageLoader<ID>, ImageLoader<ID>>;
|
|
35
|
+
/**
|
|
36
|
+
* One image constant, two faces — author side (`yield*` for the value and
|
|
37
|
+
* the phantom `ImageLoader<ID>` requirement) and provider side (`.Loader`
|
|
38
|
+
* plus {@link layer}). See `Font.Font` for the full contract; images follow
|
|
39
|
+
* it exactly.
|
|
40
|
+
*/
|
|
41
|
+
export interface ImageResource<ID extends string = string> extends Effect.Effect<Image<ID>, never, ImageLoader<ID>> {
|
|
42
|
+
readonly id: ID;
|
|
43
|
+
readonly Loader: Context.Service<ImageLoader<ID>, ImageLoader<ID>>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Declare an image your scene uses.
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* Follows the same two-faced contract as `Font.Font`: `yield*` the constant
|
|
50
|
+
* in a scene to get the reference for an Image entity, and pair it with
|
|
51
|
+
* {@link layer} to provide the encoded bytes. Yielding is what puts the
|
|
52
|
+
* requirement into the scene's type.
|
|
53
|
+
*
|
|
54
|
+
* @param id - An identifier for this image, as a literal string.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const Logo = Image.Image("logo");
|
|
59
|
+
*
|
|
60
|
+
* const scene = Scene.make(function* () {
|
|
61
|
+
* const logo = yield* Logo;
|
|
62
|
+
* yield* Scene.instantiate("Image", { image: logo, width: 200, height: 80 });
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare const Image: <const ID extends string>(id: ID & EnsureLiteral<ID, "Image id must be a literal string">) => ImageResource<ID>;
|
|
67
|
+
/**
|
|
68
|
+
* Provide an image's encoded bytes. The load effect runs ONCE, at layer
|
|
69
|
+
* construction — never at frame time; compose retries on the load effect.
|
|
70
|
+
*/
|
|
71
|
+
export declare const layer: <ID extends string, E, R>(image: ImageResource<ID>, load: Effect.Effect<Uint8Array, E, R>) => Layer.Layer<ImageLoader<ID>, E, R>;
|
package/dist/Image.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import * as Resource from "./Resource.js";
|
|
6
|
+
export const tag = "effect-motion/Resources/Image";
|
|
7
|
+
/** The stored-data schema for image references (see `Shapes.Image`). */
|
|
8
|
+
export const schema = Schema.TaggedStruct(tag, {
|
|
9
|
+
id: Schema.String,
|
|
10
|
+
});
|
|
11
|
+
const loaderKeyPrefix = "effect-motion/Resources/ImageLoader/";
|
|
12
|
+
/** The context key for an image's loader, derived from the id string alone. */
|
|
13
|
+
export const Loader = (id) => Context.Service(`${loaderKeyPrefix}${id}`);
|
|
14
|
+
/**
|
|
15
|
+
* Declare an image your scene uses.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* Follows the same two-faced contract as `Font.Font`: `yield*` the constant
|
|
19
|
+
* in a scene to get the reference for an Image entity, and pair it with
|
|
20
|
+
* {@link layer} to provide the encoded bytes. Yielding is what puts the
|
|
21
|
+
* requirement into the scene's type.
|
|
22
|
+
*
|
|
23
|
+
* @param id - An identifier for this image, as a literal string.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const Logo = Image.Image("logo");
|
|
28
|
+
*
|
|
29
|
+
* const scene = Scene.make(function* () {
|
|
30
|
+
* const logo = yield* Logo;
|
|
31
|
+
* yield* Scene.instantiate("Image", { image: logo, width: 200, height: 80 });
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const Image = (id) => {
|
|
36
|
+
const value = { _tag: tag, id };
|
|
37
|
+
return Object.assign(Effect.succeed(value), {
|
|
38
|
+
id: id,
|
|
39
|
+
Loader: Loader(id),
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Provide an image's encoded bytes. The load effect runs ONCE, at layer
|
|
44
|
+
* construction — never at frame time; compose retries on the load effect.
|
|
45
|
+
*/
|
|
46
|
+
export const layer = (image, load) => Layer.effect(image.Loader, load.pipe(Effect.map((bytes) => ({
|
|
47
|
+
[Resource.LoaderTypeId]: Resource.LoaderTypeId,
|
|
48
|
+
id: image.id,
|
|
49
|
+
bytes,
|
|
50
|
+
}))));
|