@fabi-play/engine 0.0.0-stage → 0.1.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/LICENSE +76 -0
- package/README.md +99 -2
- package/dist/fabi-play-engine.d.ts +2338 -0
- package/dist/fabi-play-engine.js +105 -0
- package/dist/fabi-play-engine.lite.d.ts +2338 -0
- package/dist/fabi-play-engine.lite.js +105 -0
- package/dist/fabi-play-engine.lite.umd.js +6772 -0
- package/dist/fabi-play-engine.lite.umd.min.js +105 -0
- package/dist/fabi-play-engine.umd.js +8869 -0
- package/dist/fabi-play-engine.umd.min.js +105 -0
- package/dist/index.d.ts +2321 -0
- package/dist/index.development.js +6733 -0
- package/dist/index.js +105 -0
- package/dist/integrity.json +15 -0
- package/package.json +80 -3
|
@@ -0,0 +1,2338 @@
|
|
|
1
|
+
// fabi-play-engine.js: the Fabi Play engine as a script tag. It sets window.FabiEngine; these are its types.
|
|
2
|
+
// Generated by scripts/types.mjs from the sources: do not edit.
|
|
3
|
+
//
|
|
4
|
+
// Use it next to the script, with fabi-play.d.ts (the SDK, window.FabiPlay), in a script or a // @ts-check file:
|
|
5
|
+
// /// <reference path="fabi-play.d.ts" />
|
|
6
|
+
// /// <reference path="fabi-play-engine.d.ts" />
|
|
7
|
+
// or list both in tsconfig.json / jsconfig.json "include". The types are FabiEngine.Engine, FabiEngine.Sprite, …
|
|
8
|
+
/**
|
|
9
|
+
* The Fabi Play engine, set by fabi-play-engine.js or fabi-play-engine.lite.js (load it after fabi-play.js).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* FabiEngine.createEngine({ play: FabiPlay }).then((engine) => {
|
|
13
|
+
* engine.stage.add(engine.avatars())
|
|
14
|
+
* engine.start()
|
|
15
|
+
* FabiPlay.ready()
|
|
16
|
+
* })
|
|
17
|
+
*/
|
|
18
|
+
declare namespace FabiEngine {
|
|
19
|
+
/**
|
|
20
|
+
* One body: for each landmark x, y (0–1, y downwards) and visibility (0–1), so 33 × 3 numbers.
|
|
21
|
+
* In "player space": mirrored like a mirror, whatever camera took it, so a child moving to their left moves left.
|
|
22
|
+
*/
|
|
23
|
+
export type Body = number[];
|
|
24
|
+
/**
|
|
25
|
+
* Age ranges: each has its own games. Only the range is kept, never a birth date: it is all the catalogue and the
|
|
26
|
+
* games need (simpler rules and no losing for the little ones, scores and challenges for the older ones).
|
|
27
|
+
*/
|
|
28
|
+
export type AgeRange = "3-5" | "6-8" | "9-12";
|
|
29
|
+
/** A pose the whole body holds: arms up, arms wide, arms in a V, a star (arms and legs apart), arms down. */
|
|
30
|
+
export type BodyShape = "arms-up" | "arms-wide" | "arms-v" | "star" | "arms-down";
|
|
31
|
+
/** A point: in player space (0–1 of the camera image, y downwards) or, after toScreen, in fractions of the screen. */
|
|
32
|
+
export interface Point {
|
|
33
|
+
/** 0 left, 1 right. */
|
|
34
|
+
x: number;
|
|
35
|
+
/** 0 top, 1 bottom. */
|
|
36
|
+
y: number;
|
|
37
|
+
}
|
|
38
|
+
/** One player in a frame the game gets: the body, its hands as pointers, its shape, its avatar. */
|
|
39
|
+
export interface PlayerFrame {
|
|
40
|
+
/** The player, from 1. */
|
|
41
|
+
id: number;
|
|
42
|
+
/** The 33 landmarks, in player space (see Body; P names the indices). */
|
|
43
|
+
body: Body;
|
|
44
|
+
/**
|
|
45
|
+
* The left hand as a pointer, in fractions of the screen (0–1): the wrist, with the space the arms reach around the
|
|
46
|
+
* shoulders stretched to the whole screen and clamped to it, so a child reaches every corner without walking. Not
|
|
47
|
+
* where the wrist is on screen: draw on the body with toScreen(wrist). null when the camera doesn't see the wrist (or the child holds Fabi Play's crossed arms).
|
|
48
|
+
*/
|
|
49
|
+
leftHand: Point | null;
|
|
50
|
+
/** The right hand as a pointer, as leftHand. */
|
|
51
|
+
rightHand: Point | null;
|
|
52
|
+
/** The pose held right now, if any. */
|
|
53
|
+
shape: BodyShape | null;
|
|
54
|
+
/** Which lane the child stands in: -1 left, 0 centre, 1 right. */
|
|
55
|
+
lane: -1 | 0 | 1;
|
|
56
|
+
/**
|
|
57
|
+
* The avatar of player id (slot id − 1): the one the child picked on the phone, or the slot's default
|
|
58
|
+
* (defaultAvatarFor). It rides on every frame, so a game never misses a change and needs no extra message. Hosts
|
|
59
|
+
* before avatars don't send it; the SDK's avatarOf falls back to the slot's default then, and for ids it does not
|
|
60
|
+
* know.
|
|
61
|
+
*/
|
|
62
|
+
avatar?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* What is behind the game's frame: 'video', the phone's camera video (mirror mode; the game paints no opaque
|
|
66
|
+
* background), or 'none', the screen's plain background (the game paints its own).
|
|
67
|
+
*/
|
|
68
|
+
export type Backdrop = "video" | "none";
|
|
69
|
+
/**
|
|
70
|
+
* Where the whole camera image is, in fractions of the game's viewport (0–1; it may reach past the edges when the
|
|
71
|
+
* image is cropped to fill the screen), with any backdrop, so avatars and hands are where the children stand (a
|
|
72
|
+
* "virtual mirror"). The screen decides it: with 'video', where the video is drawn (cover or contain); with 'none',
|
|
73
|
+
* the whole image contained at 92% of the viewport's height, bottom-aligned. Games don't compute it: they just use
|
|
74
|
+
* it. Body points are in player space (0–1 of the camera image, already mirrored like the video), so a point p is on
|
|
75
|
+
* screen at (x + p.x·w, y + p.y·h): see toScreen.
|
|
76
|
+
*/
|
|
77
|
+
export interface BackdropView {
|
|
78
|
+
/** Left edge, a fraction of the viewport's width. */
|
|
79
|
+
x: number;
|
|
80
|
+
/** Top edge, a fraction of the viewport's height. */
|
|
81
|
+
y: number;
|
|
82
|
+
/** Width, a fraction of the viewport's width. */
|
|
83
|
+
w: number;
|
|
84
|
+
/** Height, a fraction of the viewport's height. */
|
|
85
|
+
h: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Small pure helpers: 2D affine matrices, colours, easing. No DOM here, so they run in Node tests.
|
|
89
|
+
*/
|
|
90
|
+
/** An affine transform [a, b, c, d, e, f]: x' = a·x + c·y + e, y' = b·x + d·y + f (Canvas 2D's order). */
|
|
91
|
+
export type Mat = Float64Array;
|
|
92
|
+
/** A colour as 0xRRGGBB (alpha separate), or a CSS string. */
|
|
93
|
+
export type ColorLike = number | string;
|
|
94
|
+
/**
|
|
95
|
+
* A CSS colour as [0xRRGGBB, alpha 0–1]: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb(), rgba() and a few names. Anything
|
|
96
|
+
* else is resolved by a canvas when there is one (cached), else white.
|
|
97
|
+
*/
|
|
98
|
+
export function parseColor(c: ColorLike): [
|
|
99
|
+
number,
|
|
100
|
+
number
|
|
101
|
+
];
|
|
102
|
+
/**
|
|
103
|
+
* A tint and an alpha as the GPU wants them: premultiplied RGBA in one 32-bit word (little-endian byte order R, G,
|
|
104
|
+
* B, A, as an UNSIGNED_BYTE vec4 attribute reads it). additive: alpha 0, so ONE / ONE_MINUS_SRC_ALPHA blending adds.
|
|
105
|
+
*/
|
|
106
|
+
export function packColor(rgb: number, alpha: number, additive?: boolean): number;
|
|
107
|
+
/** An easing function: progress 0–1 in, eased progress out (0 at 0, 1 at 1). */
|
|
108
|
+
export type Ease = (t: number) => number;
|
|
109
|
+
/**
|
|
110
|
+
* The easings by name, for engine.tween: linear, quadIn, quadOut (the default), quadInOut, cubicOut, sineInOut,
|
|
111
|
+
* backOut (overshoots a little), elasticOut, bounceOut.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* engine.tween(star, { y: 0 }, 0.6, 'bounceOut')
|
|
115
|
+
*/
|
|
116
|
+
export const EASE: {
|
|
117
|
+
linear: (t: number) => number;
|
|
118
|
+
quadIn: (t: number) => number;
|
|
119
|
+
quadOut: (t: number) => number;
|
|
120
|
+
quadInOut: (t: number) => number;
|
|
121
|
+
cubicOut: (t: number) => number;
|
|
122
|
+
sineInOut: (t: number) => number;
|
|
123
|
+
backOut: (t: number) => number;
|
|
124
|
+
elasticOut: (t: number) => number;
|
|
125
|
+
bounceOut: (t: number) => number;
|
|
126
|
+
};
|
|
127
|
+
/** The name of an easing in EASE. */
|
|
128
|
+
export type EaseName = keyof typeof EASE;
|
|
129
|
+
/** A seedable random number generator (mulberry32), for tests and replays. */
|
|
130
|
+
export function rng(seed: number): () => number;
|
|
131
|
+
/**
|
|
132
|
+
* A filter on a node (node.filters): made by tint(), brightness(), blur(), shader() and the other factories. Its
|
|
133
|
+
* parameters are plain fields, changed (or tweened) any time without allocating.
|
|
134
|
+
*/
|
|
135
|
+
export interface Filter {
|
|
136
|
+
/** Off: skipped, as if not in the list (cheaper than taking it out and back). */
|
|
137
|
+
enabled: boolean;
|
|
138
|
+
/** @internal F_COLOR, F_BLUR, F_SHADOW or F_SHADER. */
|
|
139
|
+
readonly kind: number;
|
|
140
|
+
/** @internal Colour filters: the 4×5 matrix (row-major, offsets 0–1) into out. */
|
|
141
|
+
toMatrix?(out: Float32Array): void;
|
|
142
|
+
/**
|
|
143
|
+
* @internal Canvas 2D: the CSS filter function (lengths in backing pixels), or null when CSS can't say it. svg: the
|
|
144
|
+
* runtime's SVG colour-matrix filters, url(#…) for a matrix (null without a document).
|
|
145
|
+
*/
|
|
146
|
+
css?(scale: number, maxBlur: number, svg?: (m: ArrayLike<number>) => string | null): string | null;
|
|
147
|
+
/**
|
|
148
|
+
* @internal Canvas 2D: writes the parameters its plan depends on into out from i (at most 20); the index after them.
|
|
149
|
+
* Canvas 2D keys plans by these values, so a list rebuilt with the same values reuses its plan.
|
|
150
|
+
*/
|
|
151
|
+
params?(out: Float64Array, i: number): number;
|
|
152
|
+
/** @internal Canvas 2D: applies itself in place to a canvas holding only the node, in the region x, y, w, h (pixels). */
|
|
153
|
+
paint2d?(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number): void;
|
|
154
|
+
/** @internal How far it reaches past the node's pixels, stage units. */
|
|
155
|
+
pad?(maxBlur: number): number;
|
|
156
|
+
}
|
|
157
|
+
/** Which backend draws: engine.renderer.kind ('webgpu' is reserved for later). */
|
|
158
|
+
export type RendererKind = "webgpu" | "webgl2" | "webgl1" | "canvas2d";
|
|
159
|
+
/** What a texture is made from: a canvas (baked shapes, text, atlas pages) or a decoded image. */
|
|
160
|
+
export type TextureSource = HTMLCanvasElement | HTMLImageElement | ImageBitmap | OffscreenCanvas;
|
|
161
|
+
/** A texture on a renderer. */
|
|
162
|
+
export interface Texture {
|
|
163
|
+
/** Unique per texture, for batching. */
|
|
164
|
+
readonly id: number;
|
|
165
|
+
/** Pixels. */
|
|
166
|
+
readonly width: number;
|
|
167
|
+
/** Pixels. */
|
|
168
|
+
readonly height: number;
|
|
169
|
+
/** Kept to upload again after a context loss, and for Canvas 2D's drawImage. */
|
|
170
|
+
source: TextureSource;
|
|
171
|
+
}
|
|
172
|
+
/** A region of a texture, in its pixels. */
|
|
173
|
+
export interface Region {
|
|
174
|
+
/** Left, pixels. */
|
|
175
|
+
x: number;
|
|
176
|
+
/** Top, pixels. */
|
|
177
|
+
y: number;
|
|
178
|
+
/** Width, pixels. */
|
|
179
|
+
w: number;
|
|
180
|
+
/** Height, pixels. */
|
|
181
|
+
h: number;
|
|
182
|
+
}
|
|
183
|
+
/** How a node's pixels mix with what is under them: 'normal', or 'add' (light adds up: glows, sparks). */
|
|
184
|
+
export type Blend = "normal" | "add";
|
|
185
|
+
/** What a frame cost the renderer. */
|
|
186
|
+
export interface FrameStats {
|
|
187
|
+
/** Draw calls (GL) in the frame. */
|
|
188
|
+
drawCalls: number;
|
|
189
|
+
/** Quads drawn in the frame. */
|
|
190
|
+
quads: number;
|
|
191
|
+
/** Filters skipped in the frame: not supported by this renderer (shaders on Canvas 2D, …), or not compiling. */
|
|
192
|
+
filtersSkipped: number;
|
|
193
|
+
}
|
|
194
|
+
/** What a renderer can do with filters. */
|
|
195
|
+
export interface FilterSupport {
|
|
196
|
+
/** Colour filters, blur and drop shadows: always on GL; on Canvas 2D where ctx.filter works (tint works anyway). */
|
|
197
|
+
readonly filters: boolean;
|
|
198
|
+
/** Custom shaders (shader()): WebGL only. */
|
|
199
|
+
readonly shaders: boolean;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* engine.renderer: the backend that draws (WebGL2, WebGL1 or Canvas 2D). Games read kind for logs; the scene graph
|
|
203
|
+
* and the avatars draw with the rest.
|
|
204
|
+
*/
|
|
205
|
+
export interface Renderer {
|
|
206
|
+
/** 'webgl2', 'webgl1' or 'canvas2d'. */
|
|
207
|
+
readonly kind: RendererKind;
|
|
208
|
+
/** The canvas drawn in (after a switch to Canvas 2D, a fresh one in the old one's place). */
|
|
209
|
+
readonly canvas: HTMLCanvasElement;
|
|
210
|
+
/** The largest texture side it takes, pixels. */
|
|
211
|
+
readonly maxTextureSize: number;
|
|
212
|
+
/** GL: instancing and vertex array objects in use; for logs. */
|
|
213
|
+
readonly features: Record<string, boolean | number | string>;
|
|
214
|
+
/** What it can do with filters. */
|
|
215
|
+
readonly support: FilterSupport;
|
|
216
|
+
/** The largest blur, stage units (the engine sets the tier's; 0: blurs off, drop shadows hard). */
|
|
217
|
+
maxBlur: number;
|
|
218
|
+
/** Game time, seconds (the engine sets it every frame): a custom shader's uTime. */
|
|
219
|
+
time: number;
|
|
220
|
+
/** The backing store, in pixels. */
|
|
221
|
+
resize(width: number, height: number): void;
|
|
222
|
+
/** A texture of source (uploaded now). */
|
|
223
|
+
createTexture(source: TextureSource): Texture;
|
|
224
|
+
/** source drawn into texture at (x, y): the whole texture by default. */
|
|
225
|
+
updateTexture(texture: Texture, source?: TextureSource, x?: number, y?: number): void;
|
|
226
|
+
/** Frees a texture. */
|
|
227
|
+
destroyTexture(texture: Texture): void;
|
|
228
|
+
/** Starts a frame: clear (0xRRGGBB and alpha; alpha 0 for a transparent stage), scale: pixels per stage unit. */
|
|
229
|
+
begin(clear: number, clearAlpha: number, scale: number): void;
|
|
230
|
+
/** Draws region r of t over the local rectangle (0, 0)–(w, h), transformed by m (stage units). */
|
|
231
|
+
quad(t: Texture, r: Region, m: Mat, w: number, h: number, tint: number, alpha: number, blend: Blend): void;
|
|
232
|
+
/**
|
|
233
|
+
* @internal quad with w, h and alpha in QUAD (0, 1, 2): the scene's hot path, numbers passed through an array
|
|
234
|
+
* (as arguments they would be boxed on every call).
|
|
235
|
+
*/
|
|
236
|
+
quadQ(t: Texture, r: Region, m: Mat, tint: number, blend: Blend): void;
|
|
237
|
+
/**
|
|
238
|
+
* @internal A nine-slice drawn as one picture, alpha in QUAD[2]: frame region r of t cut by cols and rows (each
|
|
239
|
+
* [start, size, texture start, texture size] × 3, stage units then pixels of r), the box at (0, 0) of m. Optional:
|
|
240
|
+
* Canvas 2D antialiases every drawImage's edges under a rotation, so pieces drawn one by one would show hairline
|
|
241
|
+
* seams where they meet. False: not done (axis-aligned, or no such method), draw the pieces with quadQ.
|
|
242
|
+
*/
|
|
243
|
+
nineQ?(t: Texture, r: Region, cols: ArrayLike<number>, rows: ArrayLike<number>, m: Mat, tint: number, blend: Blend): boolean;
|
|
244
|
+
/** A filled ellipse centred at (cx, cy), radii rx, ry, rotated by rot radians; color 0xRRGGBB. */
|
|
245
|
+
ellipse(cx: number, cy: number, rx: number, ry: number, rot: number, color: number, alpha: number): void;
|
|
246
|
+
/** A filled polygon: pts x0, y0, x1, y1, … (n points). */
|
|
247
|
+
polygon(pts: ArrayLike<number>, n: number, color: number, alpha: number): void;
|
|
248
|
+
/** A polyline of width w, round joins and caps; closed joins the last point to the first. */
|
|
249
|
+
stroke(pts: ArrayLike<number>, n: number, w: number, color: number, alpha: number, closed: boolean): void;
|
|
250
|
+
/**
|
|
251
|
+
* Clips what follows to an axis-aligned rectangle of the stage (stage units, snapped to whole backing pixels), until
|
|
252
|
+
* the next clip or unclip. It holds inside layers and for their composites. GL clips quads on the CPU where they
|
|
253
|
+
* stay axis-aligned and uses the scissor only for the others, so a clip rarely breaks the batch.
|
|
254
|
+
*/
|
|
255
|
+
clip(x: number, y: number, w: number, h: number): void;
|
|
256
|
+
/** Turns the clip off. */
|
|
257
|
+
unclip(): void;
|
|
258
|
+
/**
|
|
259
|
+
* Draws what follows, until endLayer, into an offscreen layer the size of the canvas (cleared), then composites
|
|
260
|
+
* the layer at alpha: a faint avatar is see-through as a whole, without its overlapping parts showing through each
|
|
261
|
+
* other. Layers nest up to MAX_LAYERS deep; past that, a layer is drawn straight through (and its mask and filters
|
|
262
|
+
* ignored).
|
|
263
|
+
*
|
|
264
|
+
* @returns false when it is drawn straight through (too deep, or no layer could be made): then draw what follows at
|
|
265
|
+
* the alpha yourself, endLayer's alpha does not apply.
|
|
266
|
+
*/
|
|
267
|
+
beginLayer(): boolean;
|
|
268
|
+
/**
|
|
269
|
+
* Inside a layer: what follows, until endLayer, is drawn into a mask instead. At endLayer the layer keeps its
|
|
270
|
+
* pixels only where the mask is opaque (times the mask's alpha), then is composited.
|
|
271
|
+
*/
|
|
272
|
+
beginMask(): void;
|
|
273
|
+
/**
|
|
274
|
+
* Composites the layer begun by beginLayer at alpha (masked first, if beginMask was called), through filters if
|
|
275
|
+
* given (the enabled ones, in order). Only the area drawn into the layer is composited (plus what filters reach).
|
|
276
|
+
*/
|
|
277
|
+
endLayer(alpha: number, filters?: readonly Filter[] | null): void;
|
|
278
|
+
/** Ends the frame (flushes the batch); what it cost. */
|
|
279
|
+
end(): FrameStats;
|
|
280
|
+
/** The context was lost (stop drawing) or restored (textures already uploaded again). */
|
|
281
|
+
onLost(listener: () => void): void;
|
|
282
|
+
/** Called when the context comes back, textures already uploaded again. */
|
|
283
|
+
onRestored(listener: () => void): void;
|
|
284
|
+
/** The GL context is lost right now: nothing is drawn. */
|
|
285
|
+
readonly lost: boolean;
|
|
286
|
+
/** Information for logs and the tier cache key (GPU string where allowed). */
|
|
287
|
+
readonly info: Record<string, unknown>;
|
|
288
|
+
/** Frees the context and every texture. */
|
|
289
|
+
destroy(): void;
|
|
290
|
+
}
|
|
291
|
+
/** How deep layers nest (a masked group inside a masked group…); a mask counts as one. */
|
|
292
|
+
export const MAX_LAYERS = 4;
|
|
293
|
+
/** A rectangle in an atlas page, pixels. */
|
|
294
|
+
export interface Rect {
|
|
295
|
+
/** Left, pixels. */
|
|
296
|
+
x: number;
|
|
297
|
+
/** Top, pixels. */
|
|
298
|
+
y: number;
|
|
299
|
+
/** Width, pixels. */
|
|
300
|
+
w: number;
|
|
301
|
+
/** Height, pixels. */
|
|
302
|
+
h: number;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Shelf packing: rows ("shelves") of the height of their first item, filled left to right. Freed rectangles go to a
|
|
306
|
+
* free list and are reused by items that fit them without wasting more than half. Pure: tested in Node.
|
|
307
|
+
*/
|
|
308
|
+
export class ShelfPacker {
|
|
309
|
+
/** The page's width, pixels. */
|
|
310
|
+
readonly width: number;
|
|
311
|
+
/** The page's height, pixels. */
|
|
312
|
+
readonly height: number;
|
|
313
|
+
/** Pixels kept free around each item (no bleeding when sampled). */
|
|
314
|
+
readonly padding: number;
|
|
315
|
+
private shelves;
|
|
316
|
+
private free;
|
|
317
|
+
private bottom;
|
|
318
|
+
/** A page of width × height pixels, with padding pixels kept around each item. */
|
|
319
|
+
constructor(
|
|
320
|
+
/** The page's width, pixels. */
|
|
321
|
+
width: number,
|
|
322
|
+
/** The page's height, pixels. */
|
|
323
|
+
height: number,
|
|
324
|
+
/** Pixels kept free around each item (no bleeding when sampled). */
|
|
325
|
+
padding?: number);
|
|
326
|
+
/** A place for a w × h item (padding added around it), or null if the page is full. */
|
|
327
|
+
alloc(w: number, h: number): Rect | null;
|
|
328
|
+
/** Gives a rectangle from alloc back. */
|
|
329
|
+
release(r: Rect): void;
|
|
330
|
+
/** Share of the page's height used by shelves. */
|
|
331
|
+
get fill(): number;
|
|
332
|
+
/** Empties the page. */
|
|
333
|
+
reset(): void;
|
|
334
|
+
private inner;
|
|
335
|
+
}
|
|
336
|
+
/** A texture as the engine holds it: its pixels (kept for context loss and renderer swaps) and the renderer's copy. */
|
|
337
|
+
export interface Surface {
|
|
338
|
+
/** The pixels. */
|
|
339
|
+
source: HTMLCanvasElement | HTMLImageElement | ImageBitmap;
|
|
340
|
+
/** The renderer's texture of it, made on first draw (null until then, and after a renderer swap). */
|
|
341
|
+
texture: Texture | null;
|
|
342
|
+
/** Regions baked since the last upload: [scratch canvas, x, y]. */
|
|
343
|
+
pending: [
|
|
344
|
+
HTMLCanvasElement,
|
|
345
|
+
number,
|
|
346
|
+
number
|
|
347
|
+
][];
|
|
348
|
+
}
|
|
349
|
+
/** A frame: a region of a surface, in its pixels. scale: source pixels per stage unit (baked at the backing resolution). */
|
|
350
|
+
export interface Frame extends Region {
|
|
351
|
+
/** The surface (atlas page or image) it is a region of. */
|
|
352
|
+
surface: Surface;
|
|
353
|
+
/** Pixels per stage unit it was baked at: the frame is drawn w / scale × h / scale stage units by default. */
|
|
354
|
+
scale: number;
|
|
355
|
+
/** The atlas generation it belongs to: frames of an older one were wiped. 0: not in the atlas (an image). */
|
|
356
|
+
gen: number;
|
|
357
|
+
/** Where the frame's origin sits inside it, in stage units (the centre of a shape, the baseline of a text). */
|
|
358
|
+
ox: number;
|
|
359
|
+
/** Vertical position of the origin inside the frame, stage units. */
|
|
360
|
+
oy: number;
|
|
361
|
+
/** Paint around the content (outline, glow), stage units: the content box is the frame minus this on every side. */
|
|
362
|
+
margin: number;
|
|
363
|
+
/** A trimmed frame of a loaded atlas: the untrimmed width, pixels (the region sits at tx, ty inside it). */
|
|
364
|
+
sw?: number;
|
|
365
|
+
/** The untrimmed height, pixels. */
|
|
366
|
+
sh?: number;
|
|
367
|
+
/** Where the trimmed region starts inside the untrimmed frame, pixels. */
|
|
368
|
+
tx?: number;
|
|
369
|
+
/** Where the trimmed region starts inside the untrimmed frame, pixels (down). */
|
|
370
|
+
ty?: number;
|
|
371
|
+
/**
|
|
372
|
+
* Nine-slice insets, pixels, [top, right, bottom, left] (CSS order): a loaded atlas's `scale9Borders`. Sprites of
|
|
373
|
+
* the frame are nine-sliced by default.
|
|
374
|
+
*/
|
|
375
|
+
slice?: Insets;
|
|
376
|
+
}
|
|
377
|
+
/** Nine-slice insets: [top, right, bottom, left], pixels of the frame (CSS order). */
|
|
378
|
+
export type Insets = readonly [
|
|
379
|
+
top: number,
|
|
380
|
+
right: number,
|
|
381
|
+
bottom: number,
|
|
382
|
+
left: number
|
|
383
|
+
];
|
|
384
|
+
/** Pages of baked art. bake() draws into a scratch canvas, copies it into a page and queues the upload. */
|
|
385
|
+
export class Atlas {
|
|
386
|
+
/** A page's side, pixels. */
|
|
387
|
+
readonly pageSize: number;
|
|
388
|
+
/** Pages kept before the atlas is wiped. */
|
|
389
|
+
readonly maxPages: number;
|
|
390
|
+
/** The pages, each a canvas with its packer. */
|
|
391
|
+
readonly pages: {
|
|
392
|
+
surface: Surface;
|
|
393
|
+
packer: ShelfPacker;
|
|
394
|
+
ctx: CanvasRenderingContext2D;
|
|
395
|
+
}[];
|
|
396
|
+
/** The generation: bumped when the atlas is wiped (frames of an older one are baked again). */
|
|
397
|
+
gen: number;
|
|
398
|
+
/** Called when a page is added (the engine keeps track of surfaces). */
|
|
399
|
+
onPage: (s: Surface) => void;
|
|
400
|
+
/** Called when a page is dropped by a wipe: free its GPU texture (renderer.destroyTexture). */
|
|
401
|
+
onDrop: (s: Surface) => void;
|
|
402
|
+
/** Pages of pageSize × pageSize pixels, at most maxPages before a wipe. */
|
|
403
|
+
constructor(
|
|
404
|
+
/** A page's side, pixels. */
|
|
405
|
+
pageSize?: number,
|
|
406
|
+
/** Pages kept before the atlas is wiped. */
|
|
407
|
+
maxPages?: number);
|
|
408
|
+
/** Whether a frame is still in the atlas (not wiped by a reset). */
|
|
409
|
+
valid(f: Frame | null | undefined): f is Frame;
|
|
410
|
+
/**
|
|
411
|
+
* A w × h region (pixels) drawn by draw(ctx) with (0, 0) at its top-left: a frame of it. A new page is made while
|
|
412
|
+
* there are fewer than maxPages, or for an item bigger than a page (a page of its own, its size). When every page
|
|
413
|
+
* is full the atlas is wiped (gen changes, every frame is baked again on its next use): the first maxPages pages of
|
|
414
|
+
* the usual size are kept, emptied, the others dropped, and the item goes on the first page.
|
|
415
|
+
*/
|
|
416
|
+
bake(w: number, h: number, draw: (ctx: CanvasRenderingContext2D) => void, scale?: number, ox?: number, oy?: number, margin?: number): Frame;
|
|
417
|
+
/** An image or canvas put into the atlas as it is. */
|
|
418
|
+
add(source: CanvasImageSource & {
|
|
419
|
+
width: number;
|
|
420
|
+
height: number;
|
|
421
|
+
}, scale?: number, ox?: number, oy?: number): Frame;
|
|
422
|
+
/** Gives a frame's place back (texts that changed size). */
|
|
423
|
+
release(f: Frame): void;
|
|
424
|
+
/**
|
|
425
|
+
* Bakes again into the same place, if the new size fits in it (texts that change): the frame, updated, or null
|
|
426
|
+
* (bake a new one).
|
|
427
|
+
*/
|
|
428
|
+
rebake(f: Frame, w: number, h: number, draw: (ctx: CanvasRenderingContext2D) => void, maxW?: number, maxH?: number): Frame | null;
|
|
429
|
+
private put;
|
|
430
|
+
private newPage;
|
|
431
|
+
/**
|
|
432
|
+
* Every page emptied: frames of the old generation are baked again when used. Pages past maxPages, and pages made
|
|
433
|
+
* bigger for one big item, are dropped (onDrop frees their textures).
|
|
434
|
+
*/
|
|
435
|
+
wipe(): void;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* How a text looks (engine.text(string, style)). Texts are baked once into the atlas, and again only when the string
|
|
439
|
+
* or the style changes.
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* engine.text('0', { x: 40, y: 40, size: 48, align: 'left', outline: true })
|
|
443
|
+
*/
|
|
444
|
+
export interface TextStyle {
|
|
445
|
+
/** Font size, stage units. */
|
|
446
|
+
size?: number;
|
|
447
|
+
/** CSS font family list. */
|
|
448
|
+
font?: string;
|
|
449
|
+
/** CSS font weight (default 700). */
|
|
450
|
+
weight?: number | string;
|
|
451
|
+
/** CSS colour (default cream, #FBEFE3). */
|
|
452
|
+
color?: string;
|
|
453
|
+
/** 'left': x is the start; 'center' (default); 'right': x is the end. y is always the middle of the line. */
|
|
454
|
+
align?: "left" | "center" | "right";
|
|
455
|
+
/** A dark outline (true: the default halo colour) of this width (stage units). */
|
|
456
|
+
outline?: boolean | string;
|
|
457
|
+
/** The outline's width, stage units. */
|
|
458
|
+
outlineWidth?: number;
|
|
459
|
+
/** A soft shadow (true: the default halo colour), blur in stage units. */
|
|
460
|
+
shadow?: boolean | string;
|
|
461
|
+
/** The shadow's blur radius, stage units. */
|
|
462
|
+
shadowBlur?: number;
|
|
463
|
+
/**
|
|
464
|
+
* Draw with a bitmap font (its name, from engine.load or engine.bitmapFont, or the font): no bake per string, for
|
|
465
|
+
* texts that change often. size scales the font; the look (colour, outline) is the font's, and tint multiplies it.
|
|
466
|
+
*/
|
|
467
|
+
bitmapFont?: string | BitmapFont;
|
|
468
|
+
}
|
|
469
|
+
/** The font texts use by default: Fredoka if the page loaded it, then rounded fallbacks. */
|
|
470
|
+
export const DEFAULT_FONT = "Fredoka, Nunito, \"Varela Round\", system-ui, sans-serif";
|
|
471
|
+
/** The dark halo that makes text read over a real room or a light avatar. */
|
|
472
|
+
export const HALO = "rgba(10,16,30,0.75)";
|
|
473
|
+
/**
|
|
474
|
+
* A line of text, baked into the atlas: cheap to move, scale and fade; changing text or style bakes it again (fine
|
|
475
|
+
* for a score, not every frame). With a bitmap font nothing is baked: it may change every frame.
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* const score = stage.add(engine.text('0', { x: 40, y: 40, size: 48, align: 'left', outline: true }))
|
|
479
|
+
* score.text = String(points)
|
|
480
|
+
*/
|
|
481
|
+
export class Text extends Sprite {
|
|
482
|
+
private _text;
|
|
483
|
+
private _style;
|
|
484
|
+
/** The string or style changed since the last bake. */
|
|
485
|
+
private dirty;
|
|
486
|
+
/** What the current frame was baked with. */
|
|
487
|
+
private bakedScale;
|
|
488
|
+
private bakedFontGen;
|
|
489
|
+
private bakedEffects;
|
|
490
|
+
/** The slot's capacity, pixels, so a longer string may still fit. */
|
|
491
|
+
private capW;
|
|
492
|
+
private capH;
|
|
493
|
+
/** Use engine.text(text, style). */
|
|
494
|
+
constructor(kernel: Kernel, text: string, style?: TextStyle & NodeProps);
|
|
495
|
+
/** The string shown: setting another bakes the text again (at the next draw); a bitmap text bakes nothing. */
|
|
496
|
+
get text(): string;
|
|
497
|
+
set text(v: string);
|
|
498
|
+
/** The style it is drawn with; setting one merges it into the current one and bakes again. */
|
|
499
|
+
get style(): TextStyle;
|
|
500
|
+
/** A new style (merged with the current one). */
|
|
501
|
+
set style(s: TextStyle);
|
|
502
|
+
/** The bitmap font it is drawn with, or null (a baked text). Sets the anchor from align while it is automatic. */
|
|
503
|
+
private bitmap;
|
|
504
|
+
/** The content width, stage units: the text's own. */
|
|
505
|
+
get naturalWidth(): number;
|
|
506
|
+
/** The content height, stage units: the line's. */
|
|
507
|
+
get naturalHeight(): number;
|
|
508
|
+
protected frameNow(): Frame | null;
|
|
509
|
+
/** Draws the text (the engine calls it): its baked frame, or its glyphs. */
|
|
510
|
+
draw(ctx: DrawContext, alpha: number, tint: number): void;
|
|
511
|
+
/** Removes the text and gives its place in the atlas back. */
|
|
512
|
+
destroy(): void;
|
|
513
|
+
private bake;
|
|
514
|
+
/** bake's slow path, apart: its closure's context would otherwise be made on every call, even the cheap ones. */
|
|
515
|
+
private rebake;
|
|
516
|
+
}
|
|
517
|
+
/** A glyph of a bitmap font: its frame and where it goes, in the font's units (stage units at its size). */
|
|
518
|
+
export interface Glyph {
|
|
519
|
+
/** The glyph's picture. */
|
|
520
|
+
frame: Frame;
|
|
521
|
+
/** From the pen to the picture's left edge. */
|
|
522
|
+
x: number;
|
|
523
|
+
/** From the line's top to the picture's top. */
|
|
524
|
+
y: number;
|
|
525
|
+
/** The picture's width. */
|
|
526
|
+
w: number;
|
|
527
|
+
/** The picture's height. */
|
|
528
|
+
h: number;
|
|
529
|
+
/** How far the pen moves after it. */
|
|
530
|
+
advance: number;
|
|
531
|
+
}
|
|
532
|
+
/** A BMFont file, parsed: sizes in its pixels. */
|
|
533
|
+
export interface BMFontData {
|
|
534
|
+
/** The size it was made at (info size). */
|
|
535
|
+
size: number;
|
|
536
|
+
/** Distance between lines. */
|
|
537
|
+
lineHeight: number;
|
|
538
|
+
/** From the line's top to the baseline. */
|
|
539
|
+
base: number;
|
|
540
|
+
/** Page image files, by page id. */
|
|
541
|
+
pages: string[];
|
|
542
|
+
/** The glyphs: where each is on its page and how it sits on the line. */
|
|
543
|
+
chars: {
|
|
544
|
+
id: number;
|
|
545
|
+
x: number;
|
|
546
|
+
y: number;
|
|
547
|
+
width: number;
|
|
548
|
+
height: number;
|
|
549
|
+
xoffset: number;
|
|
550
|
+
yoffset: number;
|
|
551
|
+
xadvance: number;
|
|
552
|
+
page: number;
|
|
553
|
+
}[];
|
|
554
|
+
/** Pairs moved closer (negative) or apart. */
|
|
555
|
+
kernings: {
|
|
556
|
+
first: number;
|
|
557
|
+
second: number;
|
|
558
|
+
amount: number;
|
|
559
|
+
}[];
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* A BMFont description: text .fnt (`char id=48 x=0 …` lines), XML .fnt (`<char id="48" … />`), or JSON (a string
|
|
563
|
+
* or an object: `{ info, common, pages, chars, kernings }`).
|
|
564
|
+
*/
|
|
565
|
+
export function parseBMFont(src: string | object): BMFontData;
|
|
566
|
+
/** The look of a generated bitmap font (as engine.text's style), and the characters to bake up front. */
|
|
567
|
+
export interface BitmapFontOptions extends Pick<TextStyle, "size" | "font" | "weight" | "color" | "outline" | "outlineWidth" | "shadow" | "shadowBlur"> {
|
|
568
|
+
/** Characters baked now (default the digits); any other is baked the first time it is drawn. */
|
|
569
|
+
chars?: string;
|
|
570
|
+
}
|
|
571
|
+
/** What a generated font needs from the engine, read at each draw. */
|
|
572
|
+
export interface GlyphHost {
|
|
573
|
+
readonly atlas: Atlas;
|
|
574
|
+
readonly pixelScale: number;
|
|
575
|
+
readonly fontGen: number;
|
|
576
|
+
readonly effects: boolean;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* A bitmap font: glyphs by code point, kerning pairs, its size and line height (stage units at size). Made by
|
|
580
|
+
* engine.load (a BMFont) or engine.bitmapFont; texts use it with { bitmapFont: name }.
|
|
581
|
+
*/
|
|
582
|
+
export class BitmapFont {
|
|
583
|
+
/** A generated font's baker (null: a BMFont, glyphs given up front). */
|
|
584
|
+
private readonly baker;
|
|
585
|
+
/** A generated font's engine side (bake again when its resolution, fonts, effects or atlas change). */
|
|
586
|
+
private readonly host;
|
|
587
|
+
/** The size its units are at: a text of this size draws the glyphs 1:1. */
|
|
588
|
+
readonly size: number;
|
|
589
|
+
/** The line's height, font units. */
|
|
590
|
+
readonly lineHeight: number;
|
|
591
|
+
/** From the line's top to the baseline, font units. */
|
|
592
|
+
readonly base: number;
|
|
593
|
+
private readonly ascii;
|
|
594
|
+
private readonly more;
|
|
595
|
+
private readonly kern;
|
|
596
|
+
private bScale;
|
|
597
|
+
private bFontGen;
|
|
598
|
+
private bEffects;
|
|
599
|
+
private bGen;
|
|
600
|
+
/** Use engine.bitmapFont or engine.load. */
|
|
601
|
+
constructor(size: number, lineHeight: number, base: number,
|
|
602
|
+
/** A generated font's baker (null: a BMFont, glyphs given up front). */
|
|
603
|
+
baker?: ((code: number) => Glyph | null) | null,
|
|
604
|
+
/** A generated font's engine side (bake again when its resolution, fonts, effects or atlas change). */
|
|
605
|
+
host?: GlyphHost | null);
|
|
606
|
+
/** Sets a glyph (BMFont loading). */
|
|
607
|
+
set(code: number, g: Glyph | null): void;
|
|
608
|
+
/** Sets a kerning pair, font units. */
|
|
609
|
+
setKerning(first: number, second: number, amount: number): void;
|
|
610
|
+
/** Before a draw: a generated font whose resolution, tier, web font or atlas changed forgets its glyphs. */
|
|
611
|
+
prepare(): void;
|
|
612
|
+
/** The glyph of a code point (baked now if the font is generated), or null. */
|
|
613
|
+
glyph(code: number): Glyph | null;
|
|
614
|
+
/** Kerning between two code points, font units (0 for most pairs). */
|
|
615
|
+
kerning(a: number, b: number): number;
|
|
616
|
+
/** A string's width, font units. */
|
|
617
|
+
measure(text: string): number;
|
|
618
|
+
/**
|
|
619
|
+
* Lays text out: its width, font units. With ctx, also draws it in the space of m, the line's top-left at (x0, y0),
|
|
620
|
+
* font units scaled by kx, ky: one quad per glyph, nothing allocated (code points read in place).
|
|
621
|
+
*/
|
|
622
|
+
run(text: string, ctx?: DrawContext, m?: Mat, x0?: number, y0?: number, kx?: number, ky?: number, tint?: number, alpha?: number, blend?: Blend): number;
|
|
623
|
+
}
|
|
624
|
+
/** How a shape is painted: fill, outline, shadow or glow. Colours are CSS. */
|
|
625
|
+
export interface Paint {
|
|
626
|
+
/** The fill colour; none without it. */
|
|
627
|
+
fill?: string;
|
|
628
|
+
/** The outline colour; none without it. */
|
|
629
|
+
stroke?: string;
|
|
630
|
+
/** Outline width, stage units. */
|
|
631
|
+
lw?: number;
|
|
632
|
+
/** The outline under the fill (only its outer half shows), as when a path is stroked before it is filled. */
|
|
633
|
+
outlineBelow?: boolean;
|
|
634
|
+
/** A soft shadow or glow around the shape: colour and blur radius (stage units), optional offset. */
|
|
635
|
+
shadow?: {
|
|
636
|
+
color: string;
|
|
637
|
+
blur: number;
|
|
638
|
+
x?: number;
|
|
639
|
+
y?: number;
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* A shape for engine.shape and engine.particles: its type and size (stage units) and its paint.
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* engine.shape({ type: 'circle', r: 30, fill: '#8CC9A0', stroke: '#fff', lw: 4 })
|
|
647
|
+
* engine.shape({ type: 'roundRect', w: 300, h: 80, r: 24, fill: 'rgba(15,24,42,0.6)' })
|
|
648
|
+
* engine.shape({ type: 'star', r: 40, fill: '#FBD46A', shadow: { color: '#FBD46A', blur: 12 } })
|
|
649
|
+
*/
|
|
650
|
+
export type ShapeSpec = Paint & (
|
|
651
|
+
/** A disc of radius r. */
|
|
652
|
+
{
|
|
653
|
+
type: "circle";
|
|
654
|
+
r: number;
|
|
655
|
+
}
|
|
656
|
+
/** An ellipse of radii rx, ry. */
|
|
657
|
+
| {
|
|
658
|
+
type: "ellipse";
|
|
659
|
+
rx: number;
|
|
660
|
+
ry: number;
|
|
661
|
+
}
|
|
662
|
+
/** w × h with corner radius r; drawn nine-sliced: set the node's width and height freely. */
|
|
663
|
+
| {
|
|
664
|
+
type: "roundRect";
|
|
665
|
+
w: number;
|
|
666
|
+
h: number;
|
|
667
|
+
r: number;
|
|
668
|
+
}
|
|
669
|
+
/** A star of radius r: points (5), inner radius as a share of r (0.5). */
|
|
670
|
+
| {
|
|
671
|
+
type: "star";
|
|
672
|
+
r: number;
|
|
673
|
+
points?: number;
|
|
674
|
+
inner?: number;
|
|
675
|
+
}
|
|
676
|
+
/** A circle's outline of radius r (stroke and lw). */
|
|
677
|
+
| {
|
|
678
|
+
type: "ring";
|
|
679
|
+
r: number;
|
|
680
|
+
}
|
|
681
|
+
/** A horizontal capsule `length` long and lw thick (stroke colour, or fill). */
|
|
682
|
+
| {
|
|
683
|
+
type: "line";
|
|
684
|
+
length: number;
|
|
685
|
+
}
|
|
686
|
+
/** An SVG path (any syntax Path2D takes) in a size × size box, drawn centred; scaled to `r` (half the box) if given. */
|
|
687
|
+
| {
|
|
688
|
+
type: "path";
|
|
689
|
+
d: string;
|
|
690
|
+
size: number;
|
|
691
|
+
r?: number;
|
|
692
|
+
});
|
|
693
|
+
/** A shape baked into the atlas. */
|
|
694
|
+
export interface BakedShape {
|
|
695
|
+
/** Where it is in the atlas. */
|
|
696
|
+
frame: Frame;
|
|
697
|
+
/** Nine-slice border, in frame pixels (rounded rectangles), else 0. */
|
|
698
|
+
slice: number;
|
|
699
|
+
/** Local width, stage units. */
|
|
700
|
+
w: number;
|
|
701
|
+
/** Local height, stage units. */
|
|
702
|
+
h: number;
|
|
703
|
+
}
|
|
704
|
+
/** Options of engine.timeline(). */
|
|
705
|
+
export interface TimelineOptions {
|
|
706
|
+
/** Plays after the first: 2 plays three times in all; -1 forever (until stop). Default 0. */
|
|
707
|
+
repeat?: number;
|
|
708
|
+
/** Every other play runs backwards (with repeat). */
|
|
709
|
+
yoyo?: boolean;
|
|
710
|
+
/** Seconds before it starts. */
|
|
711
|
+
delay?: number;
|
|
712
|
+
/** Made paused: call resume() to start it. */
|
|
713
|
+
paused?: boolean;
|
|
714
|
+
/** Called when the last play ends (not on stop). */
|
|
715
|
+
onComplete?: () => void;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Where a step goes: seconds from the start, or a string: 'label', 'label+=0.2', 'label-=0.2', '+=0.5' / '-=0.5'
|
|
719
|
+
* (from the current end), '<' (where the previous step started), '<+=0.1', '>' (the end, the default).
|
|
720
|
+
*/
|
|
721
|
+
export type Position = number | string;
|
|
722
|
+
/** Options of a timeline step: a tween's, and where it goes. */
|
|
723
|
+
export interface StepOptions extends TweenOptions {
|
|
724
|
+
/** Where it starts (see Position); delay is added to it. Default: the end of the timeline so far. */
|
|
725
|
+
at?: Position;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* A timeline (engine.timeline()): chain to, set, call, label and wait to build it; it starts on the next frame
|
|
729
|
+
* unless made paused. Await it (or then) for its end; stop() ends it early (where it is).
|
|
730
|
+
*/
|
|
731
|
+
export class Timeline implements PromiseLike<void> {
|
|
732
|
+
private readonly steps;
|
|
733
|
+
private readonly labels;
|
|
734
|
+
private end;
|
|
735
|
+
private prevStart;
|
|
736
|
+
private elapsed;
|
|
737
|
+
private iter;
|
|
738
|
+
private last;
|
|
739
|
+
private delayLeft;
|
|
740
|
+
private _paused;
|
|
741
|
+
private done;
|
|
742
|
+
private rewinding;
|
|
743
|
+
private readonly promise;
|
|
744
|
+
private resolve;
|
|
745
|
+
/** Plays after the first (-1: forever). */
|
|
746
|
+
repeat: number;
|
|
747
|
+
/** Every other play backwards. */
|
|
748
|
+
yoyo: boolean;
|
|
749
|
+
/** Called when the last play ends. */
|
|
750
|
+
onComplete?: () => void;
|
|
751
|
+
/** Use engine.timeline(opts). */
|
|
752
|
+
constructor(o?: TimelineOptions);
|
|
753
|
+
/** One play's length, seconds. */
|
|
754
|
+
get duration(): number;
|
|
755
|
+
/** Where the playhead is in the current play, seconds. */
|
|
756
|
+
get time(): number;
|
|
757
|
+
/** Paused by pause() (or made paused). */
|
|
758
|
+
get paused(): boolean;
|
|
759
|
+
/** It ended (all its plays, or stop()). */
|
|
760
|
+
get finished(): boolean;
|
|
761
|
+
/** Where a position is, seconds. */
|
|
762
|
+
private at;
|
|
763
|
+
private add;
|
|
764
|
+
/**
|
|
765
|
+
* Eases target's numeric fields to `to` over `seconds` (as engine.tween), after the previous step or `at`.
|
|
766
|
+
*
|
|
767
|
+
* @example
|
|
768
|
+
* engine.timeline().to(a, { x: 600 }, 0.5).to(b, { alpha: 0 }, 0.3, { at: '<' })
|
|
769
|
+
*/
|
|
770
|
+
to<T extends object>(target: T, to: Partial<Record<keyof T, number>>, seconds: number, ease?: Ease | EaseName | StepOptions): this;
|
|
771
|
+
/** Sets fields at once, when the playhead gets there. */
|
|
772
|
+
set<T extends object>(target: T, values: Partial<Record<keyof T, number>>, at?: Position): this;
|
|
773
|
+
/** Calls fn when the playhead passes this point (either way: a yoyo calls it going back too). */
|
|
774
|
+
call(fn: () => void, at?: Position): this;
|
|
775
|
+
/** Names a point (default: the end so far), for `at: 'name'` and seek('name'). */
|
|
776
|
+
label(name: string, at?: Position): this;
|
|
777
|
+
/** A pause of `seconds` at the end: the next step starts after it. */
|
|
778
|
+
wait(seconds: number): this;
|
|
779
|
+
/** Stops the clock where it is (game pauses stop it anyway). */
|
|
780
|
+
pause(): this;
|
|
781
|
+
/** Starts or goes on after pause() or { paused: true }. */
|
|
782
|
+
resume(): this;
|
|
783
|
+
/** Ends it now, fields where they are; the promise resolves. */
|
|
784
|
+
stop(): void;
|
|
785
|
+
/** Moves the playhead to a time or a label of the current play, writing the fields there. */
|
|
786
|
+
seek(pos: Position): this;
|
|
787
|
+
then<A = void, B = never>(ok?: ((v: void) => A | PromiseLike<A>) | null, fail?: ((e: unknown) => B | PromiseLike<B>) | null): Promise<A | B>;
|
|
788
|
+
/** Advances by dt seconds of game time (the engine's tweens call it). */
|
|
789
|
+
update(dt: number): void;
|
|
790
|
+
private complete;
|
|
791
|
+
/** Writes every step at playhead t: forwards in start order, backwards in reverse (see the file's comment). */
|
|
792
|
+
private render;
|
|
793
|
+
private apply;
|
|
794
|
+
}
|
|
795
|
+
/** Options of engine.tween. */
|
|
796
|
+
export interface TweenOptions {
|
|
797
|
+
/** An EASE name ('quadOut' by default, 'backOut', 'linear', …) or a function of 0–1. */
|
|
798
|
+
ease?: Ease | EaseName;
|
|
799
|
+
/** Seconds before it starts; the start values are read then. */
|
|
800
|
+
delay?: number;
|
|
801
|
+
/** Called every step with the eased progress, 0–1. */
|
|
802
|
+
onUpdate?: (k: number) => void;
|
|
803
|
+
}
|
|
804
|
+
/** A promise that resolves when the tween ends (or is stopped), with stop(). */
|
|
805
|
+
export type TweenHandle = Promise<void> & {
|
|
806
|
+
stop(): void;
|
|
807
|
+
};
|
|
808
|
+
/** The engine's tweens (engine.tween): advanced on game time, so they stop on pause and breaks. */
|
|
809
|
+
export class Tweens {
|
|
810
|
+
private readonly list;
|
|
811
|
+
private timelines;
|
|
812
|
+
/** A timeline played on the same clock (see Timeline). */
|
|
813
|
+
timeline(opts?: TimelineOptions): Timeline;
|
|
814
|
+
/** Eases target's numeric fields to `to` over `seconds`. */
|
|
815
|
+
tween<T extends object>(target: T, to: Partial<Record<keyof T, number>>, seconds: number, ease?: Ease | EaseName | TweenOptions): TweenHandle;
|
|
816
|
+
/** Stops every tween of target (they resolve where they are). */
|
|
817
|
+
stopAll(target: object): void;
|
|
818
|
+
/** Tweens running. */
|
|
819
|
+
get size(): number;
|
|
820
|
+
/** Advances every tween by dt seconds. */
|
|
821
|
+
update(dt: number): void;
|
|
822
|
+
private step;
|
|
823
|
+
private finish;
|
|
824
|
+
/** Stops every tween (they resolve where they are). */
|
|
825
|
+
clear(): void;
|
|
826
|
+
}
|
|
827
|
+
/** What nodes need from the engine (the engine makes it; a game never does). */
|
|
828
|
+
export interface Kernel {
|
|
829
|
+
/** Where frames, baked shapes and texts live. */
|
|
830
|
+
readonly atlas: Atlas;
|
|
831
|
+
/** Backing-store pixels per stage unit (texts and shapes are baked at it). */
|
|
832
|
+
readonly pixelScale: number;
|
|
833
|
+
/** Bumped when web fonts finish loading: texts bake again. */
|
|
834
|
+
readonly fontGen: number;
|
|
835
|
+
/** The tier's effects (glows, soft shadows): shapes and texts bake without their shadow when false. */
|
|
836
|
+
readonly effects: boolean;
|
|
837
|
+
/** The engine's tweens (a destroyed node's stop). */
|
|
838
|
+
readonly tweens: Tweens;
|
|
839
|
+
/** A loaded or baked frame by name. */
|
|
840
|
+
frame(name: string): Frame;
|
|
841
|
+
/** A shape baked into the atlas. */
|
|
842
|
+
shape(spec: ShapeSpec): BakedShape;
|
|
843
|
+
/** A bitmap font by name (loaded, or made with engine.bitmapFont). */
|
|
844
|
+
bitmapFont(name: string): BitmapFont;
|
|
845
|
+
}
|
|
846
|
+
/** A frame being drawn: what Node.draw gets (for custom nodes). */
|
|
847
|
+
export interface DrawContext {
|
|
848
|
+
/** The renderer to draw with. */
|
|
849
|
+
readonly r: Renderer;
|
|
850
|
+
/** The engine's side of the nodes. */
|
|
851
|
+
readonly kernel: Kernel;
|
|
852
|
+
/** A surface's texture on this renderer. */
|
|
853
|
+
tex(s: Surface): Texture;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Fields to set on a node when making it (or with node.set): position, rotation, scale, look. Stage units.
|
|
857
|
+
*
|
|
858
|
+
* @example
|
|
859
|
+
* engine.sprite('star', { x: 640, y: 360, scale: 2, anchor: 0.5, alpha: 0.8 })
|
|
860
|
+
*/
|
|
861
|
+
export interface NodeProps {
|
|
862
|
+
/** Position in the parent, stage units (y downwards). */
|
|
863
|
+
x?: number;
|
|
864
|
+
/** Position in the parent, stage units (y downwards). */
|
|
865
|
+
y?: number;
|
|
866
|
+
/** Radians, clockwise. */
|
|
867
|
+
rotation?: number;
|
|
868
|
+
/** Both scales at once. */
|
|
869
|
+
scale?: number;
|
|
870
|
+
/** Horizontal scale (negative: mirrored). */
|
|
871
|
+
scaleX?: number;
|
|
872
|
+
/** Vertical scale. */
|
|
873
|
+
scaleY?: number;
|
|
874
|
+
/** 0–1, multiplied down the tree. */
|
|
875
|
+
alpha?: number;
|
|
876
|
+
/** Hidden nodes and their children are not drawn. */
|
|
877
|
+
visible?: boolean;
|
|
878
|
+
/** A colour multiplied with the pixels (white: none), as 0xRRGGBB or CSS. */
|
|
879
|
+
tint?: ColorLike;
|
|
880
|
+
/** 'add' for glows and sparks. */
|
|
881
|
+
blend?: Blend;
|
|
882
|
+
/** Drawing order among siblings: higher on top (equal: order of adding). */
|
|
883
|
+
z?: number;
|
|
884
|
+
/** Anchor, 0–1 of the size: 0 top-left, 0.5 the middle. A number sets both. */
|
|
885
|
+
anchor?: number | {
|
|
886
|
+
x: number;
|
|
887
|
+
y: number;
|
|
888
|
+
};
|
|
889
|
+
/** Local width, stage units (sprites and shapes: stretched to it). */
|
|
890
|
+
width?: number;
|
|
891
|
+
/** Local height, stage units. */
|
|
892
|
+
height?: number;
|
|
893
|
+
/** Hit tests use a circle of this radius (local units) around the origin instead of the rectangle. */
|
|
894
|
+
hitRadius?: number;
|
|
895
|
+
/** Clips the node and its children to this rectangle, in the node's own units (null: no clip). */
|
|
896
|
+
clip?: ClipRect | null;
|
|
897
|
+
/** Shows the node and its children only where this node is opaque (null: no mask). See Node.mask. */
|
|
898
|
+
mask?: Node | null;
|
|
899
|
+
/** Filters, in order (null: none). See Node.filters. */
|
|
900
|
+
filters?: Filter[] | null;
|
|
901
|
+
}
|
|
902
|
+
/** A clip rectangle, in a node's own units. */
|
|
903
|
+
export interface ClipRect {
|
|
904
|
+
/** Left, local units. */
|
|
905
|
+
x: number;
|
|
906
|
+
/** Top, local units. */
|
|
907
|
+
y: number;
|
|
908
|
+
/** Width, local units. */
|
|
909
|
+
width: number;
|
|
910
|
+
/** Height, local units. */
|
|
911
|
+
height: number;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* The base of everything on the stage: a transform, a look and a place in a group. Make nodes with the engine
|
|
915
|
+
* (engine.sprite, shape, text, group, particles), add them to the stage or a group, then change their fields every
|
|
916
|
+
* frame.
|
|
917
|
+
*
|
|
918
|
+
* A node is a handle on a slot of the node store (src/store.ts): x, y, rotation, scale, alpha, visible, tint and its
|
|
919
|
+
* world matrix live there. After destroy() the handle is stale: its writes reach no other node, and its reads give
|
|
920
|
+
* its last values until its slot goes to a new node, then a fresh node's (hidden).
|
|
921
|
+
*/
|
|
922
|
+
export class Node {
|
|
923
|
+
protected readonly kernel: Kernel;
|
|
924
|
+
/**
|
|
925
|
+
* @internal Slot index in the node store: the accessors read slot _i and write slot _i || SINK, unchecked. Once the
|
|
926
|
+
* slot is another node's, a destroyed handle's _i is 0 (the neutral slot) and its _g -1 (store.ts, alloc).
|
|
927
|
+
*/
|
|
928
|
+
_i: number;
|
|
929
|
+
/** @internal The slot's generation when it was given to this node. */
|
|
930
|
+
_g: number;
|
|
931
|
+
/** The world view, and the slot and store epoch it was made for. */
|
|
932
|
+
private _wv;
|
|
933
|
+
private _wi;
|
|
934
|
+
private _we;
|
|
935
|
+
/** Position in the parent, stage units. */
|
|
936
|
+
get x(): number;
|
|
937
|
+
set x(v: number);
|
|
938
|
+
/** Position in the parent, stage units (y downwards). */
|
|
939
|
+
get y(): number;
|
|
940
|
+
set y(v: number);
|
|
941
|
+
/** Radians, clockwise. */
|
|
942
|
+
get rotation(): number;
|
|
943
|
+
set rotation(v: number);
|
|
944
|
+
/** Horizontal scale (negative: mirrored). */
|
|
945
|
+
get scaleX(): number;
|
|
946
|
+
set scaleX(v: number);
|
|
947
|
+
/** Vertical scale. */
|
|
948
|
+
get scaleY(): number;
|
|
949
|
+
set scaleY(v: number);
|
|
950
|
+
/** 0–1, multiplied down the tree. */
|
|
951
|
+
get alpha(): number;
|
|
952
|
+
set alpha(v: number);
|
|
953
|
+
/** Hidden nodes and their children are not drawn. Hit tests (engine.touched, engine.hits) still see them. */
|
|
954
|
+
get visible(): boolean;
|
|
955
|
+
set visible(v: boolean);
|
|
956
|
+
/** Set by destroy(): the node is gone (its handle is stale). */
|
|
957
|
+
get destroyed(): boolean;
|
|
958
|
+
/**
|
|
959
|
+
* The transform to the stage, as of the last draw (or updateWorld): a view on the node store, written in place.
|
|
960
|
+
* Read it when you need it: a view kept across frames stops following the node once the store grows (new arrays).
|
|
961
|
+
*/
|
|
962
|
+
get world(): Mat;
|
|
963
|
+
/** 'add' for glows and sparks; 'normal' otherwise. */
|
|
964
|
+
blend: Blend;
|
|
965
|
+
private _data;
|
|
966
|
+
/** Free for the game: speed, state, anything (made on first use). */
|
|
967
|
+
get data(): Record<string, any>;
|
|
968
|
+
set data(v: Record<string, any>);
|
|
969
|
+
/** The group the node is in, or null. */
|
|
970
|
+
parent: Group | null;
|
|
971
|
+
/** Hit tests use a circle of this radius (local units) around the origin instead of the rectangle; null: the rectangle. */
|
|
972
|
+
hitRadius: number | null;
|
|
973
|
+
/**
|
|
974
|
+
* Clips the node and its children to this rectangle, in the node's own units (x, y, width, height). While no
|
|
975
|
+
* rotation is above it, the renderer clips on whole pixels without breaking the batch; rotated, it is drawn as a
|
|
976
|
+
* mask. Clips nest (they intersect). Hit tests ignore it.
|
|
977
|
+
*
|
|
978
|
+
* @example
|
|
979
|
+
* list.clip = { x: 0, y: 0, width: 400, height: 300 } // a scrolling list: move its children, not the clip
|
|
980
|
+
*/
|
|
981
|
+
clip: ClipRect | null;
|
|
982
|
+
/**
|
|
983
|
+
* Shows the node and its children only where this node is opaque, times its alpha: a circle, a star, a text, any
|
|
984
|
+
* node. The mask is placed as a child of the masked node would be, and is not drawn otherwise (keep it out of the
|
|
985
|
+
* stage). The masked node is drawn into a layer: about two full-canvas composites per mask, so use a few.
|
|
986
|
+
*
|
|
987
|
+
* @example
|
|
988
|
+
* photo.mask = engine.shape({ type: 'circle', r: 80, fill: '#fff' })
|
|
989
|
+
*/
|
|
990
|
+
mask: Node | null;
|
|
991
|
+
/**
|
|
992
|
+
* Filters the node and its children go through, in order: tint, brightness, contrast, saturate, grayscale, sepia,
|
|
993
|
+
* hueRotate, invert, colorMatrix, blur, dropShadow (the same on WebGL and Canvas 2D), shader (WebGL only). The node
|
|
994
|
+
* is drawn into a layer and composited through them (the filters inside a clip and under a mask). Change their
|
|
995
|
+
* fields freely (`flash.amount = 0.5`, tweens); null or [] for none.
|
|
996
|
+
*
|
|
997
|
+
* @example
|
|
998
|
+
* import { blur, dropShadow } from '@fabi-play/engine'
|
|
999
|
+
* card.filters = [dropShadow(0, 8, 6, 'rgba(0,0,0,0.35)')]
|
|
1000
|
+
* background.filters = [blur(6)]
|
|
1001
|
+
*/
|
|
1002
|
+
filters: Filter[] | null;
|
|
1003
|
+
private _z;
|
|
1004
|
+
/** Horizontal anchor, 0–1 of the width: 0 left, 0.5 the middle. */
|
|
1005
|
+
anchorX: number;
|
|
1006
|
+
/** Vertical anchor, 0–1 of the height: 0 top, 0.5 the middle. */
|
|
1007
|
+
anchorY: number;
|
|
1008
|
+
/** @internal The node pool that made it (nodePool), if any. */
|
|
1009
|
+
_pool: {
|
|
1010
|
+
forget(n: Node): void;
|
|
1011
|
+
} | null;
|
|
1012
|
+
/** @internal Released to its pool, waiting for get(). */
|
|
1013
|
+
_free: boolean;
|
|
1014
|
+
/** @internal Registered to give its slots back if dropped without destroy() (store.ts, track). */
|
|
1015
|
+
_reg: boolean;
|
|
1016
|
+
/** Nodes are made by the engine (engine.sprite, engine.group, …); a custom node subclass passes the kernel on. */
|
|
1017
|
+
constructor(kernel: Kernel);
|
|
1018
|
+
/** Both scales: reads scaleX, sets scaleX and scaleY. */
|
|
1019
|
+
get scale(): number;
|
|
1020
|
+
set scale(v: number);
|
|
1021
|
+
/** Drawing order among siblings: higher on top (equal: order of adding). */
|
|
1022
|
+
get z(): number;
|
|
1023
|
+
set z(v: number);
|
|
1024
|
+
/** A colour multiplied with the node's pixels (white: none), as 0xRRGGBB or CSS. */
|
|
1025
|
+
get tint(): number;
|
|
1026
|
+
set tint(v: ColorLike);
|
|
1027
|
+
/** Sets anchorX and anchorY: a number for both, or { x, y }. 0 top-left, 0.5 the middle. */
|
|
1028
|
+
set anchor(v: number | {
|
|
1029
|
+
x: number;
|
|
1030
|
+
y: number;
|
|
1031
|
+
});
|
|
1032
|
+
/** Local size, for hit tests and anchors; groups have none. */
|
|
1033
|
+
get width(): number;
|
|
1034
|
+
set width(_: number);
|
|
1035
|
+
/** Local height, for hit tests and anchors; groups have none. */
|
|
1036
|
+
get height(): number;
|
|
1037
|
+
set height(_: number);
|
|
1038
|
+
/** Sets many fields at once. */
|
|
1039
|
+
set(props: NodeProps): this;
|
|
1040
|
+
/** Removes the node: invisible at once (its parent null), out of its group's children at the end of the frame. Its tweens stop. */
|
|
1041
|
+
destroy(): void;
|
|
1042
|
+
/** world, computed now from the parents (for hit tests between draws). */
|
|
1043
|
+
updateWorld(): Mat;
|
|
1044
|
+
/** Draws the node; world is already composed. */
|
|
1045
|
+
draw(_ctx: DrawContext, _alpha: number, _tint: number): void;
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* A node holding others: they move, rotate, scale, fade and tint with it.
|
|
1049
|
+
*
|
|
1050
|
+
* @example
|
|
1051
|
+
* const stars = stage.add(engine.group())
|
|
1052
|
+
* stars.add(engine.shape({ type: 'star', r: 40, fill: '#FBD46A' }, { x: 100 }))
|
|
1053
|
+
* for (const star of stars.children) star.y += 150 * dt
|
|
1054
|
+
*/
|
|
1055
|
+
export class Group extends Node {
|
|
1056
|
+
/** The nodes in it, in drawing order (one shared empty array until the first add). Safe to destroy() them while iterating. */
|
|
1057
|
+
readonly children: Node[];
|
|
1058
|
+
/** @internal */ sortDirty: boolean;
|
|
1059
|
+
/** @internal: a child was destroyed. */ dead: boolean;
|
|
1060
|
+
constructor(kernel: Kernel);
|
|
1061
|
+
/** Adds nodes (moving them from another group); returns the first, so `const s = stage.add(engine.sprite(…))`. */
|
|
1062
|
+
add<T extends Node>(node: T, ...more: Node[]): T;
|
|
1063
|
+
private adopt;
|
|
1064
|
+
/** Takes a node out of the group (without destroying it). */
|
|
1065
|
+
remove(node: Node): void;
|
|
1066
|
+
private detach;
|
|
1067
|
+
/** Destroys every child. */
|
|
1068
|
+
clear(): void;
|
|
1069
|
+
/** Destroys the group and every child. */
|
|
1070
|
+
destroy(): void;
|
|
1071
|
+
/** @internal Drops destroyed children (end of frame). */
|
|
1072
|
+
sweep(): void;
|
|
1073
|
+
/**
|
|
1074
|
+
* Draws the children, in order of z, each placed (its world composed, store.ts place) right before it is drawn,
|
|
1075
|
+
* when it or a parent moved. The outermost draw begins a pass; a group in no group drawn inside it (a mask) has
|
|
1076
|
+
* been placed anew by its caller, so everything under it is composed.
|
|
1077
|
+
*/
|
|
1078
|
+
draw(ctx: DrawContext, alpha: number, tint: number): void;
|
|
1079
|
+
private drawChildren;
|
|
1080
|
+
}
|
|
1081
|
+
/** NodeProps, and the frame to show. */
|
|
1082
|
+
export interface SpriteProps extends NodeProps {
|
|
1083
|
+
/** An atlas frame, or the name of a loaded or baked one. */
|
|
1084
|
+
frame?: Frame | string;
|
|
1085
|
+
/**
|
|
1086
|
+
* Nine-slice insets in frame pixels: one number for all sides, or [top, right, bottom, left]. The corners keep
|
|
1087
|
+
* their size when the sprite is resized, the edges and the middle stretch. Default: the frame's own (an atlas's
|
|
1088
|
+
* scale9Borders), else none.
|
|
1089
|
+
*/
|
|
1090
|
+
slice?: number | Insets;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* A picture: an atlas frame or an image, or a baked shape or text (subclasses). Its local size is width × height
|
|
1094
|
+
* (the frame's own by default), placed by its anchor.
|
|
1095
|
+
*
|
|
1096
|
+
* @example
|
|
1097
|
+
* // A panel from a loaded image, nine-sliced: 24-pixel corners, any size.
|
|
1098
|
+
* const panel = stage.add(engine.sprite('ui/panel', { slice: 24, width: 420, height: 160 }))
|
|
1099
|
+
*/
|
|
1100
|
+
export class Sprite extends Node {
|
|
1101
|
+
protected _frame: Frame | null;
|
|
1102
|
+
protected _w: number | null;
|
|
1103
|
+
protected _h: number | null;
|
|
1104
|
+
/**
|
|
1105
|
+
* Nine-slice insets in frame pixels (0: none, or the frame's own): one number for all sides, or [top, right,
|
|
1106
|
+
* bottom, left]. The corners keep their size when the sprite is resized.
|
|
1107
|
+
*/
|
|
1108
|
+
slice: number | Insets;
|
|
1109
|
+
/** True until an anchor is set: then the frame's own origin (a shape's centre, a text's alignment) is used. */
|
|
1110
|
+
protected autoAnchor: boolean;
|
|
1111
|
+
/** Use engine.sprite(frame, props). */
|
|
1112
|
+
constructor(kernel: Kernel, frame?: Frame | string | null, props?: SpriteProps);
|
|
1113
|
+
/** What it shows: set a frame or the name of a loaded or baked one (an animation swaps it every few frames). */
|
|
1114
|
+
get frame(): Frame | null;
|
|
1115
|
+
set frame(f: Frame | string | null);
|
|
1116
|
+
/** The name the frame came from: baked frames are looked up again after the atlas was wiped. */
|
|
1117
|
+
private frameName;
|
|
1118
|
+
/** Sets anchorX and anchorY (until then the frame's own origin is used). */
|
|
1119
|
+
set anchor(v: number | {
|
|
1120
|
+
x: number;
|
|
1121
|
+
y: number;
|
|
1122
|
+
});
|
|
1123
|
+
/** The content size the frame has, stage units (without its margin; untrimmed). */
|
|
1124
|
+
get naturalWidth(): number;
|
|
1125
|
+
/** The content height the frame has, stage units (without its margin; untrimmed). */
|
|
1126
|
+
get naturalHeight(): number;
|
|
1127
|
+
/** Local width, stage units: the frame's own (naturalWidth) until set; setting it stretches the sprite. */
|
|
1128
|
+
get width(): number;
|
|
1129
|
+
set width(v: number);
|
|
1130
|
+
/** Local height, stage units: the frame's own (naturalHeight) until set; setting it stretches the sprite. */
|
|
1131
|
+
get height(): number;
|
|
1132
|
+
set height(v: number);
|
|
1133
|
+
/**
|
|
1134
|
+
* frameQ[0], [1] = width and height, drawing frame f: what the getters say, written to an array instead of returned
|
|
1135
|
+
* (a number returned through a getter on several classes is boxed on every call).
|
|
1136
|
+
*/
|
|
1137
|
+
protected sizeQ(f: Frame): void;
|
|
1138
|
+
/** The frame to draw now (subclasses bake it on demand). */
|
|
1139
|
+
protected frameNow(): Frame | null;
|
|
1140
|
+
protected useFrameAnchor(): void;
|
|
1141
|
+
/** Draws the frame (the engine calls it). */
|
|
1142
|
+
draw(ctx: DrawContext, alpha: number, tint: number): void;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* A shape baked once into the atlas (see ShapeSpec); changing spec bakes again (do it rarely, not every frame). A
|
|
1146
|
+
* roundRect resizes freely with width and height (panels, progress bars).
|
|
1147
|
+
*
|
|
1148
|
+
* @example
|
|
1149
|
+
* const star = stage.add(engine.shape({ type: 'star', r: 40, fill: '#FBD46A', stroke: '#fff', lw: 4 }, { x: 200, y: 100 }))
|
|
1150
|
+
*/
|
|
1151
|
+
export class Shape extends Sprite {
|
|
1152
|
+
private _spec;
|
|
1153
|
+
private baked;
|
|
1154
|
+
private bakedEffects;
|
|
1155
|
+
/** Use engine.shape(spec, props). */
|
|
1156
|
+
constructor(kernel: Kernel, spec: ShapeSpec, props?: NodeProps);
|
|
1157
|
+
/** What it is: setting it bakes the shape again. */
|
|
1158
|
+
get spec(): ShapeSpec;
|
|
1159
|
+
set spec(s: ShapeSpec);
|
|
1160
|
+
private bake;
|
|
1161
|
+
protected frameNow(): Frame;
|
|
1162
|
+
protected sizeQ(): void;
|
|
1163
|
+
/** Local width, stage units: the shape's own until set (a roundRect is redrawn to it, others stretched). */
|
|
1164
|
+
get width(): number;
|
|
1165
|
+
set width(v: number);
|
|
1166
|
+
/** Local height, stage units: the shape's own until set. */
|
|
1167
|
+
get height(): number;
|
|
1168
|
+
set height(v: number);
|
|
1169
|
+
}
|
|
1170
|
+
/** How an emitter behaves (engine.particles(frame, opts)), for all its particles. */
|
|
1171
|
+
export interface ParticleOptions {
|
|
1172
|
+
/** Most particles alive at once (the pool; fixed). Scale it with tier.scale. */
|
|
1173
|
+
max?: number;
|
|
1174
|
+
/** 'add' for glowing sparks. */
|
|
1175
|
+
blend?: Blend;
|
|
1176
|
+
/** Stage units per second², down. */
|
|
1177
|
+
gravity?: number;
|
|
1178
|
+
/** Velocity kept per second (0–1): 1 none lost. */
|
|
1179
|
+
drag?: number;
|
|
1180
|
+
}
|
|
1181
|
+
/** How the particles of one burst start: speed, life, direction, size, spin and look. */
|
|
1182
|
+
export interface BurstOptions {
|
|
1183
|
+
/** Speed range, stage units per second. */
|
|
1184
|
+
speed?: number | [
|
|
1185
|
+
number,
|
|
1186
|
+
number
|
|
1187
|
+
];
|
|
1188
|
+
/** Life range, seconds. */
|
|
1189
|
+
life?: number | [
|
|
1190
|
+
number,
|
|
1191
|
+
number
|
|
1192
|
+
];
|
|
1193
|
+
/** Direction (rad) and spread around it; full circle by default. */
|
|
1194
|
+
angle?: number;
|
|
1195
|
+
/** Radians around angle; without it, the full circle. */
|
|
1196
|
+
spread?: number;
|
|
1197
|
+
/** Scale at birth and at death. */
|
|
1198
|
+
scale?: number | [
|
|
1199
|
+
number,
|
|
1200
|
+
number
|
|
1201
|
+
];
|
|
1202
|
+
/** Spin range, rad per second. */
|
|
1203
|
+
spin?: number;
|
|
1204
|
+
/** Their colour (multiplied with the frame's pixels), e.g. the player's colour. */
|
|
1205
|
+
tint?: ColorLike;
|
|
1206
|
+
/** Alpha at birth; they fade out over their life. */
|
|
1207
|
+
alpha?: number;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* A pooled emitter: particles live in fixed typed arrays (no allocation while playing), move with their speed,
|
|
1211
|
+
* gravity and drag, fade out and shrink or grow. Drawn as one quad each, from one frame.
|
|
1212
|
+
*/
|
|
1213
|
+
export class Particles extends Node {
|
|
1214
|
+
/** The pool's size: most particles alive at once. */
|
|
1215
|
+
readonly max: number;
|
|
1216
|
+
private readonly f;
|
|
1217
|
+
private readonly tints;
|
|
1218
|
+
private count;
|
|
1219
|
+
/** Stage units per second², down. */
|
|
1220
|
+
gravity: number;
|
|
1221
|
+
/** Velocity kept per second (0–1): 1 none lost. */
|
|
1222
|
+
drag: number;
|
|
1223
|
+
/** The frame every particle shows (or its name). */
|
|
1224
|
+
frameRef: Frame | string;
|
|
1225
|
+
/** The random source for bursts (a seeded rng in tests and replays). */
|
|
1226
|
+
random: () => number;
|
|
1227
|
+
private static readonly W;
|
|
1228
|
+
/** Use engine.particles(frame, opts). */
|
|
1229
|
+
constructor(kernel: Kernel, frame: Frame | string, opts?: ParticleOptions);
|
|
1230
|
+
/** Particles alive. */
|
|
1231
|
+
get alive(): number;
|
|
1232
|
+
/**
|
|
1233
|
+
* n particles at (x, y) (local units); past the pool's size the oldest are reused.
|
|
1234
|
+
*
|
|
1235
|
+
* @example
|
|
1236
|
+
* sparks.burst(star.x, star.y, engine.tier.scale(24), { tint: hand.color, speed: [100, 300] })
|
|
1237
|
+
*/
|
|
1238
|
+
burst(x: number, y: number, n: number, o?: BurstOptions): void;
|
|
1239
|
+
/** Advances every particle (the engine calls it, on game time). */
|
|
1240
|
+
update(dt: number): void;
|
|
1241
|
+
/** Removes every particle at once. */
|
|
1242
|
+
clear(): void;
|
|
1243
|
+
/** Draws the particles (the engine calls it). */
|
|
1244
|
+
draw(ctx: DrawContext, alpha: number, tint: number): void;
|
|
1245
|
+
}
|
|
1246
|
+
/** An animation's frames (frames, or loaded or baked frame names) and, if it has them, each frame's seconds. */
|
|
1247
|
+
export interface AnimationDef {
|
|
1248
|
+
/** The frames, in order. */
|
|
1249
|
+
frames: readonly (Frame | string)[];
|
|
1250
|
+
/** Seconds per frame (Aseprite's durations); without them, 1 / fps each. */
|
|
1251
|
+
durations?: readonly number[];
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* How an animated sprite plays (engine.animation(frames, props)): a sprite's props and these.
|
|
1255
|
+
*
|
|
1256
|
+
* @example
|
|
1257
|
+
* engine.animation('hero/run', { fps: 12, x: 200, y: 500, anchor: 0.5 })
|
|
1258
|
+
*/
|
|
1259
|
+
export interface AnimationProps extends SpriteProps {
|
|
1260
|
+
/** Frames per second (default 12); ignored for frames with their own durations, unless set. */
|
|
1261
|
+
fps?: number;
|
|
1262
|
+
/** Start again after the last frame (default true); false: stop on it and call onComplete. */
|
|
1263
|
+
loop?: boolean;
|
|
1264
|
+
/** Starts playing at once (default true). */
|
|
1265
|
+
playing?: boolean;
|
|
1266
|
+
/** Time scale: 2 plays twice as fast, -1 backwards. Default 1. */
|
|
1267
|
+
speed?: number;
|
|
1268
|
+
/** Called when a non-looping animation reaches its last frame. */
|
|
1269
|
+
onComplete?: () => void;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* A sprite showing the frames of an animation one after the other, on game time.
|
|
1273
|
+
*
|
|
1274
|
+
* @example
|
|
1275
|
+
* const hero = stage.add(engine.animation('hero/run', { fps: 12, x: 200, y: 500, anchor: 0.5 }))
|
|
1276
|
+
* hero.gotoFrame(0)
|
|
1277
|
+
* const jump = stage.add(engine.animation('hero/jump', { loop: false, onComplete: () => jump.destroy() }))
|
|
1278
|
+
*/
|
|
1279
|
+
export class AnimatedSprite extends Sprite {
|
|
1280
|
+
/** The frames, in order. */
|
|
1281
|
+
readonly frames: readonly (Frame | string)[];
|
|
1282
|
+
/** Seconds per frame, when the animation has its own (else 1 / fps). */
|
|
1283
|
+
durations: readonly number[] | null;
|
|
1284
|
+
/** Frames per second, when there are no durations. */
|
|
1285
|
+
fps: number;
|
|
1286
|
+
/** Start again after the last frame. */
|
|
1287
|
+
loop: boolean;
|
|
1288
|
+
/** Time scale: 2 twice as fast, -1 backwards. */
|
|
1289
|
+
speed: number;
|
|
1290
|
+
/** Called when a non-looping animation reaches its end. */
|
|
1291
|
+
onComplete: (() => void) | null;
|
|
1292
|
+
private _playing;
|
|
1293
|
+
private index;
|
|
1294
|
+
/** Seconds into the current frame. */
|
|
1295
|
+
private t;
|
|
1296
|
+
/** Use engine.animation(frames, props). */
|
|
1297
|
+
constructor(kernel: Kernel, def: AnimationDef, props?: AnimationProps);
|
|
1298
|
+
/** The frame shown, from 0. */
|
|
1299
|
+
get frameIndex(): number;
|
|
1300
|
+
/** How many frames. */
|
|
1301
|
+
get frameCount(): number;
|
|
1302
|
+
/** Playing (not stopped, not at the end of a non-looping run). */
|
|
1303
|
+
get playing(): boolean;
|
|
1304
|
+
/** Plays from where it is (from the start again if a non-looping run had ended). */
|
|
1305
|
+
play(): this;
|
|
1306
|
+
/** Stops on the current frame. */
|
|
1307
|
+
stop(): this;
|
|
1308
|
+
/** Shows frame i (from 0; wrapped into range), keeping the playing state. */
|
|
1309
|
+
gotoFrame(i: number): this;
|
|
1310
|
+
/** Shows frame i and plays from it. */
|
|
1311
|
+
gotoAndPlay(i: number): this;
|
|
1312
|
+
/** Shows frame i and stops on it. */
|
|
1313
|
+
gotoAndStop(i: number): this;
|
|
1314
|
+
private show;
|
|
1315
|
+
private secondsOf;
|
|
1316
|
+
/** Advances by dt seconds of game time (the engine calls it). */
|
|
1317
|
+
update(dt: number): void;
|
|
1318
|
+
}
|
|
1319
|
+
/** A hand on the stage (engine.hands(), engine.touched()): whose, which, where. */
|
|
1320
|
+
export interface Hand {
|
|
1321
|
+
/** The player, from 1. */
|
|
1322
|
+
player: number;
|
|
1323
|
+
/** Which hand (the child's own left or right). */
|
|
1324
|
+
side: "left" | "right";
|
|
1325
|
+
/** Stage units. */
|
|
1326
|
+
x: number;
|
|
1327
|
+
/** Stage units. */
|
|
1328
|
+
y: number;
|
|
1329
|
+
/** The player's colour (CSS). */
|
|
1330
|
+
color: string;
|
|
1331
|
+
}
|
|
1332
|
+
/** The wrists of a body seen well enough, in player space. */
|
|
1333
|
+
export function wrists(body: Body): {
|
|
1334
|
+
side: "left" | "right";
|
|
1335
|
+
x: number;
|
|
1336
|
+
y: number;
|
|
1337
|
+
}[];
|
|
1338
|
+
/** A player's colour (CSS), as the TV draws them: player 1 orange, 2 blue, 3 green, 4 lilac, then again. */
|
|
1339
|
+
export const playerColor: (player: number) => "#EE6A43" | "#6FA0E0" | "#8CC9A0" | "#C49BE0";
|
|
1340
|
+
/**
|
|
1341
|
+
* Every player's hands in stage units: wrists of bodyOf(frame) (null: the frame's own body), through toScreen
|
|
1342
|
+
* (player space to screen fractions), times the stage size.
|
|
1343
|
+
*/
|
|
1344
|
+
export function handsOf(frames: readonly PlayerFrame[], toScreen: (p: Point) => Point, width: number, height: number, bodyOf?: (f: PlayerFrame) => Body | null): Hand[];
|
|
1345
|
+
/** What a node offers to hit tests: its world transform, size and anchor, or a radius. */
|
|
1346
|
+
export interface Hittable {
|
|
1347
|
+
/** The node's transform to the stage. */
|
|
1348
|
+
readonly world: Mat;
|
|
1349
|
+
/** Local size (stage units, before the world transform) and anchor (0–1): the rectangle's corner is at -anchor × size. */
|
|
1350
|
+
readonly width: number;
|
|
1351
|
+
/** Local height, stage units. */
|
|
1352
|
+
readonly height: number;
|
|
1353
|
+
/** Horizontal anchor, 0–1 of the width. */
|
|
1354
|
+
readonly anchorX: number;
|
|
1355
|
+
/** Vertical anchor, 0–1 of the height. */
|
|
1356
|
+
readonly anchorY: number;
|
|
1357
|
+
/** Set: a circle of this radius (local units) around the node's origin instead of the rectangle. */
|
|
1358
|
+
hitRadius?: number | null;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Whether the point (stage units) touches the node: in its local space, inside the rectangle (or circle) grown by
|
|
1362
|
+
* pad stage units. Rotation and scale are taken into account.
|
|
1363
|
+
*/
|
|
1364
|
+
export function hits(node: Hittable, x: number, y: number, pad?: number): boolean;
|
|
1365
|
+
/** The first hand touching the node (of one player, or anyone's), or null. */
|
|
1366
|
+
export function touching(node: Hittable, hands: readonly Hand[], player?: number, pad?: number): Hand | null;
|
|
1367
|
+
/** Options of the WebGL backends. */
|
|
1368
|
+
export interface GLOptions {
|
|
1369
|
+
/** Refuse a context the browser says is much slower than usual (software rendering). Default true. */
|
|
1370
|
+
failIfMajorPerformanceCaveat?: boolean;
|
|
1371
|
+
/** Turn off instancing / VAOs (tests, comparisons). */
|
|
1372
|
+
instancing?: boolean;
|
|
1373
|
+
/** Turn off vertex array objects (tests, comparisons). */
|
|
1374
|
+
vao?: boolean;
|
|
1375
|
+
}
|
|
1376
|
+
/** A renderer to ask for: a kind, 'webgl' (2, else 1) or 'auto' (WebGL2, WebGL1, then Canvas 2D). */
|
|
1377
|
+
export type RendererChoice = RendererKind | "auto" | "webgl";
|
|
1378
|
+
/** Options to choose and set up a renderer. */
|
|
1379
|
+
export interface DetectOptions extends GLOptions {
|
|
1380
|
+
/** Force one ('webgl': 2, else 1). 'auto' (default): the order above. A forced one that fails falls back too. */
|
|
1381
|
+
renderer?: RendererChoice;
|
|
1382
|
+
}
|
|
1383
|
+
/** The order to try, for a choice. */
|
|
1384
|
+
export function rendererOrder(choice?: RendererChoice): RendererKind[];
|
|
1385
|
+
/** A renderer on canvas (or a fresh canvas that replaced it in the page: use renderer.canvas from now on). */
|
|
1386
|
+
export function createRenderer(canvas: HTMLCanvasElement, opts?: DetectOptions): Renderer;
|
|
1387
|
+
/**
|
|
1388
|
+
* Sound, as small as it can be: Web Audio, decoded buffers by name, and simple tones for games without sound files.
|
|
1389
|
+
* Silent while the host says muted, during pauses and breaks (the engine suspends it), and until the page may play
|
|
1390
|
+
* audio (resumed on the first event the page receives).
|
|
1391
|
+
*/
|
|
1392
|
+
export interface PlayOptions {
|
|
1393
|
+
/** 0–1 (default 1). */
|
|
1394
|
+
volume?: number;
|
|
1395
|
+
/** Playback rate (pitch): 1 normal. */
|
|
1396
|
+
rate?: number;
|
|
1397
|
+
/** Start again at the end, until stop() (default false). */
|
|
1398
|
+
loop?: boolean;
|
|
1399
|
+
}
|
|
1400
|
+
/** Options of sound.tone(). */
|
|
1401
|
+
export interface ToneOptions {
|
|
1402
|
+
/** 0–1 (default 0.12). */
|
|
1403
|
+
volume?: number;
|
|
1404
|
+
/** The waveform (default 'sine'). */
|
|
1405
|
+
type?: OscillatorType;
|
|
1406
|
+
/** Glide to this frequency over the tone. */
|
|
1407
|
+
to?: number;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* engine.sound: Web Audio, loaded by name with engine.load, quiet while the host pauses, shows a break or is muted.
|
|
1411
|
+
*
|
|
1412
|
+
* @example
|
|
1413
|
+
* await engine.load({ pop: 'sounds/pop.mp3' })
|
|
1414
|
+
* engine.sound.play('pop', { volume: 0.8 })
|
|
1415
|
+
* engine.sound.tone(880, 0.15)
|
|
1416
|
+
*/
|
|
1417
|
+
export class Sound {
|
|
1418
|
+
private ctx;
|
|
1419
|
+
private master;
|
|
1420
|
+
private readonly buffers;
|
|
1421
|
+
private held;
|
|
1422
|
+
/** Muted by the host (the TV's sound is off): set by the engine. */
|
|
1423
|
+
muted: boolean;
|
|
1424
|
+
/** Master volume, 0–1. */
|
|
1425
|
+
volume: number;
|
|
1426
|
+
/** The engine makes it (engine.sound); browsers unlock its audio on the first touch, key or click. */
|
|
1427
|
+
constructor();
|
|
1428
|
+
/** The audio context, made on first use (null without Web Audio). */
|
|
1429
|
+
context(): AudioContext | null;
|
|
1430
|
+
/** Decodes a sound file (mp3, aac, ogg, wav) under a name. */
|
|
1431
|
+
load(name: string, url: string): Promise<void>;
|
|
1432
|
+
/** Whether a sound of that name is loaded. */
|
|
1433
|
+
has(name: string): boolean;
|
|
1434
|
+
/** Plays a loaded sound; returns stop(). */
|
|
1435
|
+
play(name: string, o?: PlayOptions): () => void;
|
|
1436
|
+
/** A short tone (a "ding"): frequency Hz, seconds, fading out. */
|
|
1437
|
+
tone(freq: number, seconds?: number, o?: ToneOptions): void;
|
|
1438
|
+
private get audible();
|
|
1439
|
+
/** Pauses and breaks: everything stops until release(). */
|
|
1440
|
+
hold(): void;
|
|
1441
|
+
/** Sound again after hold(). */
|
|
1442
|
+
release(): void;
|
|
1443
|
+
/** Closes the audio context. */
|
|
1444
|
+
destroy(): void;
|
|
1445
|
+
}
|
|
1446
|
+
/** The quality tiers, best first. */
|
|
1447
|
+
export type TierName = "high" | "medium" | "low";
|
|
1448
|
+
/**
|
|
1449
|
+
* engine.tier: how much the device can draw. Games read name, and scale counts with scale(n); the engine applies
|
|
1450
|
+
* the rest (resolution, effects).
|
|
1451
|
+
*
|
|
1452
|
+
* @example
|
|
1453
|
+
* const sparks = engine.particles(frame, { max: engine.tier.scale(200) })
|
|
1454
|
+
*/
|
|
1455
|
+
export interface Tier {
|
|
1456
|
+
/** 'high', 'medium' or 'low'. */
|
|
1457
|
+
readonly name: TierName;
|
|
1458
|
+
/** Most backing-store pixels per stage unit (the stage is 720 units high): 1.5, 1, 0.75. */
|
|
1459
|
+
readonly resolution: number;
|
|
1460
|
+
/** Particles and other counts: × 1, × 0.5, × 0.2. */
|
|
1461
|
+
readonly particles: number;
|
|
1462
|
+
/** Additive glows and extra layers (high only). */
|
|
1463
|
+
readonly effects: boolean;
|
|
1464
|
+
/** The largest blur (blur(), dropShadow()) radius, stage units: 32, 16, 0 (off: shadows hard). */
|
|
1465
|
+
readonly maxBlur: number;
|
|
1466
|
+
/** n scaled for this tier (at least 1 if n ≥ 1). */
|
|
1467
|
+
scale(n: number): number;
|
|
1468
|
+
}
|
|
1469
|
+
/** Every tier, best first. */
|
|
1470
|
+
export const TIERS: TierName[];
|
|
1471
|
+
/** The tier of that name. */
|
|
1472
|
+
export function tier(name: TierName): Tier;
|
|
1473
|
+
/** What the engine's short probe measured, for pickTier. */
|
|
1474
|
+
export interface ProbeResult {
|
|
1475
|
+
/** Intervals between animation frames while drawing, ms (warm-up already dropped). */
|
|
1476
|
+
intervals: number[];
|
|
1477
|
+
/** CPU time of each probe frame (drawing; on Canvas 2D, rasterising too), ms. */
|
|
1478
|
+
cpu: number[];
|
|
1479
|
+
/**
|
|
1480
|
+
* The display's frame interval, ms, measured on idle frames before drawing (displayInterval). Without it, it is
|
|
1481
|
+
* guessed from the drawing intervals, which a slow device misreads as a slower display.
|
|
1482
|
+
*/
|
|
1483
|
+
refresh?: number;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* The tier for a renderer and a probe, against the display's frame (probe.refresh). "Fits": within the frame +25%.
|
|
1487
|
+
* - GL: high when the 90th percentile fits and the drawing takes under half a frame; medium when the median fits;
|
|
1488
|
+
* else low.
|
|
1489
|
+
* - Canvas 2D: never high (no cheap glows). Medium only when the 90th percentile fits and drawing plus rasterising
|
|
1490
|
+
* take under a quarter of a frame (the probe is a light scene; a game draws more); else low.
|
|
1491
|
+
*/
|
|
1492
|
+
export function pickTier(kind: RendererKind, probe: ProbeResult): TierName;
|
|
1493
|
+
/** What the screen says about the device (init.device, DeviceHint in @fabi-play/protocol): play.device. */
|
|
1494
|
+
export interface TierHint {
|
|
1495
|
+
tier?: string;
|
|
1496
|
+
hz?: number;
|
|
1497
|
+
}
|
|
1498
|
+
/** The stage's height in stage units, on every screen: its width follows the viewport (1280 at 16:9). */
|
|
1499
|
+
export const STAGE_HEIGHT = 720;
|
|
1500
|
+
/**
|
|
1501
|
+
* What the engine needs of the SDK: the Play object of connect() or the script tag's FabiPlay (Easy). Both have
|
|
1502
|
+
* on, toScreen, smoothedBody and avatarOf; the engine listens to both kinds of events ('frame' and 'players',
|
|
1503
|
+
* 'break-done' and 'break-end').
|
|
1504
|
+
*/
|
|
1505
|
+
export interface PlayLike {
|
|
1506
|
+
/** Listens to the host's events; returns a function that stops listening. */
|
|
1507
|
+
on(event: string, listener: (m: any) => void): () => void;
|
|
1508
|
+
/** A body point on the screen, in fractions (see the SDK's toScreen). */
|
|
1509
|
+
toScreen(p: Point): Point;
|
|
1510
|
+
/** A player's body smoothed and interpolated as the SDK draws avatars. */
|
|
1511
|
+
smoothedBody?(who: PlayerFrame | number, options?: {
|
|
1512
|
+
smooth?: boolean | number;
|
|
1513
|
+
interpolate?: boolean;
|
|
1514
|
+
}): Body | null;
|
|
1515
|
+
/** The avatar id a player picked. */
|
|
1516
|
+
avatarOf?(player: number | PlayerFrame): string;
|
|
1517
|
+
/** What is behind the game now: 'video' (mirror mode) or 'none'. */
|
|
1518
|
+
readonly backdrop?: Backdrop;
|
|
1519
|
+
/** Where the camera image is on the screen, in fractions; null if unknown. */
|
|
1520
|
+
readonly view?: BackdropView | null;
|
|
1521
|
+
/** The screen's sound is off. */
|
|
1522
|
+
readonly muted?: boolean;
|
|
1523
|
+
/** What the screen knows about the device (init.device): a starting tier and the display's rate. */
|
|
1524
|
+
readonly device?: TierHint | null;
|
|
1525
|
+
/** Debug: how the game draws now, for the simulator's status line (the SDK sends it only when asked). */
|
|
1526
|
+
reportStats?(stats: {
|
|
1527
|
+
fps?: number;
|
|
1528
|
+
renderer?: string;
|
|
1529
|
+
tier?: TierName;
|
|
1530
|
+
}): void;
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Options of createEngine: all optional.
|
|
1534
|
+
*
|
|
1535
|
+
* @example
|
|
1536
|
+
* const engine = await createEngine({ play, background: '#132040', maxPixelRatio: 1.5 })
|
|
1537
|
+
*/
|
|
1538
|
+
export interface EngineOptions {
|
|
1539
|
+
/** The SDK's play object (connect()) or window.FabiPlay. Without it there are no hands or avatars. */
|
|
1540
|
+
play?: PlayLike | null;
|
|
1541
|
+
/** The canvas to draw in; by default one is made to fill the page. */
|
|
1542
|
+
canvas?: HTMLCanvasElement;
|
|
1543
|
+
/** Force a renderer ('canvas2d', 'webgl1', 'webgl2'); also ?renderer= in the address. */
|
|
1544
|
+
renderer?: RendererChoice;
|
|
1545
|
+
/**
|
|
1546
|
+
* Force a tier; also ?tier= in the address (never lowered during play). Else the screen's hint (play.device), no
|
|
1547
|
+
* higher than this game's cached tier; else the cached probe, or a new one.
|
|
1548
|
+
*/
|
|
1549
|
+
tier?: TierName;
|
|
1550
|
+
/** Refuse slow (software) WebGL; default true. */
|
|
1551
|
+
failIfMajorPerformanceCaveat?: boolean;
|
|
1552
|
+
/** Probe the device when there is no cached tier (default true; false: medium). */
|
|
1553
|
+
probe?: boolean;
|
|
1554
|
+
/** Device pixel ratio cap (default 2). */
|
|
1555
|
+
maxPixelRatio?: number;
|
|
1556
|
+
/** The stage's colour when there is no video behind it; transparent by default (the page shows through). */
|
|
1557
|
+
background?: ColorLike;
|
|
1558
|
+
/** Engine version of instancing etc. (tests). */
|
|
1559
|
+
instancing?: boolean;
|
|
1560
|
+
}
|
|
1561
|
+
/** engine.stage: the root group, 720 units high, as wide as the viewport's aspect. */
|
|
1562
|
+
export interface Stage extends Group {
|
|
1563
|
+
/** Stage units: width follows the viewport's aspect, height is always 720. */
|
|
1564
|
+
readonly width: number;
|
|
1565
|
+
/** Always 720 (STAGE_HEIGHT). */
|
|
1566
|
+
readonly height: number;
|
|
1567
|
+
/** The 16:9 area in the middle, for what must be seen on any screen. */
|
|
1568
|
+
readonly safe: {
|
|
1569
|
+
x: number;
|
|
1570
|
+
y: number;
|
|
1571
|
+
w: number;
|
|
1572
|
+
h: number;
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* engine.supports: what node.filters can do on this renderer and tier.
|
|
1577
|
+
*
|
|
1578
|
+
* @example
|
|
1579
|
+
* flag.filters = engine.supports.shaders ? [wave] : [brightness(1.1)]
|
|
1580
|
+
*/
|
|
1581
|
+
export interface Supports {
|
|
1582
|
+
/** Colour filters, blur and drop shadows draw (WebGL; Canvas 2D with ctx.filter). tint() draws everywhere. */
|
|
1583
|
+
readonly filters: boolean;
|
|
1584
|
+
/** blur() and soft drop shadows draw: filters, and the tier's maxBlur above 0 (not on the low tier). */
|
|
1585
|
+
readonly blur: boolean;
|
|
1586
|
+
/** shader() filters draw: WebGL only. */
|
|
1587
|
+
readonly shaders: boolean;
|
|
1588
|
+
/** The largest blur radius on the current tier, stage units (bigger ones are drawn at it; 0: off). */
|
|
1589
|
+
readonly maxBlur: number;
|
|
1590
|
+
/** The limits of each tier (the tier can drop during play). */
|
|
1591
|
+
readonly tiers: Readonly<Record<TierName, {
|
|
1592
|
+
readonly maxBlur: number;
|
|
1593
|
+
}>>;
|
|
1594
|
+
}
|
|
1595
|
+
/** engine.stats: what the last second cost (for a debug overlay). */
|
|
1596
|
+
export interface EngineStats extends FrameStats {
|
|
1597
|
+
/** Frames drawn in the last second. */
|
|
1598
|
+
fps: number;
|
|
1599
|
+
/** CPU time of a frame (update + draw), ms, averaged over the last second. */
|
|
1600
|
+
cpuMs: number;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* The engine, from createEngine: a stage to add nodes to, factories for them, the players' hands and avatars, a
|
|
1604
|
+
* loop on game time that follows the host (pause, breaks, mirror mode).
|
|
1605
|
+
*
|
|
1606
|
+
* @example
|
|
1607
|
+
* const engine = await createEngine({ play })
|
|
1608
|
+
* const star = engine.stage.add(engine.shape({ type: 'star', r: 40, fill: '#FBD46A' }, { x: 640, y: 100 }))
|
|
1609
|
+
* engine.onFrame((dt) => {
|
|
1610
|
+
* star.y += 120 * dt
|
|
1611
|
+
* if (engine.touched(star)) star.destroy()
|
|
1612
|
+
* })
|
|
1613
|
+
* engine.start()
|
|
1614
|
+
*/
|
|
1615
|
+
export interface Engine {
|
|
1616
|
+
/** The root group: add nodes here. 720 units high; width follows the viewport's aspect. */
|
|
1617
|
+
readonly stage: Stage;
|
|
1618
|
+
/** The quality tier: read tier.name, scale counts with tier.scale(n). */
|
|
1619
|
+
readonly tier: Tier;
|
|
1620
|
+
/** The backend that draws (renderer.kind: 'webgl2', 'webgl1' or 'canvas2d'). */
|
|
1621
|
+
readonly renderer: Renderer;
|
|
1622
|
+
/** The canvas drawn in (after a switch to Canvas 2D on context loss, a fresh one in its place). */
|
|
1623
|
+
readonly canvas: HTMLCanvasElement;
|
|
1624
|
+
/** Backing-store pixels per stage unit. */
|
|
1625
|
+
readonly pixelScale: number;
|
|
1626
|
+
/** What is behind the game now: 'video' (mirror mode: the stage is transparent, avatars hidden) or 'none'. */
|
|
1627
|
+
readonly backdrop: Backdrop;
|
|
1628
|
+
/** Where the camera image is on the screen, in fractions; null if the host didn't say. */
|
|
1629
|
+
readonly view: BackdropView | null;
|
|
1630
|
+
/** Paused by the host (pause, break) or by pause(): game time, tweens, particles and sound are stopped. */
|
|
1631
|
+
readonly paused: boolean;
|
|
1632
|
+
/** Sounds and tones, quiet while paused or muted. */
|
|
1633
|
+
readonly sound: Sound;
|
|
1634
|
+
/** What the last second cost: fps, CPU time, draw calls. */
|
|
1635
|
+
readonly stats: EngineStats;
|
|
1636
|
+
/** Game time, seconds (stops on pause). */
|
|
1637
|
+
readonly time: number;
|
|
1638
|
+
/** What filters can do here (renderer and tier): pick another look where a filter is not supported. */
|
|
1639
|
+
readonly supports: Supports;
|
|
1640
|
+
/**
|
|
1641
|
+
* A sprite of a frame (or a loaded or baked frame's name). Add it to the stage or a group to see it.
|
|
1642
|
+
*
|
|
1643
|
+
* @example
|
|
1644
|
+
* const fox = engine.stage.add(engine.sprite('fox', { x: 640, y: 500, anchor: 0.5 }))
|
|
1645
|
+
*/
|
|
1646
|
+
sprite(frame: Frame | string, props?: SpriteProps): Sprite;
|
|
1647
|
+
/** A shape, baked once (see ShapeSpec). */
|
|
1648
|
+
shape(spec: ShapeSpec, props?: NodeProps): Shape;
|
|
1649
|
+
/** A text, baked once and again when it changes. */
|
|
1650
|
+
text(text: string, style?: TextStyle & NodeProps): Text;
|
|
1651
|
+
/** An empty group, to move, fade or clear nodes together. */
|
|
1652
|
+
group(props?: NodeProps): Group;
|
|
1653
|
+
/**
|
|
1654
|
+
* A pooled particle emitter of one frame (or a shape, baked); burst() it.
|
|
1655
|
+
*
|
|
1656
|
+
* @example
|
|
1657
|
+
* const sparks = stage.add(engine.particles({ type: 'circle', r: 6, fill: '#fff' }, { max: engine.tier.scale(200), gravity: 400 }))
|
|
1658
|
+
* sparks.burst(x, y, 24)
|
|
1659
|
+
*/
|
|
1660
|
+
particles(frame: Frame | string | ShapeSpec, opts?: ParticleOptions & NodeProps): Particles;
|
|
1661
|
+
/**
|
|
1662
|
+
* Eases numeric fields of target (a node, or any object) to `to` over `seconds`, on game time. Resolves at the
|
|
1663
|
+
* end (or when stopped, or when the node is destroyed).
|
|
1664
|
+
*
|
|
1665
|
+
* @example
|
|
1666
|
+
* await engine.tween(star, { scale: 1.6, alpha: 0 }, 0.25)
|
|
1667
|
+
* star.destroy()
|
|
1668
|
+
*/
|
|
1669
|
+
tween<T extends object>(target: T, to: Partial<Record<keyof T, number>>, seconds: number, ease?: Ease | EaseName | TweenOptions): TweenHandle;
|
|
1670
|
+
/**
|
|
1671
|
+
* A timeline: tweens in sequence (each after the previous) or together (`at`), with labels, calls, waits, repeat
|
|
1672
|
+
* and yoyo, on game time (frozen on pause and breaks). It starts on the next frame; await it for its end.
|
|
1673
|
+
*
|
|
1674
|
+
* @example
|
|
1675
|
+
* await engine.timeline()
|
|
1676
|
+
* .to(title, { alpha: 1, scale: 1 }, 0.4, 'backOut')
|
|
1677
|
+
* .label('cheer')
|
|
1678
|
+
* .to(score, { y: 300 }, 0.5, { at: 'cheer' })
|
|
1679
|
+
* .call(() => engine.sound.play('ding'), 'cheer+=0.2')
|
|
1680
|
+
*/
|
|
1681
|
+
timeline(opts?: TimelineOptions): Timeline;
|
|
1682
|
+
/**
|
|
1683
|
+
* An animated sprite: an animation of a loaded atlas (its `animations` or Aseprite `frameTags`, as 'atlas/name'),
|
|
1684
|
+
* every frame whose name starts with a prefix ('atlas/run_'), or a list of frames. Plays on game time.
|
|
1685
|
+
*
|
|
1686
|
+
* @example
|
|
1687
|
+
* const hero = engine.stage.add(engine.animation('hero/run', { fps: 12, x: 200, y: 500, anchor: 0.5 }))
|
|
1688
|
+
*/
|
|
1689
|
+
animation(frames: string | readonly (Frame | string)[], props?: AnimationProps): AnimatedSprite;
|
|
1690
|
+
/**
|
|
1691
|
+
* A bitmap font for engine.text({ bitmapFont: name }): made now from a web font (each glyph baked into the atlas
|
|
1692
|
+
* on first use, with engine.text's look), or from a BMFont's data and page images. With only a name: the font
|
|
1693
|
+
* loaded or made before.
|
|
1694
|
+
*
|
|
1695
|
+
* @example
|
|
1696
|
+
* engine.bitmapFont('score', { size: 48, color: '#FBEFE3', outline: true, chars: '0123456789' })
|
|
1697
|
+
* const label = stage.add(engine.text('0', { bitmapFont: 'score', x: 40, y: 40, align: 'left' }))
|
|
1698
|
+
* engine.onFrame(() => (label.text = String(points))) // nothing baked per value
|
|
1699
|
+
*/
|
|
1700
|
+
bitmapFont(name: string, source?: BitmapFontOptions | {
|
|
1701
|
+
data: string | object;
|
|
1702
|
+
pages: readonly (HTMLImageElement | HTMLCanvasElement | ImageBitmap)[];
|
|
1703
|
+
}): BitmapFont;
|
|
1704
|
+
/** A frame drawn by you with Canvas 2D (stage units; the context is scaled), baked once, by name. */
|
|
1705
|
+
bake(name: string, width: number, height: number, draw: (ctx: CanvasRenderingContext2D) => void, origin?: {
|
|
1706
|
+
x: number;
|
|
1707
|
+
y: number;
|
|
1708
|
+
}): Frame;
|
|
1709
|
+
/** A loaded or baked frame by name. */
|
|
1710
|
+
frame(name: string): Frame;
|
|
1711
|
+
/**
|
|
1712
|
+
* Loads images, atlases (.json + image: TexturePacker or Aseprite, with trimming, scale9Borders and animations),
|
|
1713
|
+
* bitmap fonts (.fnt, or a BMFont .json, + page images), sounds and fonts, by name.
|
|
1714
|
+
*/
|
|
1715
|
+
load(assets: Record<string, string>): Promise<void>;
|
|
1716
|
+
/** This frame's hands (of one player, or everyone's), stage units: in the avatar's hands, or on the real ones. */
|
|
1717
|
+
hands(player?: number): Hand[];
|
|
1718
|
+
/** The first hand (of one player, or anyone's) touching the node, or null. padding: stage units. */
|
|
1719
|
+
touched(node: Node, player?: number, opts?: {
|
|
1720
|
+
padding?: number;
|
|
1721
|
+
}): Hand | null;
|
|
1722
|
+
/** Whether a point (stage units) is on the node. */
|
|
1723
|
+
hits(node: Node, x: number, y: number, padding?: number): boolean;
|
|
1724
|
+
/**
|
|
1725
|
+
* A node drawing every player's avatar where they stand (nothing over the video). tags: a name tag above each head,
|
|
1726
|
+
* in the player's colour: a function giving each player's tag (null: none), or true for the player numbers.
|
|
1727
|
+
*
|
|
1728
|
+
* @example
|
|
1729
|
+
* stage.add(engine.avatars({ tags: (player) => teamNames[player] ?? null }))
|
|
1730
|
+
*/
|
|
1731
|
+
avatars(opts?: AvatarsOptions): AvatarsNode;
|
|
1732
|
+
/**
|
|
1733
|
+
* A node drawing a ring on every hand, in the players' colours. Over the video (look 'auto', the default) the ring
|
|
1734
|
+
* gets the video look: a dark outline and halo under it, so it reads over a bright room.
|
|
1735
|
+
*/
|
|
1736
|
+
handMarkers(opts?: HandMarkersOptions): Node;
|
|
1737
|
+
/** The live player frames (newest from the host; empty if none for a second). */
|
|
1738
|
+
players(): readonly PlayerFrame[];
|
|
1739
|
+
/** Every animation frame while not paused: dt seconds (at most 0.1), game time. Returns a function that stops it. */
|
|
1740
|
+
onFrame(fn: (dt: number, time: number) => void): () => void;
|
|
1741
|
+
/** Every animation frame, paused or not, before drawing. */
|
|
1742
|
+
onRender(fn: (dt: number) => void): () => void;
|
|
1743
|
+
/** Called when the backdrop or the view changes (not at start: read engine.backdrop). Returns a function that stops it. */
|
|
1744
|
+
onBackdrop(fn: (backdrop: Backdrop, view: BackdropView | null) => void): () => void;
|
|
1745
|
+
/** An HTML layer over the canvas for a HUD or menus (made on first use). Its --unit is one stage unit in CSS px. */
|
|
1746
|
+
readonly hud: HTMLElement;
|
|
1747
|
+
/** A random number between min (0) and max (1). */
|
|
1748
|
+
random(min?: number, max?: number): number;
|
|
1749
|
+
/** Starts the loop: call it once the scene is set up. */
|
|
1750
|
+
start(): void;
|
|
1751
|
+
/** Pauses the game (its own pause menu): game time, tweens, particles and sound stop. The host's pauses are followed by themselves. */
|
|
1752
|
+
pause(): void;
|
|
1753
|
+
/** Resumes after pause() (the host's pauses still hold). */
|
|
1754
|
+
resume(): void;
|
|
1755
|
+
/** Stops the loop and frees the renderer, sounds and listeners. */
|
|
1756
|
+
destroy(): void;
|
|
1757
|
+
}
|
|
1758
|
+
/** Options of engine.avatars. */
|
|
1759
|
+
export interface AvatarsOptions {
|
|
1760
|
+
/** 0–1 for every avatar (below 1 each is composited as one layer). */
|
|
1761
|
+
alpha?: number;
|
|
1762
|
+
/** Name tags: a player's tag (null or '' for none), or true for the player numbers. Default none. */
|
|
1763
|
+
tags?: boolean | ((player: number) => string | null | undefined);
|
|
1764
|
+
/** The tags' height, stage units (default 34). */
|
|
1765
|
+
tagHeight?: number;
|
|
1766
|
+
/** The tags' CSS font family (default Nunito). */
|
|
1767
|
+
tagFont?: string;
|
|
1768
|
+
}
|
|
1769
|
+
/** Options of engine.handMarkers. */
|
|
1770
|
+
export interface HandMarkersOptions {
|
|
1771
|
+
/** The ring's radius, stage units (default 36). */
|
|
1772
|
+
radius?: number;
|
|
1773
|
+
/** 'auto' (default): the video look over the video, the plain one over the sky; or always one of them. */
|
|
1774
|
+
look?: "auto" | "sky" | "video";
|
|
1775
|
+
}
|
|
1776
|
+
/** The avatars of every player, drawn with the engine's renderer. Its own transform is ignored: they stand where the children stand. */
|
|
1777
|
+
export class AvatarsNode extends Node {
|
|
1778
|
+
private readonly paintAll;
|
|
1779
|
+
/** Use engine.avatars(). */
|
|
1780
|
+
constructor(kernel: Kernel, paintAll: (ctx: DrawContext, alpha: number) => void);
|
|
1781
|
+
/** Draws the avatars (the engine calls it). */
|
|
1782
|
+
draw(ctx: DrawContext, alpha: number): void;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Makes the engine: picks the renderer (WebGL2, WebGL1, Canvas 2D) and the quality tier (a short probe, cached per
|
|
1786
|
+
* device), makes the stage, and follows the host through play (pause, breaks, backdrop, hands, avatars). Resolves
|
|
1787
|
+
* in under a second; then set up the scene and call start().
|
|
1788
|
+
*
|
|
1789
|
+
* @example
|
|
1790
|
+
* import { connect } from '@fabi-play/sdk'
|
|
1791
|
+
* import { createEngine } from '@fabi-play/engine'
|
|
1792
|
+
*
|
|
1793
|
+
* const play = await connect()
|
|
1794
|
+
* const engine = await createEngine({ play })
|
|
1795
|
+
* engine.stage.add(engine.avatars())
|
|
1796
|
+
* engine.start()
|
|
1797
|
+
* play.ready()
|
|
1798
|
+
*/
|
|
1799
|
+
export function createEngine(opts?: EngineOptions): Promise<Engine>;
|
|
1800
|
+
/**
|
|
1801
|
+
* A tiny shape list, the one source of truth for every avatar picture. The rig and the portrait build a list of
|
|
1802
|
+
* shapes; two small backends draw it: on a 2D canvas (the TV, the games) and as an SVG string (the phones, the
|
|
1803
|
+
* previews). Both read the same numbers, so the canvas and the SVG pictures match.
|
|
1804
|
+
*
|
|
1805
|
+
* Paths use a small subset of SVG path syntax, absolute commands only (M L Q C Z), so the canvas backend can trace
|
|
1806
|
+
* them without Path2D (not available everywhere, e.g. React Native canvases or test mocks). The rig builds them as
|
|
1807
|
+
* PathData, number arrays (no strings built and parsed again every frame); fixed paths may stay strings.
|
|
1808
|
+
*/
|
|
1809
|
+
/** How an avatar shape is painted (CSS colours). */
|
|
1810
|
+
export interface AvatarPaint {
|
|
1811
|
+
/** Fill colour; none without it. */
|
|
1812
|
+
fill?: string;
|
|
1813
|
+
/** Outline colour; none without it. */
|
|
1814
|
+
stroke?: string;
|
|
1815
|
+
/** Stroke width, in the units of the shape (scaled by the groups around it). */
|
|
1816
|
+
lw?: number;
|
|
1817
|
+
/** 0–1, multiplied with the alpha around it. */
|
|
1818
|
+
alpha?: number;
|
|
1819
|
+
}
|
|
1820
|
+
/** One shape of an avatar picture (the rig's output), in canvas units; k says which. */
|
|
1821
|
+
export type AvatarShape = (AvatarPaint & {
|
|
1822
|
+
k: "circle";
|
|
1823
|
+
x: number;
|
|
1824
|
+
y: number;
|
|
1825
|
+
r: number;
|
|
1826
|
+
}) | (AvatarPaint & {
|
|
1827
|
+
k: "ellipse";
|
|
1828
|
+
x: number;
|
|
1829
|
+
y: number;
|
|
1830
|
+
rx: number;
|
|
1831
|
+
ry: number;
|
|
1832
|
+
rot?: number;
|
|
1833
|
+
})
|
|
1834
|
+
/** A rounded rectangle by its centre, rotated by rot radians around it. */
|
|
1835
|
+
| (AvatarPaint & {
|
|
1836
|
+
k: "rect";
|
|
1837
|
+
x: number;
|
|
1838
|
+
y: number;
|
|
1839
|
+
w: number;
|
|
1840
|
+
h: number;
|
|
1841
|
+
r: number;
|
|
1842
|
+
rot?: number;
|
|
1843
|
+
}) | (AvatarPaint & {
|
|
1844
|
+
k: "path";
|
|
1845
|
+
d: string | PathData;
|
|
1846
|
+
})
|
|
1847
|
+
/** A polyline [x0, y0, x1, y1, …], stroked with round caps and joins. */
|
|
1848
|
+
| (AvatarPaint & {
|
|
1849
|
+
k: "line";
|
|
1850
|
+
pts: number[];
|
|
1851
|
+
})
|
|
1852
|
+
/** Children drawn translated by (x, y), then rotated by rot radians, then scaled by s. */
|
|
1853
|
+
| {
|
|
1854
|
+
k: "group";
|
|
1855
|
+
x: number;
|
|
1856
|
+
y: number;
|
|
1857
|
+
rot?: number;
|
|
1858
|
+
s?: number;
|
|
1859
|
+
alpha?: number;
|
|
1860
|
+
items: AvatarShape[];
|
|
1861
|
+
};
|
|
1862
|
+
/**
|
|
1863
|
+
* A path as numbers: each command code (PATH.M, …) followed by its coordinates, absolute: M x y, L x y,
|
|
1864
|
+
* Q cx cy x y, C c1x c1y c2x c2y x y, Z. Coordinates are rounded to 2 decimals, as the SVG prints them, so canvas and
|
|
1865
|
+
* SVG pictures match.
|
|
1866
|
+
*/
|
|
1867
|
+
export type PathData = readonly number[];
|
|
1868
|
+
/** An avatar's colours (CSS). */
|
|
1869
|
+
export interface Palette {
|
|
1870
|
+
/** Head and torso. */
|
|
1871
|
+
body: string;
|
|
1872
|
+
/** Arms and legs. */
|
|
1873
|
+
limbs: string;
|
|
1874
|
+
/** Belly patch, muzzle, inner face. */
|
|
1875
|
+
belly: string;
|
|
1876
|
+
/** Details: inner ears, horns, beaks, tail tips, spikes. */
|
|
1877
|
+
accent: string;
|
|
1878
|
+
/** The outline around every part: dark, so the avatar reads on light and dark backgrounds. */
|
|
1879
|
+
outline: string;
|
|
1880
|
+
/** Hands; defaults to limbs. */
|
|
1881
|
+
hands?: string;
|
|
1882
|
+
/** Feet; defaults to limbs. */
|
|
1883
|
+
feet?: string;
|
|
1884
|
+
/** Solid ears of another colour (panda); by default ears are body-coloured with an accent inside. */
|
|
1885
|
+
ears?: string;
|
|
1886
|
+
/** The face and head when they differ from the torso (people, the lamb's face); defaults to body. */
|
|
1887
|
+
skin?: string;
|
|
1888
|
+
/** Hair (hair, curls, locks). */
|
|
1889
|
+
hair?: string;
|
|
1890
|
+
}
|
|
1891
|
+
/** The head's outline. */
|
|
1892
|
+
export type HeadShape = "round" | "wide" | "tall" | "square" | "bulb" | "bucket";
|
|
1893
|
+
/** The ears (or what is in their place: horns, antennae, a fin). */
|
|
1894
|
+
export type EarKind = "none" | "pointy" | "round" | "big-round" | "long" | "side" | "tufts" | "antenna" | "antennae" | "horns" | "unicorn" | "floppy" | "droopy" | "fin" | "human";
|
|
1895
|
+
/** How the eyes are drawn. */
|
|
1896
|
+
export type EyeKind = "dot" | "big" | "mask" | "screen" | "almond" | "visor" | "calm" | "slit" | "goggle" | "bead";
|
|
1897
|
+
/** How the mouth (or beak) is drawn. */
|
|
1898
|
+
export type MouthKind = "none" | "smile" | "open" | "cat" | "beak" | "teeth" | "grill" | "tiny" | "smirk" | "bill" | "hook" | "jaws";
|
|
1899
|
+
/** The patch around the nose and mouth. */
|
|
1900
|
+
export type MuzzleKind = "none" | "oval" | "heart" | "monkey" | "disc" | "cheeks" | "band";
|
|
1901
|
+
/** The tail, behind the body. */
|
|
1902
|
+
export type TailKind = "none" | "fluffy" | "brush" | "pom" | "long" | "curl" | "tuft" | "striped" | "spiky" | "arrow" | "fish" | "plume";
|
|
1903
|
+
/** Things on the head. */
|
|
1904
|
+
export type HeadExtra = "mane" | "rainbow-mane" | "stripes" | "spikes" | "spots" | "curl" | "helmet" | "bolts" | "whiskers" | "tuft" | "brows" | "eggshell" | "wool" | "eye-spot" | "crest" | "forehead" | "gills" | "ruff" | "hair" | "curls" | "locks" | "ninja-hood" | "cap" | "safari-hat" | "wizard-hat" | "plume" | "stripe" | "bandana" | "eye-patch" | "dive-mask" | "domino";
|
|
1905
|
+
/** Things on the body. */
|
|
1906
|
+
export type BodyExtra = "wings" | "backpack" | "sheet" | "tentacles" | "panel" | "badge" | "joints" | "feathers" | "flippers" | "boots" | "scarf" | "spots" | "wool" | "robe" | "cape" | "fins" | "tank" | "straps" | "belt" | "stripes" | "pauldrons" | "emblem" | "pocket" | "racing-stripe" | "star";
|
|
1907
|
+
/** An avatar's face. */
|
|
1908
|
+
export interface FaceDef {
|
|
1909
|
+
/** How the eyes are drawn. */
|
|
1910
|
+
eyes: EyeKind;
|
|
1911
|
+
/** Eye colour for big/screen/almond/visor eyes. */
|
|
1912
|
+
eyeColor?: string;
|
|
1913
|
+
/** Eyes higher than usual and bulging over the head (frog). */
|
|
1914
|
+
eyesOnTop?: boolean;
|
|
1915
|
+
/** Ring around the eyes (owl). */
|
|
1916
|
+
eyeRing?: string;
|
|
1917
|
+
/** How the mouth is drawn. */
|
|
1918
|
+
mouth: MouthKind;
|
|
1919
|
+
/** Nose colour; none when missing. */
|
|
1920
|
+
nose?: string;
|
|
1921
|
+
/** The patch around nose and mouth. */
|
|
1922
|
+
muzzle: MuzzleKind;
|
|
1923
|
+
/** Blush colour; none when missing. */
|
|
1924
|
+
cheeks?: string;
|
|
1925
|
+
/** Eyebrows of this colour (people): a little attitude. */
|
|
1926
|
+
brows?: string;
|
|
1927
|
+
}
|
|
1928
|
+
/** The age band an avatar is for, or 'all': offered in every band (Fabi the fox, the app's mascot). */
|
|
1929
|
+
export type AvatarBand = AgeRange | "all";
|
|
1930
|
+
/** An avatar, as data: one renderer draws any definition. */
|
|
1931
|
+
export interface AvatarDef {
|
|
1932
|
+
/** Stable kebab-case id: stored on the phone and sent around, never renamed. */
|
|
1933
|
+
id: string;
|
|
1934
|
+
/** Its name, in English and Italian, for pickers. */
|
|
1935
|
+
name: {
|
|
1936
|
+
en: string;
|
|
1937
|
+
it: string;
|
|
1938
|
+
};
|
|
1939
|
+
/**
|
|
1940
|
+
* The age band the avatar is for: its style and its body proportions (see BAND_BODY in rig.ts). 'all': in every
|
|
1941
|
+
* band's list, drawn with the 6–8 proportions.
|
|
1942
|
+
*/
|
|
1943
|
+
band: AvatarBand;
|
|
1944
|
+
/** A special avatar (the mascot): first in every band's list; pickers may mark it. */
|
|
1945
|
+
special?: boolean;
|
|
1946
|
+
/** Its colours. */
|
|
1947
|
+
palette: Palette;
|
|
1948
|
+
/** The head's outline. */
|
|
1949
|
+
head: HeadShape;
|
|
1950
|
+
/** The ears, or what is in their place. */
|
|
1951
|
+
ears: EarKind;
|
|
1952
|
+
/** The face. */
|
|
1953
|
+
face: FaceDef;
|
|
1954
|
+
/** The tail. */
|
|
1955
|
+
tail: TailKind;
|
|
1956
|
+
/** Things on the head (manes, hats, masks). */
|
|
1957
|
+
headExtras: readonly HeadExtra[];
|
|
1958
|
+
/** Things on the body (wings, capes, belts). */
|
|
1959
|
+
bodyExtras: readonly BodyExtra[];
|
|
1960
|
+
/** Colours of the rainbow mane, of the stripes, of the spots: per-avatar extra colours. */
|
|
1961
|
+
extraColors?: readonly string[];
|
|
1962
|
+
}
|
|
1963
|
+
/** Where player space (0–1) lands on the canvas. */
|
|
1964
|
+
export interface AvatarBox {
|
|
1965
|
+
/** Left edge, canvas units. */
|
|
1966
|
+
x: number;
|
|
1967
|
+
/** Top edge, canvas units. */
|
|
1968
|
+
y: number;
|
|
1969
|
+
/** Width, canvas units. */
|
|
1970
|
+
w: number;
|
|
1971
|
+
/** Height, canvas units. */
|
|
1972
|
+
h: number;
|
|
1973
|
+
}
|
|
1974
|
+
/** Bounds of a shape list, in its units (outlines included): for the bake canvas. */
|
|
1975
|
+
export function shapeBounds(shapes: readonly AvatarShape[], m?: Mat | null): {
|
|
1976
|
+
minX: number;
|
|
1977
|
+
minY: number;
|
|
1978
|
+
maxX: number;
|
|
1979
|
+
maxY: number;
|
|
1980
|
+
};
|
|
1981
|
+
/** What an AvatarPainter drew: for comparisons and logs. */
|
|
1982
|
+
export interface PaintStats {
|
|
1983
|
+
/** Groups drawn as baked sprites. */
|
|
1984
|
+
baked: number;
|
|
1985
|
+
/** Groups drawn as vectors. */
|
|
1986
|
+
vector: number;
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Turns avatar shape lists into renderer calls, baking rigid groups (heads) into an atlas. Nothing is allocated per
|
|
1990
|
+
* frame once the heads are baked: a matrix stack by group depth, scratch point buffers, colours parsed once.
|
|
1991
|
+
*/
|
|
1992
|
+
export class AvatarPainter {
|
|
1993
|
+
private readonly atlas;
|
|
1994
|
+
private readonly seen;
|
|
1995
|
+
private readonly baked;
|
|
1996
|
+
private readonly m;
|
|
1997
|
+
/** Group transforms by depth, and one for a rounded rectangle's own. */
|
|
1998
|
+
private readonly stack;
|
|
1999
|
+
private readonly local;
|
|
2000
|
+
private pts;
|
|
2001
|
+
/** Counts since it was made. */
|
|
2002
|
+
readonly stats: PaintStats;
|
|
2003
|
+
/** Bake heads (true) or draw everything as vectors (comparisons). */
|
|
2004
|
+
bakeGroups: boolean;
|
|
2005
|
+
/** Coarse head bakes (the low tier): see bakeBucket. */
|
|
2006
|
+
private coarse;
|
|
2007
|
+
private gen;
|
|
2008
|
+
/** The bakes of this generation, to free them when the tier changes. */
|
|
2009
|
+
private readonly live;
|
|
2010
|
+
/** A painter baking into atlas. */
|
|
2011
|
+
constructor(atlas: Atlas);
|
|
2012
|
+
/**
|
|
2013
|
+
* Follows the quality tier: on 'low' the heads are baked coarser (steps of 2, at most 256 pixels per unit), on the
|
|
2014
|
+
* others finer. A change frees the heads baked so far; they bake again, at the new sizes, on their next draw.
|
|
2015
|
+
*/
|
|
2016
|
+
setTier(tier: "high" | "medium" | "low"): void;
|
|
2017
|
+
/**
|
|
2018
|
+
* Draws one avatar posed like body in box (stage units). pixelScale: backing pixels per unit (bake sizes, curve
|
|
2019
|
+
* flattening). Returns whether it drew (false: shoulders not seen). Below alpha 1 the avatar is drawn into an
|
|
2020
|
+
* offscreen layer and composited at that alpha, so it is faint as a whole: its outlines and overlapping parts
|
|
2021
|
+
* don't show through each other.
|
|
2022
|
+
*/
|
|
2023
|
+
draw(r: Renderer, tex: (s: Surface) => Texture, body: Body, avatar: AvatarDef | string, box: AvatarBox, alpha: number, pixelScale: number): boolean;
|
|
2024
|
+
/** Paints a shape list under the transform m (a similarity: translate, rotate, uniform scale) with an alpha. */
|
|
2025
|
+
paint(r: Renderer, tex: (s: Surface) => Texture, shapes: readonly AvatarShape[], m: Mat | null, alpha: number, pixelScale: number, depth?: number): void;
|
|
2026
|
+
/** A shape list baked at k pixels per unit (bucketed), or null if it would be too big. */
|
|
2027
|
+
private bake;
|
|
2028
|
+
}
|
|
2029
|
+
/** One player for AvatarRenderer.draw: a body, an avatar, an optional name tag. */
|
|
2030
|
+
export interface AvatarPlayer {
|
|
2031
|
+
/** The body, in player space (PlayerFrame.body). */
|
|
2032
|
+
body: Body;
|
|
2033
|
+
/** An avatar id (fox, lion, robot, …; unknown ids: the default avatar). */
|
|
2034
|
+
avatar?: string;
|
|
2035
|
+
/** Who it is, for smoothing and interpolation (by default the place in the array, from 1). */
|
|
2036
|
+
id?: number;
|
|
2037
|
+
/**
|
|
2038
|
+
* Who it is, when the caller has a stable identity that is not a small number (a tracker's person id, a name):
|
|
2039
|
+
* smoothing and interpolation follow it, whatever the place in the array. Takes precedence over id.
|
|
2040
|
+
*/
|
|
2041
|
+
key?: string | number;
|
|
2042
|
+
/** A name tag above the head, in a pill of this colour (the player's colour). */
|
|
2043
|
+
name?: string;
|
|
2044
|
+
/** The tag's colour (CSS; the player's colour). */
|
|
2045
|
+
color?: string;
|
|
2046
|
+
/**
|
|
2047
|
+
* 0–1 for the whole avatar and its tag (extra people are drawn faint). Below 1 the avatar is composited as one
|
|
2048
|
+
* layer: faint, without its outlines and overlapping parts showing through each other.
|
|
2049
|
+
*/
|
|
2050
|
+
alpha?: number;
|
|
2051
|
+
}
|
|
2052
|
+
/** Options of createAvatarRenderer. */
|
|
2053
|
+
export interface AvatarRendererOptions extends DetectOptions {
|
|
2054
|
+
/**
|
|
2055
|
+
* The drawing units: a fixed size (as a 2D canvas's width and height, e.g. the TV's 1280 × 720 stage), stretched
|
|
2056
|
+
* by CSS; the backing store is that times pixelRatio (1 by default). Without it: CSS pixels, times
|
|
2057
|
+
* devicePixelRatio (at most 2).
|
|
2058
|
+
*/
|
|
2059
|
+
units?: {
|
|
2060
|
+
width: number;
|
|
2061
|
+
height: number;
|
|
2062
|
+
};
|
|
2063
|
+
/** Backing-store pixels per drawing unit (default: 1 with units, else devicePixelRatio, at most 2). */
|
|
2064
|
+
pixelRatio?: number;
|
|
2065
|
+
/** As the SDK's drawAvatar: smoothing per player (0.5), or false (bodies already smoothed by the caller). */
|
|
2066
|
+
smooth?: boolean | number;
|
|
2067
|
+
/** Bodies interpolated at the display's rate (true, default), or drawn as given. */
|
|
2068
|
+
interpolate?: boolean;
|
|
2069
|
+
/** Name tags: their height in drawing units (34 on the 1280 × 720 stage) and font family. */
|
|
2070
|
+
tagHeight?: number;
|
|
2071
|
+
/** The tags' CSS font family (default Nunito). */
|
|
2072
|
+
tagFont?: string;
|
|
2073
|
+
}
|
|
2074
|
+
/** createAvatarRenderer's result: avatars drawn on a canvas of their own. */
|
|
2075
|
+
export interface AvatarRenderer {
|
|
2076
|
+
/** The renderer in use: a GL one is replaced by Canvas 2D (on a fresh canvas) after 3 s of context loss. */
|
|
2077
|
+
readonly renderer: Renderer;
|
|
2078
|
+
/** Its kind: 'webgl2', 'webgl1' or 'canvas2d'. */
|
|
2079
|
+
readonly kind: RendererKind;
|
|
2080
|
+
/** The GL context is lost right now (nothing is drawn until it comes back, or Canvas 2D takes over). */
|
|
2081
|
+
readonly lost: boolean;
|
|
2082
|
+
/** The canvas drawn in: after a switch to Canvas 2D a fresh one, in the old one's place in the page. */
|
|
2083
|
+
readonly canvas: HTMLCanvasElement;
|
|
2084
|
+
/**
|
|
2085
|
+
* Clears the canvas and draws each player's avatar, posed like their body (smoothed and interpolated per player
|
|
2086
|
+
* unless the options say not), with its name tag. box: where player space (0–1) lands, in drawing units. Call it
|
|
2087
|
+
* every animation frame.
|
|
2088
|
+
*/
|
|
2089
|
+
draw(players: readonly AvatarPlayer[], box: AvatarBox): void;
|
|
2090
|
+
/** An empty canvas (nothing to draw this frame). */
|
|
2091
|
+
clear(): void;
|
|
2092
|
+
/** Forget the players' previous bodies (a new scene). */
|
|
2093
|
+
reset(): void;
|
|
2094
|
+
/** Frees the renderer and its textures. */
|
|
2095
|
+
destroy(): void;
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* The avatars on a canvas of their own, with the engine's renderer (WebGL2, WebGL1 or Canvas 2D, chosen as the
|
|
2099
|
+
* engine chooses), its own atlas and the SDK's smoothing and interpolation. For pages that are not engine games:
|
|
2100
|
+
* the TV's calibration, the simulator's preview. Throws only if the page has neither WebGL nor Canvas 2D.
|
|
2101
|
+
*/
|
|
2102
|
+
export function createAvatarRenderer(canvas: HTMLCanvasElement, opts?: AvatarRendererOptions): AvatarRenderer;
|
|
2103
|
+
/**
|
|
2104
|
+
* An avatar's portrait (head and shoulders, neutral pose) as a canvas of size × size CSS pixels at pixelRatio,
|
|
2105
|
+
* drawn once from the same shapes as the avatars and cached: for DOM places such as the TV's gesture rings.
|
|
2106
|
+
*/
|
|
2107
|
+
export function avatarPortrait(avatar: string, size: number, pixelRatio?: number): HTMLCanvasElement;
|
|
2108
|
+
/** A colour filter with one parameter, `amount`. */
|
|
2109
|
+
export interface AmountFilter extends Filter {
|
|
2110
|
+
/** Its strength (see the factory). Tween it freely. */
|
|
2111
|
+
amount: number;
|
|
2112
|
+
}
|
|
2113
|
+
/** hueRotate's filter: `angle` in degrees (`amount` is the same number). */
|
|
2114
|
+
export interface HueRotateFilter extends AmountFilter {
|
|
2115
|
+
/** Degrees. */
|
|
2116
|
+
angle: number;
|
|
2117
|
+
}
|
|
2118
|
+
/** tint's filter: the colour and how far towards it (0 none, 1 all of it). */
|
|
2119
|
+
export interface TintFilter extends AmountFilter {
|
|
2120
|
+
/** The colour mixed in, as 0xRRGGBB or CSS (reads as 0xRRGGBB). */
|
|
2121
|
+
get color(): number;
|
|
2122
|
+
set color(v: ColorLike);
|
|
2123
|
+
}
|
|
2124
|
+
/** colorMatrix's filter: 20 numbers, row-major (R, G, B, A rows; r, g, b, a, offset columns; offsets 0–1). */
|
|
2125
|
+
export interface ColorMatrixFilter extends Filter {
|
|
2126
|
+
/** The matrix: change its numbers in place, or set a new one. */
|
|
2127
|
+
values: number[] | Float32Array;
|
|
2128
|
+
}
|
|
2129
|
+
/** blur's filter: `radius`, stage units. */
|
|
2130
|
+
export interface BlurFilter extends Filter {
|
|
2131
|
+
/** The Gaussian's standard deviation, stage units (as CSS blur()); it reaches about three times as far. */
|
|
2132
|
+
radius: number;
|
|
2133
|
+
}
|
|
2134
|
+
/** dropShadow's filter: offset, blur and colour, all changeable. */
|
|
2135
|
+
export interface DropShadowFilter extends Filter {
|
|
2136
|
+
/** Offset right, stage units. */
|
|
2137
|
+
dx: number;
|
|
2138
|
+
/** Offset down, stage units. */
|
|
2139
|
+
dy: number;
|
|
2140
|
+
/** The shadow's blur, σ in stage units (as blur()). */
|
|
2141
|
+
blur: number;
|
|
2142
|
+
/** 0xRRGGBB, or CSS with alpha ('rgba(0,0,0,0.5)'); reads as 0xRRGGBB. */
|
|
2143
|
+
get color(): number;
|
|
2144
|
+
set color(v: ColorLike);
|
|
2145
|
+
/** The colour's alpha, 0–1. */
|
|
2146
|
+
opacity: number;
|
|
2147
|
+
}
|
|
2148
|
+
/** shader()'s options. */
|
|
2149
|
+
export interface ShaderOptions {
|
|
2150
|
+
/**
|
|
2151
|
+
* A GLSL ES 1.00 fragment shader: declare your own uniforms and write main(). The engine declares, before it:
|
|
2152
|
+
*
|
|
2153
|
+
* uniform sampler2D uTexture; // the node drawn so far (premultiplied alpha)
|
|
2154
|
+
* varying vec2 vUv; // where this pixel is in uTexture, 0–1, (0, 0) bottom-left
|
|
2155
|
+
* uniform vec2 uResolution; // uTexture's size, pixels
|
|
2156
|
+
* uniform float uTime; // engine.time, seconds (stops on pause)
|
|
2157
|
+
* uniform vec4 uRect; // the node's area in uTexture (x0, y0, x1, y1), 0–1
|
|
2158
|
+
*
|
|
2159
|
+
* Write a premultiplied colour to gl_FragColor; the engine applies the node's alpha after it.
|
|
2160
|
+
*/
|
|
2161
|
+
fragment: string;
|
|
2162
|
+
/** Your uniforms (float, vec2–vec4) by name: numbers or arrays of 2–4. Change them in place any time. */
|
|
2163
|
+
uniforms?: Record<string, number | number[]>;
|
|
2164
|
+
/** How far the effect reaches past the node's pixels, stage units (a ripple that moves pixels out). Default 0. */
|
|
2165
|
+
padding?: number;
|
|
2166
|
+
}
|
|
2167
|
+
/** shader()'s filter. */
|
|
2168
|
+
export interface ShaderFilter extends Filter {
|
|
2169
|
+
/** The uniforms: set numbers (`f.uniforms.uAmount = 0.5`) or change arrays in place (`f.uniforms.uCenter[0] = x`). */
|
|
2170
|
+
readonly uniforms: Record<string, number | Float32Array>;
|
|
2171
|
+
/** Stage units the effect reaches past the node's pixels. */
|
|
2172
|
+
padding: number;
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Mixes the node's colours towards `color` by `amount` (0 none, 1 a flat silhouette of that colour): a hit flash,
|
|
2176
|
+
* a frozen look. Unlike node.tint (a multiply), it can make dark pixels light.
|
|
2177
|
+
*
|
|
2178
|
+
* @example
|
|
2179
|
+
* const flash = tint('#ffffff', 0); enemy.filters = [flash]
|
|
2180
|
+
* engine.tween(flash, { amount: 1 }, 0.06).then(() => engine.tween(flash, { amount: 0 }, 0.2))
|
|
2181
|
+
*/
|
|
2182
|
+
export const tint: (color: ColorLike, amount?: number) => TintFilter;
|
|
2183
|
+
/** Brightness: 1 unchanged, 0 black, above 1 brighter (CSS brightness()). */
|
|
2184
|
+
export const brightness: (v: number) => AmountFilter;
|
|
2185
|
+
/** Contrast: 1 unchanged, 0 flat grey, above 1 more (CSS contrast()). */
|
|
2186
|
+
export const contrast: (v: number) => AmountFilter;
|
|
2187
|
+
/** Saturation: 1 unchanged, 0 grey, above 1 more vivid (CSS saturate()). */
|
|
2188
|
+
export const saturate: (v: number) => AmountFilter;
|
|
2189
|
+
/** Grey: amount 0–1 (CSS grayscale()). */
|
|
2190
|
+
export const grayscale: (amount?: number) => AmountFilter;
|
|
2191
|
+
/** Old-photo brown: amount 0–1 (CSS sepia()). */
|
|
2192
|
+
export const sepia: (amount?: number) => AmountFilter;
|
|
2193
|
+
/** Turns hues around the colour wheel by `deg` degrees (CSS hue-rotate()). */
|
|
2194
|
+
export const hueRotate: (deg: number) => HueRotateFilter;
|
|
2195
|
+
/** Negative: amount 0–1 (CSS invert()). */
|
|
2196
|
+
export const invert: (amount?: number) => AmountFilter;
|
|
2197
|
+
/**
|
|
2198
|
+
* Any colour matrix: 20 numbers, 4 rows (R, G, B, A) of 5 (times r, g, b, a, plus an offset 0–1), on unpremultiplied
|
|
2199
|
+
* colours (SVG feColorMatrix). On Canvas 2D it needs ctx.filter with url() (Chrome, Firefox); elsewhere it is skipped.
|
|
2200
|
+
*/
|
|
2201
|
+
export const colorMatrix: (m: ArrayLike<number>) => ColorMatrixFilter;
|
|
2202
|
+
/**
|
|
2203
|
+
* A Gaussian blur of `radius` stage units (the standard deviation, as CSS blur()). Capped per tier
|
|
2204
|
+
* (engine.supports.maxBlur): off on the low tier. Each blurred node costs two to six passes over its area.
|
|
2205
|
+
*/
|
|
2206
|
+
export const blur: (radius: number) => BlurFilter;
|
|
2207
|
+
/**
|
|
2208
|
+
* A shadow of the node's shape: offset (dx, dy), blurred by `blur` (σ, stage units; capped like blur(), hard on the
|
|
2209
|
+
* low tier), in `color` (with its alpha: 'rgba(0,0,0,0.4)').
|
|
2210
|
+
*/
|
|
2211
|
+
export const dropShadow: (dx: number, dy: number, blur: number, color?: ColorLike) => DropShadowFilter;
|
|
2212
|
+
/**
|
|
2213
|
+
* A custom fragment shader as a filter: **WebGL only**. On Canvas 2D it is skipped (the node is drawn without it,
|
|
2214
|
+
* engine.stats.filtersSkipped counts it, the console says so once): check engine.supports.shaders and pick another
|
|
2215
|
+
* look there. Compiled once per distinct source. With a WebGL engine running, a shader that does not compile throws
|
|
2216
|
+
* here with the compiler's log; otherwise the error is logged once at its first use and the filter skipped.
|
|
2217
|
+
*
|
|
2218
|
+
* @example
|
|
2219
|
+
* const wave = shader({
|
|
2220
|
+
* fragment: `uniform float uAmp;
|
|
2221
|
+
* void main() { gl_FragColor = texture2D(uTexture, vUv + vec2(sin(vUv.y * 40.0 + uTime * 4.0) * uAmp, 0.0)); }`,
|
|
2222
|
+
* uniforms: { uAmp: 0.004 },
|
|
2223
|
+
* padding: 8,
|
|
2224
|
+
* })
|
|
2225
|
+
* flag.filters = [wave]
|
|
2226
|
+
*/
|
|
2227
|
+
export function shader(o: ShaderOptions): ShaderFilter;
|
|
2228
|
+
/** nodePool's options. */
|
|
2229
|
+
export interface NodePoolOptions<T extends Node> {
|
|
2230
|
+
/** Most free nodes kept; released ones past it are destroyed. Default: no limit. */
|
|
2231
|
+
max?: number;
|
|
2232
|
+
/** Called on a node as it is released, after the engine reset it: reset what the engine does not (data, children). */
|
|
2233
|
+
reset?: (node: T) => void;
|
|
2234
|
+
}
|
|
2235
|
+
/** What a pool counts. */
|
|
2236
|
+
export interface NodePoolStats {
|
|
2237
|
+
/** Nodes given out by get and not released (nor destroyed). */
|
|
2238
|
+
live: number;
|
|
2239
|
+
/** Released nodes waiting for get. */
|
|
2240
|
+
free: number;
|
|
2241
|
+
/** Nodes the factory made, in all. */
|
|
2242
|
+
created: number;
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Nodes made by a factory once and used again: get() gives a free one (or makes one), release() takes it back. From
|
|
2246
|
+
* nodePool(factory, options).
|
|
2247
|
+
*
|
|
2248
|
+
* @example
|
|
2249
|
+
* const labels = nodePool(() => engine.text('', { size: 32, outline: true }), { max: 20 })
|
|
2250
|
+
* const l = hud.add(labels.get({ x, y, text: '+10' }))
|
|
2251
|
+
* engine.tween(l, { y: y - 60, alpha: 0 }, 0.8).then(() => labels.release(l))
|
|
2252
|
+
*/
|
|
2253
|
+
export class NodePool<T extends Node> {
|
|
2254
|
+
private readonly factory;
|
|
2255
|
+
private readonly opts;
|
|
2256
|
+
/** live, free and created: kept up to date (one object, read it any time). */
|
|
2257
|
+
readonly stats: NodePoolStats;
|
|
2258
|
+
private readonly free;
|
|
2259
|
+
/** Every node it made and still has (live or free), with its state as made. */
|
|
2260
|
+
private readonly states;
|
|
2261
|
+
private dead;
|
|
2262
|
+
/** Use nodePool(factory, options). */
|
|
2263
|
+
constructor(factory: () => T, opts?: NodePoolOptions<T>);
|
|
2264
|
+
/**
|
|
2265
|
+
* A node: a free one, back to how the factory made it, or a new one; then props are applied (any node field, and a
|
|
2266
|
+
* text's `text`). It is not added anywhere: add it to a group.
|
|
2267
|
+
*
|
|
2268
|
+
* What is reset: position, rotation, scale, alpha, visible, blend, tint, z, anchor, hitRadius, clip, mask, filters
|
|
2269
|
+
* (the same objects the factory set, not copies); a sprite's frame, size and slice; a shape's spec; a text's string
|
|
2270
|
+
* and style; an animation's fps, loop, speed, onComplete and playing, rewound to its first frame. Not reset: `data`,
|
|
2271
|
+
* a group's children, and nodes' own fields in your subclasses: reset those in options.reset.
|
|
2272
|
+
*/
|
|
2273
|
+
get(props?: NodeProps & {
|
|
2274
|
+
text?: string;
|
|
2275
|
+
}): T;
|
|
2276
|
+
/**
|
|
2277
|
+
* Takes a node back: out of its group, hidden, its tweens stopped (they resolve), its animation stopped, reset; not
|
|
2278
|
+
* destroyed. Keep no reference to it: the next get() gives it to someone else. Past `max` free nodes, it is
|
|
2279
|
+
* destroyed instead. Releasing a node twice, or one of another pool, does nothing (and says so once).
|
|
2280
|
+
*/
|
|
2281
|
+
release(node: T): void;
|
|
2282
|
+
/** @internal A node of this pool was destroyed: it leaves the pool. */
|
|
2283
|
+
forget(node: Node): void;
|
|
2284
|
+
/** Destroys the free nodes (the ones in use stay). */
|
|
2285
|
+
clear(): void;
|
|
2286
|
+
/** Destroys every node it made, in use or free; get() can't be called after. */
|
|
2287
|
+
destroy(): void;
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* A pool of nodes made by factory and used again: get() a node (a free one, reset to how the factory made it, or a new
|
|
2291
|
+
* one), release() it instead of destroying it. Worth it only when spawning many nodes a second; creating and
|
|
2292
|
+
* destroying nodes is fine otherwise.
|
|
2293
|
+
*
|
|
2294
|
+
* @example
|
|
2295
|
+
* import { nodePool } from '@fabi-play/engine'
|
|
2296
|
+
* const coins = nodePool(() => engine.shape({ type: 'circle', r: 16, fill: '#FBD46A' }), { max: 40 })
|
|
2297
|
+
* const coin = layer.add(coins.get({ x: 200, y: -20 }))
|
|
2298
|
+
* engine.tween(coin, { y: 740 }, 2, 'linear').then(() => coins.release(coin))
|
|
2299
|
+
*/
|
|
2300
|
+
export const nodePool: <T extends Node>(factory: () => T, options?: NodePoolOptions<T>) => NodePool<T>;
|
|
2301
|
+
/** How tags look: their height in drawing units (34 on the 1280 × 720 stage) and font family. */
|
|
2302
|
+
export interface TagOptions {
|
|
2303
|
+
/** The tag's height, drawing units (default 34). */
|
|
2304
|
+
tagHeight?: number;
|
|
2305
|
+
/** The tags' CSS font family (default Nunito). */
|
|
2306
|
+
tagFont?: string;
|
|
2307
|
+
}
|
|
2308
|
+
/** Name tags baked into an atlas, and drawn above heads. */
|
|
2309
|
+
export class NameTags {
|
|
2310
|
+
private readonly atlas;
|
|
2311
|
+
/** Baked tags by name, colour and resolution (nested maps: no key string built per lookup). */
|
|
2312
|
+
private readonly tags;
|
|
2313
|
+
private count;
|
|
2314
|
+
private readonly m;
|
|
2315
|
+
private measure;
|
|
2316
|
+
/** The tag's height, drawing units. */
|
|
2317
|
+
readonly height: number;
|
|
2318
|
+
private readonly font;
|
|
2319
|
+
/** Tags baked into atlas. */
|
|
2320
|
+
constructor(atlas: Atlas, o?: TagOptions);
|
|
2321
|
+
/** Forgets every tag (bake them again: the web font arrived). */
|
|
2322
|
+
drop(): void;
|
|
2323
|
+
/** The name in a pill of the colour, baked once per name, colour and resolution (ratio: pixels per unit). */
|
|
2324
|
+
get(name: string, color: string, ratio: number): Frame;
|
|
2325
|
+
/** get's slow path, apart (its closure's context would be made on every get otherwise). */
|
|
2326
|
+
private bake;
|
|
2327
|
+
/**
|
|
2328
|
+
* Draws a player's tag above the head of body as drawn in box (def's band proportions), inside a width × height
|
|
2329
|
+
* area (drawing units). ratio: pixels per unit.
|
|
2330
|
+
*/
|
|
2331
|
+
draw(r: Renderer, tex: (s: Surface) => Texture, body: Body, def: AvatarDef, box: AvatarBox, name: string, color: string, alpha: number, ratio: number, width: number, height: number): void;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
interface Window {
|
|
2336
|
+
/** The Fabi Play engine, set by fabi-play-engine.js (see the global FabiEngine). */
|
|
2337
|
+
readonly FabiEngine: typeof FabiEngine
|
|
2338
|
+
}
|