@rig-c/parts 0.0.0-stage → 1.5.4
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 +21 -0
- package/NOTICE.md +134 -0
- package/README.md +433 -2
- package/bin/rig-parts.cjs +43 -0
- package/cli.ts +1240 -0
- package/docs/AUTHORING.md +2387 -0
- package/docs/MIGRATION.md +150 -0
- package/docs/STABILITY.md +262 -0
- package/package.json +70 -4
- package/skills/rig-parts/SKILL.md +167 -0
- package/src/apng.ts +381 -0
- package/src/assemble.ts +2259 -0
- package/src/autoamplitude.ts +194 -0
- package/src/autoenvelope.ts +404 -0
- package/src/automesh.ts +585 -0
- package/src/automotion.ts +447 -0
- package/src/autoreplay.ts +516 -0
- package/src/build.ts +1240 -0
- package/src/check.ts +2942 -0
- package/src/comfy/client.ts +245 -0
- package/src/comfy/index.ts +19 -0
- package/src/comfy/painting.ts +136 -0
- package/src/comfy/seethrough.ts +171 -0
- package/src/config.ts +2283 -0
- package/src/contour.ts +1901 -0
- package/src/coords.ts +26 -0
- package/src/diagnostics.ts +198 -0
- package/src/errors.ts +39 -0
- package/src/gif.ts +213 -0
- package/src/graphs.ts +302 -0
- package/src/headbox.ts +173 -0
- package/src/heat.ts +313 -0
- package/src/inputs.ts +100 -0
- package/src/instruments.ts +1145 -0
- package/src/keypoints.ts +296 -0
- package/src/layers.ts +598 -0
- package/src/localweights.ts +217 -0
- package/src/mesh.ts +362 -0
- package/src/motion.ts +311 -0
- package/src/palette.ts +208 -0
- package/src/parts.ts +410 -0
- package/src/propose.ts +2540 -0
- package/src/pyfmt.ts +135 -0
- package/src/raster/blur.ts +80 -0
- package/src/raster/components.ts +165 -0
- package/src/raster/composite.ts +81 -0
- package/src/raster/index.ts +9 -0
- package/src/raster/morph.ts +116 -0
- package/src/raster/png.ts +82 -0
- package/src/raster/poly.ts +166 -0
- package/src/raster/resize.ts +301 -0
- package/src/raster/types.ts +89 -0
- package/src/raster/warp.ts +121 -0
- package/src/requirements.ts +1240 -0
- package/src/rig.ts +1440 -0
- package/src/round.ts +62 -0
- package/src/scene.ts +1414 -0
- package/src/sheet.ts +128 -0
- package/src/skeleton.ts +400 -0
- package/src/structure.ts +858 -0
- package/src/tags.ts +121 -0
- package/src/weights.ts +124 -0
- package/workflows/seethrough.json +68 -0
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instruments behind `check`'s six judgement lines (issue #11, and #31
|
|
3
|
+
* for texture stretch): what an eye used to be asked about a rendered idle,
|
|
4
|
+
* turned into figures read off rigc's frames, its `geometry.json`,
|
|
5
|
+
* `parts.json` and `motion.json`.
|
|
6
|
+
*
|
|
7
|
+
* Everything here is pure: frames, part records and specs in, figures out.
|
|
8
|
+
* `src/check.ts` owns the rigc renders that produce the frames and the bars
|
|
9
|
+
* the figures are held to.
|
|
10
|
+
*
|
|
11
|
+
* ⭐ **A region is chosen by the See-through tag a part came from, never by a
|
|
12
|
+
* part's name** (`parts.json`'s `from`, read through `src/tags.ts`). The
|
|
13
|
+
* torso is `topwear`, the feet `footwear`, the eyes `eyewhite`, the swinging
|
|
14
|
+
* cloth `handwear` and `bottomwear`, the face `face` less the parts drawn on
|
|
15
|
+
* it (`eyewhite`, `irides`, `eyelash`, `eyebrow`, `mouth`). A part is drawn by
|
|
16
|
+
* the slot of its own name — the rig stage's contract, which `check` holds
|
|
17
|
+
* with `CHECK_PART_SLOT_PRESENT` — so a region's slots follow from its parts.
|
|
18
|
+
*
|
|
19
|
+
* A chain is read off the rig's bone TREE and the idle's rotate tracks, not
|
|
20
|
+
* off link names: a keyed bone's chain parent is its nearest ancestor that is
|
|
21
|
+
* itself keyed, which is also how the control bones `<bone>_ctl` fall into
|
|
22
|
+
* place without being recognised by their suffix.
|
|
23
|
+
*/
|
|
24
|
+
import type { StageBox, Viewport } from './check.ts';
|
|
25
|
+
import { cropToSpineY } from './coords.ts';
|
|
26
|
+
import { OPAQUE_ALPHA_ABOVE } from './layers.ts';
|
|
27
|
+
import { type PartRecord, type PartsFile, tagOf } from './parts.ts';
|
|
28
|
+
import type { Raster } from './raster/types.ts';
|
|
29
|
+
import { type BaseTag, readTag } from './tags.ts';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// regions, by tag
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The v3 base tag a part came from, or null for a `painting:` patch — which no
|
|
37
|
+
* tag classifies, so no region picks it — or a `from` that is neither
|
|
38
|
+
* (readParts already refused that).
|
|
39
|
+
*/
|
|
40
|
+
export function baseTagOf(p: PartRecord): BaseTag | null {
|
|
41
|
+
const t = tagOf(p.from);
|
|
42
|
+
return t === null ? null : (readTag(t)?.base ?? null);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function partsTagged(parts: PartsFile, tags: readonly BaseTag[]): PartRecord[] {
|
|
46
|
+
return parts.parts.filter((p) => {
|
|
47
|
+
const t = baseTagOf(p);
|
|
48
|
+
return t !== null && tags.includes(t);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const TORSO_TAGS: readonly BaseTag[] = ['topwear'];
|
|
53
|
+
export const FEET_TAGS: readonly BaseTag[] = ['footwear'];
|
|
54
|
+
export const EYE_TAGS: readonly BaseTag[] = ['eyewhite'];
|
|
55
|
+
export const SWING_TAGS: readonly BaseTag[] = ['handwear', 'bottomwear'];
|
|
56
|
+
export const FACE_TAGS: readonly BaseTag[] = ['face'];
|
|
57
|
+
/** Drawn on the face and moved by the blink or the mouth: left out of the face-outline region. */
|
|
58
|
+
export const FACE_FEATURE_TAGS: readonly BaseTag[] = ['eyewhite', 'irides', 'eyelash', 'eyebrow', 'mouth'];
|
|
59
|
+
|
|
60
|
+
/** A box in frame pixels, half-open: x0 <= x < x1, y0 <= y < y1, clipped to the frame. */
|
|
61
|
+
export interface PixelBox {
|
|
62
|
+
x0: number;
|
|
63
|
+
y0: number;
|
|
64
|
+
x1: number;
|
|
65
|
+
y1: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Frame pixel x of rig (crop) pixel column u: crop -> world through the stage box, world -> frame through the viewport. */
|
|
69
|
+
function frameX(u: number, stage: StageBox, vp: Viewport): number {
|
|
70
|
+
return (stage.x + u - vp.x) * vp.scale;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Frame pixel y of rig (crop) pixel row v; world y goes through `cropToSpineY`, the one door. */
|
|
74
|
+
function frameY(v: number, H: number, stage: StageBox, vp: Viewport): number {
|
|
75
|
+
return (vp.y + vp.height - (stage.y + cropToSpineY(v, H))) * vp.scale;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The frame-pixel box covering the union of parts' rig boxes: every frame
|
|
80
|
+
* pixel any of them touches (floor of the near edge, ceil of the far one),
|
|
81
|
+
* clipped to the frame. Null for no part or a box entirely off the frame.
|
|
82
|
+
*
|
|
83
|
+
* {@link frameX} and {@link frameY} carry pixel EDGES, continuous positions,
|
|
84
|
+
* so they take no `0.5 (s - 1)` term: that term belongs to a map between
|
|
85
|
+
* pixel indices, centre to centre (`rigToFrameMap` in check.ts, issue #209).
|
|
86
|
+
* Rig pixel u's left edge is frame position s (stage.x + u - vp.x), and the
|
|
87
|
+
* frame pixel holding it is that position's floor.
|
|
88
|
+
*/
|
|
89
|
+
export function frameBox(ps: readonly PartRecord[], rigH: number, stage: StageBox, vp: Viewport): PixelBox | null {
|
|
90
|
+
if (ps.length === 0) return null;
|
|
91
|
+
const x0 = Math.max(0, Math.floor(Math.min(...ps.map((p) => frameX(p.x, stage, vp)))));
|
|
92
|
+
const x1 = Math.min(vp.pixelWidth, Math.ceil(Math.max(...ps.map((p) => frameX(p.x + p.w, stage, vp)))));
|
|
93
|
+
const y0 = Math.max(0, Math.floor(Math.min(...ps.map((p) => frameY(p.y, rigH, stage, vp)))));
|
|
94
|
+
const y1 = Math.min(vp.pixelHeight, Math.ceil(Math.max(...ps.map((p) => frameY(p.y + p.h, rigH, stage, vp)))));
|
|
95
|
+
return x1 > x0 && y1 > y0 ? { x0, y0, x1, y1 } : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function boxLabel(b: PixelBox): string {
|
|
99
|
+
return `${b.x0},${b.y0} ${b.x1 - b.x0}x${b.y1 - b.y0}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Which part is on top at every rig pixel of the flat stack: the index (in
|
|
104
|
+
* parts.json order) of the last part whose alpha there is above
|
|
105
|
+
* `OPAQUE_ALPHA_ABOVE`, or -1. `alphaOf(i)` is part i's PNG.
|
|
106
|
+
*/
|
|
107
|
+
export function topmostIndex(parts: PartsFile, alphaOf: (i: number) => Raster): Int32Array {
|
|
108
|
+
const [W, H] = parts.rig_size;
|
|
109
|
+
const top = new Int32Array(W * H).fill(-1);
|
|
110
|
+
parts.parts.forEach((p, i) => {
|
|
111
|
+
const im = alphaOf(i);
|
|
112
|
+
for (let y = 0; y < p.h; y++) {
|
|
113
|
+
for (let x = 0; x < p.w; x++) if (im.data[(y * p.w + x) * 4 + 3] > OPAQUE_ALPHA_ABOVE) top[(p.y + y) * W + p.x + x] = i;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return top;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// heat
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/** Each pixel's largest per-channel RGB change from frame 0 across the frames — the red channel `motion_heat.png` blends in. */
|
|
124
|
+
export function heatField(frames: readonly Raster[]): Uint8Array {
|
|
125
|
+
const f0 = frames[0];
|
|
126
|
+
const n = f0.width * f0.height;
|
|
127
|
+
const d = new Uint8Array(n);
|
|
128
|
+
for (const f of frames) {
|
|
129
|
+
for (let p = 0; p < n; p++) {
|
|
130
|
+
for (let c = 0; c < 3; c++) {
|
|
131
|
+
const v = Math.abs(f.data[p * 4 + c] - f0.data[p * 4 + c]);
|
|
132
|
+
if (v > d[p]) d[p] = v;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return d;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface RegionHeat {
|
|
140
|
+
/** Pixels measured. */
|
|
141
|
+
px: number;
|
|
142
|
+
mean: number;
|
|
143
|
+
max: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Heat over a box, or over the pixels of `mask` inside it. */
|
|
147
|
+
export function regionHeat(heat: Uint8Array, width: number, box: PixelBox, mask?: Uint8Array): RegionHeat {
|
|
148
|
+
let px = 0;
|
|
149
|
+
let sum = 0;
|
|
150
|
+
let max = 0;
|
|
151
|
+
for (let y = box.y0; y < box.y1; y++) {
|
|
152
|
+
for (let x = box.x0; x < box.x1; x++) {
|
|
153
|
+
const i = y * width + x;
|
|
154
|
+
if (mask !== undefined && mask[i] === 0) continue;
|
|
155
|
+
px++;
|
|
156
|
+
sum += heat[i];
|
|
157
|
+
if (heat[i] > max) max = heat[i];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { px, mean: px === 0 ? 0 : sum / px, max };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// swing: how far a band of a part's art travels
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/** One attachment on one frame as `rigc-geometry/1` writes it: rest vertices (the setup pose's bones, no deform), posed vertices (world, y up) and triangles. */
|
|
168
|
+
export interface PosedTriangles {
|
|
169
|
+
rest: readonly number[];
|
|
170
|
+
posed: readonly number[];
|
|
171
|
+
triangles: readonly number[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** A part's art on the stage: its box's top-left in stage px and, per pixel, 1 where it is art. */
|
|
175
|
+
export interface ArtMask {
|
|
176
|
+
x: number;
|
|
177
|
+
y: number;
|
|
178
|
+
width: number;
|
|
179
|
+
height: number;
|
|
180
|
+
data: Uint8Array;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** How far each half of a part's box sees its art travel, in rig px (world units); null where frame 0 shows no art in that half. */
|
|
184
|
+
export interface HalfTravels {
|
|
185
|
+
root: number | null;
|
|
186
|
+
tip: number | null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A polygon as x, y pairs. */
|
|
190
|
+
type Polygon = number[];
|
|
191
|
+
|
|
192
|
+
/** Sutherland-Hodgman against one half-plane: keeps a x + b y + c >= 0. */
|
|
193
|
+
function clipHalfPlane(poly: Polygon, a: number, b: number, c: number): Polygon {
|
|
194
|
+
const out: Polygon = [];
|
|
195
|
+
const n = poly.length / 2;
|
|
196
|
+
for (let i = 0; i < n; i++) {
|
|
197
|
+
const j = i + 1 === n ? 0 : i + 1;
|
|
198
|
+
const x1 = poly[2 * i];
|
|
199
|
+
const y1 = poly[2 * i + 1];
|
|
200
|
+
const x2 = poly[2 * j];
|
|
201
|
+
const y2 = poly[2 * j + 1];
|
|
202
|
+
const d1 = a * x1 + b * y1 + c;
|
|
203
|
+
const d2 = a * x2 + b * y2 + c;
|
|
204
|
+
if (d1 >= 0) out.push(x1, y1);
|
|
205
|
+
if (d1 >= 0 !== d2 >= 0) {
|
|
206
|
+
const t = d1 / (d1 - d2);
|
|
207
|
+
out.push(x1 + t * (x2 - x1), y1 + t * (y2 - y1));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** A polygon clipped to the rectangle x0 <= x <= x1, y0 <= y <= y1. */
|
|
214
|
+
function clipRect(poly: Polygon, x0: number, y0: number, x1: number, y1: number): Polygon {
|
|
215
|
+
let p = clipHalfPlane(poly, 1, 0, -x0);
|
|
216
|
+
if (p.length >= 6) p = clipHalfPlane(p, -1, 0, x1);
|
|
217
|
+
if (p.length >= 6) p = clipHalfPlane(p, 0, 1, -y0);
|
|
218
|
+
if (p.length >= 6) p = clipHalfPlane(p, 0, -1, y1);
|
|
219
|
+
return p;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A polygon's area and first moments (shoelace), signed by its winding: [A, A cx, A cy]. */
|
|
223
|
+
function polygonMoments(poly: Polygon): [number, number, number] {
|
|
224
|
+
let a = 0;
|
|
225
|
+
let mx = 0;
|
|
226
|
+
let my = 0;
|
|
227
|
+
const n = poly.length / 2;
|
|
228
|
+
for (let i = 0; i < n; i++) {
|
|
229
|
+
const j = i + 1 === n ? 0 : i + 1;
|
|
230
|
+
const cr = poly[2 * i] * poly[2 * j + 1] - poly[2 * j] * poly[2 * i + 1];
|
|
231
|
+
a += cr;
|
|
232
|
+
mx += (poly[2 * i] + poly[2 * j]) * cr;
|
|
233
|
+
my += (poly[2 * i + 1] + poly[2 * j + 1]) * cr;
|
|
234
|
+
}
|
|
235
|
+
return [a / 2, mx / 6, my / 6];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The art of one attachment cut by its rest triangles, frame-independent:
|
|
240
|
+
* for each triangle, every piece of an art pixel's square inside it, as the
|
|
241
|
+
* barycentric coordinates (l1, l2 per corner) of the piece's corners, and the
|
|
242
|
+
* triangle's art area and the barycentric coordinates of its art centroid.
|
|
243
|
+
*/
|
|
244
|
+
interface ArtPieces {
|
|
245
|
+
/** Per triangle: the art area inside it at rest (rig px²), and its centroid as (l1, l2). */
|
|
246
|
+
area: Float64Array;
|
|
247
|
+
centroid: Float64Array;
|
|
248
|
+
/** Per triangle: its pieces, each a flat list of (l1, l2) corner pairs. */
|
|
249
|
+
pieces: Polygon[][];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function artPieces(art: ArtMask, a: PosedTriangles, stage: StageBox): ArtPieces {
|
|
253
|
+
const H = stage.height;
|
|
254
|
+
const R = a.rest;
|
|
255
|
+
const T = a.triangles;
|
|
256
|
+
const nt = T.length / 3;
|
|
257
|
+
const area = new Float64Array(nt);
|
|
258
|
+
const centroid = new Float64Array(2 * nt);
|
|
259
|
+
const pieces: Polygon[][] = [];
|
|
260
|
+
// World y of each row edge of the art's box: row v's square spans [wy[v + 1 - y0], wy[v - y0]].
|
|
261
|
+
const wy: number[] = [];
|
|
262
|
+
for (let v = art.y; v <= art.y + art.height; v++) wy.push(stage.y + cropToSpineY(v, H));
|
|
263
|
+
for (let t = 0; t < nt; t++) {
|
|
264
|
+
const i = T[3 * t];
|
|
265
|
+
const j = T[3 * t + 1];
|
|
266
|
+
const k = T[3 * t + 2];
|
|
267
|
+
const x0 = R[2 * i];
|
|
268
|
+
const y0 = R[2 * i + 1];
|
|
269
|
+
const ex1 = R[2 * j] - x0;
|
|
270
|
+
const ey1 = R[2 * j + 1] - y0;
|
|
271
|
+
const ex2 = R[2 * k] - x0;
|
|
272
|
+
const ey2 = R[2 * k + 1] - y0;
|
|
273
|
+
const det = ex1 * ey2 - ex2 * ey1;
|
|
274
|
+
const list: Polygon[] = [];
|
|
275
|
+
pieces.push(list);
|
|
276
|
+
if (det === 0) continue;
|
|
277
|
+
const s = det > 0 ? 1 : -1;
|
|
278
|
+
const xs = [x0, R[2 * j], R[2 * k]];
|
|
279
|
+
const ys = [y0, R[2 * j + 1], R[2 * k + 1]];
|
|
280
|
+
const u0 = Math.max(art.x, Math.floor(Math.min(...xs) - stage.x));
|
|
281
|
+
const u1 = Math.min(art.x + art.width, Math.ceil(Math.max(...xs) - stage.x));
|
|
282
|
+
const yLo = Math.min(...ys);
|
|
283
|
+
const yHi = Math.max(...ys);
|
|
284
|
+
let A = 0;
|
|
285
|
+
let L1 = 0;
|
|
286
|
+
let L2 = 0;
|
|
287
|
+
for (let r = 0; r < art.height; r++) {
|
|
288
|
+
const top = wy[r];
|
|
289
|
+
const bottom = wy[r + 1];
|
|
290
|
+
if (bottom >= yHi || top <= yLo) continue;
|
|
291
|
+
for (let u = u0; u < u1; u++) {
|
|
292
|
+
if (art.data[r * art.width + (u - art.x)] !== 1) continue;
|
|
293
|
+
const left = stage.x + u;
|
|
294
|
+
let poly: Polygon = [left, bottom, left + 1, bottom, left + 1, top, left, top];
|
|
295
|
+
for (let e = 0; e < 3 && poly.length >= 6; e++) {
|
|
296
|
+
const ax = xs[e];
|
|
297
|
+
const ay = ys[e];
|
|
298
|
+
const bx = xs[(e + 1) % 3];
|
|
299
|
+
const by = ys[(e + 1) % 3];
|
|
300
|
+
// inside: (b - a) x (q - a) has the triangle's winding
|
|
301
|
+
poly = clipHalfPlane(poly, -(by - ay) * s, (bx - ax) * s, ((by - ay) * ax - (bx - ax) * ay) * s);
|
|
302
|
+
}
|
|
303
|
+
if (poly.length < 6) continue;
|
|
304
|
+
const [pa, pmx, pmy] = polygonMoments(poly);
|
|
305
|
+
if (pa === 0) continue;
|
|
306
|
+
const bary: Polygon = [];
|
|
307
|
+
for (let q = 0; q < poly.length; q += 2) {
|
|
308
|
+
const dx = poly[q] - x0;
|
|
309
|
+
const dy = poly[q + 1] - y0;
|
|
310
|
+
bary.push((dx * ey2 - ex2 * dy) / det, (ex1 * dy - dx * ey1) / det);
|
|
311
|
+
}
|
|
312
|
+
list.push(bary);
|
|
313
|
+
// The rest pieces are counter-clockwise (y up), so their area is positive.
|
|
314
|
+
A += pa;
|
|
315
|
+
const cx = pmx / pa - x0;
|
|
316
|
+
const cy = pmy / pa - y0;
|
|
317
|
+
L1 += pa * ((cx * ey2 - ex2 * cy) / det);
|
|
318
|
+
L2 += pa * ((ex1 * cy - cx * ey1) / det);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
area[t] = A;
|
|
322
|
+
centroid[2 * t] = A === 0 ? 0 : L1 / A;
|
|
323
|
+
centroid[2 * t + 1] = A === 0 ? 0 : L2 / A;
|
|
324
|
+
}
|
|
325
|
+
return { area, centroid, pieces };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* How far the art in each half of a part's box travels over the frames
|
|
330
|
+
* (issue #118): the centroid of the art's AREA inside the half — a fixed
|
|
331
|
+
* band of the stage, the box's upper or lower half, cut at h / 2 — on each
|
|
332
|
+
* frame, and the largest distance of that centroid from frame 0's, in rig px.
|
|
333
|
+
*
|
|
334
|
+
* It is the figure `check` read off a render of the part alone until #118 (a
|
|
335
|
+
* binary-coverage centroid per half of the frame box, frame px), taken in its
|
|
336
|
+
* continuous limit from the posed geometry instead. A centroid rather than
|
|
337
|
+
* the band's heat, as before: heat is texture times motion, and the question
|
|
338
|
+
* is how far the cloth moves. It is a proxy for displacement, not a
|
|
339
|
+
* displacement — art crossing a half's fixed edge moves the centroid too, by
|
|
340
|
+
* design: a band fixed on the stage is the definition. Every art pixel's square (art = the
|
|
341
|
+
* part's alpha above the threshold its mesh is built from) is cut by the
|
|
342
|
+
* attachment's rest triangles, carried by each triangle's own affine map to
|
|
343
|
+
* its posed vertices — the map the runtime draws the texture with — and
|
|
344
|
+
* clipped to the band, exactly (polygon clipping, no sampling). No render
|
|
345
|
+
* grid enters it, so the figure does not change with `--max`, and a travel
|
|
346
|
+
* well under one frame pixel is measured as exactly as a large one.
|
|
347
|
+
*
|
|
348
|
+
* Art outside every rest triangle is not drawn by the runtime and is not
|
|
349
|
+
* counted. A frame on which the slot shows nothing (null) is skipped; a half
|
|
350
|
+
* with no art on frame 0 is null. Each distinct rest attachment is cut once.
|
|
351
|
+
*/
|
|
352
|
+
export function halfTravels(art: ArtMask, box: { x: number; y: number; w: number; h: number }, stage: StageBox, frames: ReadonlyArray<PosedTriangles | null>): HalfTravels {
|
|
353
|
+
const H = stage.height;
|
|
354
|
+
const bx0 = stage.x + box.x;
|
|
355
|
+
const bx1 = stage.x + box.x + box.w;
|
|
356
|
+
const top = stage.y + cropToSpineY(box.y, H);
|
|
357
|
+
const mid = stage.y + cropToSpineY(box.y + box.h / 2, H);
|
|
358
|
+
const bottom = stage.y + cropToSpineY(box.y + box.h, H);
|
|
359
|
+
const bands = { root: [mid, top], tip: [bottom, mid] } as const;
|
|
360
|
+
const cut = new Map<readonly number[], ArtPieces>();
|
|
361
|
+
const centroidIn = (a: PosedTriangles, y0: number, y1: number): [number, number] | null => {
|
|
362
|
+
let pc = cut.get(a.rest);
|
|
363
|
+
if (pc === undefined) {
|
|
364
|
+
pc = artPieces(art, a, stage);
|
|
365
|
+
cut.set(a.rest, pc);
|
|
366
|
+
}
|
|
367
|
+
const P = a.posed;
|
|
368
|
+
const T = a.triangles;
|
|
369
|
+
let A = 0;
|
|
370
|
+
let MX = 0;
|
|
371
|
+
let MY = 0;
|
|
372
|
+
for (let t = 0; t < pc.area.length; t++) {
|
|
373
|
+
if (pc.area[t] === 0) continue;
|
|
374
|
+
const i = T[3 * t];
|
|
375
|
+
const j = T[3 * t + 1];
|
|
376
|
+
const k = T[3 * t + 2];
|
|
377
|
+
const px = P[2 * i];
|
|
378
|
+
const py = P[2 * i + 1];
|
|
379
|
+
const e1x = P[2 * j] - px;
|
|
380
|
+
const e1y = P[2 * j + 1] - py;
|
|
381
|
+
const e2x = P[2 * k] - px;
|
|
382
|
+
const e2y = P[2 * k + 1] - py;
|
|
383
|
+
const xmin = Math.min(px, px + e1x, px + e2x);
|
|
384
|
+
const xmax = Math.max(px, px + e1x, px + e2x);
|
|
385
|
+
const ymin = Math.min(py, py + e1y, py + e2y);
|
|
386
|
+
const ymax = Math.max(py, py + e1y, py + e2y);
|
|
387
|
+
if (xmax <= bx0 || xmin >= bx1 || ymax <= y0 || ymin >= y1) continue;
|
|
388
|
+
const R = a.rest;
|
|
389
|
+
const restDet = (R[2 * j] - R[2 * i]) * (R[2 * k + 1] - R[2 * i + 1]) - (R[2 * k] - R[2 * i]) * (R[2 * j + 1] - R[2 * i + 1]);
|
|
390
|
+
const posedDet = e1x * e2y - e2x * e1y;
|
|
391
|
+
// The posed area of a rest area: |posed det / rest det| times it.
|
|
392
|
+
const scale = Math.abs(posedDet / restDet);
|
|
393
|
+
if (xmin >= bx0 && xmax <= bx1 && ymin >= y0 && ymax <= y1) {
|
|
394
|
+
const w = pc.area[t] * scale;
|
|
395
|
+
const l1 = pc.centroid[2 * t];
|
|
396
|
+
const l2 = pc.centroid[2 * t + 1];
|
|
397
|
+
A += w;
|
|
398
|
+
MX += w * (px + l1 * e1x + l2 * e2x);
|
|
399
|
+
MY += w * (py + l1 * e1y + l2 * e2y);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
for (const bary of pc.pieces[t]) {
|
|
403
|
+
const poly: Polygon = [];
|
|
404
|
+
for (let q = 0; q < bary.length; q += 2) poly.push(px + bary[q] * e1x + bary[q + 1] * e2x, py + bary[q] * e1y + bary[q + 1] * e2y);
|
|
405
|
+
const c = clipRect(poly, bx0, y0, bx1, y1);
|
|
406
|
+
if (c.length < 6) continue;
|
|
407
|
+
const [pa, pmx, pmy] = polygonMoments(c);
|
|
408
|
+
// A triangle the pose turns over winds its pieces the other way; what is drawn is the area either way.
|
|
409
|
+
const sg = pa < 0 ? -1 : 1;
|
|
410
|
+
A += sg * pa;
|
|
411
|
+
MX += sg * pmx;
|
|
412
|
+
MY += sg * pmy;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return A > 0 ? [MX / A, MY / A] : null;
|
|
416
|
+
};
|
|
417
|
+
const travel = (y0: number, y1: number): number | null => {
|
|
418
|
+
const first = frames[0];
|
|
419
|
+
const c0 = first === null ? null : centroidIn(first, y0, y1);
|
|
420
|
+
if (c0 === null) return null;
|
|
421
|
+
let max = 0;
|
|
422
|
+
for (const f of frames) {
|
|
423
|
+
const c = f === null ? null : centroidIn(f, y0, y1);
|
|
424
|
+
if (c === null) continue;
|
|
425
|
+
max = Math.max(max, Math.hypot(c[0] - c0[0], c[1] - c0[1]));
|
|
426
|
+
}
|
|
427
|
+
return max;
|
|
428
|
+
};
|
|
429
|
+
return { root: travel(bands.root[0], bands.root[1]), tip: travel(bands.tip[0], bands.tip[1]) };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// sines and chains, off motion.json
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
export interface SineReading {
|
|
437
|
+
period: number;
|
|
438
|
+
/** |amplitude|: a sine's sign is a half-cycle of phase, and the keys cannot tell the two apart. */
|
|
439
|
+
amp: number;
|
|
440
|
+
/** Cycles, in [0, 1): `v = base + amp sin(2 pi (t / period - phase))`. */
|
|
441
|
+
phase: number;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
interface Key {
|
|
445
|
+
t: number;
|
|
446
|
+
v: number[];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
450
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function keysOf(track: Record<string, unknown>): Key[] | null {
|
|
454
|
+
const ks = track.keys;
|
|
455
|
+
if (!Array.isArray(ks)) return null;
|
|
456
|
+
const out: Key[] = [];
|
|
457
|
+
for (const k of ks) {
|
|
458
|
+
if (!isRecord(k) || typeof k.t !== 'number' || !Array.isArray(k.v) || typeof k.v[0] !== 'number') return null;
|
|
459
|
+
out.push({ t: k.t, v: k.v as number[] });
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Read a track as the sampled sine `src/motion.ts` writes: keys at equal steps
|
|
466
|
+
* from 0 to the idle's duration, last equal to first. The fundamental is the
|
|
467
|
+
* strongest bin of the discrete Fourier transform of the first n keys; its
|
|
468
|
+
* magnitude is the amplitude and its argument the phase. Null for a track that
|
|
469
|
+
* is not sampled that way (a hand-authored curve) — it is listed as unread,
|
|
470
|
+
* never guessed at.
|
|
471
|
+
*/
|
|
472
|
+
export function readSine(keys: readonly Key[], duration: number): SineReading | null {
|
|
473
|
+
const n = keys.length - 1;
|
|
474
|
+
if (n < 3 || keys[0].t !== 0 || Math.abs(keys[n].t - duration) > 1e-6) return null;
|
|
475
|
+
for (let k = 0; k <= n; k++) if (Math.abs(keys[k].t - (duration * k) / n) > 1e-6) return null;
|
|
476
|
+
const v = keys.slice(0, n).map((k) => k.v[0]);
|
|
477
|
+
let best = { j: 0, a: 0, b: 0, mag: 0 };
|
|
478
|
+
for (let j = 1; j < n / 2; j++) {
|
|
479
|
+
let a = 0;
|
|
480
|
+
let b = 0;
|
|
481
|
+
for (let k = 0; k < n; k++) {
|
|
482
|
+
a += v[k] * Math.sin((2 * Math.PI * j * k) / n);
|
|
483
|
+
b += v[k] * Math.cos((2 * Math.PI * j * k) / n);
|
|
484
|
+
}
|
|
485
|
+
a *= 2 / n;
|
|
486
|
+
b *= 2 / n;
|
|
487
|
+
const mag = Math.hypot(a, b);
|
|
488
|
+
if (mag > best.mag) best = { j, a, b, mag };
|
|
489
|
+
}
|
|
490
|
+
if (best.j === 0) return null;
|
|
491
|
+
// v = A sin(x - psi) = A cos(psi) sin x - A sin(psi) cos x, so a = A cos psi and b = -A sin psi.
|
|
492
|
+
const psi = Math.atan2(-best.b, best.a);
|
|
493
|
+
const phase = (((psi / (2 * Math.PI)) % 1) + 1) % 1;
|
|
494
|
+
return { period: duration / best.j, amp: best.mag, phase };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** A phase difference in cycles, folded into (-1/4, 1/4]: the sign of a sine is half a cycle, so a lag is only readable modulo half a cycle. */
|
|
498
|
+
export function lagStep(from: number, to: number): number {
|
|
499
|
+
let d = (((to - from) % 0.5) + 0.5) % 0.5;
|
|
500
|
+
if (d > 0.25) d -= 0.5;
|
|
501
|
+
return d;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export interface ChainLink {
|
|
505
|
+
bone: string;
|
|
506
|
+
reading: SineReading;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export interface ChainEdge {
|
|
510
|
+
parent: string;
|
|
511
|
+
child: string;
|
|
512
|
+
step: number;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export interface ChainFigures {
|
|
516
|
+
/** Unbranched runs of rotate-keyed bones, each link the keyed child of the one before at the same period. */
|
|
517
|
+
chains: ChainLink[][];
|
|
518
|
+
/** Every keyed bone under a keyed ancestor of the same period: the lag from that ancestor. */
|
|
519
|
+
edges: ChainEdge[];
|
|
520
|
+
/** Keyed bones under a keyed ancestor of another period: their phases are not comparable. */
|
|
521
|
+
otherPeriod: Array<{ parent: string; child: string }>;
|
|
522
|
+
/** Rotate-keyed bones whose track is not a sampled sine, or that carry more than one rotate track. */
|
|
523
|
+
unread: string[];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* The rotate channels of the idle, read as sines and arranged by the rig's
|
|
528
|
+
* bone tree. Only bone tracks and group tracks with property `rotate` are
|
|
529
|
+
* read; a group track keys each of its members.
|
|
530
|
+
*/
|
|
531
|
+
export function chainFigures(rig: Record<string, unknown>, motion: Record<string, unknown>): ChainFigures {
|
|
532
|
+
const bones = (Array.isArray(rig.bones) ? rig.bones : []).filter(isRecord).filter((b) => typeof b.name === 'string') as Array<Record<string, unknown> & { name: string }>;
|
|
533
|
+
const order = new Map(bones.map((b, i) => [b.name, i]));
|
|
534
|
+
const parent = new Map(bones.map((b) => [b.name, typeof b.parent === 'string' ? b.parent : null]));
|
|
535
|
+
const anims = isRecord(motion.animations) ? motion.animations : {};
|
|
536
|
+
const idle = isRecord(anims.idle) ? anims.idle : {};
|
|
537
|
+
const duration = typeof idle.duration === 'number' ? idle.duration : 0;
|
|
538
|
+
const groups = isRecord(motion.groups) ? motion.groups : {};
|
|
539
|
+
const tracksOn = new Map<string, Key[][]>();
|
|
540
|
+
const unreadable = new Set<string>();
|
|
541
|
+
for (const t of Array.isArray(idle.tracks) ? idle.tracks.filter(isRecord) : []) {
|
|
542
|
+
if (t.property !== 'rotate') continue;
|
|
543
|
+
const targets = typeof t.bone === 'string' ? [t.bone] : typeof t.group === 'string' && Array.isArray(groups[t.group]) ? (groups[t.group] as unknown[]).filter((m): m is string => typeof m === 'string') : [];
|
|
544
|
+
const keys = keysOf(t);
|
|
545
|
+
for (const b of targets) {
|
|
546
|
+
if (keys === null) unreadable.add(b);
|
|
547
|
+
else tracksOn.set(b, [...(tracksOn.get(b) ?? []), keys]);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const reading = new Map<string, SineReading>();
|
|
551
|
+
for (const [b, ts] of tracksOn) {
|
|
552
|
+
const r = ts.length === 1 ? readSine(ts[0], duration) : null;
|
|
553
|
+
if (r === null) unreadable.add(b);
|
|
554
|
+
else reading.set(b, r);
|
|
555
|
+
}
|
|
556
|
+
const byOrder = (a: string, b: string): number => (order.get(a) ?? Infinity) - (order.get(b) ?? Infinity) || (a < b ? -1 : a > b ? 1 : 0);
|
|
557
|
+
const keyed = [...reading.keys()].sort(byOrder);
|
|
558
|
+
const keyedParent = new Map<string, string | null>();
|
|
559
|
+
for (const b of keyed) {
|
|
560
|
+
let p = parent.get(b) ?? null;
|
|
561
|
+
const seen = new Set<string>();
|
|
562
|
+
while (p !== null && !reading.has(p) && !seen.has(p)) {
|
|
563
|
+
seen.add(p);
|
|
564
|
+
p = parent.get(p) ?? null;
|
|
565
|
+
}
|
|
566
|
+
keyedParent.set(b, p !== null && reading.has(p) ? p : null);
|
|
567
|
+
}
|
|
568
|
+
const kids = new Map<string, string[]>();
|
|
569
|
+
for (const b of keyed) {
|
|
570
|
+
const p = keyedParent.get(b) ?? null;
|
|
571
|
+
if (p !== null) kids.set(p, [...(kids.get(p) ?? []), b]);
|
|
572
|
+
}
|
|
573
|
+
const samePeriod = (a: string, b: string): boolean => Math.abs((reading.get(a) as SineReading).period - (reading.get(b) as SineReading).period) < 1e-9;
|
|
574
|
+
const edges: ChainEdge[] = [];
|
|
575
|
+
const otherPeriod: Array<{ parent: string; child: string }> = [];
|
|
576
|
+
for (const b of keyed) {
|
|
577
|
+
const p = keyedParent.get(b) ?? null;
|
|
578
|
+
if (p === null) continue;
|
|
579
|
+
if (samePeriod(p, b)) edges.push({ parent: p, child: b, step: lagStep((reading.get(p) as SineReading).phase, (reading.get(b) as SineReading).phase) });
|
|
580
|
+
else otherPeriod.push({ parent: p, child: b });
|
|
581
|
+
}
|
|
582
|
+
const continues = (b: string): string | null => {
|
|
583
|
+
const k = kids.get(b) ?? [];
|
|
584
|
+
return k.length === 1 && samePeriod(b, k[0]) ? k[0] : null;
|
|
585
|
+
};
|
|
586
|
+
const chains: ChainLink[][] = [];
|
|
587
|
+
for (const b of keyed) {
|
|
588
|
+
const p = keyedParent.get(b) ?? null;
|
|
589
|
+
if (p !== null && continues(p) === b) continue;
|
|
590
|
+
const links: ChainLink[] = [];
|
|
591
|
+
let cur: string | null = b;
|
|
592
|
+
while (cur !== null) {
|
|
593
|
+
links.push({ bone: cur, reading: reading.get(cur) as SineReading });
|
|
594
|
+
cur = continues(cur);
|
|
595
|
+
}
|
|
596
|
+
chains.push(links);
|
|
597
|
+
}
|
|
598
|
+
return { chains, edges, otherPeriod, unread: [...unreadable].sort(byOrder) };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
// the blink
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
export interface BlinkTrack {
|
|
606
|
+
/** How the track names what it keys, as motion.json spells it. */
|
|
607
|
+
target: { bone: string } | { group: string };
|
|
608
|
+
/** The smallest `scaley` it keys — the closed eye. */
|
|
609
|
+
closed: number;
|
|
610
|
+
/** The first and last key time at that value. */
|
|
611
|
+
window: [number, number];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* The bones an eye part's slot is moved by through `scaley` without changing
|
|
616
|
+
* shape: the slot's bone, and each ancestor it sits at the origin of (local
|
|
617
|
+
* x = y = 0) — which is how a control bone is attached, so a blink moved to a
|
|
618
|
+
* control is found without reading its name.
|
|
619
|
+
*/
|
|
620
|
+
export function eyeBones(rig: Record<string, unknown>, slotBones: readonly string[]): Set<string> {
|
|
621
|
+
const bones = new Map((Array.isArray(rig.bones) ? rig.bones : []).filter(isRecord).map((b) => [String(b.name), b]));
|
|
622
|
+
const out = new Set<string>();
|
|
623
|
+
for (const s of slotBones) {
|
|
624
|
+
let cur: string | null = s;
|
|
625
|
+
while (cur !== null && !out.has(cur)) {
|
|
626
|
+
out.add(cur);
|
|
627
|
+
const b = bones.get(cur);
|
|
628
|
+
if (b === undefined || (b.x ?? 0) !== 0 || (b.y ?? 0) !== 0) break;
|
|
629
|
+
cur = typeof b.parent === 'string' ? b.parent : null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return out;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** The idle's `scaley` tracks on the eye bones (directly or through a group) that go below their first key: the blink. */
|
|
636
|
+
export function blinkTracks(motion: Record<string, unknown>, eyes: ReadonlySet<string>): BlinkTrack[] {
|
|
637
|
+
const anims = isRecord(motion.animations) ? motion.animations : {};
|
|
638
|
+
const idle = isRecord(anims.idle) ? anims.idle : {};
|
|
639
|
+
const groups = isRecord(motion.groups) ? motion.groups : {};
|
|
640
|
+
const out: BlinkTrack[] = [];
|
|
641
|
+
for (const t of Array.isArray(idle.tracks) ? idle.tracks.filter(isRecord) : []) {
|
|
642
|
+
if (t.property !== 'scaley') continue;
|
|
643
|
+
let target: BlinkTrack['target'] | null = null;
|
|
644
|
+
if (typeof t.bone === 'string' && eyes.has(t.bone)) target = { bone: t.bone };
|
|
645
|
+
else if (typeof t.group === 'string' && Array.isArray(groups[t.group]) && (groups[t.group] as unknown[]).some((m) => typeof m === 'string' && eyes.has(m))) target = { group: t.group };
|
|
646
|
+
const keys = keysOf(t);
|
|
647
|
+
if (target === null || keys === null || keys.length === 0) continue;
|
|
648
|
+
const closed = Math.min(...keys.map((k) => k.v[0]));
|
|
649
|
+
if (!(closed < keys[0].v[0])) continue;
|
|
650
|
+
const at = keys.filter((k) => k.v[0] === closed).map((k) => k.t);
|
|
651
|
+
out.push({ target, closed, window: [at[0], at[at.length - 1]] });
|
|
652
|
+
}
|
|
653
|
+
return out;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export interface BlinkFigures {
|
|
657
|
+
/** Pixels in the box that show the background with the eyes closed and showed art with them open. */
|
|
658
|
+
holePx: number;
|
|
659
|
+
/** Of the box's pixels with the eyes closed, the largest max-channel distance to the nearest colour the open-eye box holds. */
|
|
660
|
+
patchMax: number;
|
|
661
|
+
/** How many exceed `patchLevel`. */
|
|
662
|
+
patchOver: number;
|
|
663
|
+
px: number;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* The closed-eye frame against the open-eye frame on the same grid, inside
|
|
668
|
+
* the eye box. A hole is background where there was art — the rasteriser
|
|
669
|
+
* paints the background exactly where nothing is drawn. A colour patch is a
|
|
670
|
+
* pixel far from every colour the open eye's box held; that half is a
|
|
671
|
+
* reported figure, not a bar (see AUTHORING §7).
|
|
672
|
+
*/
|
|
673
|
+
export function blinkFigures(open: Raster, closed: Raster, bg: readonly number[], box: PixelBox, patchLevel: number): BlinkFigures {
|
|
674
|
+
const w = open.width;
|
|
675
|
+
const isBg = (r: Raster, i: number): boolean => r.data[i] === bg[0] && r.data[i + 1] === bg[1] && r.data[i + 2] === bg[2];
|
|
676
|
+
const colours = new Map<number, [number, number, number]>();
|
|
677
|
+
for (let y = box.y0; y < box.y1; y++) {
|
|
678
|
+
for (let x = box.x0; x < box.x1; x++) {
|
|
679
|
+
const i = (y * w + x) * 4;
|
|
680
|
+
colours.set((open.data[i] << 16) | (open.data[i + 1] << 8) | open.data[i + 2], [open.data[i], open.data[i + 1], open.data[i + 2]]);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const palette = [...colours.entries()].sort((a, b) => a[0] - b[0]).map((e) => e[1]);
|
|
684
|
+
let holePx = 0;
|
|
685
|
+
let patchMax = 0;
|
|
686
|
+
let patchOver = 0;
|
|
687
|
+
let px = 0;
|
|
688
|
+
for (let y = box.y0; y < box.y1; y++) {
|
|
689
|
+
for (let x = box.x0; x < box.x1; x++) {
|
|
690
|
+
const i = (y * w + x) * 4;
|
|
691
|
+
px++;
|
|
692
|
+
if (isBg(closed, i) && !isBg(open, i)) holePx++;
|
|
693
|
+
let near = 255;
|
|
694
|
+
for (const c of palette) {
|
|
695
|
+
const d = Math.max(Math.abs(closed.data[i] - c[0]), Math.abs(closed.data[i + 1] - c[1]), Math.abs(closed.data[i + 2] - c[2]));
|
|
696
|
+
if (d < near) near = d;
|
|
697
|
+
if (near === 0) break;
|
|
698
|
+
}
|
|
699
|
+
if (near > patchMax) patchMax = near;
|
|
700
|
+
if (near > patchLevel) patchOver++;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return { holePx, patchMax, patchOver, px };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ---------------------------------------------------------------------------
|
|
707
|
+
// texture stretch (issue #31)
|
|
708
|
+
// ---------------------------------------------------------------------------
|
|
709
|
+
|
|
710
|
+
/** One mesh attachment's rest shape and topology, as rigc's `geometry.json` `rest` table carries it: world units, the setup pose's bones with no deform. */
|
|
711
|
+
export interface MeshRest {
|
|
712
|
+
slot: string;
|
|
713
|
+
attachment: string;
|
|
714
|
+
/** x, y per vertex. */
|
|
715
|
+
vertices: readonly number[];
|
|
716
|
+
/** Vertex index triplets. */
|
|
717
|
+
triangles: readonly number[];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** One frame's skinned attachments, as `geometry.json`'s `frames[i].attachments` carries them. */
|
|
721
|
+
export interface GeometryPose {
|
|
722
|
+
index: number;
|
|
723
|
+
attachments: ReadonlyArray<{ slot: string; attachment: string; vertices: readonly number[] }>;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** Where one edge ratio was found: the triangle, its three vertex indices, the edge's two, the frame, and deformed length over rest length. */
|
|
727
|
+
export interface StretchAt {
|
|
728
|
+
triangle: number;
|
|
729
|
+
vertices: [number, number, number];
|
|
730
|
+
edge: [number, number];
|
|
731
|
+
frame: number;
|
|
732
|
+
ratio: number;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export interface MeshStretch {
|
|
736
|
+
slot: string;
|
|
737
|
+
attachment: string;
|
|
738
|
+
triangles: number;
|
|
739
|
+
/** Frames that show this attachment; 0 leaves `max` and `min` null. */
|
|
740
|
+
frames: number;
|
|
741
|
+
/** The largest edge ratio over every triangle and every frame. */
|
|
742
|
+
max: StretchAt | null;
|
|
743
|
+
/** The smallest. */
|
|
744
|
+
min: StretchAt | null;
|
|
745
|
+
/** Rest edges of length 0, which no ratio can be read off: `triangle`, the edge's two vertices. */
|
|
746
|
+
degenerate: Array<{ triangle: number; edge: [number, number] }>;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* How far a ratio is from 1 in either direction: `max(r, 1/r)`. Compression
|
|
751
|
+
* distorts a texture as much as stretch does — an edge at half its rest length
|
|
752
|
+
* squeezes the texels on it by the factor an edge at twice its rest length
|
|
753
|
+
* spreads them — so the one figure held to a bar reads both, as |ln r| does.
|
|
754
|
+
*/
|
|
755
|
+
export function stretchSeverity(ratio: number): number {
|
|
756
|
+
return ratio >= 1 ? ratio : 1 / ratio;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* For every mesh in `rest`, every triangle's three edges in every frame that
|
|
761
|
+
* shows the attachment: deformed length over rest length, the largest and the
|
|
762
|
+
* smallest, each with the triangle, its vertices, the edge and the frame.
|
|
763
|
+
* Frames are walked in the order given and triangles in index order, and a
|
|
764
|
+
* later value replaces the kept one only when strictly more extreme, so a tie
|
|
765
|
+
* names the earliest frame and then the lowest triangle.
|
|
766
|
+
*/
|
|
767
|
+
export function stretchFigures(rest: readonly MeshRest[], frames: readonly GeometryPose[]): MeshStretch[] {
|
|
768
|
+
return rest.map((m) => {
|
|
769
|
+
const t = m.triangles;
|
|
770
|
+
const v0 = m.vertices;
|
|
771
|
+
const len = (v: readonly number[], a: number, b: number): number => Math.hypot(v[2 * a] - v[2 * b], v[2 * a + 1] - v[2 * b + 1]);
|
|
772
|
+
const out: MeshStretch = { slot: m.slot, attachment: m.attachment, triangles: t.length / 3, frames: 0, max: null, min: null, degenerate: [] };
|
|
773
|
+
const restLen: number[] = [];
|
|
774
|
+
for (let i = 0; i < t.length; i += 3) {
|
|
775
|
+
for (const [a, b] of [[t[i], t[i + 1]], [t[i + 1], t[i + 2]], [t[i + 2], t[i]]] as const) {
|
|
776
|
+
const l = len(v0, a, b);
|
|
777
|
+
restLen.push(l);
|
|
778
|
+
if (l === 0) out.degenerate.push({ triangle: i / 3, edge: [a, b] });
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
for (const f of frames) {
|
|
782
|
+
const pose = f.attachments.find((a) => a.slot === m.slot && a.attachment === m.attachment);
|
|
783
|
+
if (pose === undefined) continue;
|
|
784
|
+
out.frames++;
|
|
785
|
+
for (let i = 0; i < t.length; i += 3) {
|
|
786
|
+
const edges = [[t[i], t[i + 1]], [t[i + 1], t[i + 2]], [t[i + 2], t[i]]] as const;
|
|
787
|
+
for (let e = 0; e < 3; e++) {
|
|
788
|
+
const l0 = restLen[i + e];
|
|
789
|
+
if (l0 === 0) continue;
|
|
790
|
+
const [a, b] = edges[e];
|
|
791
|
+
const ratio = len(pose.vertices, a, b) / l0;
|
|
792
|
+
const at = (): StretchAt => ({ triangle: i / 3, vertices: [t[i], t[i + 1], t[i + 2]], edge: [a, b], frame: f.index, ratio });
|
|
793
|
+
if (out.max === null || ratio > out.max.ratio) out.max = at();
|
|
794
|
+
if (out.min === null || ratio < out.min.ratio) out.min = at();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return out;
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
// the face in the head's own frame (issue #33)
|
|
804
|
+
// ---------------------------------------------------------------------------
|
|
805
|
+
|
|
806
|
+
/** A bone's world transform as rigc's `geometry.json` records it: world = [a b; c d] local + (worldX, worldY), Spine's y-up world units. */
|
|
807
|
+
export interface BoneWorld {
|
|
808
|
+
a: number;
|
|
809
|
+
b: number;
|
|
810
|
+
c: number;
|
|
811
|
+
d: number;
|
|
812
|
+
worldX: number;
|
|
813
|
+
worldY: number;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** One bone's world transform at the setup pose and on every frame of a render, in frame order, with each frame's time in seconds. */
|
|
817
|
+
export interface BoneTrack {
|
|
818
|
+
bone: string;
|
|
819
|
+
setup: BoneWorld;
|
|
820
|
+
frames: BoneWorld[];
|
|
821
|
+
times: number[];
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* The world -> world map that carries the bone from its setup pose to a
|
|
826
|
+
* frame: `frame * setup^-1`, as a {@link BoneWorld} (matrix and translation).
|
|
827
|
+
* Null when the setup transform is singular.
|
|
828
|
+
*/
|
|
829
|
+
export function setupToFrame(setup: BoneWorld, frame: BoneWorld): BoneWorld | null {
|
|
830
|
+
const det = setup.a * setup.d - setup.b * setup.c;
|
|
831
|
+
if (!(Math.abs(det) > 1e-12)) return null;
|
|
832
|
+
const ia = setup.d / det;
|
|
833
|
+
const ib = -setup.b / det;
|
|
834
|
+
const ic = -setup.c / det;
|
|
835
|
+
const id = setup.a / det;
|
|
836
|
+
const a = frame.a * ia + frame.b * ic;
|
|
837
|
+
const b = frame.a * ib + frame.b * id;
|
|
838
|
+
const c = frame.c * ia + frame.d * ic;
|
|
839
|
+
const d = frame.c * ib + frame.d * id;
|
|
840
|
+
return { a, b, c, d, worldX: frame.worldX - (a * setup.worldX + b * setup.worldY), worldY: frame.worldY - (c * setup.worldX + d * setup.worldY) };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** A box in Spine world units at the setup pose (y up): x0 <= x <= x1, y0 <= y <= y1. */
|
|
844
|
+
export interface WorldBox {
|
|
845
|
+
x0: number;
|
|
846
|
+
y0: number;
|
|
847
|
+
x1: number;
|
|
848
|
+
y1: number;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Where a part's art can be, in the head's frame, over the frames: the part's
|
|
853
|
+
* setup-pose box carried by the bone its slot rides to each frame and back
|
|
854
|
+
* through the head's transform, as the bounding box of every corner on every
|
|
855
|
+
* frame (the setup box included). For a feature the head carries rigidly it is
|
|
856
|
+
* the setup box; for one its own bone moves on the face — a brow the blink
|
|
857
|
+
* drops — it is the whole of where it goes. Null when a transform is singular.
|
|
858
|
+
*/
|
|
859
|
+
export function sweptBox(box: WorldBox, part: BoneTrack, head: BoneTrack): WorldBox | null {
|
|
860
|
+
const corners: Array<[number, number]> = [
|
|
861
|
+
[box.x0, box.y0],
|
|
862
|
+
[box.x1, box.y0],
|
|
863
|
+
[box.x1, box.y1],
|
|
864
|
+
[box.x0, box.y1],
|
|
865
|
+
];
|
|
866
|
+
let out: WorldBox = { ...box };
|
|
867
|
+
for (let k = 0; k < part.frames.length; k++) {
|
|
868
|
+
const f = setupToFrame(part.setup, part.frames[k]);
|
|
869
|
+
const g = setupToFrame(head.setup, head.frames[k]);
|
|
870
|
+
if (f === null || g === null) return null;
|
|
871
|
+
const det = g.a * g.d - g.b * g.c;
|
|
872
|
+
if (!(Math.abs(det) > 1e-12)) return null;
|
|
873
|
+
for (const [x, y] of corners) {
|
|
874
|
+
// world at frame k, then back through the head's setup -> frame map
|
|
875
|
+
const wx = f.a * x + f.b * y + f.worldX - g.worldX;
|
|
876
|
+
const wy = f.c * x + f.d * y + f.worldY - g.worldY;
|
|
877
|
+
const hx = (g.d * wx - g.b * wy) / det;
|
|
878
|
+
const hy = (-g.c * wx + g.a * wy) / det;
|
|
879
|
+
out = { x0: Math.min(out.x0, hx), y0: Math.min(out.y0, hy), x1: Math.max(out.x1, hx), y1: Math.max(out.y1, hy) };
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return out;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** A part's rig box (crop pixels, y down) as a world box at the setup pose, through the stage box and `cropToSpineY`. */
|
|
886
|
+
export function partWorldBox(p: PartRecord, rigH: number, stage: StageBox): WorldBox {
|
|
887
|
+
return { x0: stage.x + p.x, x1: stage.x + p.x + p.w, y0: stage.y + cropToSpineY(p.y + p.h, rigH), y1: stage.y + cropToSpineY(p.y, rigH) };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ---------------------------------------------------------------------------
|
|
891
|
+
// still: art that keeps its place in a bone's frame (issue #123)
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* How many floating-point roundings stand between the rig and one coordinate
|
|
896
|
+
* of a displacement {@link stillReading} returns, on its longest path. Every
|
|
897
|
+
* number on it is a double: rigc's core poser (`rig-c/src/core/vertices.ts`,
|
|
898
|
+
* rig-c 2.15.0) reads a vertex's local coordinates and weights through
|
|
899
|
+
* `Math.fround` and does the arithmetic in doubles, and `geometry.json` carries
|
|
900
|
+
* those doubles. The float32 inputs are the same numbers in the rest vertex and
|
|
901
|
+
* in the posed one, so they cancel. Per coordinate:
|
|
902
|
+
*
|
|
903
|
+
* - rigc's rest vertex, `a x + b y + worldX` (a mesh wholly on one bone
|
|
904
|
+
* multiplies by its weight 1 and adds the product to 0, both exact): 2
|
|
905
|
+
* products and 2 sums, **4**;
|
|
906
|
+
* - the posed vertex, the same way, **4**;
|
|
907
|
+
* - {@link setupToFrame}: each entry of the map takes the setup determinant
|
|
908
|
+
* (3), one division by it (1) and a two-term product sum (3), 7 roundings
|
|
909
|
+
* relative to the entry; the map's translation `worldX - (a sx + b sy)` adds
|
|
910
|
+
* 4 to those, **11**;
|
|
911
|
+
* - carrying the posed vertex back through the map ({@link stillReading}):
|
|
912
|
+
* the translation taken off (1), the map's determinant (3), `d wx - b wy`
|
|
913
|
+
* (3) and the division (1), with the entries' 7, **15**;
|
|
914
|
+
* - the difference with the rest vertex, **1**.
|
|
915
|
+
*
|
|
916
|
+
* That is 35. Each rounding is at most the unit roundoff u = 2^-53 of a
|
|
917
|
+
* quantity no larger than a two-term sum of entries times coordinates,
|
|
918
|
+
* 2 M^2 C (M the largest matrix entry, at least 1; C the largest coordinate),
|
|
919
|
+
* and 2 u C <= 2 ulp(C). So one coordinate is off by at most 70 M^2 ulp(C),
|
|
920
|
+
* and a displacement, the length of two of them, by sqrt(2) times that
|
|
921
|
+
* ({@link stillTolerance}). It is a numerical tolerance and nothing else: no
|
|
922
|
+
* visual allowance is in it.
|
|
923
|
+
*/
|
|
924
|
+
export const STILL_ROUNDINGS = 35;
|
|
925
|
+
|
|
926
|
+
/** The spacing of doubles at `x` (> 0): 2^(e - 52) for 2^e <= x < 2^(e + 1). */
|
|
927
|
+
export function ulpOf(x: number): number {
|
|
928
|
+
let e = Math.floor(Math.log2(x));
|
|
929
|
+
if (2 ** e > x) e--;
|
|
930
|
+
if (2 ** (e + 1) <= x) e++;
|
|
931
|
+
return 2 ** (e - 52);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** The largest displacement the arithmetic alone can produce ({@link STILL_ROUNDINGS}): sqrt(2) x 2 x 35 x M^2 x ulp(C), with M and C at least 1. */
|
|
935
|
+
export function stillTolerance(coordMax: number, entryMax: number): number {
|
|
936
|
+
const m = Math.max(1, entryMax);
|
|
937
|
+
return Math.SQRT2 * 2 * STILL_ROUNDINGS * m * m * ulpOf(Math.max(1, coordMax));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** A frame's map from the setup pose into it, carried back into the reference frame: a bone's {@link setupToFrame}, or null for the screen (the identity). */
|
|
941
|
+
export type FrameMap = BoneWorld | null;
|
|
942
|
+
|
|
943
|
+
/** What {@link stillReading} measures of one slot. */
|
|
944
|
+
export interface StillReading {
|
|
945
|
+
/** The largest displacement of any point of its art, rig px, and the frame (index into the set) it is on; null when no frame shows the slot. */
|
|
946
|
+
max: number | null;
|
|
947
|
+
frame: number | null;
|
|
948
|
+
/** Over the frames that show it and each rest triangle with art: that art's area times its centroid's squared displacement, summed, and the areas summed — the RMS's two halves. */
|
|
949
|
+
sumSq: number;
|
|
950
|
+
weight: number;
|
|
951
|
+
/** The largest |coordinate| of its rest and posed vertices, for {@link stillTolerance}. */
|
|
952
|
+
coordMax: number;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function backInto(m: FrameMap, x: number, y: number): [number, number] {
|
|
956
|
+
if (m === null) return [x, y];
|
|
957
|
+
const det = m.a * m.d - m.b * m.c;
|
|
958
|
+
const wx = x - m.worldX;
|
|
959
|
+
const wy = y - m.worldY;
|
|
960
|
+
return [(m.d * wx - m.b * wy) / det, (-m.c * wx + m.a * wy) / det];
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* How far one slot's art moves relative to a reference frame over the frames
|
|
965
|
+
* (issue #123): on each frame every vertex of the attachment the slot shows is
|
|
966
|
+
* carried back through that frame's map — the reference bone's
|
|
967
|
+
* {@link setupToFrame}, or the screen for null — and compared with its rest
|
|
968
|
+
* position, the setup pose's. Inside a rest triangle the displacement is
|
|
969
|
+
* affine, so its largest length over the art is reached at a corner of an art
|
|
970
|
+
* piece ({@link artPieces}: each art pixel's square cut by the triangle); the
|
|
971
|
+
* RMS weights each triangle's art centroid by its art area. A slot its
|
|
972
|
+
* reference bone carries rigidly reads its arithmetic's rounding and nothing
|
|
973
|
+
* else ({@link stillTolerance}). No render grid enters it.
|
|
974
|
+
*/
|
|
975
|
+
export function stillReading(art: ArtMask, stage: StageBox, frames: ReadonlyArray<PosedTriangles | null>, maps: readonly FrameMap[]): StillReading {
|
|
976
|
+
const cut = new Map<readonly number[], ArtPieces>();
|
|
977
|
+
let max: number | null = null;
|
|
978
|
+
let frame: number | null = null;
|
|
979
|
+
let sumSq = 0;
|
|
980
|
+
let weight = 0;
|
|
981
|
+
let coordMax = 0;
|
|
982
|
+
frames.forEach((a, k) => {
|
|
983
|
+
if (a === null) return;
|
|
984
|
+
let pc = cut.get(a.rest);
|
|
985
|
+
if (pc === undefined) {
|
|
986
|
+
pc = artPieces(art, a, stage);
|
|
987
|
+
cut.set(a.rest, pc);
|
|
988
|
+
}
|
|
989
|
+
const n = a.rest.length / 2;
|
|
990
|
+
const d = new Float64Array(2 * n);
|
|
991
|
+
for (let v = 0; v < n; v++) {
|
|
992
|
+
const [qx, qy] = backInto(maps[k], a.posed[2 * v], a.posed[2 * v + 1]);
|
|
993
|
+
d[2 * v] = qx - a.rest[2 * v];
|
|
994
|
+
d[2 * v + 1] = qy - a.rest[2 * v + 1];
|
|
995
|
+
coordMax = Math.max(coordMax, Math.abs(a.rest[2 * v]), Math.abs(a.rest[2 * v + 1]), Math.abs(a.posed[2 * v]), Math.abs(a.posed[2 * v + 1]));
|
|
996
|
+
}
|
|
997
|
+
const T = a.triangles;
|
|
998
|
+
let fmax = 0;
|
|
999
|
+
for (let t = 0; t < pc.area.length; t++) {
|
|
1000
|
+
if (pc.area[t] === 0) continue;
|
|
1001
|
+
const i = T[3 * t];
|
|
1002
|
+
const j = T[3 * t + 1];
|
|
1003
|
+
const l = T[3 * t + 2];
|
|
1004
|
+
const x0 = d[2 * i];
|
|
1005
|
+
const y0 = d[2 * i + 1];
|
|
1006
|
+
const e1x = d[2 * j] - x0;
|
|
1007
|
+
const e1y = d[2 * j + 1] - y0;
|
|
1008
|
+
const e2x = d[2 * l] - x0;
|
|
1009
|
+
const e2y = d[2 * l + 1] - y0;
|
|
1010
|
+
for (const bary of pc.pieces[t]) {
|
|
1011
|
+
for (let q = 0; q < bary.length; q += 2) fmax = Math.max(fmax, Math.hypot(x0 + bary[q] * e1x + bary[q + 1] * e2x, y0 + bary[q] * e1y + bary[q + 1] * e2y));
|
|
1012
|
+
}
|
|
1013
|
+
const cx = x0 + pc.centroid[2 * t] * e1x + pc.centroid[2 * t + 1] * e2x;
|
|
1014
|
+
const cy = y0 + pc.centroid[2 * t] * e1y + pc.centroid[2 * t + 1] * e2y;
|
|
1015
|
+
sumSq += pc.area[t] * (cx * cx + cy * cy);
|
|
1016
|
+
weight += pc.area[t];
|
|
1017
|
+
}
|
|
1018
|
+
if (max === null || fmax > max) {
|
|
1019
|
+
max = fmax;
|
|
1020
|
+
frame = k;
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
return { max, frame, sumSq, weight, coordMax };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** A set of the stage's pixels (crop coordinates, y down): 1 where the pixel is in. */
|
|
1027
|
+
export interface RigRegion {
|
|
1028
|
+
width: number;
|
|
1029
|
+
height: number;
|
|
1030
|
+
data: Uint8Array;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** How far another slot's art reaches into a region — reported, never a bar (issue #123). */
|
|
1034
|
+
export interface Crossing {
|
|
1035
|
+
/** The largest distance from a point of its art inside the region to the region's edge, rig px. */
|
|
1036
|
+
depth: number;
|
|
1037
|
+
/** The most of the region its art covers on one frame, rig px², and that frame (index into the set). */
|
|
1038
|
+
area: number;
|
|
1039
|
+
frame: number;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* How far a slot's art swings into `region` (the stage's pixels at the setup
|
|
1044
|
+
* pose) in the reference frame: each art piece carried through its posed
|
|
1045
|
+
* triangle and back through the frame's map, clipped exactly to the region's
|
|
1046
|
+
* pixel squares. Null when it never enters. The depth is read at the corners
|
|
1047
|
+
* of the clipped pieces, each inside one rig pixel, so it can fall short of the
|
|
1048
|
+
* true depth by at most a pixel's diagonal; a point that starts outside the
|
|
1049
|
+
* region and travels s is never deeper than s.
|
|
1050
|
+
*/
|
|
1051
|
+
export function crossingOf(art: ArtMask, stage: StageBox, frames: ReadonlyArray<PosedTriangles | null>, maps: readonly FrameMap[], region: RigRegion): Crossing | null {
|
|
1052
|
+
const { width: W, height: H } = region;
|
|
1053
|
+
const inR = (u: number, v: number): boolean => u >= 0 && u < W && v >= 0 && v < H && region.data[v * W + u] === 1;
|
|
1054
|
+
// The region's box in world units, to pass over the triangles nowhere near it.
|
|
1055
|
+
let rx0 = Infinity;
|
|
1056
|
+
let rx1 = -Infinity;
|
|
1057
|
+
let ry0 = Infinity;
|
|
1058
|
+
let ry1 = -Infinity;
|
|
1059
|
+
for (let v = 0; v < H; v++) {
|
|
1060
|
+
for (let u = 0; u < W; u++) {
|
|
1061
|
+
if (region.data[v * W + u] !== 1) continue;
|
|
1062
|
+
rx0 = Math.min(rx0, stage.x + u);
|
|
1063
|
+
rx1 = Math.max(rx1, stage.x + u + 1);
|
|
1064
|
+
ry0 = Math.min(ry0, stage.y + cropToSpineY(v + 1, H));
|
|
1065
|
+
ry1 = Math.max(ry1, stage.y + cropToSpineY(v, H));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (rx0 === Infinity) return null;
|
|
1069
|
+
// Distance from a world point to the nearest pixel outside the region: rings of pixels around the one holding it.
|
|
1070
|
+
const depthAt = (x: number, y: number): number => {
|
|
1071
|
+
const u0 = Math.floor(x - stage.x);
|
|
1072
|
+
const v0 = Math.floor(H - (y - stage.y));
|
|
1073
|
+
let best = Infinity;
|
|
1074
|
+
for (let r = 0; r - 1 < best; r++) {
|
|
1075
|
+
for (let v = v0 - r; v <= v0 + r; v++) {
|
|
1076
|
+
for (let u = u0 - r; u <= u0 + r; u++) {
|
|
1077
|
+
if (Math.max(Math.abs(u - u0), Math.abs(v - v0)) !== r || inR(u, v)) continue;
|
|
1078
|
+
const sx0 = stage.x + u;
|
|
1079
|
+
const sy1 = stage.y + cropToSpineY(v, H);
|
|
1080
|
+
best = Math.min(best, Math.hypot(Math.max(sx0 - x, 0, x - sx0 - 1), Math.max(sy1 - 1 - y, 0, y - sy1)));
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return best;
|
|
1085
|
+
};
|
|
1086
|
+
const cut = new Map<readonly number[], ArtPieces>();
|
|
1087
|
+
let out: Crossing | null = null;
|
|
1088
|
+
frames.forEach((a, k) => {
|
|
1089
|
+
if (a === null) return;
|
|
1090
|
+
let pc = cut.get(a.rest);
|
|
1091
|
+
if (pc === undefined) {
|
|
1092
|
+
pc = artPieces(art, a, stage);
|
|
1093
|
+
cut.set(a.rest, pc);
|
|
1094
|
+
}
|
|
1095
|
+
const P = a.posed;
|
|
1096
|
+
const T = a.triangles;
|
|
1097
|
+
let area = 0;
|
|
1098
|
+
let depth = 0;
|
|
1099
|
+
for (let t = 0; t < pc.area.length; t++) {
|
|
1100
|
+
if (pc.area[t] === 0) continue;
|
|
1101
|
+
const [px, py] = backInto(maps[k], P[2 * T[3 * t]], P[2 * T[3 * t] + 1]);
|
|
1102
|
+
const [qx, qy] = backInto(maps[k], P[2 * T[3 * t + 1]], P[2 * T[3 * t + 1] + 1]);
|
|
1103
|
+
const [sx, sy] = backInto(maps[k], P[2 * T[3 * t + 2]], P[2 * T[3 * t + 2] + 1]);
|
|
1104
|
+
if (Math.max(px, qx, sx) <= rx0 || Math.min(px, qx, sx) >= rx1 || Math.max(py, qy, sy) <= ry0 || Math.min(py, qy, sy) >= ry1) continue;
|
|
1105
|
+
for (const bary of pc.pieces[t]) {
|
|
1106
|
+
const poly: Polygon = [];
|
|
1107
|
+
for (let q = 0; q < bary.length; q += 2) poly.push(px + bary[q] * (qx - px) + bary[q + 1] * (sx - px), py + bary[q] * (qy - py) + bary[q + 1] * (sy - py));
|
|
1108
|
+
let bx0 = Infinity;
|
|
1109
|
+
let bx1 = -Infinity;
|
|
1110
|
+
let by0 = Infinity;
|
|
1111
|
+
let by1 = -Infinity;
|
|
1112
|
+
for (let q = 0; q < poly.length; q += 2) {
|
|
1113
|
+
bx0 = Math.min(bx0, poly[q]);
|
|
1114
|
+
bx1 = Math.max(bx1, poly[q]);
|
|
1115
|
+
by0 = Math.min(by0, poly[q + 1]);
|
|
1116
|
+
by1 = Math.max(by1, poly[q + 1]);
|
|
1117
|
+
}
|
|
1118
|
+
for (let u = Math.floor(bx0 - stage.x); u <= Math.floor(bx1 - stage.x); u++) {
|
|
1119
|
+
for (let v = Math.floor(H - (by1 - stage.y)); v <= Math.floor(H - (by0 - stage.y)); v++) {
|
|
1120
|
+
if (!inR(u, v)) continue;
|
|
1121
|
+
const sx0 = stage.x + u;
|
|
1122
|
+
const sy1 = stage.y + cropToSpineY(v, H);
|
|
1123
|
+
const c = clipRect(poly, sx0, sy1 - 1, sx0 + 1, sy1);
|
|
1124
|
+
if (c.length < 6) continue;
|
|
1125
|
+
const pa = Math.abs(polygonMoments(c)[0]);
|
|
1126
|
+
if (pa === 0) continue;
|
|
1127
|
+
area += pa;
|
|
1128
|
+
for (let q = 0; q < c.length; q += 2) depth = Math.max(depth, depthAt(c[q], c[q + 1]));
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (area === 0) return;
|
|
1134
|
+
if (out === null) out = { depth, area, frame: k };
|
|
1135
|
+
else {
|
|
1136
|
+
out.depth = Math.max(out.depth, depth);
|
|
1137
|
+
if (area > out.area) {
|
|
1138
|
+
out.area = area;
|
|
1139
|
+
out.frame = k;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
return out;
|
|
1144
|
+
}
|
|
1145
|
+
|