Esta parte da por sabido cómo se cierra un loop y cómo se trabaja. Si algo no suena, está explicado allá, con sus laboratorios.
Loops de motion design, primera parteMarca qué vértice va primero. Si no coincide entre dos formas, el morphing se tuerce.
clic derecho en un vértice › Mask and Shape Path
La gota
Se vuelve un recorrido, con un keyframe por vértice.
Ctrl+C en Trazado › Ctrl+V en Posición
El cardumen
Repite un grupo con un desplazamiento fijo. Las copias cambian juntas.
Contenido › Agregar › Repeater
El cardumen
Separa el tiempo de una capa del de la composición; con un módulo, repite un tramo.
Ctrl+Alt+T
El viaje
Una capa usa la forma de la que tiene encima como ventana.
columna Track Matte › Alpha Matte
El trompo
Offset corre el contenido de una capa y lo hace dar la vuelta; Spherize lo curva como una esfera.
Effect › Distort
El trompo
Huesos, controladores e IK para mover un personaje por piezas. Es gratuito.
Ventana › Duik Ángela
El paseo
Una gota cae, se aplasta en charco, del charco brota un tallo que abre un tulipán, y el tulipán se cierra en la gota del principio.
Es una sola forma de ocho vértices que pasa por seis siluetas.
◆ Apaga «Primer vértice» y mira cómo se tuerceUna sola capa, «Forma», con keyframes de Trazado. Las cinco guías tienen los mismos ocho vértices, en el mismo orden, así que el morphing los lleva uno a uno.
El vértice 1, el más grande, está abajo al centro en las tres formas.
| Frame | Trazado | Qué pasa | Principio |
|---|---|---|---|
| 0 | Gota | Cuelga en lo alto | — |
| 10 | Gota, copia del 0 | Empieza a caer | Easy Ease Out |
| 20 | Caída | Se estira por la velocidad | Smear frame |
| 21 | Salpicadura | Se aplasta contra el suelo | Estirar y encoger |
| 26 | Charco | Se asienta | Acción compl. |
| 34 | Charco, copia del 26 | Espera | — |
| 56 | Brote | Brota el tallo | Transformation |
| 72 | Tulipán | Abre en tres puntas | Transformation |
| 106 | Tulipán, copia del 72 | Florece | — |
| 114 | Brote, copia del 56 | Se cierra | Anticipación |
| 132 | Gota, copia del 0 | El tallo sube y se vuelve gota | El empalme |
01-la-gota.jsx y contar en frames (Ctrl+clic en el tiempo actual).
Se abre «La gota»: 6 s a 24 fps, la gota en lo alto y cinco guías
tenues.// 01 · La gota · Loops de motion design, segunda parte
// Dibuja la gota en lo alto y la deja quieta, sobre un suelo y su sombra.
// La forma vive en una sola capa, «Forma», con el punto de anclaje en el suelo,
// justo debajo de ella. Al lado deja cinco capas guía con las otras formas por
// las que pasa: caída, salpicadura, charco, brote y tulipán. Las seis tienen
// los mismos ocho vértices, en el mismo orden, para que el morphing no se
// tuerza. Las guías no salen en el render.
// No crea keyframes ni expresiones.
// Archivo › Secuencias de comandos › Ejecutar archivo de secuencias de comandos
(function () {
var T = 6; // duración del loop, en segundos
var SUELO = 820; // la línea del suelo
var FONDO = "#F4EEE6", PISO = "#E3D5C3", AZUL = "#3B82F6", GUIA = "#A72BC9", SOMBRA = "#2B2118";
// Cada forma: ocho vértices [x, y, k] relativos al punto de apoyo. Con k = 1
// el vértice es curvo; con k = 0, una esquina. Mismo orden en todas: abajo al
// centro, abajo a la derecha, derecha, arriba a la derecha, arriba, y de
// vuelta por la izquierda.
var FORMAS = {
"Gota": [[0, -498, 1], [44, -516, 1], [62, -560, 1], [40, -614, 1], [0, -690, 0], [-40, -614, 1], [-62, -560, 1], [-44, -516, 1]],
"Caída": [[0, -40, 1], [24, -50, 1], [34, -110, 1], [22, -210, 1], [0, -340, 0], [-22, -210, 1], [-34, -110, 1], [-24, -50, 1]],
"Salpicadura": [[0, 0, 1], [110, -2, 1], [180, -18, 1], [120, -40, 1], [0, -52, 1], [-120, -40, 1], [-180, -18, 1], [-110, -2, 1]],
"Charco": [[0, 0, 1], [80, -2, 1], [130, -12, 1], [90, -28, 1], [0, -34, 1], [-90, -28, 1], [-130, -12, 1], [-80, -2, 1]],
"Brote": [[0, 0, 1], [9, 0, 0], [9, -300, 0], [36, -352, 1], [0, -420, 0], [-36, -352, 1], [-9, -300, 0], [-9, 0, 0]],
"Tulipán": [[0, 0, 1], [9, 0, 0], [9, -300, 0], [84, -470, 0], [0, -428, 0], [-84, -470, 0], [-9, -300, 0], [-9, 0, 0]]
};
app.beginUndoGroup("La gota");
var comp = app.project.items.addComp("La gota", 1080, 1080, 1, T, 24);
comp.bgColor = rgb(FONDO);
function rgb(h) {
return [parseInt(h.substr(1, 2), 16) / 255, parseInt(h.substr(3, 2), 16) / 255, parseInt(h.substr(5, 2), 16) / 255];
}
function tr(capa) { return capa.property("ADBE Transform Group"); }
function nuevoGrupo(capa, nombre) {
var raiz = capa.property("ADBE Root Vectors Group");
var i = raiz.addProperty("ADBE Vector Group").propertyIndex;
raiz.property(i).name = nombre;
return function () { return capa.property("ADBE Root Vectors Group").property(i).property("ADBE Vectors Group"); };
}
// Las tangentes salen de los vértices vecinos: curvas suaves sin dibujarlas a mano
function trazado(puntos) {
var n = puntos.length, v = [], ent = [], sal = [];
for (var i = 0; i < n; i++) {
var a = puntos[(i + n - 1) % n], b = puntos[(i + 1) % n], k = puntos[i][2] * 0.17;
var t = [(b[0] - a[0]) * k, (b[1] - a[1]) * k];
v.push([puntos[i][0], puntos[i][1]]); sal.push(t); ent.push([-t[0], -t[1]]);
}
var s = new Shape(); s.vertices = v; s.inTangents = ent; s.outTangents = sal; s.closed = true;
return s;
}
function capaDeForma(nombre, grupo, puntos) {
var capa = comp.layers.addShape();
capa.name = nombre;
var g = nuevoGrupo(capa, grupo);
g().addProperty("ADBE Vector Shape - Group");
g().property(1).property("ADBE Vector Shape").setValue(trazado(puntos));
tr(capa).property("ADBE Position").setValue([540, SUELO]);
return { capa: capa, g: g };
}
// ── Fondo, suelo y sombra ──
comp.layers.addSolid(rgb(FONDO), "Fondo", 1080, 1080, 1, T);
var suelo = comp.layers.addShape();
suelo.name = "Suelo";
var gs = nuevoGrupo(suelo, "Suelo");
gs().addProperty("ADBE Vector Shape - Rect");
gs().property(1).property("ADBE Vector Rect Size").setValue([1080, 1080 - SUELO]);
gs().property(1).property("ADBE Vector Rect Position").setValue([540, (1080 + SUELO) / 2]);
gs().addProperty("ADBE Vector Graphic - Fill");
gs().property(2).property("ADBE Vector Fill Color").setValue(rgb(PISO));
tr(suelo).property("ADBE Position").setValue([0, 0]);
var sombra = comp.layers.addShape();
sombra.name = "Sombra";
var gb = nuevoGrupo(sombra, "Sombra");
gb().addProperty("ADBE Vector Shape - Ellipse");
gb().property(1).property("ADBE Vector Ellipse Size").setValue([120, 18]);
gb().addProperty("ADBE Vector Graphic - Fill");
gb().property(2).property("ADBE Vector Fill Color").setValue(rgb(SOMBRA));
tr(sombra).property("ADBE Position").setValue([540, SUELO + 2]);
tr(sombra).property("ADBE Opacity").setValue(18);
// ── La forma que se anima, en lo alto ──
var forma = capaDeForma("Forma", "Forma", FORMAS["Gota"]);
forma.g().addProperty("ADBE Vector Graphic - Fill");
forma.g().property(2).property("ADBE Vector Fill Color").setValue(rgb(AZUL));
// ── Las guías: las otras cinco formas, en su sitio ──
var GUIAS = ["Caída", "Salpicadura", "Charco", "Brote", "Tulipán"];
for (var i = GUIAS.length - 1; i >= 0; i--) {
var guia = capaDeForma(GUIAS[i] + " (guía)", "Guía", FORMAS[GUIAS[i]]);
guia.g().addProperty("ADBE Vector Graphic - Stroke");
guia.g().property(2).property("ADBE Vector Stroke Color").setValue(rgb(GUIA));
guia.g().property(2).property("ADBE Vector Stroke Width").setValue(4);
guia.capa.guideLayer = true;
tr(guia.capa).property("ADBE Opacity").setValue(55);
}
comp.openInViewer();
app.endUndoGroup();
})();
El frame 20 dura uno solo y casi no se ve quieto, pero en movimiento se lee como velocidad: es el smear frame. Para revisarlo frame a frame, Re Pág y Av Pág.
#3B82F6 en el 0 y el 34, verde #2E9E6A en el 56, rosa
#E2467F en el 72 y el 106, y azul otra vez en el 132. El del 34 y el del 106 repiten color y
escribirlo no crea keyframe: se ponen con el botón central del navegador de keyframes, a la
izquierda de Color Add or Remove Keyframe at Current Time.
El color cambia con cada forma.Veinticuatro peces siguen a un guía por una ruta en ocho; cada uno mueve la cola y al fondo suben burbujas.
Solo el guía tiene keyframes: los demás leen su recorrido un poco después.
◆ Apaga «Offset & Delay» y mira cómo se amontonanDos peces quietos y la ruta que recorrerá el guía. El cardumen sale de duplicar «Pez 1».
Cada ciclo divide los 12 s del loop. Los clones leen la ruta del guía con un módulo de 6 s, así que su retraso tampoco abre un hueco.
| Pieza | Ciclo | Veces en 12 s |
|---|---|---|
| Ruta del guía | 144 frames · 6 s | 2 |
| Coleteo | 12 frames | 24 |
| Burbujas | 24 frames · 1 s | 12 |
| Cada clon | la ruta, 0.15 s después que el anterior | 2 |
02-el-cardumen.jsx y contar en frames.
Se abre «El cardumen»: 12 s, dos peces y una ruta en ocho, tenue.loopOut("cycle").
Dos vueltas en 12 s, siempre mirando hacia donde nada.// 02 · El cardumen · Loops de motion design, segunda parte
// Dibuja el fondo del mar y dos peces quietos: «Pez guía», que recorrerá la ruta,
// y «Pez 1», el que se clona. Cada pez tiene la cola en su propio grupo, con el
// punto de anclaje donde se une al cuerpo, y mira hacia la derecha.
// También deja la ruta en forma de ocho como capa guía (no sale en el render),
// lista para copiar su trazado en la Posición del guía, y una burbuja.
// No crea keyframes ni expresiones.
// Archivo › Secuencias de comandos › Ejecutar archivo de secuencias de comandos
(function () {
var T = 12; // duración del loop, en segundos
var CENTRO = [540, 500], A = 380, B = 220; // la ruta: x = A cos t, y = B sen 2t
var MAR = "#0B3D6B", LUZ = "#2E86C1", GUIA = "#FFD166";
var CORAL = "#FF6F59", AMBAR = "#FFB347", ALGA = "#2A9D8F", TINTA = "#14213D";
app.beginUndoGroup("El cardumen");
var comp = app.project.items.addComp("El cardumen", 1080, 1080, 1, T, 24);
comp.bgColor = rgb(MAR);
function rgb(h) {
return [parseInt(h.substr(1, 2), 16) / 255, parseInt(h.substr(3, 2), 16) / 255, parseInt(h.substr(5, 2), 16) / 255];
}
function tr(capa) { return capa.property("ADBE Transform Group"); }
function nuevoGrupo(capa, nombre) {
var raiz = capa.property("ADBE Root Vectors Group");
var i = raiz.addProperty("ADBE Vector Group").propertyIndex;
raiz.property(i).name = nombre;
return function () { return capa.property("ADBE Root Vectors Group").property(i).property("ADBE Vectors Group"); };
}
function transformacion(g) { return g().parentProperty.property("ADBE Vector Transform Group"); }
function forma(v, ent, sal, cerrada) {
var s = new Shape(); s.vertices = v; s.inTangents = ent; s.outTangents = sal; s.closed = cerrada; return s;
}
function sinTangentes(n) { var t = []; for (var i = 0; i < n; i++) t.push([0, 0]); return t; }
function relleno(g, color, opacidad) {
g().addProperty("ADBE Vector Graphic - Fill");
var f = g().property(g().numProperties);
f.property("ADBE Vector Fill Color").setValue(rgb(color));
if (opacidad != null) f.property("ADBE Vector Fill Opacity").setValue(opacidad);
}
function trazo(g, color, grosor, opacidad) {
g().addProperty("ADBE Vector Graphic - Stroke");
var t = g().property(g().numProperties);
t.property("ADBE Vector Stroke Color").setValue(rgb(color));
t.property("ADBE Vector Stroke Width").setValue(grosor);
t.property("ADBE Vector Stroke Line Cap").setValue(2);
if (opacidad != null) t.property("ADBE Vector Stroke Opacity").setValue(opacidad);
}
function elipse(capa, nombre, centro, tam, color, opacidad) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Ellipse");
g().property(1).property("ADBE Vector Ellipse Size").setValue(tam);
g().property(1).property("ADBE Vector Ellipse Position").setValue(centro);
relleno(g, color, opacidad);
return g;
}
function poligono(capa, nombre, puntos, color, opacidad) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Group");
g().property(1).property("ADBE Vector Shape").setValue(forma(puntos, sinTangentes(puntos.length), sinTangentes(puntos.length), true));
relleno(g, color, opacidad);
return g;
}
// una capa de forma que dibuja en coordenadas de la composición
function pieza(nombre) {
var capa = comp.layers.addShape();
capa.name = nombre;
tr(capa).property("ADBE Position").setValue([0, 0]);
return capa;
}
// ── El mar ──
comp.layers.addSolid(rgb(MAR), "Fondo", 1080, 1080, 1, T);
var luz = pieza("Luz");
elipse(luz, "Luz", [540, -100], [1400, 760], LUZ, 35);
var rayos = pieza("Rayos");
poligono(rayos, "Rayo 1", [[180, 0], [300, 0], [520, 1080], [380, 1080]], "#FFFFFF", 6);
poligono(rayos, "Rayo 2", [[520, 0], [600, 0], [760, 1080], [650, 1080]], "#FFFFFF", 5);
poligono(rayos, "Rayo 3", [[820, 0], [900, 0], [1040, 1080], [960, 1080]], "#FFFFFF", 4);
var algas = pieza("Algas");
var TALLOS = [[[110, 1090], [128, 960], [104, 860]], [[180, 1090], [196, 990], [176, 900]], [[930, 1090], [948, 950], [926, 840]]];
for (var a = 0; a < TALLOS.length; a++) {
var ga = nuevoGrupo(algas, "Alga " + (a + 1));
ga().addProperty("ADBE Vector Shape - Group");
ga().property(1).property("ADBE Vector Shape").setValue(forma(TALLOS[a], [[0, 0], [0, 40], [0, 30]], [[0, -40], [0, -30], [0, 0]], false));
trazo(ga, ALGA, 16);
}
// ── Una burbuja: el Repeater hará las demás ──
var burbujas = comp.layers.addShape();
burbujas.name = "Burbujas";
var gb = nuevoGrupo(burbujas, "Burbuja");
gb().addProperty("ADBE Vector Shape - Ellipse");
gb().property(1).property("ADBE Vector Ellipse Size").setValue([22, 22]);
trazo(gb, "#FFFFFF", 3, 70);
tr(burbujas).property("ADBE Position").setValue([300, 1010]);
// ── La ruta en ocho, como guía: x = A cos t, y = B sen 2t, en ocho vértices ──
var ruta = pieza("Ruta (guía)");
var v = [], ent = [], sal = [], paso = Math.PI / 4, h = paso / 3;
for (var k = 0; k < 8; k++) {
var t = k * paso;
v.push([CENTRO[0] + A * Math.cos(t), CENTRO[1] + B * Math.sin(2 * t)]);
var d = [-A * Math.sin(t) * h, 2 * B * Math.cos(2 * t) * h];
sal.push(d); ent.push([-d[0], -d[1]]);
}
var gr = nuevoGrupo(ruta, "Ruta");
gr().addProperty("ADBE Vector Shape - Group");
gr().property(1).property("ADBE Vector Shape").setValue(forma(v, ent, sal, true));
trazo(gr, GUIA, 3, 80);
ruta.guideLayer = true;
tr(ruta).property("ADBE Opacity").setValue(60);
// ── Los peces: el cuerpo mira a la derecha; la cola gira desde donde se une ──
function pez(nombre, color, pos) {
var capa = comp.layers.addShape();
capa.name = nombre;
elipse(capa, "Pupila", [35, -8], [6, 6], TINTA);
elipse(capa, "Ojo", [33, -8], [14, 14], "#FFFFFF");
poligono(capa, "Aleta", [[-4, -24], [-34, -44], [-26, -20]], color);
elipse(capa, "Cuerpo", [0, 0], [124, 58], color);
var cola = poligono(capa, "Cola", [[-48, 0], [-96, -32], [-88, 0], [-96, 32]], color);
transformacion(cola).property("ADBE Vector Anchor").setValue([-48, 0]);
transformacion(cola).property("ADBE Vector Position").setValue([-48, 0]);
tr(capa).property("ADBE Position").setValue(pos);
return capa;
}
pez("Pez 1", AMBAR, [700, 360]);
pez("Pez guía", CORAL, v[0]);
comp.openInViewer();
app.endUndoGroup();
})();
Pegar un trazado en Posición crea un keyframe por vértice. La ruta está cerrada, así que el último keyframe vuelve al primer punto y loopOut repite la vuelta sin salto.
loopOut("cycle"). Lo mismo en la cola del
guía.
Las dos colas aletean dos veces por segundo.// Posición: sigue al guía n × 0.15 s después, un poco a un lado
var n = parseInt(thisLayer.name.split(" ")[1]); // «Pez 7» → 7
var g = thisComp.layer("Pez guía").transform.position;
var t = (time - n * 0.15 + 6) % 6; // la ruta dura 6 s
var v = g.velocityAtTime(t), l = length(v);
seedRandom(n, true);
add(g.valueAtTime(t), mul([-v[1] / l, v[0] / l], random(-70, 70)))
// Rotación: hacia donde se mueve el guía en ese instante
var n = parseInt(thisLayer.name.split(" ")[1]);
var v = thisComp.layer("Pez guía").transform.position.velocityAtTime((time - n * 0.15 + 6) % 6);
radiansToDegrees(Math.atan2(v[1], v[0]))
Con 24 peces, poner keyframes a mano sería repetir la ruta 23 veces. El módulo hace que los primeros peces, al empezar el loop, lean el final de la vuelta anterior.
loopOut("cycle").
Cada segundo una burbuja ocupa el lugar de la siguiente: suben sin
salto.valueAtTime((time + parseInt(thisLayer.name.split(" ")[1]) * 0.07) % 0.5) y copiar
la expresión de «Pez 2» a «Pez 23». El guía conserva loopOut: su nombre no lleva número.
Cada pez aletea a su ritmo.seedRandom(parseInt(thisLayer.name.split(" ")[1]), true); var s = random(70, 105); [s, s],
y copiarla de «Pez 2» a «Pez 23».
Peces chicos y grandes: el cardumen gana profundidad.Un tren cruza un paisaje de seis planos al atardecer: cuanto más cerca está un plano, más rápido corre.
El tren no avanza; el que se mueve es el paisaje.
◆ Apaga «Parallax» y mira cómo se aplanaEl script dibuja cada plano como un patrón repetido varias veces seguidas. Si un plano avanza exactamente un patrón, el dibujo vuelve a ser idéntico y el salto no se ve; loopOut lo repite sin fin.
El cielo y el sol no se mueven. La velocidad crece con la cercanía, y eso es lo que la vista lee como profundidad.
| Plano | Patrón | Velocidad | Un patrón en | Posición |
|---|---|---|---|---|
| Montañas | 540 px | 90 px/s | 144 frames | 0, 0 → −540, 0 |
| Colinas | 540 px | 180 px/s | 72 frames | 0, 0 → −540, 0 |
| Árboles | 360 px | 360 px/s | 24 frames | 0, 0 → −360, 0 |
| Durmientes | 120 px | 720 px/s | 4 frames | 0, 0 → −120, 0 |
| Postes | 720 px | 1440 px/s | 12 frames | 0, 0 → −720, 0 |
| Cielo y sol | — | quietos | — | — |
Cada «un patrón en» divide los 288 frames del loop: el empalme cierra en todos los planos.
03-el-viaje.jsx y contar en frames.
Se abre «El viaje»: 12 s, el tren detenido frente al paisaje.loopOut("cycle").
En el 144 el dibujo es idéntico al del 0: el salto no se nota.// 03 · El viaje · Loops de motion design, segunda parte
// Dibuja un tren detenido frente a un paisaje de planos, al atardecer. Cada plano
// que se moverá es un patrón que se repite cada W píxeles, dibujado las veces
// necesarias para cubrir la pantalla y un patrón más: montañas y colinas cada
// 540 px, árboles cada 360, durmientes cada 120 y postes cada 720. Todos están
// en coordenadas de la composición, con la Posición en 0, 0.
// Las seis ruedas tienen el punto de anclaje en su centro. El humo es una
// precomposición, «Bocanada», que dura lo mismo que el loop: la bocanada se anima en
// su primer segundo y Time Remapping la repite. Está puesta cuatro veces.
// No crea keyframes ni expresiones.
// Archivo › Secuencias de comandos › Ejecutar archivo de secuencias de comandos
(function () {
var T = 12; // duración del loop, en segundos
var CIELO = "#FFD6A5", SOL = "#FF9F68", MONTE = "#C8A2C8", COLINA = "#9A8BC6", ARBOL = "#4E6E9A";
var TIERRA = "#3D405B", RIEL = "#8D99AE", DURMIENTE = "#2B2D42", POSTE = "#22223B";
var ROJO = "#E63946", ROJO_2 = "#C1121F", AZUL = "#457B9D", MARINO = "#1D3557", HUMO = "#F1FAEE";
app.beginUndoGroup("El viaje");
function rgb(h) {
return [parseInt(h.substr(1, 2), 16) / 255, parseInt(h.substr(3, 2), 16) / 255, parseInt(h.substr(5, 2), 16) / 255];
}
function tr(capa) { return capa.property("ADBE Transform Group"); }
function nuevoGrupo(capa, nombre) {
var raiz = capa.property("ADBE Root Vectors Group");
var i = raiz.addProperty("ADBE Vector Group").propertyIndex;
raiz.property(i).name = nombre;
return function () { return capa.property("ADBE Root Vectors Group").property(i).property("ADBE Vectors Group"); };
}
function relleno(g, color, opacidad) {
g().addProperty("ADBE Vector Graphic - Fill");
var f = g().property(g().numProperties);
f.property("ADBE Vector Fill Color").setValue(rgb(color));
if (opacidad != null) f.property("ADBE Vector Fill Opacity").setValue(opacidad);
}
function trazo(g, color, grosor) {
g().addProperty("ADBE Vector Graphic - Stroke");
var t = g().property(g().numProperties);
t.property("ADBE Vector Stroke Color").setValue(rgb(color));
t.property("ADBE Vector Stroke Width").setValue(grosor);
t.property("ADBE Vector Stroke Line Cap").setValue(2);
}
function rect(capa, nombre, x0, y0, x1, y1, color, redondeo, opacidad) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Rect");
g().property(1).property("ADBE Vector Rect Size").setValue([x1 - x0, y1 - y0]);
g().property(1).property("ADBE Vector Rect Position").setValue([(x0 + x1) / 2, (y0 + y1) / 2]);
if (redondeo) g().property(1).property("ADBE Vector Rect Roundness").setValue(redondeo);
relleno(g, color, opacidad);
return g;
}
function elipse(capa, nombre, cx, cy, w, h, color, opacidad) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Ellipse");
g().property(1).property("ADBE Vector Ellipse Size").setValue([w, h]);
g().property(1).property("ADBE Vector Ellipse Position").setValue([cx, cy]);
relleno(g, color, opacidad);
return g;
}
function poligono(capa, nombre, puntos, color) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Group");
var s = new Shape(); s.vertices = puntos; s.closed = true;
g().property(1).property("ADBE Vector Shape").setValue(s);
relleno(g, color);
return g;
}
function linea(capa, nombre, a, b, color, grosor) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Group");
var s = new Shape(); s.vertices = [a, b]; s.closed = false;
g().property(1).property("ADBE Vector Shape").setValue(s);
trazo(g, color, grosor);
return g;
}
function pieza(comp, nombre) {
var capa = comp.layers.addShape();
capa.name = nombre;
tr(capa).property("ADBE Position").setValue([0, 0]);
return capa;
}
// cuántas copias de un patrón de ancho W cubren la pantalla y un patrón más
function copias(W) { return Math.ceil((1080 + W) / W); }
// ── La bocanada: se anima en su primer segundo y Time Remapping la repite ──
var bocanada = app.project.items.addComp("Bocanada", 1080, 1080, 1, T, 24);
var humo = bocanada.layers.addShape();
humo.name = "Humo";
elipse(humo, "Humo", 0, 0, 60, 60, HUMO, 85);
tr(humo).property("ADBE Position").setValue([785, 572]);
var comp = app.project.items.addComp("El viaje", 1080, 1080, 1, T, 24);
comp.bgColor = rgb(CIELO);
// ── Cielo y sol ──
comp.layers.addSolid(rgb(CIELO), "Cielo", 1080, 1080, 1, T);
var sol = pieza(comp, "Sol");
elipse(sol, "Halo", 790, 330, 290, 290, SOL, 30);
elipse(sol, "Sol", 790, 330, 180, 180, SOL);
// ── Los planos, del más lejano al más cercano ──
var montes = pieza(comp, "Montañas");
for (var i = 0; i < copias(540); i++) {
var x = i * 540;
poligono(montes, "Montañas " + (i + 1), [[x, 650], [x + 120, 470], [x + 230, 560], [x + 360, 420], [x + 540, 650]], MONTE);
}
var colinas = pieza(comp, "Colinas");
rect(colinas, "Base", 0, 700, 540 * copias(540), 820, COLINA);
for (i = 0; i < copias(540); i++) {
x = i * 540;
elipse(colinas, "Colina " + (2 * i + 1), x + 130, 705, 420, 220, COLINA);
elipse(colinas, "Colina " + (2 * i + 2), x + 400, 725, 380, 190, COLINA);
}
var arboles = pieza(comp, "Árboles");
for (i = 0; i < copias(360); i++) {
x = i * 360;
rect(arboles, "Tronco " + (2 * i + 1), x + 82, 690, x + 98, 790, ARBOL);
elipse(arboles, "Copa " + (2 * i + 1), x + 90, 662, 92, 92, ARBOL);
rect(arboles, "Tronco " + (2 * i + 2), x + 254, 710, x + 268, 790, ARBOL);
elipse(arboles, "Copa " + (2 * i + 2), x + 261, 690, 70, 70, ARBOL);
}
// ── El terraplén y la vía ──
var tierra = pieza(comp, "Terraplén");
rect(tierra, "Terraplén", 0, 790, 1080, 1080, TIERRA);
var riel = pieza(comp, "Riel");
linea(riel, "Riel", [0, 800], [1080, 800], RIEL, 6);
var durmientes = pieza(comp, "Durmientes");
for (i = 0; i < copias(120); i++) rect(durmientes, "Durmiente " + (i + 1), i * 120 + 20, 806, i * 120 + 64, 822, DURMIENTE);
// ── El tren ──
var vagon2 = pieza(comp, "Vagón 2");
rect(vagon2, "Ventanas", 166, 680, 324, 712, CIELO, 4);
rect(vagon2, "Caja", 140, 660, 350, 770, MARINO, 10);
var vagon1 = pieza(comp, "Vagón 1");
linea(vagon1, "Enganche", [350, 750], [370, 750], DURMIENTE, 6);
rect(vagon1, "Ventanas", 396, 680, 554, 712, CIELO, 4);
rect(vagon1, "Caja", 370, 660, 580, 770, AZUL, 10);
var loco = pieza(comp, "Locomotora");
linea(loco, "Enganche", [580, 750], [600, 750], DURMIENTE, 6);
poligono(loco, "Quitapiedras", [[820, 730], [858, 778], [820, 778]], DURMIENTE);
rect(loco, "Ventana", 615, 595, 675, 630, CIELO, 4);
rect(loco, "Chimenea", 770, 590, 800, 642, DURMIENTE, 3);
rect(loco, "Cabina", 600, 580, 690, 642, ROJO_2, 6);
rect(loco, "Caldera", 600, 640, 820, 770, ROJO, 10);
var CENTROS = [640, 780, 410, 540, 180, 310];
for (i = 0; i < CENTROS.length; i++) {
var rueda = comp.layers.addShape();
rueda.name = "Rueda " + (i + 1);
elipse(rueda, "Eje", 0, 0, 12, 12, HUMO);
linea(rueda, "Rayo 1", [-24, 0], [24, 0], HUMO, 4);
linea(rueda, "Rayo 2", [0, -24], [0, 24], HUMO, 4);
elipse(rueda, "Rueda", 0, 0, 60, 60, MARINO);
tr(rueda).property("ADBE Position").setValue([CENTROS[i], 768]);
}
// ── Cuatro bocanadas, todas en el mismo punto hasta que se desfasen ──
for (i = 0; i < 4; i++) {
var capa = comp.layers.add(bocanada);
capa.name = "Bocanada " + (i + 1);
}
// ── Los postes, en primer plano ──
var postes = pieza(comp, "Postes");
for (i = 0; i < copias(720); i++) {
x = i * 720;
rect(postes, "Travesaño " + (i + 1), x + 262, 404, x + 358, 418, POSTE, 2);
rect(postes, "Poste " + (i + 1), x + 301, 380, x + 319, 1080, POSTE);
}
comp.openInViewer();
app.endUndoGroup();
})();
Aquí loopOut hace el trabajo del módulo: repite el tramo de un patrón y, como el dibujo se repite igual, el corte cae justo donde un patrón termina y empieza el siguiente.
loopOut("cycle") en «Rueda 1», copiar la expresión a las otras cinco.
Una vuelta cada 6 frames.loopOut("cycle"). «Vagón 1», al revés: 0, 3; 0, 0; 0, 3.
Los vagones saltan a destiempo, uno sí y uno no.// Repite el primer segundo de la precomposición;
// cada capa, 0.25 s después de la anterior
var n = parseInt(thisLayer.name.split(" ")[1]);
(time + n * 0.25) % 1
Time Remapping dice qué segundo de la precomposición se ve en cada momento. Con el módulo, siempre es uno entre 0 y 1: la bocanada se repite sin keyframes nuevos.
Un trompo de colores gira sobre la punta y se tambalea. Es plano: el volumen sale de cómo corren las franjas, de una luz que no gira y de su sombra.
Ni una capa 3D.
◆ Apaga «Curvatura» y mira cómo se aplanaEl trompo quieto, por capas, en el orden en que tienen que quedar para el mate.
| Pista | Qué hace el ojo | Con qué |
|---|---|---|
| Las franjas corren de lado | Lee un giro | Offset |
| Se aprietan en los bordes | Lee un cuerpo redondo | Spherize |
| La luz se queda quieta | Lee que gira el cuerpo, no el dibujo | Brillo y sombra propia |
Las franjas recorren su patrón de 320 px cada 8 frames: 24 vueltas en los 192 del loop. El tambaleo dura 48 frames y cabe cuatro veces.
04-el-trompo.jsx y contar en frames.
Se abre «El trompo»: 8 s; las franjas asoman como un rectángulo detrás del
cuerpo blanco.loopOut("cycle").
24 vueltas en 8 s, sin salto.// 04 · El trompo · Loops de motion design, segunda parte
// Dibuja un trompo de colores parado sobre la punta, todavía sin girar.
// Las franjas viven en su propia precomposición, «Franjas», de 320 × 400: ocho
// franjas verticales de 40 px, así que el patrón se repite cada 320 px y el
// efecto Offset puede correrlo sin que se note la unión. Justo encima está la
// «Silueta», que servirá de mate. La luz y la sombra propia ya están dibujadas
// encima, y la sombra en el piso, debajo.
// No crea keyframes, expresiones, mates ni efectos.
// Archivo › Secuencias de comandos › Ejecutar archivo de secuencias de comandos
(function () {
var T = 8; // duración del loop, en segundos
var PUNTA = [540, 842]; // donde el trompo toca el piso
var FONDO = "#FFF1DC", PISO = "#F3D9B1", MADERA = "#B07A4A", VETA = "#8B5A2B", METAL = "#8A8A8A", TINTA = "#2B1B10";
var COLORES = ["#D7263D", "#F4D35E", "#1B998B", "#2E86AB"];
// La silueta del cuerpo: [x, y, k], k = 1 curva y k = 0 esquina
var CUERPO = [[540, 800, 0], [600, 720, 1], [690, 590, 1], [660, 470, 1], [540, 430, 1], [420, 470, 1], [390, 590, 1], [480, 720, 1]];
// La sombra propia: media luna del lado derecho, dentro del cuerpo
var MEDIA_LUNA = [[540, 432, 0], [660, 470, 1], [690, 590, 1], [600, 720, 1], [540, 800, 0], [588, 700, 1], [624, 590, 1], [604, 482, 1]];
app.beginUndoGroup("El trompo");
function rgb(h) {
return [parseInt(h.substr(1, 2), 16) / 255, parseInt(h.substr(3, 2), 16) / 255, parseInt(h.substr(5, 2), 16) / 255];
}
function tr(capa) { return capa.property("ADBE Transform Group"); }
function nuevoGrupo(capa, nombre) {
var raiz = capa.property("ADBE Root Vectors Group");
var i = raiz.addProperty("ADBE Vector Group").propertyIndex;
raiz.property(i).name = nombre;
return function () { return capa.property("ADBE Root Vectors Group").property(i).property("ADBE Vectors Group"); };
}
function transformacion(g) { return g().parentProperty.property("ADBE Vector Transform Group"); }
function relleno(g, color, opacidad) {
g().addProperty("ADBE Vector Graphic - Fill");
var f = g().property(g().numProperties);
f.property("ADBE Vector Fill Color").setValue(rgb(color));
if (opacidad != null) f.property("ADBE Vector Fill Opacity").setValue(opacidad);
}
function rect(capa, nombre, x0, y0, x1, y1, color, redondeo) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Rect");
g().property(1).property("ADBE Vector Rect Size").setValue([x1 - x0, y1 - y0]);
g().property(1).property("ADBE Vector Rect Position").setValue([(x0 + x1) / 2, (y0 + y1) / 2]);
if (redondeo) g().property(1).property("ADBE Vector Rect Roundness").setValue(redondeo);
relleno(g, color);
return g;
}
function elipse(capa, nombre, cx, cy, w, h, color, opacidad) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Ellipse");
g().property(1).property("ADBE Vector Ellipse Size").setValue([w, h]);
relleno(g, color, opacidad);
transformacion(g).property("ADBE Vector Position").setValue([cx, cy]);
return g;
}
// Las tangentes salen de los vértices vecinos
function trazado(puntos) {
var n = puntos.length, v = [], ent = [], sal = [];
for (var i = 0; i < n; i++) {
var a = puntos[(i + n - 1) % n], b = puntos[(i + 1) % n], k = puntos[i][2] * 0.17;
var t = [(b[0] - a[0]) * k, (b[1] - a[1]) * k];
v.push([puntos[i][0], puntos[i][1]]); sal.push(t); ent.push([-t[0], -t[1]]);
}
var s = new Shape(); s.vertices = v; s.inTangents = ent; s.outTangents = sal; s.closed = true;
return s;
}
function figura(capa, nombre, puntos, color, opacidad) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Group");
g().property(1).property("ADBE Vector Shape").setValue(trazado(puntos));
relleno(g, color, opacidad);
return g;
}
function pieza(comp, nombre) {
var capa = comp.layers.addShape();
capa.name = nombre;
tr(capa).property("ADBE Position").setValue([0, 0]);
return capa;
}
// ── Las franjas: 320 × 400, el patrón se repite cada 320 px ──
var franjas = app.project.items.addComp("Franjas", 320, 400, 1, T, 24);
var capaFranjas = pieza(franjas, "Franjas");
rect(capaFranjas, "Veta alta", 0, 140, 320, 164, VETA);
rect(capaFranjas, "Veta baja", 0, 292, 320, 310, VETA);
for (var i = 0; i < 8; i++) rect(capaFranjas, "Franja " + (i + 1), i * 40, 0, i * 40 + 40, 400, COLORES[i % 4]);
var comp = app.project.items.addComp("El trompo", 1080, 1080, 1, T, 24);
comp.bgColor = rgb(FONDO);
comp.layers.addSolid(rgb(FONDO), "Fondo", 1080, 1080, 1, T);
var piso = pieza(comp, "Piso");
rect(piso, "Piso", 0, PUNTA[1], 1080, 1080, PISO);
var sombra = comp.layers.addShape();
sombra.name = "Sombra";
elipse(sombra, "Sombra", 0, 0, 220, 34, TINTA, 22);
tr(sombra).property("ADBE Position").setValue([PUNTA[0] + 16, PUNTA[1] + 4]);
var punta = pieza(comp, "Punta");
var gp = nuevoGrupo(punta, "Punta");
gp().addProperty("ADBE Vector Shape - Group");
var sp = new Shape(); sp.vertices = [[522, 796], [558, 796], [PUNTA[0], PUNTA[1]]]; sp.closed = true;
gp().property(1).property("ADBE Vector Shape").setValue(sp);
relleno(gp, METAL);
// Las franjas en la principal, cubriendo el cuerpo, y la silueta justo encima
var capa = comp.layers.add(franjas);
capa.name = "Franjas";
tr(capa).property("ADBE Position").setValue([540, 615]);
var silueta = pieza(comp, "Silueta");
figura(silueta, "Silueta", CUERPO, "#FFFFFF");
var volumen = pieza(comp, "Sombra propia");
figura(volumen, "Sombra propia", MEDIA_LUNA, TINTA, 28);
var brillo = pieza(comp, "Brillo");
var gb = elipse(brillo, "Brillo", 452, 590, 36, 150, "#FFFFFF", 40);
transformacion(gb).property("ADBE Vector Rotation").setValue(-12);
var cabeza = pieza(comp, "Cabeza");
rect(cabeza, "Cabeza", 522, 392, 558, 436, MADERA, 6);
comp.openInViewer();
app.endUndoGroup();
})();
El orden de los efectos importa: primero se corren las franjas y después se curvan. Al revés, la curva correría con ellas y el trompo se vería plano.
loopOut("cycle").
Se inclina a un lado y al otro, cada segundo, sobre la punta.loopOut("cycle").
La sombra se corre hacia donde se inclina el trompo.Un personaje geométrico camina en su sitio mientras el piso corre bajo sus pies.
Cuatro poses por paso: contacto, bajada, paso y subida. Un ciclo de dos pasos dura 24 frames.
◆ Apaga «Pies plantados» y mira cómo patinaEn el contacto los dos pies tocan el piso; en la bajada el cuerpo cae sobre la pierna de adelante; en el paso la otra pierna cruza por debajo; en la subida el cuerpo se impulsa. Después, lo mismo con el otro pie.
El pie apoyado recorre 240 px en 12 frames, a 480 px/s: la misma velocidad que el piso.
| Frame | Pose | Pie D | Pie I | Cadera |
|---|---|---|---|---|
| 0 | Contacto | 660, 930 | 420, 930 | 540, 620 |
| 3 | Bajada | 600, 930 | 410, 910 | 540, 636 |
| 6 | Paso | 540, 930 | 540, 885 | 540, 618 |
| 9 | Subida | 480, 930 | 610, 905 | 540, 604 |
| 12 | Contacto | 420, 930 | 660, 930 | 540, 620 |
| 15 | Bajada | 410, 910 | 600, 930 | 540, 636 |
| 18 | Paso | 540, 885 | 540, 930 | 540, 618 |
| 21 | Subida | 610, 905 | 480, 930 | 540, 604 |
| 24 | Contacto | 660, 930 | 420, 930 | 540, 620 |
Manos: la del lado contrario al pie de adelante, adelante. Mano D 490, 640 y Mano I 600, 640 en el 0; las dos en 545, 640 en el 6 y el 18; al revés en el 12.
05-el-paseo.jsx y contar en frames.
Se abre «El paseo»: 4 s y el personaje de pie, de perfil.// 05 · El paseo · Loops de motion design, segunda parte
// Dibuja un personaje geométrico de perfil, mirando a la derecha, de pie y
// quieto. Cada pieza es su propia capa y tiene el punto de anclaje en la
// articulación de la que cuelga: el muslo en la cadera, la pierna en la
// rodilla, el pie en el tobillo, el brazo en el hombro, el antebrazo en el codo
// y la mano en la muñeca. Así Link Art de Duik puede unir cada pieza al hueso
// más cercano. «I» es el lado izquierdo, el de atrás; «D», el de adelante.
// Las baldosas del piso se repiten cada 240 px, para correrlas mientras camina.
// No crea keyframes, expresiones ni huesos.
// Archivo › Secuencias de comandos › Ejecutar archivo de secuencias de comandos
(function () {
var T = 4; // duración del loop: cuatro ciclos de 1 s
var SUELO = 960;
var CADERA = [540, 620], RODILLA = [552, 780], TOBILLO = [540, 930];
var HOMBRO = [540, 440], CODO = [546, 540], MUNECA = [550, 630], CUELLO = [540, 410];
var FONDO = "#F3F0FF", PISO = "#E4DEFA", BALDOSA = "#D6CCF7";
var CERCA = "#3A3170", LEJOS = "#241E4E", CUERPO = "#5632DA", PIEL = "#FFC94A", ZAPATO = "#1B1B2F";
app.beginUndoGroup("El paseo");
var comp = app.project.items.addComp("El paseo", 1080, 1080, 1, T, 24);
comp.bgColor = rgb(FONDO);
function rgb(h) {
return [parseInt(h.substr(1, 2), 16) / 255, parseInt(h.substr(3, 2), 16) / 255, parseInt(h.substr(5, 2), 16) / 255];
}
function tr(capa) { return capa.property("ADBE Transform Group"); }
function nuevoGrupo(capa, nombre) {
var raiz = capa.property("ADBE Root Vectors Group");
var i = raiz.addProperty("ADBE Vector Group").propertyIndex;
raiz.property(i).name = nombre;
return function () { return capa.property("ADBE Root Vectors Group").property(i).property("ADBE Vectors Group"); };
}
function relleno(g, color) {
g().addProperty("ADBE Vector Graphic - Fill");
g().property(g().numProperties).property("ADBE Vector Fill Color").setValue(rgb(color));
}
function rect(capa, nombre, x0, y0, x1, y1, color, redondeo) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Rect");
g().property(1).property("ADBE Vector Rect Size").setValue([x1 - x0, y1 - y0]);
g().property(1).property("ADBE Vector Rect Position").setValue([(x0 + x1) / 2, (y0 + y1) / 2]);
if (redondeo) g().property(1).property("ADBE Vector Rect Roundness").setValue(redondeo);
relleno(g, color);
}
function circulo(capa, nombre, cx, cy, d, color) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Ellipse");
g().property(1).property("ADBE Vector Ellipse Size").setValue([d, d]);
g().property(1).property("ADBE Vector Ellipse Position").setValue([cx, cy]);
relleno(g, color);
}
// un segmento del cuerpo: un trazo grueso con puntas redondas, desde el anclaje
function segmento(nombre, junta, fin, grosor, color) {
var capa = comp.layers.addShape();
capa.name = nombre;
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Group");
var s = new Shape(); s.vertices = [[0, 0], fin]; s.closed = false;
g().property(1).property("ADBE Vector Shape").setValue(s);
g().addProperty("ADBE Vector Graphic - Stroke");
var t = g().property(2);
t.property("ADBE Vector Stroke Color").setValue(rgb(color));
t.property("ADBE Vector Stroke Width").setValue(grosor);
t.property("ADBE Vector Stroke Line Cap").setValue(2);
tr(capa).property("ADBE Position").setValue(junta);
return capa;
}
function pieza(nombre, junta) {
var capa = comp.layers.addShape();
capa.name = nombre;
tr(capa).property("ADBE Position").setValue(junta);
return capa;
}
function rel(p, junta) { return [p[0] - junta[0], p[1] - junta[1]]; }
// ── Fondo y piso ──
comp.layers.addSolid(rgb(FONDO), "Fondo", 1080, 1080, 1, T);
var piso = pieza("Piso", [0, 0]);
rect(piso, "Piso", 0, SUELO, 1080, 1080, PISO);
var baldosas = pieza("Baldosas", [0, 0]);
for (var i = 0; i < Math.ceil((1080 + 240) / 240); i++) rect(baldosas, "Baldosa " + (i + 1), i * 240, SUELO, i * 240 + 120, 1080, BALDOSA);
// ── Un lado del cuerpo; primero el de atrás, luego el de adelante ──
function lado(L, color) {
segmento("Brazo " + L, HOMBRO, rel(CODO, HOMBRO), 34, color);
segmento("Antebrazo " + L, CODO, rel(MUNECA, CODO), 30, color);
var mano = pieza("Mano " + L, MUNECA);
circulo(mano, "Mano", 0, 16, 38, PIEL);
segmento("Muslo " + L, CADERA, rel(RODILLA, CADERA), 46, color);
segmento("Pierna " + L, RODILLA, rel(TOBILLO, RODILLA), 40, color);
var pie = pieza("Pie " + L, TOBILLO);
rect(pie, "Pie", -24, -14, 76, SUELO - TOBILLO[1], ZAPATO, 16); // la suela, a ras del suelo
}
lado("I", LEJOS);
var torso = pieza("Torso", CADERA);
rect(torso, "Torso", -50, HOMBRO[1] - 30 - CADERA[1], 50, 22, CUERPO, 46);
var cabeza = pieza("Cabeza", CUELLO);
circulo(cabeza, "Ojo", 36, -70, 12, ZAPATO);
circulo(cabeza, "Cabeza", 8, -60, 120, PIEL);
lado("D", CERCA);
// el brazo de adelante va encima de todo
comp.layer("Mano D").moveToBeginning();
comp.layer("Antebrazo D").moveToBeginning();
comp.layer("Brazo D").moveToBeginning();
comp.layer("Mano D").moveToBeginning();
comp.openInViewer();
app.endUndoGroup();
})();
Link Art también puede unir por nombre, pero los nombres tienen que estar en el idioma del panel de Duik. Con Alt une por cercanía, y eso funciona con los nombres en español del script.
loopOut("cycle") en cada propiedad animada, con Editar ›
Copy Expression Only para repetirlo.
Cuatro ciclos en 4 s.loopOut("cycle").
El pie apoyado y las baldosas avanzan juntos: no patina.loopOut("cycle").
Asiente con cada bajada, un poco después que la cadera.loopOut("cycle") en los dos.
El pie apoya el talón y despega con la punta.0 de 7
Cada respuesta explica por qué, acierte o no.
Tutoriales del proceso completo en After Effects: formas que se transforman, volumen sin capas 3D y texturas que hacen que una pieza plana parezca ilustrada.
Una forma simple repetida muchas veces, cada copia un poco después que la anterior. El movimiento sale del desfase, no de la forma.
Ciclos de caminata y de carrera con personajes muy sencillos, donde el peso se nota en la cadera y en los pies.
El autor de Duik, la herramienta libre de rig y animación de personajes para After Effects, con su guía en línea.
Loops de motion design, primera parte. El empalme, las cinco formas de cerrar un loop y el método.
El editor de gráficos de After Effects. Las curvas detrás de cada Easy Ease.
Los doce principios del motion graphics. Cloning, Parallax, Dimensionality y los demás, con sus laboratorios.
Los doce principios de la animación. Pose a pose, arcos y acción complementaria.
Adobe. (s. f.). Expression language reference. After Effects User Guide. https://helpx.adobe.com/after-effects/using/expression-language-reference.html
RxLaboratory. (s. f.). Duik Ángela user guide. https://duik.org/guide/Angela/
Thomas, F. y Johnston, O. (1981). The illusion of life: Disney animation. Abbeville Press.
Williams, R. (2009). The animator's survival kit (ed. ampliada). Faber & Faber.
Willenskomer, I. (2017, 31 de marzo). Creating usability with motion: The UX in motion manifesto. UX in Motion. https://medium.com/ux-in-motion/creating-usability-with-motion-the-ux-in-motion-manifesto-a87a4584ddc
Esta presentación está pensada para pantalla horizontal.