Motion as code with an agent|INEMA.CLUB-PRO

Lesson 6 · 15 min

GSAP, D3, Three.js, or Lottie: which library for each piece

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
Four small screens side by side on an editing desk, with a bar chart, a 3D shape, a line icon, and a title.

In this lesson you will

  1. 1The question that picks the library: what kind of motion is it?
  2. 2D3 calculates the shape once; GSAP animates it over time
  3. 3Three.js in HyperFrames: hf-seek and a required data-duration
  4. 4The error the lint catches when the duration is missing

Goal: choose the right library for each piece and render a data chart with D3 and GSAP.

01What kind of motion is it?

Click each option. Several can live together in the same composition.

Click each block in the diagram to see what it does.

Map based on the adapters documented in the HyperFrames skills (GSAP, Lottie, Three.js, Anime.js, CSS, WAAPI, TypeGPU) as of October 19, 2026.

02Data with D3, motion with GSAP

Four bars with sample data. D3 computes the scale and position; the timeline makes the bars grow in sequence.

File · kit/exemplos/a6-dados/index.html
<!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>
Terminal · check, render, and run the checker
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: $?"
Example from the repository
◇  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.

03Three.js tied to the engine's time

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.

File · kit/exemplos/a6-three/index.html
<!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>
Terminal · lint without and with duration, render and check
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: $?"
Example from the repository
## 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.

04Practice

Two trials and the choice for your pieces.

E1 · Animated chart

10 min

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.

E2 · 3D without and with duration

10 min

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.

E3 · Your data

15 min

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.

05Lesson quiz

The explanation appears after you answer.

Question 1 of 3

A sales-by-month chart, with bars growing one after another. Which combination?

06In three sentences

QuestionChoreography, data, 3D, ready-made animation, or repetition: the type picks the library.
CombinationD3 calculates once, GSAP animates; Three.js draws through hf-seek.
DurationWithout a GSAP timeline, the root needs data-duration.

Going further

Previous lessonNext lesson