@pieai/swimmer-avatar-kit 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 +21 -0
- package/NOTICE.md +23 -0
- package/README.md +125 -0
- package/UPSTREAM.md +47 -0
- package/dist/avatar.d.ts +2 -0
- package/dist/avatar.js +69 -0
- package/dist/avatar.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/materials.d.ts +20 -0
- package/dist/materials.js +61 -0
- package/dist/materials.js.map +1 -0
- package/dist/react-three-fiber.d.ts +16 -0
- package/dist/react-three-fiber.js +52 -0
- package/dist/react-three-fiber.js.map +1 -0
- package/dist/recipe.d.ts +18 -0
- package/dist/recipe.js +118 -0
- package/dist/recipe.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +102 -0
- package/upstream-lock.json +38 -0
- package/vendor/kindergrimm/LICENSE +24 -0
- package/vendor/kindergrimm/src/gloss/catmullClark.js +129 -0
- package/vendor/kindergrimm/src/gloss/gface.d.ts +25 -0
- package/vendor/kindergrimm/src/gloss/gface.js +226 -0
- package/vendor/kindergrimm/src/gloss/gform.js +201 -0
- package/vendor/kindergrimm/src/gloss/ghair.js +534 -0
- package/vendor/kindergrimm/src/gloss/glayout.js +365 -0
- package/vendor/kindergrimm/src/gloss/gmedia.d.ts +28 -0
- package/vendor/kindergrimm/src/gloss/gmedia.js +470 -0
- package/vendor/kindergrimm/src/gloss/gpalette.d.ts +10 -0
- package/vendor/kindergrimm/src/gloss/gpalette.js +205 -0
- package/vendor/kindergrimm/src/gloss/gparts/blush.js +57 -0
- package/vendor/kindergrimm/src/gloss/gparts/body.js +47 -0
- package/vendor/kindergrimm/src/gloss/gparts/brows.js +55 -0
- package/vendor/kindergrimm/src/gloss/gparts/crest.js +88 -0
- package/vendor/kindergrimm/src/gloss/gparts/eyes.js +357 -0
- package/vendor/kindergrimm/src/gloss/gparts/frame.js +225 -0
- package/vendor/kindergrimm/src/gloss/gparts/hair.js +100 -0
- package/vendor/kindergrimm/src/gloss/gparts/hat.js +163 -0
- package/vendor/kindergrimm/src/gloss/gparts/index.js +33 -0
- package/vendor/kindergrimm/src/gloss/gparts/mark.js +139 -0
- package/vendor/kindergrimm/src/gloss/gparts/mouth.js +170 -0
- package/vendor/kindergrimm/src/gloss/gparts/nose.js +63 -0
- package/vendor/kindergrimm/src/gloss/gparts/specs.js +124 -0
- package/vendor/kindergrimm/src/gloss/grig.d.ts +41 -0
- package/vendor/kindergrimm/src/gloss/grig.js +286 -0
- package/vendor/kindergrimm/src/gloss/gshape.js +421 -0
- package/vendor/kindergrimm/src/gloss/gspecies.d.ts +10 -0
- package/vendor/kindergrimm/src/gloss/gspecies.js +338 -0
- package/vendor/kindergrimm/src/gloss/gtexture.js +406 -0
- package/vendor/kindergrimm/src/rng.d.ts +11 -0
- package/vendor/kindergrimm/src/rng.js +27 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Group, Material, Texture } from "three";
|
|
2
|
+
export interface AvatarPartRecipe {
|
|
3
|
+
params?: unknown | null;
|
|
4
|
+
rr?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface AvatarRecipe {
|
|
7
|
+
seed: number;
|
|
8
|
+
species: string | null;
|
|
9
|
+
body: string | null;
|
|
10
|
+
stance: string | null;
|
|
11
|
+
palette: string | null;
|
|
12
|
+
colorIx: number | null;
|
|
13
|
+
material: string | null;
|
|
14
|
+
parts: Record<string, AvatarPartRecipe>;
|
|
15
|
+
}
|
|
16
|
+
export type AvatarSeed = number | string;
|
|
17
|
+
export type AvatarExpression = "idle" | "happy" | "angry" | "sad" | "surprised";
|
|
18
|
+
export interface AvatarOption {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AvatarPaletteOption extends AvatarOption {
|
|
23
|
+
colors: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
export type AvatarMaterialFor = (finish: string, color: string, shell?: boolean, print?: {
|
|
26
|
+
key: string;
|
|
27
|
+
tex: Texture;
|
|
28
|
+
} | null) => Material;
|
|
29
|
+
export interface AvatarBuildStats {
|
|
30
|
+
buildMs: number;
|
|
31
|
+
verts: number;
|
|
32
|
+
meshes: number;
|
|
33
|
+
}
|
|
34
|
+
export interface AvatarBounds {
|
|
35
|
+
w: number;
|
|
36
|
+
h: number;
|
|
37
|
+
cy: number;
|
|
38
|
+
minY: number;
|
|
39
|
+
maxY: number;
|
|
40
|
+
}
|
|
41
|
+
export interface AvatarBuildOptions {
|
|
42
|
+
materialFor: AvatarMaterialFor;
|
|
43
|
+
gaze?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface AvatarHandle {
|
|
46
|
+
readonly object: Group;
|
|
47
|
+
readonly recipe: AvatarRecipe;
|
|
48
|
+
readonly stats: AvatarBuildStats;
|
|
49
|
+
readonly bounds: AvatarBounds;
|
|
50
|
+
update(deltaSeconds: number): void;
|
|
51
|
+
getExpression(): AvatarExpression;
|
|
52
|
+
setExpression(expression: AvatarExpression): void;
|
|
53
|
+
isDisposed(): boolean;
|
|
54
|
+
dispose(): void;
|
|
55
|
+
}
|
|
56
|
+
export interface AvatarRecipeDocumentV1 {
|
|
57
|
+
schema: "@pieai/swimmer-avatar-kit/recipe";
|
|
58
|
+
version: 1;
|
|
59
|
+
renderer: "gloss";
|
|
60
|
+
recipe: AvatarRecipe;
|
|
61
|
+
}
|
|
62
|
+
export type AvatarRecipeDocument = AvatarRecipeDocumentV1;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pieai/swimmer-avatar-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared procedural 3D avatar recipes, Three.js runtime, and React Three Fiber adapter for PieAI products.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"avatar",
|
|
7
|
+
"procedural-generation",
|
|
8
|
+
"react-three-fiber",
|
|
9
|
+
"swimmer",
|
|
10
|
+
"threejs",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/PieAIStudio/SwimmerAvatarKit#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/PieAIStudio/SwimmerAvatarKit/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/PieAIStudio/SwimmerAvatarKit.git"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"vendor",
|
|
25
|
+
"README.md",
|
|
26
|
+
"NOTICE.md",
|
|
27
|
+
"UPSTREAM.md",
|
|
28
|
+
"upstream-lock.json",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./materials": {
|
|
41
|
+
"types": "./dist/materials.d.ts",
|
|
42
|
+
"import": "./dist/materials.js"
|
|
43
|
+
},
|
|
44
|
+
"./react-three-fiber": {
|
|
45
|
+
"types": "./dist/react-three-fiber.d.ts",
|
|
46
|
+
"import": "./dist/react-three-fiber.js"
|
|
47
|
+
},
|
|
48
|
+
"./package.json": "./package.json"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"registry": "https://registry.npmjs.org/"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@pieai/doc-gov": "0.9.3",
|
|
56
|
+
"@pieai/pro-gov": "0.9.3",
|
|
57
|
+
"@react-three/fiber": "9.6.1",
|
|
58
|
+
"@types/node": "24.13.2",
|
|
59
|
+
"@types/react": "19.2.17",
|
|
60
|
+
"@types/three": "0.185.1",
|
|
61
|
+
"lefthook": "2.1.10",
|
|
62
|
+
"oxfmt": "0.58.0",
|
|
63
|
+
"oxlint": "1.73.0",
|
|
64
|
+
"react": "19.2.7",
|
|
65
|
+
"three": "0.185.1",
|
|
66
|
+
"typescript": "6.0.3",
|
|
67
|
+
"vitest": "4.1.10"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"@react-three/fiber": ">=9 <10",
|
|
71
|
+
"react": ">=19 <20",
|
|
72
|
+
"three": ">=0.180.0 <1"
|
|
73
|
+
},
|
|
74
|
+
"peerDependenciesMeta": {
|
|
75
|
+
"@react-three/fiber": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"react": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"engines": {
|
|
83
|
+
"node": ">=24 <25"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "tsc -p tsconfig.build.json",
|
|
87
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
88
|
+
"lint": "oxlint src tests scripts -c ./.oxlintrc.json",
|
|
89
|
+
"format": "oxfmt package.json tsconfig.json tsconfig.build.json .oxlintrc.json src tests scripts --write --ignore-path ./.oxfmtignore",
|
|
90
|
+
"format:check": "oxfmt package.json tsconfig.json tsconfig.build.json .oxlintrc.json src tests scripts --check --ignore-path ./.oxfmtignore",
|
|
91
|
+
"test": "vitest run --pool threads",
|
|
92
|
+
"test:package-consumer": "node scripts/test-package-consumer.mjs",
|
|
93
|
+
"host-links:check": "node scripts/verify-host-links.mjs",
|
|
94
|
+
"vendor:verify": "node scripts/verify-vendor.mjs",
|
|
95
|
+
"upstream:check": "node scripts/check-upstream.mjs",
|
|
96
|
+
"verify": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm vendor:verify && pnpm test && pnpm build",
|
|
97
|
+
"verify:release": "pnpm verify && pnpm docs:check && pnpm test:package-consumer",
|
|
98
|
+
"doc-gov": "doc-gov",
|
|
99
|
+
"pro-gov": "pro-gov",
|
|
100
|
+
"docs:check": "pnpm pro-gov doctor --strict-hooks && pnpm host-links:check && pnpm doc-gov router-check && pnpm doc-gov check && pnpm doc-gov scan --check && pnpm doc-gov links && pnpm doc-gov audit && pnpm doc-gov doctor"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"repository": "https://github.com/albertobeiz/kindergrimm.git",
|
|
4
|
+
"defaultBranch": "main",
|
|
5
|
+
"commit": "811214c6dd5de18cc20335cd3d4ab0a06e45ffd4",
|
|
6
|
+
"commitDate": "2026-08-21T12:51:30+02:00",
|
|
7
|
+
"importedAt": "2026-08-22",
|
|
8
|
+
"license": "Unlicense",
|
|
9
|
+
"excludedEntrypoints": ["src/gloss/gcrowd.js", "src/gloss/gloss.js"],
|
|
10
|
+
"files": {
|
|
11
|
+
"LICENSE": "c290d5a44ba844c2aa271e3be836c25176fe1f8b",
|
|
12
|
+
"src/gloss/catmullClark.js": "3fa76213faa27595836365a451218fb650529fc5",
|
|
13
|
+
"src/gloss/gface.js": "f0e90be3039ddd93adb06254943985ee1a37e0d4",
|
|
14
|
+
"src/gloss/gform.js": "c07865a29b7741fa454238a724e330f05f0f0747",
|
|
15
|
+
"src/gloss/ghair.js": "bfb35aa765415f9d2fe4f14b6dd5cf947ecece4b",
|
|
16
|
+
"src/gloss/glayout.js": "21667e34b7f486b89608698abe2527a9dc565c26",
|
|
17
|
+
"src/gloss/gmedia.js": "aef88b617f3f694b1051237055fd5edcd8be546e",
|
|
18
|
+
"src/gloss/gpalette.js": "31d1fd17decc8a8f779aa06288bf2fe057aec9dd",
|
|
19
|
+
"src/gloss/gparts/blush.js": "daed9e66e45b918569490be71392858fa5e502ec",
|
|
20
|
+
"src/gloss/gparts/body.js": "5f15d76362ec47dc24846a3eacb5f5ba6a435484",
|
|
21
|
+
"src/gloss/gparts/brows.js": "d646a477b73de7e7d0fa4443848aff9d5c328f45",
|
|
22
|
+
"src/gloss/gparts/crest.js": "fb5470509ff75ad2d03addc4d9ba02109f7f5521",
|
|
23
|
+
"src/gloss/gparts/eyes.js": "9188e13c0df544620d951ecf5b0dba05b59ab55b",
|
|
24
|
+
"src/gloss/gparts/frame.js": "957727c53a6f9f9c4bd95399378ae3cdaa5a1423",
|
|
25
|
+
"src/gloss/gparts/hair.js": "b962f6fe0e30bbed537054b4fa0d6193052e5b87",
|
|
26
|
+
"src/gloss/gparts/hat.js": "27fbc817ab9797dd706bde93913dccbdfebb2c48",
|
|
27
|
+
"src/gloss/gparts/index.js": "82fc74c3c2cec69e11b28bfebbdee2df60dc3052",
|
|
28
|
+
"src/gloss/gparts/mark.js": "046143fdedab6bc03c0e7495338b342cef5c4fc9",
|
|
29
|
+
"src/gloss/gparts/mouth.js": "1d6cdcb271b002b37469c985517764aed1647091",
|
|
30
|
+
"src/gloss/gparts/nose.js": "82683b6bda2bb2fcae9b833686aaf9a10c039e52",
|
|
31
|
+
"src/gloss/gparts/specs.js": "780de9e2584f68eb34c199756c1fc045187f0159",
|
|
32
|
+
"src/gloss/grig.js": "2bc489a2bf05d9299a56bca808522a9e2f5fdf49",
|
|
33
|
+
"src/gloss/gshape.js": "c44e0b43ddfebef130d7ec8d6be26f4e9b4e7448",
|
|
34
|
+
"src/gloss/gspecies.js": "1805c18339956068354a6c85c1ca93d39e1758f1",
|
|
35
|
+
"src/gloss/gtexture.js": "00a4c3ddc46496357f7232ea6f68ff537a432697",
|
|
36
|
+
"src/rng.js": "5a6e8fe001552cf8ae860d70318e1c85da58baec"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catmull-Clark subdivision for closed quad/n-gon meshes.
|
|
3
|
+
*
|
|
4
|
+
* three.js used to ship SubdivisionModifier but it was removed, and the
|
|
5
|
+
* community replacement (three-subdivide) is Loop subdivision on triangles,
|
|
6
|
+
* which does not give the same surface. Catmull-Clark on quads is what C4D and
|
|
7
|
+
* Blender do, and it is about 60 lines, so we just do it.
|
|
8
|
+
*
|
|
9
|
+
* Mesh format is plain arrays:
|
|
10
|
+
* verts: [[x, y, z], ...]
|
|
11
|
+
* faces: [[i, j, k, l], ...] (any arity, wound consistently)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function addTo(out, v) {
|
|
15
|
+
out[0] += v[0];
|
|
16
|
+
out[1] += v[1];
|
|
17
|
+
out[2] += v[2];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function scale(v, s) {
|
|
21
|
+
return [v[0] * s, v[1] * s, v[2] * s];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function subdivide({ verts, faces }) {
|
|
25
|
+
const nv = verts.length;
|
|
26
|
+
|
|
27
|
+
// 1. face points: the centroid of each face
|
|
28
|
+
const facePoints = faces.map((f) => {
|
|
29
|
+
const c = [0, 0, 0];
|
|
30
|
+
for (const vi of f) addTo(c, verts[vi]);
|
|
31
|
+
return scale(c, 1 / f.length);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 2. edges. Every edge in a closed mesh is shared by exactly two faces.
|
|
35
|
+
const edges = new Map(); // "a_b" (a<b) -> { a, b, faces: [] }
|
|
36
|
+
const edgeKey = (a, b) => (a < b ? `${a}_${b}` : `${b}_${a}`);
|
|
37
|
+
faces.forEach((f, fi) => {
|
|
38
|
+
for (let i = 0; i < f.length; i++) {
|
|
39
|
+
const a = f[i];
|
|
40
|
+
const b = f[(i + 1) % f.length];
|
|
41
|
+
const key = edgeKey(a, b);
|
|
42
|
+
let e = edges.get(key);
|
|
43
|
+
if (!e) edges.set(key, (e = { a: Math.min(a, b), b: Math.max(a, b), faces: [] }));
|
|
44
|
+
e.faces.push(fi);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 3. edge points: average of the two endpoints and the two face points.
|
|
49
|
+
// A border edge (one face) falls back to the midpoint.
|
|
50
|
+
const edgePointIndex = new Map();
|
|
51
|
+
const edgePoints = [];
|
|
52
|
+
for (const [key, e] of edges) {
|
|
53
|
+
const p = [0, 0, 0];
|
|
54
|
+
addTo(p, verts[e.a]);
|
|
55
|
+
addTo(p, verts[e.b]);
|
|
56
|
+
if (e.faces.length === 2) {
|
|
57
|
+
addTo(p, facePoints[e.faces[0]]);
|
|
58
|
+
addTo(p, facePoints[e.faces[1]]);
|
|
59
|
+
edgePointIndex.set(key, nv + faces.length + edgePoints.length);
|
|
60
|
+
edgePoints.push(scale(p, 0.25));
|
|
61
|
+
} else {
|
|
62
|
+
edgePointIndex.set(key, nv + faces.length + edgePoints.length);
|
|
63
|
+
edgePoints.push(scale(p, 0.5));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. move the original vertices: (F + 2R + (n-3)P) / n
|
|
68
|
+
const F = Array.from({ length: nv }, () => [0, 0, 0]);
|
|
69
|
+
const R = Array.from({ length: nv }, () => [0, 0, 0]);
|
|
70
|
+
const faceValence = new Array(nv).fill(0);
|
|
71
|
+
const edgeValence = new Array(nv).fill(0);
|
|
72
|
+
|
|
73
|
+
faces.forEach((f, fi) => {
|
|
74
|
+
for (const vi of f) {
|
|
75
|
+
addTo(F[vi], facePoints[fi]);
|
|
76
|
+
faceValence[vi]++;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
for (const e of edges.values()) {
|
|
80
|
+
const mid = scale([
|
|
81
|
+
verts[e.a][0] + verts[e.b][0],
|
|
82
|
+
verts[e.a][1] + verts[e.b][1],
|
|
83
|
+
verts[e.a][2] + verts[e.b][2],
|
|
84
|
+
], 0.5);
|
|
85
|
+
addTo(R[e.a], mid);
|
|
86
|
+
edgeValence[e.a]++;
|
|
87
|
+
addTo(R[e.b], mid);
|
|
88
|
+
edgeValence[e.b]++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const newVerts = new Array(nv);
|
|
92
|
+
for (let i = 0; i < nv; i++) {
|
|
93
|
+
const n = faceValence[i];
|
|
94
|
+
const f = scale(F[i], 1 / n);
|
|
95
|
+
const r = scale(R[i], 1 / edgeValence[i]);
|
|
96
|
+
const p = verts[i];
|
|
97
|
+
newVerts[i] = [
|
|
98
|
+
(f[0] + 2 * r[0] + (n - 3) * p[0]) / n,
|
|
99
|
+
(f[1] + 2 * r[1] + (n - 3) * p[1]) / n,
|
|
100
|
+
(f[2] + 2 * r[2] + (n - 3) * p[2]) / n,
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5. each n-gon becomes n quads
|
|
105
|
+
const outVerts = newVerts.concat(facePoints, edgePoints);
|
|
106
|
+
const outFaces = [];
|
|
107
|
+
faces.forEach((f, fi) => {
|
|
108
|
+
const fp = nv + fi;
|
|
109
|
+
for (let i = 0; i < f.length; i++) {
|
|
110
|
+
const prev = f[(i - 1 + f.length) % f.length];
|
|
111
|
+
const cur = f[i];
|
|
112
|
+
const next = f[(i + 1) % f.length];
|
|
113
|
+
outFaces.push([
|
|
114
|
+
cur,
|
|
115
|
+
edgePointIndex.get(edgeKey(cur, next)),
|
|
116
|
+
fp,
|
|
117
|
+
edgePointIndex.get(edgeKey(prev, cur)),
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return { verts: outVerts, faces: outFaces };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function subdivideN(mesh, times) {
|
|
126
|
+
let m = mesh;
|
|
127
|
+
for (let i = 0; i < times; i++) m = subdivide(m);
|
|
128
|
+
return m;
|
|
129
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { GlossBuilt } from "./grig.js";
|
|
2
|
+
|
|
3
|
+
export type GlossExpression = "idle" | "happy" | "angry" | "sad" | "surprised";
|
|
4
|
+
|
|
5
|
+
export const GLOSS_FACES: readonly GlossExpression[];
|
|
6
|
+
|
|
7
|
+
export interface GlossHeadOffset {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
yaw: number;
|
|
11
|
+
pitch: number;
|
|
12
|
+
rot: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GlossLife {
|
|
16
|
+
head: GlossHeadOffset;
|
|
17
|
+
face: () => GlossExpression;
|
|
18
|
+
setFace: (id: GlossExpression) => void;
|
|
19
|
+
update: (elapsedSeconds: number, deltaSeconds: number) => GlossHeadOffset;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createGlossFace(
|
|
23
|
+
built: GlossBuilt,
|
|
24
|
+
options?: { gaze?: boolean },
|
|
25
|
+
): GlossLife;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// ---------------------------------------------------------------
|
|
2
|
+
// THE FACE LIFE — the autonomic half of `anim.js`, ported to a character
|
|
3
|
+
// that has no bones.
|
|
4
|
+
//
|
|
5
|
+
// The drawn rig gets its life from blink, gaze, sway and breath. Only
|
|
6
|
+
// one of those needs a skeleton, so the other three come over intact,
|
|
7
|
+
// and gaze is the one that matters:
|
|
8
|
+
//
|
|
9
|
+
// SOMETHING CATCHES THE EYE, gets looked at, and is let go. The eyes
|
|
10
|
+
// move FIRST and FAST — a saccade is ballistic, it lands and stops —
|
|
11
|
+
// and the head follows late, slower, and only some of the time,
|
|
12
|
+
// because most real glances are eyes-only. Two separate springs,
|
|
13
|
+
// both critically damped: an under-damped head was tried (the
|
|
14
|
+
// cartoon whip-and-overshoot) and it read as bouncing, not looking.
|
|
15
|
+
// The life is in the eyes leading, not in the head ringing.
|
|
16
|
+
//
|
|
17
|
+
// Here the character IS the head, so the head spring drives the whole body —
|
|
18
|
+
// and because every feature is its own mesh (see `gshape.js`), the
|
|
19
|
+
// eyes leading the head is two translations, not a rebuild.
|
|
20
|
+
//
|
|
21
|
+
// EVERYTHING WRITTEN HERE IS AN OFFSET FROM REST. Each frame the
|
|
22
|
+
// features are put back where the rig left them and the offsets are
|
|
23
|
+
// re-applied, so expressions blend and nothing accumulates drift —
|
|
24
|
+
// the same rule the drawn poses follow.
|
|
25
|
+
// ---------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
import { Vector3 } from 'three';
|
|
28
|
+
|
|
29
|
+
// the ball-eye lid's hinge. WORLD x, not the lid's own: a wide-set
|
|
30
|
+
// eye's normal tips ~30° off the camera, and a lid rolled about its
|
|
31
|
+
// own tilted axis closes off-camera and leaves a rim of white showing
|
|
32
|
+
const X_AXIS = new Vector3(1, 0, 0);
|
|
33
|
+
|
|
34
|
+
const DIRS = ['left', 'right', 'up', 'down'];
|
|
35
|
+
const OPPOSITE = { left: 'right', right: 'left', up: 'down', down: 'up' };
|
|
36
|
+
|
|
37
|
+
// Expressions are OFFSETS, not drawings. A gloss face cannot swap a
|
|
38
|
+
// mouth without rebuilding it, but it can raise a brow, narrow an eye
|
|
39
|
+
// and open a mouth — which turns out to be most of what an expression
|
|
40
|
+
// is anyway.
|
|
41
|
+
const FACES = {
|
|
42
|
+
idle: {},
|
|
43
|
+
happy: { eyeSY: .78, browLift: .16, mouthSY: 1.14 },
|
|
44
|
+
angry: { eyeSY: .88, browLift: -.26, browRoll: -.55 },
|
|
45
|
+
sad: { eyeSY: .94, browLift: .12, browRoll: .48, mouthSY: .9 },
|
|
46
|
+
surprised: { eyeS: 1.13, browLift: .36, mouthSY: 1.3 },
|
|
47
|
+
};
|
|
48
|
+
export const GLOSS_FACES = Object.keys(FACES);
|
|
49
|
+
|
|
50
|
+
const isEye = id => id.startsWith('eye');
|
|
51
|
+
const isBrow = id => id.startsWith('brow');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* built → an animator that owns every write to the face meshes.
|
|
55
|
+
* `update(t, dt)` then leaves the head's offset on `.head`, which the
|
|
56
|
+
* page adds to whatever else it is doing with that character.
|
|
57
|
+
*/
|
|
58
|
+
export function createGlossFace(built, opts = {}) {
|
|
59
|
+
const u = built.L.s; // one unit: the body radius
|
|
60
|
+
const rest = new Map();
|
|
61
|
+
for (const [id, m] of Object.entries(built.face)) {
|
|
62
|
+
rest.set(id, {
|
|
63
|
+
pos: m.position.clone(), quat: m.quaternion.clone(),
|
|
64
|
+
scale: m.scale.clone(), side: id.endsWith('L') || id.includes('L') ? -1 : 1,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let nextBlink = 1 + Math.random() * 3, blinkT = -1;
|
|
69
|
+
let dir = null, until = 0, next = 1 + Math.random() * 4, queue = [];
|
|
70
|
+
let ex = 0, ey = 0, evx = 0, evy = 0; // the eyes — a saccade
|
|
71
|
+
let gx = 0, gy = 0, gvx = 0, gvy = 0; // the head — settles, never rings
|
|
72
|
+
let headFollow = 1; // does THIS glance turn the head?
|
|
73
|
+
let cur = 'idle', target = 'idle', w = 1;
|
|
74
|
+
|
|
75
|
+
const api = {
|
|
76
|
+
// x/y in the CHARACTER's own units (a page at cell scale must multiply),
|
|
77
|
+
// yaw/pitch/rot in radians
|
|
78
|
+
head: { x: 0, y: 0, yaw: 0, pitch: 0, rot: 0 },
|
|
79
|
+
face: () => target,
|
|
80
|
+
setFace(id) {
|
|
81
|
+
if (!FACES[id] || id === target) return;
|
|
82
|
+
cur = target; target = id; w = 0;
|
|
83
|
+
// pull a blink forward: a face that changes behind shut lids
|
|
84
|
+
// reads as a decision, and one that changes in the open reads
|
|
85
|
+
// as a glitch
|
|
86
|
+
if (blinkT <= 0) { blinkT = .13; nextBlink = 1.2 + Math.random() * 3; }
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
update(t, dt) {
|
|
90
|
+
// ---- blink ----
|
|
91
|
+
if (blinkT > 0) blinkT -= dt;
|
|
92
|
+
else if (t > nextBlink) { blinkT = .13; nextBlink = t + 1.4 + Math.random() * 3.4; }
|
|
93
|
+
const shut = blinkT > 0 ? Math.max(.06, 1 - Math.sin((1 - blinkT / .13) * Math.PI)) : 1;
|
|
94
|
+
|
|
95
|
+
// ---- gaze: caught, held, let go ----
|
|
96
|
+
if (opts.gaze !== false) {
|
|
97
|
+
if (!dir && t > next) {
|
|
98
|
+
const d = DIRS[(Math.random() * 4) | 0];
|
|
99
|
+
// a third of the time it looks back the other way, which is
|
|
100
|
+
// what stops the sheet reading as everyone scanning a room
|
|
101
|
+
queue = Math.random() < .3 ? [d, OPPOSITE[d]] : [d];
|
|
102
|
+
dir = queue.shift();
|
|
103
|
+
// most real glances are eyes-only — the head stays put and
|
|
104
|
+
// only the eyes dart. The head joining in is the exception,
|
|
105
|
+
// and an eyes-only glance is quicker: a dart, not a stare
|
|
106
|
+
headFollow = Math.random() < .45 ? 1 : 0;
|
|
107
|
+
until = t + (headFollow ? .5 + Math.random() * 1.4
|
|
108
|
+
: .3 + Math.random() * .8);
|
|
109
|
+
} else if (dir && t > until) {
|
|
110
|
+
dir = queue.shift() ?? null;
|
|
111
|
+
if (dir) until = t + .4 + Math.random() * 1.1;
|
|
112
|
+
else next = t + 1.4 + Math.random() * 4;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const tx = dir === 'left' ? -1 : dir === 'right' ? 1 : 0;
|
|
116
|
+
const ty = dir === 'up' ? 1 : dir === 'down' ? -1 : 0;
|
|
117
|
+
// Both springs are stepped with the EXACT critically-damped
|
|
118
|
+
// solution, not Euler: the eye spring is stiff (ω·dt > 1 on any
|
|
119
|
+
// long frame, and building a character costs ~20ms), and an Euler step
|
|
120
|
+
// there flips the damping term's sign and flings every eye into
|
|
121
|
+
// a corner. The closed form cannot blow up at any dt.
|
|
122
|
+
const step = (x, v, tgt, om, h) => {
|
|
123
|
+
const dx = x - tgt, e = Math.exp(-om * h), b = v + om * dx;
|
|
124
|
+
return [tgt + (dx + b * h) * e, (v - om * b * h) * e];
|
|
125
|
+
};
|
|
126
|
+
// the EYES: fast — a saccade is ballistic, it lands and stops,
|
|
127
|
+
// it does not ease and it does not ring
|
|
128
|
+
const eo = 24;
|
|
129
|
+
[ex, evx] = step(ex, evx, tx, eo, dt);
|
|
130
|
+
[ey, evy] = step(ey, evy, ty, eo, dt);
|
|
131
|
+
// the HEAD: slower, chasing the same target only when this
|
|
132
|
+
// glance turns it — so the eyes visibly arrive first and the
|
|
133
|
+
// head settles after them without bouncing
|
|
134
|
+
const ho = 6;
|
|
135
|
+
[gx, gvx] = step(gx, gvx, tx * headFollow, ho, dt);
|
|
136
|
+
[gy, gvy] = step(gy, gvy, ty * headFollow, ho, dt);
|
|
137
|
+
|
|
138
|
+
// ---- expression blend ----
|
|
139
|
+
if (w < 1) w = Math.min(1, w + dt * 4);
|
|
140
|
+
const A = FACES[cur], B = FACES[target];
|
|
141
|
+
const mix = (key, d = 0) => (A[key] ?? d) * (1 - w) + (B[key] ?? d) * w;
|
|
142
|
+
const eyeSY = mix('eyeSY', 1), eyeS = mix('eyeS', 1);
|
|
143
|
+
const browLift = mix('browLift'), browRoll = mix('browRoll');
|
|
144
|
+
const mouthSY = mix('mouthSY', 1);
|
|
145
|
+
|
|
146
|
+
// ---- write ----
|
|
147
|
+
for (const [id, m] of Object.entries(built.face)) {
|
|
148
|
+
const r = rest.get(id);
|
|
149
|
+
m.position.copy(r.pos);
|
|
150
|
+
m.quaternion.copy(r.quat);
|
|
151
|
+
m.scale.copy(r.scale);
|
|
152
|
+
|
|
153
|
+
if (isEye(id)) {
|
|
154
|
+
// A feature carrying its own budget uses it: that is a PUPIL
|
|
155
|
+
// crossing its white, or the white itself barely stirring.
|
|
156
|
+
// Anything else is a whole eye, and it has to travel far
|
|
157
|
+
// enough to see — an offset you must look for is the same as
|
|
158
|
+
// no offset at all.
|
|
159
|
+
//
|
|
160
|
+
// The budget is scaled by `shut` so a pupil is back at centre
|
|
161
|
+
// by the time the lid closes over it: the white squashes
|
|
162
|
+
// about the eye's middle and a pupil left out at the edge
|
|
163
|
+
// would be squashed about somewhere else and slide clear.
|
|
164
|
+
const tv = m.userData.travel;
|
|
165
|
+
if (tv) { m.translateX(ex * tv[0] * shut); m.translateY(ey * tv[1] * shut); }
|
|
166
|
+
else { m.translateX(ex * u * .17); m.translateY(ey * u * .13); }
|
|
167
|
+
|
|
168
|
+
// A parked pupil comes back to the middle as the lid closes.
|
|
169
|
+
// The white squashes about the EYE's centre; a pupil resting
|
|
170
|
+
// up at the top would squash about its OWN and slide clear.
|
|
171
|
+
const park = m.userData.anchorY;
|
|
172
|
+
if (park) m.translateY(-park * (1 - shut));
|
|
173
|
+
|
|
174
|
+
// A LID does not squash, it COMES DOWN. Scaling it with the
|
|
175
|
+
// eye would just shrink it out of the way, which is the one
|
|
176
|
+
// thing a closing lid must not do.
|
|
177
|
+
const drop = m.userData.lidDrop;
|
|
178
|
+
if (drop) { m.translateY(-(1 - shut) * drop); continue; }
|
|
179
|
+
|
|
180
|
+
// A BALL eye's lid does not slide either, it ROLLS: the cap
|
|
181
|
+
// is centred on the ball, so pitching it forward wraps it
|
|
182
|
+
// over the front the way a real lid wraps an eyeball. A
|
|
183
|
+
// translate here would slide the cap off its own sphere.
|
|
184
|
+
const roll = m.userData.lidRoll;
|
|
185
|
+
if (roll) { m.rotateOnWorldAxis(X_AXIS, roll * (1 - shut)); continue; }
|
|
186
|
+
|
|
187
|
+
m.scale.set(r.scale.x * eyeS, r.scale.y * eyeS * eyeSY * (m.userData.shut ? 1 : shut), r.scale.z);
|
|
188
|
+
} else if (isBrow(id)) {
|
|
189
|
+
// brows belong to the eyes, not the skull — they ride the
|
|
190
|
+
// saccade, so an eyes-only glance still moves them
|
|
191
|
+
m.translateX(ex * u * .13);
|
|
192
|
+
m.translateY(ey * u * .12 + browLift * u * .11);
|
|
193
|
+
m.rotateZ(browRoll * r.side);
|
|
194
|
+
} else if (id.startsWith('mouth')) {
|
|
195
|
+
// ALL the mouth's plates: a maw is an outline, an interior,
|
|
196
|
+
// teeth and a tongue, and an expression that opened only the
|
|
197
|
+
// outline would leave the furniture poking through the lip
|
|
198
|
+
m.translateX(gx * u * .07);
|
|
199
|
+
m.translateY(gy * u * .04);
|
|
200
|
+
m.scale.set(r.scale.x, r.scale.y * mouthSY, r.scale.z);
|
|
201
|
+
} else if (id.startsWith('hair') || id.startsWith('hat')) {
|
|
202
|
+
// HAIR AND HATS DO NOT RIDE. Every other feature slides a
|
|
203
|
+
// little with the gaze because it is painted on a face that
|
|
204
|
+
// is turning. These are not on the face — the group's own
|
|
205
|
+
// spring already carries them, and a second offset on top
|
|
206
|
+
// slides the cut off the skull and the hat off the head.
|
|
207
|
+
continue;
|
|
208
|
+
} else {
|
|
209
|
+
m.translateX(gx * u * .09); // nose, cheeks: they ride
|
|
210
|
+
m.translateY(gy * u * .07);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// What the head does about it. The TURN is what sells a look —
|
|
215
|
+
// a head that only slides is a head on a rail — so the yaw and
|
|
216
|
+
// pitch carry it and the shift is the follow-through.
|
|
217
|
+
api.head.x = gx * u * .16;
|
|
218
|
+
api.head.y = gy * u * .11;
|
|
219
|
+
api.head.yaw = gx * .34;
|
|
220
|
+
api.head.pitch = -gy * .22;
|
|
221
|
+
api.head.rot = -gx * .07;
|
|
222
|
+
return api.head;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
return api;
|
|
226
|
+
}
|