@fungi.computer/sprite 0.0.0-stage → 0.0.1
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/README.md +152 -2
- package/dist/assets.d.ts +20 -0
- package/dist/assets.js +83 -0
- package/dist/avatar.d.ts +152 -0
- package/dist/avatar.js +228 -0
- package/dist/controls.d.ts +32 -0
- package/dist/controls.js +64 -0
- package/dist/customization.d.ts +21 -0
- package/dist/customization.js +13 -0
- package/dist/document.d.ts +43 -0
- package/dist/document.js +15 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -0
- package/dist/ink.d.ts +50 -0
- package/dist/ink.js +225 -0
- package/dist/mycelium.d.ts +139 -0
- package/dist/mycelium.js +242 -0
- package/dist/player.d.ts +99 -0
- package/dist/player.js +368 -0
- package/dist/react.d.ts +23 -0
- package/dist/react.js +61 -0
- package/dist/render-style.d.ts +45 -0
- package/dist/render-style.js +639 -0
- package/dist/video.d.ts +4 -0
- package/dist/video.js +141 -0
- package/package.json +93 -3
package/dist/video.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
const videoSchema = z
|
|
4
|
+
.strictObject({
|
|
5
|
+
clips: z.record(z.string().min(1), z.string().min(1)),
|
|
6
|
+
})
|
|
7
|
+
.refine((data) => Object.hasOwn(data.clips, "idle"), {
|
|
8
|
+
message: "Video avatar needs an idle clip",
|
|
9
|
+
});
|
|
10
|
+
const awaitFirstFrame = (video, signal) => new Promise((resolve, reject) => {
|
|
11
|
+
const cleanup = () => {
|
|
12
|
+
video.removeEventListener("loadeddata", loaded);
|
|
13
|
+
video.removeEventListener("error", failed);
|
|
14
|
+
signal.removeEventListener("abort", aborted);
|
|
15
|
+
};
|
|
16
|
+
const loaded = () => {
|
|
17
|
+
cleanup();
|
|
18
|
+
resolve();
|
|
19
|
+
};
|
|
20
|
+
const failed = () => {
|
|
21
|
+
cleanup();
|
|
22
|
+
reject(new Error("Avatar video could not be decoded"));
|
|
23
|
+
};
|
|
24
|
+
const aborted = () => {
|
|
25
|
+
cleanup();
|
|
26
|
+
reject(new Error("Avatar video load was cancelled"));
|
|
27
|
+
};
|
|
28
|
+
video.addEventListener("loadeddata", loaded, { once: true });
|
|
29
|
+
video.addEventListener("error", failed, { once: true });
|
|
30
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
31
|
+
if (signal.aborted)
|
|
32
|
+
aborted();
|
|
33
|
+
});
|
|
34
|
+
/** One character, with browser-native MP4 clips selected by semantic activity. */
|
|
35
|
+
const renderVideo = (context) => Effect.gen(function* () {
|
|
36
|
+
const data = videoSchema.parse(context.document.data);
|
|
37
|
+
const posterURL = context.document.poster
|
|
38
|
+
? yield* context.assetURL(context.document.poster)
|
|
39
|
+
: undefined;
|
|
40
|
+
if (posterURL) {
|
|
41
|
+
const poster = yield* Effect.acquireRelease(Effect.sync(() => context.target.ownerDocument.createElement("img")), (image) => Effect.sync(() => {
|
|
42
|
+
image.remove();
|
|
43
|
+
}));
|
|
44
|
+
poster.alt = "";
|
|
45
|
+
poster.src = posterURL;
|
|
46
|
+
poster.style.cssText =
|
|
47
|
+
"width:100%;height:100%;object-fit:contain;display:block";
|
|
48
|
+
context.target.replaceChildren(poster);
|
|
49
|
+
}
|
|
50
|
+
const videos = new Map();
|
|
51
|
+
let decodedPixels = 0;
|
|
52
|
+
for (const reference of new Set(Object.values(data.clips))) {
|
|
53
|
+
const src = yield* context.assetURL(reference);
|
|
54
|
+
const video = yield* Effect.acquireRelease(Effect.sync(() => context.target.ownerDocument.createElement("video")), (element) => Effect.sync(() => {
|
|
55
|
+
element.pause();
|
|
56
|
+
element.removeAttribute("src");
|
|
57
|
+
element.removeAttribute("poster");
|
|
58
|
+
element.remove();
|
|
59
|
+
}));
|
|
60
|
+
video.muted = true;
|
|
61
|
+
video.defaultMuted = true;
|
|
62
|
+
video.loop = true;
|
|
63
|
+
video.playsInline = true;
|
|
64
|
+
video.preload = "auto";
|
|
65
|
+
video.style.cssText =
|
|
66
|
+
"width:100%;height:100%;object-fit:contain;display:none";
|
|
67
|
+
if (posterURL)
|
|
68
|
+
video.poster = posterURL;
|
|
69
|
+
context.target.append(video);
|
|
70
|
+
const frame = awaitFirstFrame(video, context.signal);
|
|
71
|
+
video.src = src;
|
|
72
|
+
yield* Effect.tryPromise({
|
|
73
|
+
try: () => frame,
|
|
74
|
+
catch: (error) => error instanceof Error ? error : new Error(String(error)),
|
|
75
|
+
});
|
|
76
|
+
context.signal.throwIfAborted();
|
|
77
|
+
decodedPixels += video.videoWidth * video.videoHeight;
|
|
78
|
+
if (video.videoWidth <= 0 ||
|
|
79
|
+
video.videoHeight <= 0 ||
|
|
80
|
+
decodedPixels > context.maxPixels)
|
|
81
|
+
throw new Error("Avatar videos exceed the pixel limit or have no frame");
|
|
82
|
+
videos.set(reference, video);
|
|
83
|
+
}
|
|
84
|
+
const idleReference = data.clips.idle;
|
|
85
|
+
const idle = idleReference ? videos.get(idleReference) : undefined;
|
|
86
|
+
if (!idle)
|
|
87
|
+
throw new Error("Video avatar has no decoded idle clip");
|
|
88
|
+
let current = idle;
|
|
89
|
+
let paused = true;
|
|
90
|
+
const play = (video) => {
|
|
91
|
+
void video.play().then(() => {
|
|
92
|
+
if (paused || current !== video)
|
|
93
|
+
video.pause();
|
|
94
|
+
}, (error) => {
|
|
95
|
+
if (!paused && current === video && !context.signal.aborted)
|
|
96
|
+
context.reportError(error instanceof Error ? error : new Error(String(error)));
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
for (const video of videos.values()) {
|
|
100
|
+
const failed = () => {
|
|
101
|
+
if (current === video)
|
|
102
|
+
context.reportError(new Error("Avatar video playback failed"));
|
|
103
|
+
};
|
|
104
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
105
|
+
video.addEventListener("error", failed);
|
|
106
|
+
}), () => Effect.sync(() => {
|
|
107
|
+
video.removeEventListener("error", failed);
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
idle.style.display = "block";
|
|
111
|
+
context.target.replaceChildren(idle);
|
|
112
|
+
return {
|
|
113
|
+
activities: Object.keys(data.clips),
|
|
114
|
+
expressions: [],
|
|
115
|
+
set(presentation) {
|
|
116
|
+
const reference = data.clips[presentation.activity];
|
|
117
|
+
const next = reference ? videos.get(reference) : undefined;
|
|
118
|
+
if (!next)
|
|
119
|
+
throw new Error(`Unknown video activity: ${presentation.activity}`);
|
|
120
|
+
if (next === current)
|
|
121
|
+
return;
|
|
122
|
+
current.pause();
|
|
123
|
+
current.style.display = "none";
|
|
124
|
+
current = next;
|
|
125
|
+
current.style.display = "block";
|
|
126
|
+
context.target.replaceChildren(current);
|
|
127
|
+
if (!paused)
|
|
128
|
+
play(current);
|
|
129
|
+
},
|
|
130
|
+
pause(value) {
|
|
131
|
+
if (paused === value)
|
|
132
|
+
return;
|
|
133
|
+
paused = value;
|
|
134
|
+
if (value)
|
|
135
|
+
current.pause();
|
|
136
|
+
else
|
|
137
|
+
play(current);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
export default renderVideo;
|
package/package.json
CHANGED
|
@@ -1,6 +1,96 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/sprite",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"author": "Levi <levi@technomancy.dev>",
|
|
5
|
+
"description": "Typed renderer-bounded presentation for Fungi agents",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./react": {
|
|
14
|
+
"types": "./dist/react.d.ts",
|
|
15
|
+
"import": "./dist/react.js"
|
|
16
|
+
},
|
|
17
|
+
"./video": {
|
|
18
|
+
"types": "./dist/video.d.ts",
|
|
19
|
+
"import": "./dist/video.js"
|
|
20
|
+
},
|
|
21
|
+
"./mycelium": {
|
|
22
|
+
"types": "./dist/mycelium.d.ts",
|
|
23
|
+
"import": "./dist/mycelium.js"
|
|
24
|
+
},
|
|
25
|
+
"./ink": {
|
|
26
|
+
"types": "./dist/ink.d.ts",
|
|
27
|
+
"import": "./dist/ink.js"
|
|
28
|
+
},
|
|
29
|
+
"./avatar": {
|
|
30
|
+
"types": "./dist/avatar.d.ts",
|
|
31
|
+
"import": "./dist/avatar.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"effect": "4.0.2",
|
|
39
|
+
"zod": "4.4.3",
|
|
40
|
+
"@fungi.computer/cairn": "0.0.1"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": "^18.2.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"react": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@ladle/react": "5.1.1",
|
|
52
|
+
"@types/node": "24.13.3",
|
|
53
|
+
"@types/react": "18.3.3",
|
|
54
|
+
"@types/react-dom": "18.3.0",
|
|
55
|
+
"jsdom": "29.0.1",
|
|
56
|
+
"playwright": "1.62.1",
|
|
57
|
+
"react": "18.3.1",
|
|
58
|
+
"react-dom": "18.3.1",
|
|
59
|
+
"typescript": "5.9.3",
|
|
60
|
+
"vitest": "4.1.11"
|
|
61
|
+
},
|
|
62
|
+
"botanical": {
|
|
63
|
+
"role": "package",
|
|
64
|
+
"operations": {
|
|
65
|
+
"lint": "required",
|
|
66
|
+
"typecheck": "required",
|
|
67
|
+
"build": "required",
|
|
68
|
+
"test": "required",
|
|
69
|
+
"docs": "required",
|
|
70
|
+
"pack": "required",
|
|
71
|
+
"smoke": "required",
|
|
72
|
+
"stranger": "required"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"license": "MIT",
|
|
76
|
+
"repository": {
|
|
77
|
+
"type": "git",
|
|
78
|
+
"url": "git+https://github.com/fungi-computer/sprite.git"
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"clean": "node --input-type=module --eval \"import { rm } from 'node:fs/promises'; await rm('dist', { recursive: true, force: true });\"",
|
|
82
|
+
"lint": "eslint src test vitest.config.ts --max-warnings=0 && ../../node_modules/.bin/oxlint --config ../../oxlint.config.ts --deny-warnings src test vitest.config.ts",
|
|
83
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
84
|
+
"build": "pnpm run clean && tsc -p tsconfig.build.json",
|
|
85
|
+
"test": "vitest run --config vitest.config.ts",
|
|
86
|
+
"docs": "tsx ../../tooling/repository/src/docs/cli.ts check --root ../..",
|
|
87
|
+
"docs:write": "tsx ../../tooling/repository/src/docs/cli.ts write --root ../..",
|
|
88
|
+
"pack": "mkdir -p ../../.botanical/sprite-pack && pnpm pack --pack-destination ../../.botanical/sprite-pack",
|
|
89
|
+
"smoke": "node --input-type=module -e \"import('./dist/index.js').then(({ createSprite }) => { if (typeof createSprite !== 'function') process.exit(1); })\"",
|
|
90
|
+
"stranger": "tsx ../../tooling/repository/src/artifacts/cli.ts --repo-root ../.. .",
|
|
91
|
+
"workbench:contract": "node scripts/workbench-contract.mjs",
|
|
92
|
+
"workbench:build": "ladle build",
|
|
93
|
+
"format:check": "node ../../tooling/repository/scripts/format-package-check.mjs",
|
|
94
|
+
"lint:anti-slop": "node ../../tooling/repository/scripts/lint-package-anti-slop.mjs"
|
|
95
|
+
}
|
|
6
96
|
}
|