Loading...
Loading...
Integrate Three.js 3D scenes into Editframe compositions via addFrameTask. Scenes are pure functions of time, fully scrubable, and renderable to MP4. Use when creating 3D animations, WebGL content in compositions, or integrating Three.js with Editframe's timeline system.
npx skill4agent add editframe/skills threejs-compositionsaddFrameTaskEFTimegroup.addFrameTask(({ ownCurrentTimeMs, durationMs }) => {
scene.update(ownCurrentTimeMs, durationMs);
})<canvas>// my-scene.ts
import * as THREE from "three";
export function createMyScene(canvas: HTMLCanvasElement) {
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: true,
preserveDrawingBuffer: true, // REQUIRED for renderToVideo
});
// ... scene setup ...
function update(timeMs: number, durationMs: number) {
// Position everything deterministically based on timeMs
// NO Math.random() for positions (breaks scrubbing)
// NO internal clocks or requestAnimationFrame
renderer.render(scene, camera);
renderer.getContext().finish(); // REQUIRED for renderToVideo
}
function resize(w: number, h: number) {
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
function dispose() { /* clean up GPU resources */ }
return { update, resize, dispose };
}preserveDrawingBuffer: truegl.finish()update(timeMs)Math.random()timeMsrequestAnimationFrameaddFrameTaskuseEffectuseEffect(() => {
if (!isClient) return;
const container = containerRef.current;
if (!container) return;
let scene = null;
const setup = async () => {
const { createMyScene } = await import("./my-scene");
const canvas = container.querySelector("canvas");
const tg = container.querySelector("ef-timegroup");
if (!canvas || !tg) return;
scene = createMyScene(canvas);
const { width, height } = container.getBoundingClientRect();
scene.resize(width, height);
tg.addFrameTask(({ ownCurrentTimeMs, durationMs }) => {
scene.update(ownCurrentTimeMs, durationMs);
});
};
setup();
return () => scene?.dispose();
}, [isClient]);renderToVideouseEffectinitializer// Inside the setup function, AFTER creating the prime scene:
tg.initializer = (instance) => {
if (instance === tg) return; // skip prime, already set up
let cloneScene = null;
instance.addFrameTask(({ ownCurrentTimeMs, durationMs }) => {
if (!cloneScene) {
// Lazy creation (initializer must be <100ms)
const cvs = instance.querySelector("canvas");
if (!cvs) return;
cloneScene = createMyScene(cvs);
const rect = cvs.getBoundingClientRect();
cloneScene.resize(rect.width || cvs.clientWidth || 800,
rect.height || cvs.clientHeight || 500);
}
cloneScene.update(ownCurrentTimeMs, durationMs);
});
};instance === tggetBoundingClientRect()clientWidth<Preview id={rootId} loop>
<Timegroup mode="fixed" duration="14s"
className="relative w-full overflow-hidden"
style={{ aspectRatio: "16/10", background: "#1e2233" }}>
<canvas style={{
position: "absolute", inset: 0,
width: "100%", height: "100%", display: "block",
}} />
{/* HTML overlays on top of the canvas */}
<div style={{ position: "absolute", ... }}>Text labels</div>
</Timegroup>
</Preview>scene.add(new THREE.AmbientLight(0xd0d8f0, 0.9)); // strong ambient
const key = new THREE.DirectionalLight(0xffffff, 1.8); // key light with shadows
const spot = new THREE.SpotLight(0xffffff, 2.0, 25); // specular catch
scene.add(new THREE.PointLight(0x82b1ff, 0.9, 25)); // rim/accentMeshPhysicalMaterialnew THREE.MeshPhysicalMaterial({
color: 0x448aff,
roughness: 0.12, // low = shiny
metalness: 0.15,
clearcoat: 1.0, // glossy lacquer layer
clearcoatRoughness: 0.15,
emissive: new THREE.Color(0x448aff),
emissiveIntensity: 0.1, // self-illumination for dark scenes
transparent: true,
opacity: 0, // start hidden, fade in
});MeshStandardMaterialrenderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.6–1.8; // push brightsize >= 0.08GridHelpercastShadowfunction setOpacity(mesh, opacity) {
mesh.material.opacity = opacity;
mesh.castShadow = opacity > 0.1; // no shadow when nearly invisible
}const CAM_CLOSE = new THREE.Vector3(0, 0.8, 2.8); // hero shot, fills frame
const CAM_WIDE = new THREE.Vector3(0, 3.8, 10); // reveals full scene
const CAM_WIN = new THREE.Vector3(2, 3, 8); // orbits toward payoff
// In update():
const pullBack = easeInOut(progress(timeMs, startMs, endMs));
lerpV3(camPos, CAM_CLOSE, CAM_WIDE, pullBack);const BG = 0x1e2233;
scene.background = new THREE.Color(BG);
scene.fog = new THREE.Fog(BG, 16, 35); // objects fade into background at distance
// Floor with grid
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(50, 35),
new THREE.MeshStandardMaterial({ color: 0x2a2e42, roughness: 0.75, metalness: 0.1 }),
);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -0.7;
floor.receiveShadow = true;
const grid = new THREE.GridHelper(30, 30, 0x3a3f58, 0x3a3f58);
grid.position.y = -0.69;
grid.material.transparent = true;
grid.material.opacity = 0.25;ownCurrentTimeMs_setLocalTimeMspreserveDrawingBuffer: truegl.finish()castShadowsize >= 0.08streamingstreaming: false# Install in telecine (via Docker scripts)
telecine/scripts/npm install three
telecine/scripts/npm install --save-dev @types/threeconst { createMyScene } = await import("./my-scene");