keyframes 0.1.0__py3-none-any.whl

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.
Files changed (77) hide show
  1. keyframes/__init__.py +1 -0
  2. keyframes/build.py +102 -0
  3. keyframes/check.py +107 -0
  4. keyframes/cli.py +149 -0
  5. keyframes/deck.py +67 -0
  6. keyframes/export.py +106 -0
  7. keyframes/guide/__init__.py +25 -0
  8. keyframes/guide/components/keyframes-code.md +60 -0
  9. keyframes/guide/components/keyframes-excalidraw.md +148 -0
  10. keyframes/guide/components/keyframes-mujoco.md +76 -0
  11. keyframes/guide/components/keyframes-slider.md +65 -0
  12. keyframes/guide/components/keyframes-three.md +81 -0
  13. keyframes/guide/core.md +113 -0
  14. keyframes/patch.py +77 -0
  15. keyframes/render.py +32 -0
  16. keyframes/scaffold/.gitignore +3 -0
  17. keyframes/scaffold/CLAUDE.md +1 -0
  18. keyframes/scaffold/assets/deck.css +1 -0
  19. keyframes/scaffold/assets/figure.svg +1 -0
  20. keyframes/scaffold/deck.toml +10 -0
  21. keyframes/scaffold/references/.gitkeep +0 -0
  22. keyframes/scaffold/slides/01-title.html +4 -0
  23. keyframes/scaffold/slides/02-intro.html +17 -0
  24. keyframes/server.py +204 -0
  25. keyframes/static/components/keyframes-code.js +423 -0
  26. keyframes/static/components/keyframes-excalidraw.js +189 -0
  27. keyframes/static/components/keyframes-mujoco.js +515 -0
  28. keyframes/static/components/keyframes-slider.js +255 -0
  29. keyframes/static/components/keyframes-three.js +224 -0
  30. keyframes/static/editor/editor.css +228 -0
  31. keyframes/static/editor/editor.js +594 -0
  32. keyframes/static/editor/panel.css +127 -0
  33. keyframes/static/editor/panel.js +357 -0
  34. keyframes/static/index.html +23 -0
  35. keyframes/static/runtime/component.js +126 -0
  36. keyframes/static/runtime/live.js +31 -0
  37. keyframes/static/runtime/math.js +33 -0
  38. keyframes/static/runtime/presenter.js +91 -0
  39. keyframes/static/runtime/runtime.css +91 -0
  40. keyframes/static/runtime/runtime.js +215 -0
  41. keyframes/static/runtime/steps.js +15 -0
  42. keyframes/terminal.py +144 -0
  43. keyframes/themes/default/STYLE_GUIDE.md +73 -0
  44. keyframes/themes/default/assets/arc-top.webp +0 -0
  45. keyframes/themes/default/assets/orbit-ring.webp +0 -0
  46. keyframes/themes/default/assets/streaks-top.webp +0 -0
  47. keyframes/themes/default/assets/streaks-wide.webp +0 -0
  48. keyframes/themes/default/assets/sun-top.webp +0 -0
  49. keyframes/themes/default/assets/title-orbit.webp +0 -0
  50. keyframes/themes/default/layouts/content.html +16 -0
  51. keyframes/themes/default/layouts/figure.html +7 -0
  52. keyframes/themes/default/layouts/grid-2.html +19 -0
  53. keyframes/themes/default/layouts/grid-3x2.html +28 -0
  54. keyframes/themes/default/layouts/grid-4.html +44 -0
  55. keyframes/themes/default/layouts/media-side.html +16 -0
  56. keyframes/themes/default/layouts/section-arc.html +4 -0
  57. keyframes/themes/default/layouts/section-streaks.html +4 -0
  58. keyframes/themes/default/layouts/section.html +4 -0
  59. keyframes/themes/default/layouts/split-wide.html +7 -0
  60. keyframes/themes/default/layouts/split.html +13 -0
  61. keyframes/themes/default/layouts/statement.html +4 -0
  62. keyframes/themes/default/layouts/title.html +5 -0
  63. keyframes/themes/default/layouts/two-column.html +5 -0
  64. keyframes/themes/default/theme.css +330 -0
  65. keyframes/themes/minimal/STYLE_GUIDE.md +41 -0
  66. keyframes/themes/minimal/layouts/figure.html +7 -0
  67. keyframes/themes/minimal/layouts/section.html +4 -0
  68. keyframes/themes/minimal/layouts/statement.html +3 -0
  69. keyframes/themes/minimal/layouts/title-body.html +10 -0
  70. keyframes/themes/minimal/layouts/title.html +5 -0
  71. keyframes/themes/minimal/layouts/two-column.html +11 -0
  72. keyframes/themes/minimal/theme.css +196 -0
  73. keyframes/vendor.py +181 -0
  74. keyframes-0.1.0.dist-info/METADATA +119 -0
  75. keyframes-0.1.0.dist-info/RECORD +77 -0
  76. keyframes-0.1.0.dist-info/WHEEL +4 -0
  77. keyframes-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,189 @@
