E1 · Animated chart
10 minRun the chart block.
Done when: ◇ Check passed, render: 0 and OK: 6,0 s · 1920×1080 · 30 fps · sem áudio with saída: 0.
Lesson 6 · 15 min
Each type of motion has one library that solves it in a few lines and another that fights you. Text choreography is GSAP; a chart that comes from data is D3 drawing and GSAP animating; a 3D scene is Three.js; a ready-made After Effects animation is Lottie. In this lesson you choose by asking the right question, and you render a chart and a 3D scene.
Start the lesson
Goal: choose the right library for each piece and render a data chart with D3 and GSAP.
Click each option. Several can live together in the same composition.
Map based on the adapters documented in the HyperFrames skills (GSAP, Lottie, Three.js, Anime.js, CSS, WAAPI, TypeGPU) as of October 19, 2026.
Four bars with sample data. D3 computes the scale and position; the timeline makes the bars grow in sequence.
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 1920px; height: 1080px; overflow: hidden; background: #0c0c10; }
#root { width: 100%; height: 100%; position: relative; font-family: ui-sans-serif, system-ui, sans-serif; }
#titulo { position: absolute; left: 200px; top: 120px; color: #f4f4f5; font-size: 72px; font-weight: 700; line-height: 1.1; }
#nota { position: absolute; left: 200px; top: 220px; color: #a1a1aa; font-size: 36px; }
#grafico { position: absolute; left: 200px; top: 320px; }
.rotulo { fill: #a1a1aa; font-size: 34px; }
.valor { fill: #f4f4f5; font-size: 34px; font-weight: 700; }
</style>
</head>
<body>
<div id="root" data-composition-id="main" data-start="0" data-duration="6" data-width="1920" data-height="1080">
<h1 id="titulo">Planos por cena</h1>
<p id="nota">dados de exemplo, para o ensaio</p>
<svg id="grafico" width="1520" height="600"></svg>
</div>
<script>
// D3 computes the shape (scales, positions) once; GSAP animates it over time.
const dados = [
{ cena: "Abertura", planos: 4 },
{ cena: "Conflito", planos: 9 },
{ cena: "Virada", planos: 6 },
{ cena: "Fecho", planos: 3 },
];
const svg = d3.select("#grafico");
const x = d3.scaleLinear().domain([0, 10]).range([0, 1100]);
const y = d3.scaleBand().domain(dados.map((d) => d.cena)).range([0, 560]).padding(0.3);
const linha = svg.selectAll("g").data(dados).join("g").attr("transform", (d) => `translate(0,${y(d.cena)})`);
linha.append("text").attr("class", "rotulo").attr("x", 0).attr("y", y.bandwidth() / 2 + 12).text((d) => d.cena);
linha.append("rect").attr("class", "barra").attr("x", 260).attr("height", y.bandwidth()).attr("width", (d) => x(d.planos))
.attr("fill", (d, i) => (i === 1 ? "#e9b949" : "#3f3f47"));
linha.append("text").attr("class", "valor").attr("x", (d) => 260 + x(d.planos) + 20).attr("y", y.bandwidth() / 2 + 12).text((d) => d.planos);
const tl = gsap.timeline({ paused: true });
tl.fromTo("#titulo", { y: 30, opacity: 0 }, { y: 0, opacity: 1, duration: 0.6, ease: "power3.out" }, 0.2);
tl.fromTo("#nota", { opacity: 0 }, { opacity: 1, duration: 0.4 }, 0.6);
tl.fromTo(".rotulo", { opacity: 0 }, { opacity: 1, duration: 0.4, stagger: 0.1 }, 0.8);
tl.fromTo(".barra", { scaleX: 0, transformOrigin: "0% 50%" }, { scaleX: 1, duration: 0.9, ease: "power2.out", stagger: 0.25 }, 1.2);
tl.fromTo(".valor", { opacity: 0 }, { opacity: 1, duration: 0.3, stagger: 0.25 }, 1.9);
window.__timelines["main"] = tl;
</script>
</body>
</html>K=~/projetos/formacao-cinema-ia-motion-v72/kit
export HYPERFRAMES_NO_TELEMETRY=1 NO_COLOR=1
E=${TMPDIR:-/tmp}/ensaio-motion/a6; rm -rf "$E"; mkdir -p "$E"
cp -r "$K/exemplos/a6-dados" "$E/dados"; cd "$E/dados"
npx -y hyperframes@0.8.176 check 2>&1 | grep -E "Check passed|✗"
npx -y hyperframes@0.8.176 render --quality draft --workers 2 --quiet --output dados.mp4 > render.log 2>&1
echo "render: $?"
python3 "$K/conferir.py" dados.mp4 6 1920x1080; echo "saída: $?"◇ Check passed render: 0 OK: 6,0 s · 1920×1080 · 30 fps · sem áudio saída: 0
Both libraries come from the CDN with a pinned version (gsap@3.14.2, d3@7.9.0): the render fetches them when the page opens, before the first frame.
Computing a position with getBoundingClientRect inside the animation gives different values depending on which frame is requested, because the engine renders in parallel. Compute everything once, at setup, as the example does with the D3 scales.
A spinning shape. The rotation comes from the time of the hf-seek event, never from the clock. The trial removes the duration from the root to show the error, then renders the correct version.
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 1920px; height: 1080px; overflow: hidden; background: #0c0c10; }
#root { width: 100%; height: 100%; position: relative; }
#cena3d { position: absolute; inset: 0; width: 1920px; height: 1080px; }
</style>
</head>
<body>
<div id="root" data-composition-id="main" data-no-timeline data-start="0" data-duration="4" data-width="1920" data-height="1080">
<canvas id="cena3d"></canvas>
</div>
<script type="module">
// Three.js in HyperFrames: the frame comes from the time HyperFrames sends (hf-seek), never from the clock.
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.181.2/+esm";
const canvas = document.getElementById("cena3d");
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(1920, 1080, false);
renderer.setPixelRatio(1);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0c0c10);
const camera = new THREE.PerspectiveCamera(35, 1920 / 1080, 0.1, 100);
camera.position.set(0, 0, 6);
const forma = new THREE.Mesh(
new THREE.IcosahedronGeometry(1.4, 1),
new THREE.MeshStandardMaterial({ color: 0xd4d4d8, roughness: 0.4, flatShading: true })
);
scene.add(forma);
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const luz = new THREE.DirectionalLight(0xffffff, 2);
luz.position.set(3, 4, 5);
scene.add(luz);
function desenha(t) {
forma.rotation.y = t * 0.8;
forma.rotation.x = t * 0.3;
renderer.render(scene, camera);
}
window.addEventListener("hf-seek", (e) => desenha(e.detail.time));
desenha(window.__hfThreeTime || 0);
</script>
</body>
</html>K=~/projetos/formacao-cinema-ia-motion-v72/kit
export HYPERFRAMES_NO_TELEMETRY=1 NO_COLOR=1
E=${TMPDIR:-/tmp}/ensaio-motion/a6; mkdir -p "$E"
rm -rf "$E/three"; cp -r "$K/exemplos/a6-three" "$E/three"; cd "$E/three"
sed -i 's/ data-duration="4"//' index.html # the common mistake: root without a duration
echo "## sem data-duration"; npx -y hyperframes@0.8.176 lint 2>&1 | grep -oE "root_composition_missing_duration_source|[0-9]+ errors?.*"
cp "$K/exemplos/a6-three/index.html" index.html
echo "## com data-duration"; npx -y hyperframes@0.8.176 lint 2>&1 | grep -oE "root_composition_missing_duration_source|[0-9]+ errors?.*"
npx -y hyperframes@0.8.176 render --quality draft --workers 1 --quiet --output three.mp4 > render.log 2>&1
python3 "$K/conferir.py" three.mp4 4 1920x1080; echo "saída: $?"## sem data-duration root_composition_missing_duration_source 1 error(s), 0 warning(s) ## com data-duration 0 errors, 0 warnings OK: 4,0 s · 1920×1080 · 30 fps · sem áudio saída: 0
Without GSAP, the root takes data-no-timeline: in 0.8.176 the lint asks for registration in window.__timelines and accepts this attribute as "I don't have a timeline" (found in the lint code on 10/19/2026). The skill's documentation mentions the attribute for a still composition; here the scene moves through hf-seek, not through a timeline.
Two trials and the choice for your pieces.
Run the chart block.
Done when: ◇ Check passed, render: 0 and OK: 6,0 s · 1920×1080 · 30 fps · sem áudio with saída: 0.
Run the Three.js block.
Done when: Without duration: root_composition_missing_duration_source and 1 error(s); with duration: 0 errors, 0 warnings and OK: 4,0 s · 1920×1080 · 30 fps · sem áudio.
In the copy /tmp/ensaio-motion/a6/dados, replace the dados list with four numbers of your own (and the title), then run check and render again.
Done when: python3 ~/projetos/formacao-cinema-ia-motion-v72/kit/conferir.py dados.mp4 6 1920x1080; echo "saída: $?" → OK: 6,0 s at the start of the line and saída: 0.
The explanation appears after you answer.
Question 1 of 3
A sales-by-month chart, with bars growing one after another. Which combination?
Question 2 of 3
Why does the Three.js composition need data-duration on the root?
Question 3 of 3
Where does the 3D scene get the angle for each frame?
Pick one option. The explanation appears after your attempt.