tools/capture drives a store page's skin viewer in headless Firefox, stepping its clock one tick at a time and taking every tick from ten fixed cameras. It then fits our own rig to the frames by rendering the model (a Python port of LimbBend and PoseApplier) and matching outlines and colours, and writes the result as Emotecraft JSON. It measures pixels only and never reads the page's animation data. The README covers the steps, the checks and the limits. Co-Authored-By: Claude Opus 5.5 <[email protected]>
89 lines
3.2 KiB
JavaScript
89 lines
3.2 KiB
JavaScript
// Injected before the page loads. It puts the page on a clock we step by hand, and uses three.js's
|
|
// devtools hook to find the renderer and the camera it draws with. Only the clock and the camera are
|
|
// touched: the frames are measured from pixels, never from the scene's animation data.
|
|
(() => {
|
|
let now = 0;
|
|
let callbacks = [];
|
|
let nextId = 1;
|
|
|
|
const realDateNow = Date.now.bind(Date);
|
|
const epoch = realDateNow();
|
|
performance.now = () => now;
|
|
Date.now = () => epoch + now;
|
|
window.requestAnimationFrame = (cb) => {
|
|
const id = nextId++;
|
|
callbacks.push({ id, cb });
|
|
return id;
|
|
};
|
|
window.cancelAnimationFrame = (id) => {
|
|
callbacks = callbacks.filter((c) => c.id !== id);
|
|
};
|
|
|
|
// The camera placement to force on every render: a point the camera orbits, its distance, and the
|
|
// yaw/pitch it looks from (degrees; yaw 0 looks at the model's front).
|
|
let view = null;
|
|
let lastFrame = null;
|
|
|
|
const capture = {
|
|
renderers: [],
|
|
camera: null,
|
|
/** Advances the clock by ms and runs one animation frame. Returns the frame as a PNG data URL. */
|
|
step(ms, grab) {
|
|
now += ms;
|
|
lastFrame = null;
|
|
const due = callbacks;
|
|
callbacks = [];
|
|
for (const { cb } of due) {
|
|
try { cb(now); } catch (e) { console.error(e); }
|
|
}
|
|
return grab ? lastFrame : null;
|
|
},
|
|
time() { return now; },
|
|
setView(v) { view = v; },
|
|
camera_info() {
|
|
const cam = capture.camera;
|
|
return cam && { fov: cam.fov, aspect: cam.aspect, near: cam.near, far: cam.far,
|
|
position: cam.position.toArray(), quaternion: cam.quaternion.toArray() };
|
|
},
|
|
};
|
|
window.__capture = capture;
|
|
|
|
function place(camera) {
|
|
const toRad = Math.PI / 180;
|
|
const yaw = view.yaw * toRad;
|
|
const pitch = view.pitch * toRad;
|
|
const [tx, ty, tz] = view.target;
|
|
camera.position.set(
|
|
tx + view.distance * Math.cos(pitch) * Math.sin(yaw),
|
|
ty + view.distance * Math.sin(pitch),
|
|
tz + view.distance * Math.cos(pitch) * Math.cos(yaw));
|
|
camera.up.set(0, 1, 0);
|
|
camera.lookAt(tx, ty, tz);
|
|
if (view.fov) {
|
|
camera.fov = view.fov;
|
|
}
|
|
camera.updateProjectionMatrix();
|
|
camera.updateMatrixWorld(true);
|
|
}
|
|
|
|
const hook = new EventTarget();
|
|
hook.addEventListener('observe', (event) => {
|
|
const obj = event.detail;
|
|
if (obj && obj.isWebGLRenderer && !capture.renderers.includes(obj)) {
|
|
capture.renderers.push(obj);
|
|
const render = obj.render.bind(obj);
|
|
obj.render = (scene, camera) => {
|
|
capture.camera = camera;
|
|
if (view) {
|
|
place(camera);
|
|
}
|
|
const result = render(scene, camera);
|
|
// Read the drawing buffer in the same task as the draw, before the browser clears it.
|
|
lastFrame = obj.domElement.toDataURL('image/png');
|
|
return result;
|
|
};
|
|
}
|
|
});
|
|
window.__THREE_DEVTOOLS__ = hook;
|
|
})();
|