1
+ import { SwComponent } from "../runtime/component.js";
2
+
3
+ const STEP_FRAME = /^step-(\d+)$/i;
4
+ const FRAME_RENDERING = { enabled: true, name: false, outline: false, clip: false };
5
+ const BOUND_TEXT_PADDING = 5;
6
+ const ALIGN = { left: 0, top: 0, center: 0.5, middle: 0.5, right: 1, bottom: 1 };
7
+
8
+ const style = document.createElement("style");
9
+ style.textContent = `
10
+ keyframes-excalidraw { display: block; position: relative; width: 100%; height: 100%; }
11
+ keyframes-excalidraw > svg { position: absolute; inset: 0; width: 100%; height: 100%; transition: opacity 0.35s ease; }
12
+ keyframes-excalidraw > svg.is-hidden { opacity: 0; }
13
+ html.keyframes-print keyframes-excalidraw > svg { transition: none; }
14
+ `;
15
+ document.head.append(style);
16
+
17
+ let excalidraw = null;
18
+ let svgCount = 0;
19
+
20
+ function loadExcalidraw() {
21
+ window.EXCALIDRAW_ASSET_PATH ??= import.meta.resolve("@excalidraw/excalidraw/dist/prod/");
22
+ return (excalidraw ??= import("@excalidraw/excalidraw"));
23
+ }
24
+
25
+ function stepResolver(elements) {
26
+ const byId = new Map(elements.map((el) => [el.id, el]));
27
+ const frameSteps = new Map();
28
+ for (const el of elements) {
29
+ const match = el.type === "frame" && el.name?.match(STEP_FRAME);
30
+ if (match) frameSteps.set(el.id, Number(match[1]));
31
+ }
32
+ const stepOf = (el) => {
33
+ const own = el.customData?.step ?? frameSteps.get(el.id) ?? frameSteps.get(el.frameId);
34
+ if (own != null) return Number(own);
35
+ const container = byId.get(el.containerId);
36
+ return container ? stepOf(container) : 0;
37
+ };
38
+ return stepOf;
39
+ }
40
+
41
+ function textBox({ type, x, y, width, height }) {
42
+ const inset = { ellipse: 1 - Math.SQRT1_2, diamond: 0.5 }[type] ?? 0;
43
+ return {
44
+ x: x + BOUND_TEXT_PADDING + (width * inset) / 2,
45
+ y: y + BOUND_TEXT_PADDING + (height * inset) / 2,
46
+ width: width * (1 - inset) - 2 * BOUND_TEXT_PADDING,
47
+ height: height * (1 - inset) - 2 * BOUND_TEXT_PADDING,
48
+ };
49
+ }
50
+
51
+ function unsizeText(el) {
52
+ const sized = el.width > 0 && el.height > 0;
53
+ return el.type === "text" && !sized ? { ...el, width: undefined, height: undefined } : el;
54
+ }
55
+
56
+ function placeBoundText(elements) {
57
+ const byId = new Map(elements.map((el) => [el.id, el]));
58
+ return elements.map((el) => {
59
+ const container = el.type === "text" && byId.get(el.containerId);
60
+ if (!container || container.type === "arrow") return el;
61
+ const box = textBox(container);
62
+ const x = box.x + (box.width - el.width) * (ALIGN[el.textAlign] ?? 0.5);
63
+ const y = box.y + (box.height - el.height) * (ALIGN[el.verticalAlign] ?? 0.5);
64
+ return { ...el, x, y };
65
+ });
66
+ }
67
+
68
+ function scopeIds(svg) {
69
+ const prefix = `keyframes-excalidraw-${++svgCount}-`;
70
+ const ids = new Set();
71
+ for (const el of svg.querySelectorAll("[id]")) {
72
+ ids.add(el.id);
73
+ el.id = prefix + el.id;
74
+ }
75
+ const scope = (ref, id) => (ids.has(id) ? ref.replace(id, prefix + id) : ref);
76
+ for (const el of svg.querySelectorAll("*")) {
77
+ for (const attr of el.attributes) {
78
+ const value = attr.value.replace(/url\(#([^)]+)\)/g, scope).replace(/^#(.+)$/, scope);
79
+ if (value !== attr.value) attr.value = value;
80
+ }
81
+ }
82
+ }
83
+
84
+ async function loadSvgFonts(svg) {
85
+ const texts = [...svg.querySelectorAll("text")];
86
+ await Promise.all(
87
+ texts.map((t) =>
88
+ document.fonts.load(`${t.getAttribute("font-size")} ${t.getAttribute("font-family")}`, t.textContent),
89
+ ),
90
+ );
91
+ }
92
+
93
+ class SwExcalidraw extends SwComponent {
94
+ #scene = null;
95
+ #layers = [];
96
+
97
+ connectedCallback() {
98
+ if (this.#scene) return;
99
+ this.#scene = this.#fetchScene();
100
+ this.#scene.catch(() => {});
101
+ }
102
+
103
+ prepare() {
104
+ return this.#scene;
105
+ }
106
+
107
+ async init() {
108
+ const [lib, data] = await Promise.all([loadExcalidraw(), this.#scene]);
109
+ const { elements, appState, files } = await this.#restore(lib, data);
110
+ const stepOf = stepResolver(elements);
111
+ const steps = [...new Set(elements.map(stepOf))].sort((a, b) => a - b);
112
+ const background = this.getAttribute("background");
113
+ const exportAppState = {
114
+ ...appState,
115
+ exportWithDarkMode: this.hasAttribute("dark"),
116
+ viewBackgroundColor: background || appState.viewBackgroundColor,
117
+ frameRendering: FRAME_RENDERING,
118
+ };
119
+ const exportPadding = Number(this.getAttribute("padding") ?? 16);
120
+ this.#layers = await Promise.all(
121
+ steps.map(async (n, i) => {
122
+ const svg = await lib.exportToSvg({
123
+ elements: elements.map((el) => (stepOf(el) === n ? el : { ...el, opacity: 0 })),
124
+ appState: { ...exportAppState, exportBackground: background != null && i === 0 },
125
+ files,
126
+ exportPadding,
127
+ });
128
+ scopeIds(svg);
129
+ this.#fitSvg(svg);
130
+ svg.dataset.layer = n;
131
+ return svg;
132
+ }),
133
+ );
134
+ this.replaceChildren(...this.#layers, ...this.querySelectorAll(":scope > [data-step]"));
135
+ }
136
+
137
+ destroy() {
138
+ for (const svg of this.#layers) svg.remove();
139
+ this.#layers = [];
140
+ }
141
+
142
+ step(n) {
143
+ for (const svg of this.#layers) svg.classList.toggle("is-hidden", Number(svg.dataset.layer) > n);
144
+ }
145
+
146
+ async #fetchScene() {
147
+ const response = await fetch(this.resolveUrl(this.getAttribute("src")), { cache: "no-store" });
148
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}: ${response.url}`);
149
+ const scene = await response.json();
150
+ this.#addStepMarkers(scene.elements ?? []);
151
+ return scene;
152
+ }
153
+
154
+ #addStepMarkers(elements) {
155
+ const live = elements.filter((el) => !el.isDeleted);
156
+ const stepOf = stepResolver(live);
157
+ for (const n of new Set(live.map(stepOf))) {
158
+ if (n <= 0) continue;
159
+ const marker = document.createElement("span");
160
+ marker.dataset.step = n;
161
+ marker.hidden = true;
162
+ this.append(marker);
163
+ }
164
+ }
165
+
166
+ async #restore({ exportToSvg, restore }, data) {
167
+ data = { ...data, elements: (data.elements ?? []).map(unsizeText) };
168
+ const draft = restore(data, null, null);
169
+ const probe = await exportToSvg({ ...draft, exportPadding: 0 });
170
+ probe.style.cssText = "position: absolute; visibility: hidden";
171
+ this.append(probe);
172
+ try {
173
+ await loadSvgFonts(probe);
174
+ const scene = restore(data, null, null, { refreshDimensions: true, repairBindings: true });
175
+ return { ...scene, elements: placeBoundText(scene.elements) };
176
+ } finally {
177
+ probe.remove();
178
+ }
179
+ }
180
+
181
+ #fitSvg(svg) {
182
+ svg.removeAttribute("width");
183
+ svg.removeAttribute("height");
184
+ const cover = this.getAttribute("fit") === "cover";
185
+ svg.setAttribute("preserveAspectRatio", cover ? "xMidYMid slice" : "xMidYMid meet");
186
+ }
187
+ }
188
+
189
+ customElements.define("keyframes-excalidraw", SwExcalidraw);
@@ -0,0 +1,515 @@
1
+ import { SwComponent, onResize } from "../runtime/component.js";
2
+
3
+ const MAX_SUBSTEPS = 200;
4
+ const HIDDEN_GROUP = 3;
5
+ const DEFAULT_RGBA = [0.5, 0.5, 0.5, 1];
6
+
7
+ let mujocoModule = null;
8
+ const loadMujoco = () =>
9
+ (mujocoModule ??= import("@mujoco/mujoco").then((m) => m.default()).catch((e) => {
10
+ mujocoModule = null;
11
+ throw e;
12
+ }));
13
+
14
+ const style = document.createElement("style");
15
+ style.textContent = `
16
+ keyframes-mujoco { display: block; position: relative; width: 100%; height: 100%; }
17
+ keyframes-mujoco > canvas, keyframes-mujoco > img { display: block; width: 100%; height: 100%; touch-action: none; }
18
+ `;
19
+ document.head.append(style);
20
+
21
+ const parseVec = (value) => value?.split(",").map(Number);
22
+
23
+ async function fetchOk(url) {
24
+ const response = await fetch(url);
25
+ if (!response.ok) throw new Error(`${response.status} ${url}`);
26
+ return response;
27
+ }
28
+
29
+ export class SwMujoco extends SwComponent {
30
+ #three;
31
+ #mj;
32
+ #model;
33
+ #data;
34
+ #controller = {};
35
+ #renderer;
36
+ #scene;
37
+ #camera;
38
+ #controls;
39
+ #bodies = [];
40
+ #geoms = [];
41
+ #disposables = [];
42
+ #cleanup = [];
43
+ #drag = null;
44
+ #frame = 0;
45
+ #last = 0;
46
+ #lag = 0;
47
+ #timeline = null;
48
+
49
+ get mujoco() {
50
+ return this.#mj;
51
+ }
52
+
53
+ get mjModel() {
54
+ return this.#model;
55
+ }
56
+
57
+ get mjData() {
58
+ return this.#data;
59
+ }
60
+
61
+ async init() {
62
+ const [THREE, { OrbitControls }, { RoomEnvironment }, mujoco] = await Promise.all([
63
+ import("three"),
64
+ import("three/addons/controls/OrbitControls.js"),
65
+ import("three/addons/environments/RoomEnvironment.js"),
66
+ loadMujoco(),
67
+ ]);
68
+ this.#three = THREE;
69
+ this.#mj = mujoco;
70
+ try {
71
+ await this.#loadModel();
72
+ await this.#loadController();
73
+ this.#buildRenderer(RoomEnvironment);
74
+ this.#buildCamera(OrbitControls);
75
+ this.#buildGeoms();
76
+ this.#buildLights();
77
+ this.#bindEvents();
78
+ this.reset();
79
+ } catch (e) {
80
+ this.destroy();
81
+ throw e;
82
+ }
83
+ }
84
+
85
+ async #loadModel() {
86
+ const url = this.resolveUrl(this.getAttribute("model"));
87
+ const assets = (this.getAttribute("assets") || "").split(",").map((s) => s.trim()).filter(Boolean);
88
+ const files = await Promise.all(
89
+ [url, ...assets.map((a) => new URL(a, url).href)].map(async (u) => new Uint8Array(await (await fetchOk(u)).arrayBuffer())),
90
+ );
91
+ const name = url.split("/").pop().split("?")[0];
92
+ const vfs = new this.#mj.MjVFS();
93
+ try {
94
+ vfs.addBuffer(name, files[0]);
95
+ assets.forEach((a, i) => vfs.addBuffer(a, files[i + 1]));
96
+ this.#model = this.#mj.MjModel.from_xml_path(name, vfs);
97
+ } finally {
98
+ vfs.delete();
99
+ }
100
+ this.#data = new this.#mj.MjData(this.#model);
101
+ }
102
+
103
+ async #loadController() {
104
+ const src = this.getAttribute("controller");
105
+ if (!src) return;
106
+ const { default: setup } = await import(this.resolveUrl(src));
107
+ const context = { mujoco: this.#mj, model: this.#model, data: this.#data, container: this, params: this.params };
108
+ this.#controller = (await setup(context)) || {};
109
+ for (const [name, value] of Object.entries(this.params)) this.#controller.onParam?.(name, value);
110
+ }
111
+
112
+ #buildRenderer(RoomEnvironment) {
113
+ const THREE = this.#three;
114
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
115
+ renderer.setPixelRatio(Math.min(this.pixelRatio, 3));
116
+ renderer.shadowMap.enabled = true;
117
+ renderer.shadowMap.type = THREE.PCFShadowMap;
118
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
119
+ this.append(renderer.domElement);
120
+ this.#renderer = renderer;
121
+
122
+ this.#scene = new THREE.Scene();
123
+ const pmrem = new THREE.PMREMGenerator(renderer);
124
+ const room = new RoomEnvironment();
125
+ this.#scene.environment = pmrem.fromScene(room, 0.04).texture;
126
+ this.#scene.environmentIntensity = 0.35;
127
+ this.#disposables.push(this.#scene.environment);
128
+ room.traverse((o) => o.geometry?.dispose());
129
+ pmrem.dispose();
130
+ }
131
+
132
+ #buildCamera(OrbitControls) {
133
+ const THREE = this.#three;
134
+ const { extent, center } = this.#model.stat;
135
+ const target = new THREE.Vector3(...(parseVec(this.getAttribute("target")) ?? center));
136
+ const eye = parseVec(this.getAttribute("camera"));
137
+ this.#camera = new THREE.PerspectiveCamera(40, 1, extent * 0.005, extent * 100);
138
+ this.#camera.up.set(0, 0, 1);
139
+ if (eye) this.#camera.position.set(...eye);
140
+ else this.#camera.position.copy(target).addScaledVector(new THREE.Vector3(1, -1.5, 0.9).normalize(), extent * 1.8);
141
+ this.#controls = new OrbitControls(this.#camera, this.#renderer.domElement);
142
+ this.#controls.target.copy(target);
143
+ this.#controls.enableDamping = true;
144
+ this.#controls.update();
145
+ }
146
+
147
+ #buildLights() {
148
+ const THREE = this.#three;
149
+ const { extent, center } = this.#model.stat;
150
+ const c = new THREE.Vector3(...center);
151
+ const key = new THREE.DirectionalLight(0xffffff, 3);
152
+ key.position.copy(c).add(new THREE.Vector3(-0.6, -1.2, 2.5).multiplyScalar(extent));
153
+ key.target.position.copy(c);
154
+ key.castShadow = true;
155
+ const s = key.shadow;
156
+ s.mapSize.set(2048, 2048);
157
+ s.radius = 4;
158
+ s.bias = -0.0004;
159
+ s.normalBias = extent * 0.002;
160
+ Object.assign(s.camera, { left: -extent, right: extent, top: extent, bottom: -extent, near: 0.01, far: extent * 8 });
161
+ const fill = new THREE.HemisphereLight(0xf4f7ff, 0x9a958c, 0.4);
162
+ fill.position.set(0, 0, 1);
163
+ this.#scene.add(key, key.target, fill);
164
+ }
165
+
166
+ #buildGeoms() {
167
+ const THREE = this.#three;
168
+ const m = this.#model;
169
+ const T = this.#mj.mjtGeom;
170
+ const builders = {
171
+ [T.mjGEOM_PLANE.value]: (s, i) => this.#plane(s, i),
172
+ [T.mjGEOM_SPHERE.value]: (s) => new THREE.SphereGeometry(s[0], 32, 16),
173
+ [T.mjGEOM_ELLIPSOID.value]: (s) => new THREE.SphereGeometry(1, 32, 16).scale(s[0], s[1], s[2]),
174
+ [T.mjGEOM_CAPSULE.value]: (s) => new THREE.CapsuleGeometry(s[0], 2 * s[1], 8, 24).rotateX(Math.PI / 2),
175
+ [T.mjGEOM_CYLINDER.value]: (s) => new THREE.CylinderGeometry(s[0], s[0], 2 * s[1], 32).rotateX(Math.PI / 2),
176
+ [T.mjGEOM_BOX.value]: (s) => new THREE.BoxGeometry(2 * s[0], 2 * s[1], 2 * s[2]),
177
+ [T.mjGEOM_MESH.value]: (s, i) => this.#mesh(m.geom_dataid[i]),
178
+ };
179
+ for (let i = 0; i < m.ngeom; i++) {
180
+ const rgba = this.#geomRgba(i);
181
+ const build = builders[m.geom_type[i]];
182
+ if (!build || m.geom_group[i] >= HIDDEN_GROUP || rgba[3] === 0) continue;
183
+ const built = build(m.geom_size.subarray(3 * i, 3 * i + 3), i);
184
+ const mesh = built.isMesh ? built : new THREE.Mesh(built, this.#material(i, rgba));
185
+ mesh.castShadow = rgba[3] > 0.5 && m.geom_type[i] !== T.mjGEOM_PLANE.value;
186
+ mesh.receiveShadow = true;
187
+ mesh.matrixAutoUpdate = false;
188
+ mesh.userData.body = m.geom_bodyid[i];
189
+ this.#disposables.push(mesh.geometry, mesh.material);
190
+ this.#scene.add(mesh);
191
+ this.#geoms.push([i, mesh]);
192
+ if (m.geom_bodyid[i] > 0) this.#bodies.push(mesh);
193
+ }
194
+ }
195
+
196
+ #geomRgba(i) {
197
+ const m = this.#model;
198
+ const rgba = [...m.geom_rgba.subarray(4 * i, 4 * i + 4)];
199
+ const mat = m.geom_matid[i];
200
+ const isDefault = rgba.every((v, k) => Math.abs(v - DEFAULT_RGBA[k]) < 1e-6);
201
+ return mat >= 0 && isDefault ? [...m.mat_rgba.subarray(4 * mat, 4 * mat + 4)] : rgba;
202
+ }
203
+
204
+ #material(i, [r, g, b, a]) {
205
+ const THREE = this.#three;
206
+ const mat = this.#model.geom_matid[i];
207
+ const shininess = mat >= 0 ? this.#model.mat_shininess[mat] : 0.5;
208
+ return new THREE.MeshStandardMaterial({
209
+ color: new THREE.Color().setRGB(r, g, b, THREE.SRGBColorSpace),
210
+ roughness: 1 - 0.6 * shininess,
211
+ metalness: mat >= 0 ? this.#model.mat_reflectance[mat] : 0,
212
+ transparent: a < 1,
213
+ opacity: a,
214
+ });
215
+ }
216
+
217
+ #plane(size, i) {
218
+ const THREE = this.#three;
219
+ const extent = this.#model.stat.extent;
220
+ const infinite = size[0] <= 0 || size[1] <= 0;
221
+ const sx = infinite ? extent * 6 : size[0];
222
+ const sy = infinite ? extent * 6 : size[1];
223
+ const cell = size[2] > 0 ? size[2] : extent / 10;
224
+ const [r, g, b, a] = this.#geomRgba(i);
225
+ const color = new THREE.Color().setRGB(r, g, b, THREE.SRGBColorSpace);
226
+ const map = this.#canvasTexture(128, (ctx, n) => {
227
+ ctx.fillStyle = `#${color.getHexString()}`;
228
+ ctx.fillRect(0, 0, n, n);
229
+ ctx.fillStyle = `#${color.clone().multiplyScalar(0.86).getHexString()}`;
230
+ ctx.fillRect(0, 0, n / 2, n / 2);
231
+ ctx.fillRect(n / 2, n / 2, n / 2, n / 2);
232
+ });
233
+ map.colorSpace = THREE.SRGBColorSpace;
234
+ map.wrapS = map.wrapT = THREE.RepeatWrapping;
235
+ map.repeat.set(sx / cell, sy / cell);
236
+ map.anisotropy = 8;
237
+ const material = new THREE.MeshStandardMaterial({ map, roughness: 0.9, transparent: infinite || a < 1, opacity: a });
238
+ if (infinite) {
239
+ material.alphaMap = this.#canvasTexture(256, (ctx, n) => {
240
+ const fade = ctx.createRadialGradient(n / 2, n / 2, 0, n / 2, n / 2, n / 2);
241
+ fade.addColorStop(0.25, "#fff");
242
+ fade.addColorStop(1, "#000");
243
+ ctx.fillStyle = fade;
244
+ ctx.fillRect(0, 0, n, n);
245
+ });
246
+ material.depthWrite = false;
247
+ }
248
+ const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2 * sx, 2 * sy), material);
249
+ mesh.renderOrder = -1;
250
+ return mesh;
251
+ }
252
+
253
+ #canvasTexture(n, draw) {
254
+ const canvas = document.createElement("canvas");
255
+ canvas.width = canvas.height = n;
256
+ draw(canvas.getContext("2d"), n);
257
+ const texture = new this.#three.CanvasTexture(canvas);
258
+ this.#disposables.push(texture);
259
+ return texture;
260
+ }
261
+
262
+ #mesh(id) {
263
+ const THREE = this.#three;
264
+ const m = this.#model;
265
+ const vert = m.mesh_vert.subarray(3 * m.mesh_vertadr[id], 3 * (m.mesh_vertadr[id] + m.mesh_vertnum[id]));
266
+ const normal = m.mesh_normal.subarray(3 * m.mesh_normaladr[id], 3 * (m.mesh_normaladr[id] + m.mesh_normalnum[id]));
267
+ const faces = 3 * m.mesh_facenum[id];
268
+ const face = m.mesh_face.subarray(3 * m.mesh_faceadr[id], 3 * m.mesh_faceadr[id] + faces);
269
+ const faceNormal = m.mesh_facenormal.subarray(3 * m.mesh_faceadr[id], 3 * m.mesh_faceadr[id] + faces);
270
+ const position = new Float32Array(3 * faces);
271
+ const normals = new Float32Array(3 * faces);
272
+ for (let k = 0; k < faces; k++) {
273
+ position.set(vert.subarray(3 * face[k], 3 * face[k] + 3), 3 * k);
274
+ normals.set(normal.subarray(3 * faceNormal[k], 3 * faceNormal[k] + 3), 3 * k);
275
+ }
276
+ const geometry = new THREE.BufferGeometry();
277
+ geometry.setAttribute("position", new THREE.BufferAttribute(position, 3));
278
+ geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
279
+ return geometry;
280
+ }
281
+
282
+ #bindEvents() {
283
+ const canvas = this.#renderer.domElement;
284
+ const listen = (target, type, fn, options) => {
285
+ target.addEventListener(type, fn, options);
286
+ this.#cleanup.push(() => target.removeEventListener(type, fn, options));
287
+ };
288
+ listen(this, "pointerdown", (e) => this.#startDrag(e), { capture: true });
289
+ listen(canvas, "pointermove", (e) => this.#moveDrag(e));
290
+ listen(canvas, "pointerup", () => this.#endDrag());
291
+ listen(canvas, "pointercancel", () => this.#endDrag());
292
+ listen(canvas, "dblclick", () => this.reset());
293
+ listen(window.keyframes ?? window, "scale", () => this.#resize());
294
+ this.#cleanup.push(onResize(this, () => this.#resize()));
295
+ this.#resize();
296
+ }
297
+
298
+ #resize() {
299
+ const w = this.clientWidth;
300
+ const h = this.clientHeight;
301
+ if (!w || !h) return;
302
+ this.#renderer.setPixelRatio(Math.min(this.pixelRatio, 3));
303
+ this.#renderer.setSize(w, h, false);
304
+ this.#camera.aspect = w / h;
305
+ this.#camera.updateProjectionMatrix();
306
+ this.#render();
307
+ }
308
+
309
+ #ray(e) {
310
+ const THREE = this.#three;
311
+ const rect = this.#renderer.domElement.getBoundingClientRect();
312
+ const ndc = new THREE.Vector2(((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
313
+ const raycaster = new THREE.Raycaster();
314
+ raycaster.setFromCamera(ndc, this.#camera);
315
+ return raycaster;
316
+ }
317
+
318
+ #startDrag(e) {
319
+ if (!(e.ctrlKey || e.metaKey) || e.button !== 0) return;
320
+ const hit = this.#ray(e).intersectObjects(this.#bodies, false)[0];
321
+ if (!hit) return;
322
+ e.stopPropagation();
323
+ e.preventDefault();
324
+ const THREE = this.#three;
325
+ const body = hit.object.userData.body;
326
+ const pert = new this.#mj.MjvPerturb();
327
+ pert.select = body;
328
+ pert.active = this.#mj.mjtPertBit.mjPERT_TRANSLATE.value;
329
+ pert.localmass = this.#model.body_subtreemass[body];
330
+ pert.localpos.set(this.#toBodyFrame(body, hit.point));
331
+ pert.refselpos.set(hit.point.toArray());
332
+ const normal = this.#camera.getWorldDirection(new THREE.Vector3());
333
+ const line = new THREE.Line(
334
+ new THREE.BufferGeometry().setFromPoints([hit.point, hit.point]),
335
+ new THREE.LineBasicMaterial({ color: 0xf2994a, depthTest: false }),
336
+ );
337
+ line.renderOrder = 10;
338
+ line.frustumCulled = false;
339
+ this.#scene.add(line);
340
+ this.#drag = { body, pert, line, plane: new THREE.Plane().setFromNormalAndCoplanarPoint(normal, hit.point) };
341
+ this.#controls.enabled = false;
342
+ this.#renderer.domElement.setPointerCapture(e.pointerId);
343
+ }
344
+
345
+ #toBodyFrame(body, point) {
346
+ const p = this.#data.xpos.subarray(3 * body, 3 * body + 3);
347
+ const R = this.#data.xmat.subarray(9 * body, 9 * body + 9);
348
+ const d = [point.x - p[0], point.y - p[1], point.z - p[2]];
349
+ return [0, 1, 2].map((i) => R[i] * d[0] + R[3 + i] * d[1] + R[6 + i] * d[2]);
350
+ }
351
+
352
+ #moveDrag(e) {
353
+ if (!this.#drag) return;
354
+ const point = this.#ray(e).ray.intersectPlane(this.#drag.plane, new this.#three.Vector3());
355
+ if (point) this.#drag.pert.refselpos.set(point.toArray());
356
+ }
357
+
358
+ #endDrag() {
359
+ if (!this.#drag) return;
360
+ const { body, pert, line } = this.#drag;
361
+ this.#data.xfrc_applied.fill(0, 6 * body, 6 * body + 6);
362
+ pert.delete();
363
+ line.geometry.dispose();
364
+ line.material.dispose();
365
+ line.removeFromParent();
366
+ this.#drag = null;
367
+ this.#controls.enabled = true;
368
+ }
369
+
370
+ #applyDrag() {
371
+ const { body, pert } = this.#drag;
372
+ this.#data.xfrc_applied.fill(0, 6 * body, 6 * body + 6);
373
+ this.#mj.mjv_applyPerturbForce(this.#model, this.#data, pert);
374
+ }
375
+
376
+ #updateDragLine() {
377
+ const { body, pert, line } = this.#drag;
378
+ const THREE = this.#three;
379
+ const p = this.#data.xpos.subarray(3 * body, 3 * body + 3);
380
+ const R = this.#data.xmat.subarray(9 * body, 9 * body + 9);
381
+ const l = pert.localpos;
382
+ const anchor = new THREE.Vector3(...[0, 1, 2].map((i) => p[i] + R[3 * i] * l[0] + R[3 * i + 1] * l[1] + R[3 * i + 2] * l[2]));
383
+ line.geometry.setFromPoints([anchor, new THREE.Vector3(...pert.refselpos)]);
384
+ }
385
+
386
+ reset() {
387
+ this.#restart();
388
+ if (this.#timeline != null) this.#seekTo(this.#timeline);
389
+ this.#render();
390
+ }
391
+
392
+ seek(t) {
393
+ this.#timeline = t;
394
+ if (this.#data) this.#seekTo(t);
395
+ }
396
+
397
+ paramChanged(name, value) {
398
+ this.#controller.onParam?.(name, value);
399
+ }
400
+
401
+ #restart() {
402
+ const { mujoco, mjModel: model, mjData: data } = this;
403
+ if (model.nkey > 0) mujoco.mj_resetDataKeyframe(model, data, 0);
404
+ else mujoco.mj_resetData(model, data);
405
+ this.#controller.reset?.();
406
+ mujoco.mj_forward(model, data);
407
+ this.#lag = 0;
408
+ }
409
+
410
+ #seekTo(t) {
411
+ const half = this.#model.opt.timestep / 2;
412
+ if (t < this.#data.time - half) this.#restart();
413
+ while (this.#data.time + half < t) this.#stepOnce();
414
+ }
415
+
416
+ #advance(seconds) {
417
+ const dt = this.#model.opt.timestep;
418
+ this.#lag += seconds * Number(this.getAttribute("speed") ?? 1);
419
+ let n = Math.floor(this.#lag / dt);
420
+ if (n > MAX_SUBSTEPS) {
421
+ n = MAX_SUBSTEPS;
422
+ this.#lag = 0;
423
+ } else {
424
+ this.#lag -= n * dt;
425
+ }
426
+ for (let k = 0; k < n; k++) this.#stepOnce();
427
+ }
428
+
429
+ #stepOnce() {
430
+ this.#controller.step?.(this.#data.time);
431
+ if (this.#drag) this.#applyDrag();
432
+ this.#mj.mj_step(this.#model, this.#data);
433
+ }
434
+
435
+ #sync() {
436
+ const xpos = this.#data.geom_xpos;
437
+ const xmat = this.#data.geom_xmat;
438
+ for (const [i, mesh] of this.#geoms) {
439
+ const p = 3 * i;
440
+ const r = 9 * i;
441
+ mesh.matrix.set(
442
+ xmat[r], xmat[r + 1], xmat[r + 2], xpos[p],
443
+ xmat[r + 3], xmat[r + 4], xmat[r + 5], xpos[p + 1],
444
+ xmat[r + 6], xmat[r + 7], xmat[r + 8], xpos[p + 2],
445
+ 0, 0, 0, 1,
446
+ );
447
+ mesh.matrixWorldNeedsUpdate = true;
448
+ }
449
+ if (this.#drag) this.#updateDragLine();
450
+ }
451
+
452
+ #render() {
453
+ if (!this.#renderer) return;
454
+ this.#sync();
455
+ this.#renderer.render(this.#scene, this.#camera);
456
+ }
457
+
458
+ #tick = (now) => {
459
+ this.#frame = requestAnimationFrame(this.#tick);
460
+ const elapsed = Math.min((now - this.#last) / 1000, 0.1);
461
+ this.#last = now;
462
+ if (this.#timeline == null && !this.hasAttribute("paused")) this.#advance(elapsed);
463
+ this.#controls.update();
464
+ this.#render();
465
+ };
466
+
467
+ play() {
468
+ this.#last = performance.now();
469
+ this.#frame = requestAnimationFrame(this.#tick);
470
+ }
471
+
472
+ pause() {
473
+ cancelAnimationFrame(this.#frame);
474
+ this.#endDrag();
475
+ }
476
+
477
+ step(n) {
478
+ this.#controller.onStep?.(n);
479
+ }
480
+
481
+ async snapshot() {
482
+ if (!this.#renderer) return;
483
+ this.pause();
484
+ this.#seekTo(this.#timeline ?? Number(this.getAttribute("snapshot-time") ?? 2));
485
+ this.#render();
486
+ const img = document.createElement("img");
487
+ img.src = this.#renderer.domElement.toDataURL("image/png");
488
+ await img.decode();
489
+ this.#renderer.domElement.replaceWith(img);
490
+ }
491
+
492
+ destroy() {
493
+ cancelAnimationFrame(this.#frame);
494
+ this.#endDrag();
495
+ this.#cleanup.forEach((fn) => fn());
496
+ this.#controller.dispose?.();
497
+ this.#controls?.dispose();
498
+ this.#disposables.forEach((d) => d?.dispose());
499
+ if (this.#renderer) {
500
+ this.#renderer.dispose();
501
+ this.#renderer.forceContextLoss();
502
+ }
503
+ this.replaceChildren();
504
+ this.#data?.delete();
505
+ this.#model?.delete();
506
+ this.#cleanup = [];
507
+ this.#disposables = [];
508
+ this.#geoms = [];
509
+ this.#bodies = [];
510
+ this.#controller = {};
511
+ this.#renderer = this.#scene = this.#camera = this.#controls = this.#data = this.#model = null;
512
+ }
513
+ }
514
+
515
+ customElements.define("keyframes-mujoco", SwMujoco);