Un loop pasa del último frame al primero sin pausa. Ese paso es el empalme, y en un buen loop no se nota: en inglés se le llama seamless loop.
Lo que gira, oscila o se desplaza solo vuelve a su posición inicial si completa un número entero de ciclos en la duración del loop.
◆ Pon 1.5 ciclos y repasa el empalmePara lo que ya está animado a mano: el último keyframe debe ser igual al primero, y la expresión repite el tramo sin fin. Con «pingpong» lo recorre de ida y de vuelta; con «offset», suma cada vuelta a la anterior.
loopOut("cycle")
Tipografía que se transforma
Para lo que se mueve por fórmula, como un seno o un giro: n, las vueltas en el loop, tiene que ser entero.
Math.cos(2 * Math.PI * n * time / T)
Péndulos en ola
Un desplazamiento que vuelve a cero cada vez que el patrón se repite. Cierra si el loop recorre un número entero de patrones.
(time * vel) % ancho
Un día en la casa de la alberca
El zoom que no termina: cada ciclo multiplica la escala por lo mismo y la escena de dentro ocupa el lugar de la de fuera. Con escala lineal, el acercamiento parece frenar.
100 * Math.pow(k, time / T)
Zoom infinito
Una pieza recorre un sistema y vuelve al punto de partida en el último frame. Ninguna fórmula lo garantiza; sale del timing chart.
frame final = frame 0
Reacción en cadena
Un style frame, un frame terminado, con la paleta y el acabado definitivos y un timing chart: qué pasa en cada segundo y cómo cierra.
Las poses clave, con linear keyframes y sin pulir. Sirve para revisar el ritmo del conjunto antes de invertir en detalle.
Curvas, arcos, estirar y encoger, anticipación y acción complementaria: el tramo donde el movimiento gana peso.
Textura, desenfoque de movimiento y color. Al final, revisar el empalme y exportar el ciclo exacto.
En las cinco prácticas, un script dibuja la escena y la deja quieta. La animación se construye paso a paso, con keyframes y, donde los keyframes no son suficientes, con una expresión.
Doce péndulos cuelgan de una barra y cada uno oscila un poco más rápido que el anterior. Se desfasan, dibujan una ola, se ordenan en grupos y a los 15 s vuelven a alinearse.
Nadie anima la ola: aparece sola del desfase entre frecuencias.
◆ Apaga una parte y mira qué se pierdeLa escena completa y quieta: doce péndulos en vertical, la luz y una cámara fija. Cada péndulo cuelga de un nulo «Eje», y ese nulo es lo único que se anima.
El péndulo número n hace 5 + n vaivenes en
15 s. Mientras ese número sea entero, todos terminan donde empezaron.
| Péndulo | Vaivenes en 15 s | Frames por vaivén | Hilo |
|---|---|---|---|
| 1 | 6 | 60 | 520 px |
| 2 | 7 | 51.43 | 446 px |
| 3 | 8 | 45 | 390 px |
| 4 | 9 | 40 | 347 px |
| 5 | 10 | 36 | 312 px |
| 6 | 11 | 32.73 | 284 px |
| 7 | 12 | 30 | 260 px |
| 8 | 13 | 27.69 | 240 px |
| 9 | 14 | 25.71 | 223 px |
| 10 | 15 | 24 | 208 px |
| 11 | 16 | 22.5 | 195 px |
| 12 | 17 | 21.18 | 184 px |
Seis de los doce vaivenes no duran un número entero de frames: con keyframes a mano no cerrarían. Para los doce hace falta una expresión.
01-pendulos-en-ola.jsx.
Se abre «Péndulos en ola»: 1080 × 1080, 15 s a 24 fps, con los doce péndulos
quietos.// 01 · Péndulos en ola · Loops de motion design
// Dibuja la escena y la deja quieta: doce péndulos en 3D colgados de una barra,
// una luz que proyecta sombras, el piso que las recibe y una cámara fija. Cada
// péndulo cuelga de un nulo «Eje», que es lo que se anima en la guía.
// No crea keyframes ni expresiones.
// Archivo › Secuencias de comandos › Ejecutar archivo de secuencias de comandos
(function () {
var T = 15; // duración del loop, en segundos
var N = 12; // número de péndulos
var N0 = 6; // vaivenes del péndulo más largo en el loop: decide el largo de los hilos
var LMAX = 520; // largo del hilo más largo, en píxeles
var SEP = 62; // separación entre péndulos, en profundidad
var EJE_Y = 230; // altura de la barra
var PISO_Y = 900; // altura del piso
var R = 34; // radio de las esferas
var COLORES = ["#E2467F", "#7D5FD0", "#3FB7C6"]; // primer péndulo, el de en medio y el último
var FONDO = "#EFE5D8", PISO = "#E5D5C2", TINTA = "#3B2F3A";
app.beginUndoGroup("Péndulos en ola");
var comp = app.project.items.addComp("Péndulos en ola", 1080, 1080, 1, T, 24);
comp.bgColor = rgb(FONDO);
try { comp.renderer = "ADBE Advanced 3d"; } catch (e) {} // 3D clásico: admite sombras
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 mezcla(a, b, t) {
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
}
function tr(capa) { return capa.property("ADBE Transform Group"); }
// Cada forma va en su propio grupo. Se vuelve a pedir el grupo por su índice
// después de cada addProperty: las referencias anteriores dejan de valer.
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 circulo(capa, nombre, d, pos, color, opacidad) {
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(pos);
g().addProperty("ADBE Vector Graphic - Fill");
g().property(2).property("ADBE Vector Fill Color").setValue(color);
g().property(2).property("ADBE Vector Fill Opacity").setValue(opacidad);
}
function rectangulo(capa, nombre, tam, color, redondeo) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Rect");
g().property(1).property("ADBE Vector Rect Size").setValue(tam);
g().property(1).property("ADBE Vector Rect Roundness").setValue(redondeo);
g().addProperty("ADBE Vector Graphic - Fill");
g().property(2).property("ADBE Vector Fill Color").setValue(color);
}
function linea(capa, nombre, largo, color, grosor) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Group");
var forma = new Shape();
forma.vertices = [[0, 0], [0, largo]];
forma.closed = false;
g().property(1).property("ADBE Vector Shape").setValue(forma);
g().addProperty("ADBE Vector Graphic - Stroke");
g().property(2).property("ADBE Vector Stroke Color").setValue(color);
g().property(2).property("ADBE Vector Stroke Width").setValue(grosor);
g().property(2).property("ADBE Vector Stroke Line Cap").setValue(2);
}
// Una propiedad de un grupo, por el final de su nombre interno
function buscar(grupo, fin) {
for (var i = 1; i <= grupo.numProperties; i++) {
var m = grupo.property(i).matchName;
if (m.substr(m.length - fin.length) === fin) return grupo.property(i);
}
return null;
}
function material(capa, proyecta, recibe, luces) {
var m = capa.property("ADBE Material Options Group");
m.property("ADBE Casts Shadows").setValue(proyecta ? 1 : 0);
m.property("ADBE Accepts Shadows").setValue(recibe ? 1 : 0);
m.property("ADBE Accepts Lights").setValue(luces ? 1 : 0);
}
// ── Piso, barra y postes ──
var piso = comp.layers.addSolid(rgb(PISO), "Piso", 4000, 4000, 1, T);
piso.threeDLayer = true;
tr(piso).property("ADBE Rotate X").setValue(90);
tr(piso).property("ADBE Position").setValue([540, PISO_Y, 0]);
material(piso, false, true, true);
var largoFila = (N - 1) * SEP;
var tinta = rgb(TINTA);
var barra = comp.layers.addShape();
barra.name = "Barra";
rectangulo(barra, "Barra", [14, largoFila + 90], tinta, 7);
barra.threeDLayer = true;
tr(barra).property("ADBE Rotate X").setValue(90);
tr(barra).property("ADBE Position").setValue([540, EJE_Y, 0]);
material(barra, true, false, false);
for (var p = 0; p < 2; p++) {
var poste = comp.layers.addShape();
poste.name = "Poste " + (p + 1);
rectangulo(poste, "Poste", [12, PISO_Y - EJE_Y], tinta, 6);
poste.threeDLayer = true;
poste.autoOrient = AutoOrientType.CAMERA_OR_POINT_OF_INTEREST;
tr(poste).property("ADBE Position").setValue([540, (EJE_Y + PISO_Y) / 2, (p ? 1 : -1) * (largoFila / 2 + 45)]);
material(poste, true, false, false);
}
// ── Los péndulos: un nulo «Eje» en el punto de giro, con el hilo y la esfera de hijos ──
var C0 = rgb(COLORES[0]), C1 = rgb(COLORES[1]), C2 = rgb(COLORES[2]);
var blanco = [1, 1, 1];
for (var k = 0; k < N; k++) {
var u = k / (N - 1);
var color = u < 0.5 ? mezcla(C0, C1, u * 2) : mezcla(C1, C2, (u - 0.5) * 2);
var L = LMAX * N0 / (N0 + k); // el hilo se acorta con la frecuencia
var z = (k - (N - 1) / 2) * SEP;
var eje = comp.layers.addNull(T);
eje.name = "Eje " + (k + 1);
eje.threeDLayer = true;
tr(eje).property("ADBE Position").setValue([540, EJE_Y, z]);
var ancla = tr(eje).property("ADBE Anchor Point").value;
var hilo = comp.layers.addShape();
hilo.name = "Hilo " + (k + 1);
linea(hilo, "Hilo", L, tinta, 2.5);
hilo.threeDLayer = true;
hilo.parent = eje;
tr(hilo).property("ADBE Position").setValue([ancla[0], ancla[1], ancla[2]]);
material(hilo, true, false, false);
var esfera = comp.layers.addShape();
esfera.name = "Esfera " + (k + 1);
// del frente hacia el fondo: el grupo que se añade primero queda encima
circulo(esfera, "Brillo", R * 0.32, [-R * 0.38, -R * 0.42], blanco, 85);
circulo(esfera, "Luz", R * 0.95, [-R * 0.26, -R * 0.3], blanco, 28);
circulo(esfera, "Base", R * 2, [0, 0], color, 100);
esfera.threeDLayer = true;
esfera.parent = eje;
tr(esfera).property("ADBE Position").setValue([ancla[0], ancla[1] + L, ancla[2]]);
esfera.autoOrient = AutoOrientType.CAMERA_OR_POINT_OF_INTEREST;
material(esfera, true, false, false);
}
// ── Luz: la que proyecta las sombras, y un ambiente que no deja nada en negro ──
var luz = comp.layers.addLight("Luz", [540, 540]);
luz.lightType = LightType.POINT;
tr(luz).property("ADBE Position").setValue([540, -400, 0]);
luz.property("ADBE Light Options Group").property("ADBE Light Intensity").setValue(35);
// «Proyecta sombras» se busca por el final de su nombre interno: Adobe no lo
// publica para las luces. El grupo se vuelve a pedir después de cada cambio.
var sombras = buscar(luz.property("ADBE Light Options Group"), "Casts Shadows");
if (sombras) {
sombras.setValue(1);
luz.property("ADBE Light Options Group").property("ADBE Light Shadow Darkness").setValue(32);
luz.property("ADBE Light Options Group").property("ADBE Light Shadow Diffusion").setValue(30);
} else {
alert("La luz quedó sin sombras: en «Luz» › Opciones de luz, activar Proyecta sombras.");
}
var amb = comp.layers.addLight("Ambiente", [540, 540]);
amb.lightType = LightType.AMBIENT;
amb.property("ADBE Light Options Group").property("ADBE Light Intensity").setValue(72);
// ── Cámara fija: mira al centro de la fila desde 40° a un lado y 16° arriba ──
// En la guía se vincula a un nulo en su punto de interés para que gire.
var POI = [540, 600, 0], D = 1700;
var a = 40 * Math.PI / 180, b = 16 * Math.PI / 180;
var cam = comp.layers.addCamera("Cámara", [540, 540]);
cam.property("ADBE Camera Options Group").property("ADBE Camera Zoom").setValue(1500);
tr(cam).property("ADBE Anchor Point").setValue(POI); // en una cámara, el punto de interés
tr(cam).property("ADBE Position").setValue([
POI[0] - D * Math.cos(b) * Math.sin(a),
POI[1] - D * Math.sin(b),
POI[2] - D * Math.cos(b) * Math.cos(a)
]);
comp.openInViewer();
app.endUndoGroup();
})();
// Repite el tramo entre el primer y el último keyframe
loopOut("cycle")
Cierra porque el tramo dura 60 frames y cabe seis veces en los 360 del loop, y porque el último keyframe vale lo mismo que el primero.
loopOut por la expresión de la derecha y borrar los tres keyframes.
El péndulo se mueve igual: el coseno dibuja casi la misma curva que
los keyframes.loopOut("pingpong").
La cámara va y vuelve; en el 359 llega casi a −10, donde empezó.// El número del péndulo sale del nombre de la capa
var num = parseInt(thisLayer.name.split(" ")[1]); // «Eje 4» → 4
var n = 5 + num; // vaivenes en 15 s: el 1 hace 6; el 12, 17
var A = 22; // grados hacia cada lado
A * Math.cos(2 * Math.PI * n * time / 15)
Con n entero, cada péndulo termina en el ángulo en que empezó. En el tiempo 0
el coseno vale 1: arranca en 22°, como los keyframes del primero. pingpong recorre el
tramo de ida y de vuelta: 180 frames cada sentido.
A = 30; copiarla otra vez a los
once.
Los péndulos abren más y el loop sigue cerrando: el ángulo no cambia los
vaivenes.Una bola sube por un tubo, rueda sobre cinco teclas que se hunden a su paso, rebota en un trampolín y vuelve al tubo por otra rampa. El loop cierra porque termina donde empezó.
En bloqueo la máquina ya funciona; el pulido es lo que le da peso.
◆ Apaga «Pulido» para ver el bloqueoLos trece keyframes del bloqueo, a 24 fps. Entre uno y otro, lo que se trabaja al pulir.
| Frame | Segundo | Qué pasa | Se pule |
|---|---|---|---|
| 0 | 0.00 | Se carga en el fondo del tubo | Anticipación |
| 10 | 0.42 | Sube por el tubo | Easing · Estirar |
| 58 | 2.42 | Sale disparada por arriba | Arcos |
| 64 | 2.67 | Lo más alto; empieza a caer | Arcos |
| 72 | 3.00 | Cae en la primera tecla y rueda | Encoger · Easing |
| 144 | 6.00 | Deja la rampa A | Arcos |
| 155 | 6.46 | Toca la membrana | Encoger |
| Frame | Segundo | Qué pasa | Se pule |
|---|---|---|---|
| 158 | 6.58 | Lo más hondo de la membrana | Anticipación |
| 162 | 6.75 | Sale disparada | Arcos |
| 176 | 7.33 | Lo más alto del vuelo | Arcos |
| 192 | 8.00 | Cae en la rampa B y rueda | Encoger · Easing |
| 264 | 11.00 | Llega al tubo | Arcos |
| 272 | 11.33 | Entra al fondo y queda quieta hasta el 287 | Acción compl. |
02-reaccion-en-cadena.jsx desde Archivo › Secuencias de
comandos › Ejecutar archivo de secuencias de comandos, y contar en frames
(Ctrl+clic en el tiempo actual).
Se abre «Reacción en cadena»: 1080 × 1080, 12 s a 24 fps. La bola espera en el
fondo del tubo.// 02 · Reacción en cadena · Loops de motion design
// Dibuja la máquina y la deja quieta: un tubo, una rampa de cinco teclas, un
// trampolín y una segunda rampa que vuelve al tubo. La bola espera en el fondo
// del tubo, que es donde empieza y termina el loop.
// Cada tecla tiene el punto de anclaje en su bisagra, y la bola en su centro.
// 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 R = 26; // radio de la bola
var A0 = [860, 280], A1 = [300, 470]; // rampa A: de la boca del tubo hacia la izquierda
var B0 = [380, 820], B1 = [880, 960]; // rampa B: del trampolín de vuelta al tubo
var TRAMP = { x: 160, y: 720, ancho: 70 };
app.beginUndoGroup("Reacción en cadena");
var comp = app.project.items.addComp("Reacción en cadena", 1080, 1080, 1, T, 24);
comp.bgColor = rgb("#E8473F");
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 resta(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
function unitario(v) { var l = Math.sqrt(v[0] * v[0] + v[1] * v[1]); return [v[0] / l, v[1] / l]; }
// la normal de una rampa que apunta hacia arriba (la Y crece hacia abajo)
function normal(a, b) {
var d = unitario(resta(b, a)), n = [d[1], -d[0]];
return n[1] > 0 ? [-n[0], -n[1]] : n;
}
// la línea que se dibuja va 7 px por debajo de la superficie: su grosor es 14
function bajo(p, a, b) { var n = normal(a, b); return [p[0] - n[0] * 7, p[1] - n[1] * 7]; }
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 trazo(capa, nombre, puntos, color, grosor, entradas, salidas) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Group");
var forma = new Shape();
forma.vertices = puntos;
if (entradas) { forma.inTangents = entradas; forma.outTangents = salidas; }
forma.closed = false;
g().property(1).property("ADBE Vector Shape").setValue(forma);
g().addProperty("ADBE Vector Graphic - Stroke");
g().property(2).property("ADBE Vector Stroke Color").setValue(color);
g().property(2).property("ADBE Vector Stroke Width").setValue(grosor);
g().property(2).property("ADBE Vector Stroke Line Cap").setValue(2);
return g;
}
function circulo(capa, nombre, d, pos, color, opacidad) {
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(pos);
g().addProperty("ADBE Vector Graphic - Fill");
g().property(2).property("ADBE Vector Fill Color").setValue(color);
g().property(2).property("ADBE Vector Fill Opacity").setValue(opacidad);
}
function rectangulo(capa, nombre, tam, pos, color, opacidad, redondeo) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Rect");
g().property(1).property("ADBE Vector Rect Size").setValue(tam);
g().property(1).property("ADBE Vector Rect Position").setValue(pos);
g().property(1).property("ADBE Vector Rect Roundness").setValue(redondeo);
g().addProperty("ADBE Vector Graphic - Fill");
g().property(2).property("ADBE Vector Fill Color").setValue(color);
g().property(2).property("ADBE Vector Fill Opacity").setValue(opacidad);
}
// 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;
}
var CREMA = rgb("#F6E7D4"), CREMA_2 = rgb("#EAD3BA"), SOPORTE = rgb("#B8322C");
var MARINO = rgb("#1F2A44"), AMARILLO = rgb("#FFC94A"), BLANCO = [1, 1, 1];
// ── Fondo y soportes (lo de más atrás) ──
comp.layers.addSolid(rgb("#E8473F"), "Fondo", 1080, 1080, 1, T);
var soportes = pieza("Soportes");
trazo(soportes, "Rampa A · 1", [[700, 335], [700, 1080]], SOPORTE, 8);
trazo(soportes, "Rampa A · 2", [[480, 410], [480, 1080]], SOPORTE, 8);
trazo(soportes, "Rampa B · 1", [[560, 875], [560, 1080]], SOPORTE, 8);
trazo(soportes, "Rampa B · 2", [[780, 935], [780, 1080]], SOPORTE, 8);
// ── Rampa B, de una pieza ──
var rampaB = pieza("Rampa B");
trazo(rampaB, "Rampa", [bajo(B0, B0, B1), bajo(B1, B0, B1)], CREMA, 14);
// ── Rampa A: cinco teclas; la capa está en la bisagra, así que la Rotación las hunde ──
for (var i = 0; i < 5; i++) {
var p0 = [A0[0] + (A1[0] - A0[0]) * i / 5, A0[1] + (A1[1] - A0[1]) * i / 5];
var p1 = [A0[0] + (A1[0] - A0[0]) * (i + 1) / 5, A0[1] + (A1[1] - A0[1]) * (i + 1) / 5];
var bisagra = bajo(p0, A0, A1), fin = bajo(p1, A0, A1);
var v = resta(fin, bisagra), largo = Math.sqrt(v[0] * v[0] + v[1] * v[1]), k = (largo - 6) / largo;
var t = comp.layers.addShape();
t.name = "Tecla " + (i + 1);
tr(t).property("ADBE Position").setValue(bisagra);
trazo(t, "Tecla", [[0, 0], [v[0] * k, v[1] * k]], i % 2 ? CREMA_2 : CREMA, 14);
}
// ── Trampolín: postes y membrana ──
var postes = pieza("Trampolín");
trazo(postes, "Poste izquierdo", [[TRAMP.x - TRAMP.ancho, TRAMP.y], [TRAMP.x - TRAMP.ancho, TRAMP.y + 100]], MARINO, 10);
trazo(postes, "Poste derecho", [[TRAMP.x + TRAMP.ancho, TRAMP.y], [TRAMP.x + TRAMP.ancho, TRAMP.y + 100]], MARINO, 10);
trazo(postes, "Base", [[TRAMP.x - TRAMP.ancho - 22, TRAMP.y + 100], [TRAMP.x + TRAMP.ancho + 22, TRAMP.y + 100]], MARINO, 10);
// La membrana tiene un vértice en medio, con tangentes horizontales: al bajarlo, se curva.
var membrana = pieza("Membrana");
var m = TRAMP.ancho * 0.55;
trazo(membrana, "Membrana", [[TRAMP.x - TRAMP.ancho, TRAMP.y], [TRAMP.x, TRAMP.y], [TRAMP.x + TRAMP.ancho, TRAMP.y]], MARINO, 8,
[[0, 0], [-m, 0], [0, 0]], [[0, 0], [m, 0], [0, 0]]);
// ── La bola, quieta en el fondo del tubo ──
var bola = comp.layers.addShape();
bola.name = "Bola";
circulo(bola, "Brillo", R * 0.34, [-R * 0.38, -R * 0.42], BLANCO, 85);
circulo(bola, "Luz", R * 0.95, [-R * 0.26, -R * 0.3], BLANCO, 26);
// La franja es lo único que rueda: si girara la capa entera, al aplastarla
// con la Escala la deformación giraría con ella.
var franja = trazo(bola, "Franja", [[-18, -14], [18, -14]], BLANCO, 5);
franja().property(2).property("ADBE Vector Stroke Opacity").setValue(55);
circulo(bola, "Base", R * 2, [0, 0], AMARILLO, 100);
tr(bola).property("ADBE Position").setValue([930, 962]);
// ── El tubo, delante de la bola para que se vea dentro ──
var tubo = pieza("Tubo");
trazo(tubo, "Pared izquierda", [[893, 140], [893, 912]], CREMA, 7);
trazo(tubo, "Pared derecha y fondo", [[967, 140], [967, 995], [878, 995]], CREMA, 7);
trazo(tubo, "Reflejo", [[905, 170], [905, 420]], BLANCO, 4);
rectangulo(tubo, "Vidrio", [74, 855], [930, 567], CREMA, 14, 0);
comp.openInViewer();
app.endUndoGroup();
})();
Frame Posición Frame Posición
0 930, 962 158 160, 734
10 930, 962 162 160, 694
58 930, 150 176 300, 560
64 895, 105 192 407, 801
72 832, 262 264 867, 929
144 302, 442 272 930, 962
155 160, 694
Del 272 al 287 no hay más keyframes: la bola se queda donde estaba en el 0, y por eso el empalme no salta.
Tecla 1 › Rotación 72 → 0 74 → −8 82 → −8 86 → 3 90 → −1.2 94 → 0 Bola › Contenido › Franja › Transformar: Franja › Rotación 72 → 0 144 → −1170 192 → −1170 264 → 0
Gira la franja y no la capa, para que la Escala aplaste la bola siempre en vertical. −1170° son tres vueltas y cuarto: lo que avanza la bola en la rampa A, entre su radio.
Una casa junto a una alberca vive un día entero en 12 s: el sol y la luna giran, el cielo cambia de color, la sombra se estira y las ventanas se encienden una tras otra.
Cada hora dura 12 frames.
◆ Recorre el día con la barraLa escena de día, quieta. Lo que solo se ve de noche ya está en su sitio, en opacidad 0.
Cada hora dura 12 frames: frame = (hora − 6) × 12. La tabla marca los momentos en que caen los keyframes.
| Hora | Frame | Qué pasa |
|---|---|---|
| 6:00 | 0 | Amanece: el sol sale por la izquierda |
| 7:30 | 18 | Pleno día: el cielo ya es azul |
| 12:00 | 72 | Mediodía: el sol arriba y la sombra recta |
| 17:00 | 132 | Empieza el atardecer |
| 18:00 | 144 | Se oculta el sol, sale la luna y se enciende la primera ventana |
| 19:36 | 163 | Noche cerrada |
| 0:00 | 216 | Medianoche: la luna arriba |
| 3:50 | 262 | Se apaga la primera ventana |
03-casa-de-la-alberca.jsx y contar en frames.
Se abre «Un día en la casa de la alberca»: 12 s, de día, con el sol a ras del
horizonte.// 03 · Un día en la casa de la alberca · Loops de motion design
// Dibuja la escena de día y la deja quieta: cielo, sol, nubes, montañas, la casa
// con su sombra, la alberca, dos palmeras y, en opacidad 0, lo que solo se ve de
// noche: el sólido «Noche», la luna, las estrellas y la luz de las ventanas.
// El sol espera a ras del horizonte, a la izquierda, donde amanece; la luna, a la
// derecha. Los dos están a la misma distancia del punto 540, 640.
// No crea keyframes ni expresiones.
// Archivo › Secuencias de comandos › Ejecutar archivo de secuencias de comandos
(function () {
var T = 12; // un día, en segundos
var HORIZONTE = 640; // línea del horizonte
var CIELO = "#8FD3E8", VIDRIO = "#3B5566", LUZ = "#FFC870";
app.beginUndoGroup("Un día en la casa de la alberca");
var comp = app.project.items.addComp("Un día en la casa de la alberca", 1080, 1080, 1, T, 24);
comp.bgColor = rgb(CIELO);
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 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]);
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 trazo(capa, nombre, puntos, entradas, salidas, color, grosor) {
var g = nuevoGrupo(capa, nombre);
g().addProperty("ADBE Vector Shape - Group");
var s = new Shape(); s.vertices = puntos; s.closed = false;
if (entradas) { s.inTangents = entradas; s.outTangents = salidas; }
g().property(1).property("ADBE Vector Shape").setValue(s);
g().addProperty("ADBE Vector Graphic - Stroke");
g().property(2).property("ADBE Vector Stroke Color").setValue(rgb(color));
g().property(2).property("ADBE Vector Stroke Width").setValue(grosor);
g().property(2).property("ADBE Vector Stroke Line Cap").setValue(2);
return g;
}
function transformacion(g) { return g().parentProperty.property("ADBE Vector Transform Group"); }
// 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;
}
function opacidad(capa, valor) { tr(capa).property("ADBE Opacity").setValue(valor); }
// ── Cielo, brillo del horizonte y sol (lo de más atrás) ──
var cielo = pieza("Cielo");
rect(cielo, "Cielo", 0, 0, 1080, 1080, CIELO);
var brillo = pieza("Brillo del horizonte");
elipse(brillo, "Brillo", 540, HORIZONTE, 1500, 520, "#FFD29A", 100);
opacidad(brillo, 0);
var sol = comp.layers.addShape();
sol.name = "Sol";
elipse(sol, "Halo", 0, 0, 170, 170, "#FFE08A", 30);
elipse(sol, "Sol", 0, 0, 110, 110, "#FFE08A", 100);
tr(sol).property("ADBE Position").setValue([540 - 470, HORIZONTE]); // amanece
// ── Nubes lejanas, montañas y nubes cercanas: el parallax ──
function nube(nombre, x, y, escala, opacidadNube) {
var n = comp.layers.addShape();
n.name = nombre;
elipse(n, "a", -70 * escala, 10 * escala, 150 * escala, 70 * escala, "#FFFFFF", opacidadNube);
elipse(n, "b", 10 * escala, -12 * escala, 150 * escala, 100 * escala, "#FFFFFF", opacidadNube);
elipse(n, "c", 90 * escala, 12 * escala, 130 * escala, 64 * escala, "#FFFFFF", opacidadNube);
tr(n).property("ADBE Position").setValue([x, y]);
return n;
}
nube("Nubes lejanas", 90, 200, 1, 70);
var montanas = pieza("Montañas");
poligono(montanas, "Cercanas", [[0, 640], [0, 600], [180, 560], [330, 605], [480, 575], [640, 612], [800, 570], [960, 605], [1080, 585], [1080, 640]], "#6E93A6");
poligono(montanas, "Lejanas", [[0, 640], [0, 560], [140, 500], [260, 545], [400, 470], [560, 540], [700, 490], [860, 555], [980, 505], [1080, 540], [1080, 640]], "#9CB8C9");
nube("Nubes cercanas", 690, 310, 1.3, 90);
// ── Suelo, sombra, casa y alberca ──
var suelo = pieza("Suelo");
rect(suelo, "Terraza", 0, HORIZONTE, 1080, 1080, "#E9D8B8");
var sombra = pieza("Sombra de la casa");
poligono(sombra, "Sombra", [[220, 716], [860, 716], [860, 780], [220, 780]], "#2A2F3A");
opacidad(sombra, 35);
var casa = pieza("Casa");
var CENTROS = [315, 427, 540, 653, 765];
for (var c = 0; c < CENTROS.length; c++) rect(casa, "Vidrio " + (c + 1), CENTROS[c] - 43, 548, CENTROS[c] + 43, 676, VIDRIO, 3);
rect(casa, "Techo", 210, 494, 870, 520, "#2E3440", 4);
rect(casa, "Zócalo", 230, 700, 850, 716, "#D9C7A8");
rect(casa, "Muro", 250, 520, 830, 700, "#F6F1E8");
var alberca = pieza("Alberca");
rect(alberca, "Agua", 206, 804, 874, 926, "#3FC1D9", 6);
rect(alberca, "Borde", 190, 790, 890, 940, "#FFFFFF", 10);
// Los reflejos son rayas cada 80 px; la máscara los recorta al agua.
var reflejos = pieza("Reflejos del agua");
var gReflejos = nuevoGrupo(reflejos, "Reflejos");
for (var fila = 0; fila < 3; fila++) {
for (var x = 120 + (fila % 2) * 40; x < 1000; x += 80) {
gReflejos().addProperty("ADBE Vector Shape - Group");
var raya = new Shape(); raya.vertices = [[x, 835 + fila * 30], [x + 34, 835 + fila * 30]]; raya.closed = false;
gReflejos().property(gReflejos().numProperties).property("ADBE Vector Shape").setValue(raya);
}
}
gReflejos().addProperty("ADBE Vector Graphic - Stroke");
var tReflejos = gReflejos().property(gReflejos().numProperties);
tReflejos.property("ADBE Vector Stroke Color").setValue(rgb("#E8FBFF"));
tReflejos.property("ADBE Vector Stroke Width").setValue(5);
tReflejos.property("ADBE Vector Stroke Line Cap").setValue(2);
tReflejos.property("ADBE Vector Stroke Opacity").setValue(75);
var mascara = reflejos.property("ADBE Mask Parade").addProperty("ADBE Mask Atom");
var forma = new Shape();
forma.vertices = [[206, 804], [874, 804], [874, 926], [206, 926]]; forma.closed = true;
mascara.property("ADBE Mask Shape").setValue(forma);
// ── Palmeras: el tronco tiene el anclaje en la base y las hojas son sus hijas ──
function palmera(n, base, puntos, entradas, salidas, copa) {
var tronco = comp.layers.addShape();
tronco.name = "Palmera " + n;
trazo(tronco, "Tronco", puntos, entradas, salidas, "#8A5A3C", 16);
tr(tronco).property("ADBE Position").setValue(base);
var hojas = comp.layers.addShape();
hojas.name = "Hojas " + n;
var angulos = [-170, -135, -100, -60, -25, 12];
for (var a = 0; a < angulos.length; a++) {
var g = elipse(hojas, "Hoja " + (a + 1), 80, 0, 170, 40, "#2F7D5B", 100);
transformacion(g).property("ADBE Vector Rotation").setValue(angulos[a]);
}
hojas.parent = tronco;
tr(hojas).property("ADBE Position").setValue(copa);
}
palmera(1, [150, 790], [[0, 0], [-10, -130], [-28, -260]], [[0, 0], [4, 40], [6, 30]], [[0, 0], [-4, -40], [0, 0]], [-28, -260]);
palmera(2, [950, 760], [[0, 0], [12, -120], [30, -240]], [[0, 0], [-4, 40], [-6, 30]], [[0, 0], [4, -40], [0, 0]], [30, -240]);
// ── Noche: oscurece todo lo de abajo; luna, estrellas y ventanas van encima ──
var noche = comp.layers.addSolid(rgb("#1B2347"), "Noche", 1080, 1080, 1, T);
noche.blendingMode = BlendingMode.MULTIPLY;
opacidad(noche, 0);
var luna = comp.layers.addShape();
luna.name = "Luna";
elipse(luna, "Luna", 0, 0, 78, 78, "#F4F1E6", 100);
tr(luna).property("ADBE Position").setValue([540 + 430, HORIZONTE]);
opacidad(luna, 0);
var estrellas = pieza("Estrellas");
var semilla = 7;
function azar() { semilla = (semilla * 16807) % 2147483647; return semilla / 2147483647; }
for (var e = 0; e < 24; e++) {
var d = 3 + azar() * 4;
elipse(estrellas, "Estrella " + (e + 1), 40 + azar() * 1000, 50 + azar() * 380, d, d, "#FFFFFF", 70 + azar() * 30);
}
opacidad(estrellas, 0);
// La luz de cada ventana, encima del vidrio oscuro de «Casa»
for (var v = 0; v < CENTROS.length; v++) {
var ventana = pieza("Ventana " + (v + 1));
rect(ventana, "Luz", CENTROS[v] - 43, 548, CENTROS[v] + 43, 676, LUZ, 3);
opacidad(ventana, 0);
}
comp.openInViewer();
app.endUndoGroup();
})();
// Repite el tramo de 0 a 180° sumándolo cada vez
loopOut("offset")
Con offset el segundo tramo va de 180 a 360°, y un círculo completo deja
todo igual que en el frame 0. Con cycle, el sol volvería a la izquierda de
golpe.
n = 1 por n = 2.
Las cercanas van al doble de velocidad y la escena gana profundidad.loopOut("cycle"). «Palmera 2», con los signos al revés.
Cuatro vaivenes por loop, a destiempo.Cielo › Color día #8FD3E8 · tarde #F7A072 · noche #2B3A6B 0 tarde 18 día 132 día 149 tarde 163 noche 270 noche 287 tarde Noche › Opacidad 0 → 35 12 → 0 138 → 0 162 → 70 270 → 70 287 → 35 Estrellas › Opacidad 156 → 0 174 → 100 270 → 100 282 → 0 Brillo del horizonte › Opacidad 0 → 75 18 → 0 133 → 0 149 → 80 164 → 0 275 → 0 287 → 75 Ventana 1 › Opacidad 144 → 0 147 → 100 262 → 100 265 → 0
Los keyframes del 287 repiten el valor del 0: el amanecer sigue del último frame al primero.
// El módulo regresa la nube al otro lado var n = 1; // vueltas por loop; las cercanas, 2 var W = 1500; // recorrido: la pantalla más el ancho de la nube var x = (value[0] + 210 + time / thisComp.duration * n * W) % W - 210; [x, value[1]]
loopOut("cycle").
Las rayas avanzan sin salto: a los 80 px el patrón es idéntico.// Las hojas siguen al tronco un cuarto de segundo después var p = parent.transform.rotation; var t = (time - 0.25 + thisComp.duration) % thisComp.duration; p.valueAtTime(t) * 1.6 - p
Las hojas son hijas del tronco: su rotación se suma a la de él. Con el módulo, los primeros seis frames leen el final del loop, donde el tronco viene de la misma pose.
LOOP sube letra por letra desde una línea base. Sus dos O se convierten en un ∞ que una luz recorre, y todo vuelve a su sitio y se hunde.
Las cuatro letras llevan los mismos keyframes, cada una dos frames después de la anterior.
◆ Apaga «Masking» y mira de dónde salenCada letra se mueve con los keyframes de su grupo «Letra». Se animan los de L y se pegan en las demás, dos frames después de la anterior.
La transformación funciona porque el círculo y la gota tienen los mismos cuatro vértices, en el mismo orden.
| Frames | Qué pasa | Principio |
|---|---|---|
| 0 – 25 | Las letras suben una tras otra desde la línea base, se pasan un poco y se asientan | Masking · Offset & Delay |
| 48 – 65 | L y P se hunden; las O se quedan solas | Obscuration |
| 58 – 72 | Cada O se convierte en una mitad del ∞ | Transformation |
| 72 – 115 | Una luz recorre el ∞ dos veces por segundo | loopOut |
| 115 – 142 | El ∞ vuelve a ser O O y regresan L y P | Transformation |
| 151 – 174 | Cada letra sube un poco y se hunde, una tras otra | Anticipación |
| 175 – 191 | Vacío, igual que el frame 0 | El empalme |
04-tipografia-que-se-transforma.jsx y contar en
frames.
Se abre «Tipografía que se transforma», de 8 s; en «Letras», la palabra quieta y
dos gotas guía, tenues.// 04 · Tipografía que se transforma · Loops de motion design
// Dibuja la palabra LOOP con trazos, quieta sobre su línea base, dentro de la
// precomposición «Letras». Cada letra tiene un grupo llamado «Letra»: así los
// keyframes de una se pueden pegar en las otras. Las O tienen cuatro vértices.
// También deja dos capas guía con la gota en que se convierte cada O (no salen
// en el render) y la «Luz» del ∞, en opacidad 0.
// No crea keyframes, expresiones ni máscaras.
// Archivo › Secuencias de comandos › Ejecutar archivo de secuencias de comandos
(function () {
var T = 8; // duración del loop, en segundos
var BASE = 690; // la línea base: lo que baje de aquí no se verá
var GROSOR = 40; // grosor del trazo de las letras
var COLOR = "#5632DA", LUZ = "#A72BC9", FONDO = "#F3F3EB";
var K = 0.5523; // la constante que aproxima un círculo con cuatro curvas
app.beginUndoGroup("Tipografía que se transforma");
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 forma(v, ent, sal, cerrada) {
var s = new Shape(); s.vertices = v; s.inTangents = ent; s.outTangents = sal; s.closed = cerrada; return s;
}
function mover(s, dx, dy) {
var v = []; for (var i = 0; i < s.vertices.length; i++) v.push([s.vertices[i][0] + dx, s.vertices[i][1] + dy]);
return forma(v, s.inTangents, s.outTangents, s.closed);
}
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 trazar(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);
t.property("ADBE Vector Stroke Line Join").setValue(2);
}
function capaDeTrazo(comp, nombre, grupo, pos, trazado, color, grosor) {
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);
trazar(g, color, grosor);
tr(capa).property("ADBE Position").setValue(pos);
return capa;
}
// El círculo de las O y la gota en que se convierte cada una: los mismos
// cuatro vértices, en el mismo orden (derecha, abajo, izquierda, arriba).
var r = 120, k = r * K;
var CIRCULO = forma([[r, 0], [0, r], [-r, 0], [0, -r]],
[[0, -k], [k, 0], [0, k], [-k, 0]], [[0, k], [-k, 0], [0, -k], [k, 0]], true);
var GOTA_IZQ = forma([[130, 0], [0, 95], [-130, 0], [0, -95]],
[[-50, -45], [60, 0], [0, 60], [-75, 0]], [[-50, 45], [-75, 0], [0, -60], [60, 0]], true);
var GOTA_DER = forma([[130, 0], [0, 95], [-130, 0], [0, -95]],
[[0, -60], [75, 0], [50, 45], [-60, 0]], [[0, 60], [-60, 0], [50, -45], [75, 0]], true);
var POS_O1 = [413, 540], POS_O2 = [703, 540];
// Donde termina cada gota: juntas forman el ∞
var FIN_O1 = [410, 540], FIN_O2 = [670, 540];
// ── La precomposición «Letras» ──
var letras = app.project.items.addComp("Letras", 1080, 1080, 1, T, 24);
var luz = letras.layers.addShape();
luz.name = "Luz";
tr(luz).property("ADBE Position").setValue([0, 0]);
var gLuz = nuevoGrupo(luz, "Infinito");
gLuz().addProperty("ADBE Vector Shape - Group");
gLuz().property(1).property("ADBE Vector Shape").setValue(mover(GOTA_IZQ, FIN_O1[0], FIN_O1[1]));
gLuz().addProperty("ADBE Vector Shape - Group");
gLuz().property(2).property("ADBE Vector Shape").setValue(mover(GOTA_DER, FIN_O2[0], FIN_O2[1]));
trazar(gLuz, LUZ, 16);
tr(luz).property("ADBE Opacity").setValue(0);
// Guías: cada gota dibujada en la capa de su O, ya en su sitio final
var guias = [["Gota 1 (guía)", POS_O1, mover(GOTA_IZQ, FIN_O1[0] - POS_O1[0], 0)],
["Gota 2 (guía)", POS_O2, mover(GOTA_DER, FIN_O2[0] - POS_O2[0], 0)]];
for (var q = 0; q < guias.length; q++) {
var guia = capaDeTrazo(letras, guias[q][0], "Gota", guias[q][1], guias[q][2], LUZ, 6);
guia.guideLayer = true;
tr(guia).property("ADBE Opacity").setValue(45);
}
capaDeTrazo(letras, "P", "Letra", [888, 660], forma([[0, 0], [0, -240], [60, -240], [60, -110], [0, -110]],
[[0, 0], [0, 0], [0, 0], [40, 0], [0, 0]], [[0, 0], [0, 0], [40, 0], [0, 0], [0, 0]], false), COLOR, GROSOR);
capaDeTrazo(letras, "O2", "Letra", POS_O2, CIRCULO, COLOR, GROSOR);
capaDeTrazo(letras, "O1", "Letra", POS_O1, CIRCULO, COLOR, GROSOR);
capaDeTrazo(letras, "L", "Letra", [103, 660], forma([[0, -240], [0, 0], [130, 0]],
[[0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0]], false), COLOR, GROSOR);
luz.moveToBeginning(); // la luz va encima de las letras
// ── La composición principal ──
var comp = app.project.items.addComp("Tipografía que se transforma", 1080, 1080, 1, T, 24);
comp.bgColor = rgb(FONDO);
comp.layers.addSolid(rgb(FONDO), "Fondo", 1080, 1080, 1, T);
var linea = comp.layers.addShape();
linea.name = "Línea base";
tr(linea).property("ADBE Position").setValue([0, 0]);
var gLinea = nuevoGrupo(linea, "Línea");
gLinea().addProperty("ADBE Vector Shape - Group");
gLinea().property(1).property("ADBE Vector Shape").setValue(forma([[60, BASE + 2], [1020, BASE + 2]], [[0, 0], [0, 0]], [[0, 0], [0, 0]], false));
trazar(gLinea, COLOR, 4);
tr(linea).property("ADBE Opacity").setValue(30);
var capaLetras = comp.layers.add(letras);
capaLetras.collapseTransformation = true; // vectores nítidos a cualquier escala
comp.openInViewer();
app.endUndoGroup();
})();
Entra 0 → 0, 320 11 → 0, −26 15 → 0, 10 19 → 0, 0 Se hunde 48 → 0, 0 50 → 0, −19 59 → 0, 320 Regresa 124 → 0, 320 132 → 0, −13 136 → 0, 0 Sale 151 → 0, 0 156 → 0, −26 168 → 0, 320
Es la posición del grupo, no la de la capa: en las cuatro letras vale 0, 0 en reposo, así que los mismos keyframes sirven para todas. Dentro de «Letras» la letra se ve también bajo la línea; la máscara está en la composición principal.
loopOut("cycle").
La capa sigue en opacidad 0: todavía no se ve.Por la ventana de una fachada se entra a una habitación, por su cuadro a un escritorio y por su pantalla otra vez a la fachada.
Siete escenas encadenadas, cada una al 25 % de la anterior. Solo la primera crece, con escala exponencial.
◆ Apaga «Escala exponencial» y mira cómo frenaTres escenas, cada una en su precomposición y con un marco cuadrado de 270 px en el centro: 1080 entre 4. Una escena al 25 % cabe justo en el marco de otra, y la última lleva otra vez a la primera.
Cada escena es hija de la anterior, con Escala 25 %. Solo la primera crece; las demás la siguen.
| Capa | Escena | Escala | Tamaño real |
|---|---|---|---|
| 1 | A · Fachada | 100 % | 100 % |
| 2 | B · Habitación | 25 % | 25 % |
| 3 | C · Pantalla | 25 % | 6.25 % |
| 4 | A · Fachada | 25 % | 1.56 % |
| 5 | B · Habitación | 25 % | 0.39 % |
| 6 | C · Pantalla | 25 % | 0.098 % |
| 7 | A · Fachada | 25 % | 0.024 % |
La capa 1 es la de abajo; cada escena nueva va encima de la anterior.
05-zoom-infinito.jsx y contar en frames.
Aparecen las tres escenas y «Zoom infinito», vacía.// 05 · Zoom infinito · Loops de motion design
// Dibuja tres escenas, cada una en su precomposición y con un marco cuadrado en
// el centro: la fachada tiene una ventana, la habitación un cuadro y el
// escritorio una pantalla. El marco mide 1080 / 4 = 270 px, de 405 a 675 en los
// dos ejes: una escena al 25 % cabe justo en el marco de la otra.
// También crea la composición «Zoom infinito», vacía: ahí se encadenan las escenas.
// 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
app.beginUndoGroup("Zoom infinito");
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 elipse(capa, nombre, cx, cy, w, h, color, giro) {
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);
var t = g().parentProperty.property("ADBE Vector Transform Group");
t.property("ADBE Vector Position").setValue([cx, cy]);
if (giro) t.property("ADBE Vector Rotation").setValue(giro);
}
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);
}
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);
g().addProperty("ADBE Vector Graphic - Stroke");
g().property(2).property("ADBE Vector Stroke Color").setValue(rgb(color));
g().property(2).property("ADBE Vector Stroke Width").setValue(grosor);
g().property(2).property("ADBE Vector Stroke Line Cap").setValue(2);
}
// Una escena: precomposición de 1080 × 1080 con una capa de dibujo. Los grupos
// que se añaden primero quedan delante, así que se dibuja de adelante hacia atrás.
function escena(nombre, dibujar) {
var c = app.project.items.addComp(nombre, 1080, 1080, 1, T, 24);
var capa = c.layers.addShape();
capa.name = "Dibujo";
tr(capa).property("ADBE Position").setValue([0, 0]);
dibujar(capa);
return c;
}
var MARINO = "#1F2A44";
escena("Escena A · Fachada", function (d) {
rect(d, "Alféizar", 370, 687, 710, 706, "#D9A43A");
rect(d, "Ventana del centro", 393, 393, 687, 687, MARINO);
var CENTROS = [108, 324, 540, 756, 972], encendidas = [2, 5, 9, 13, 16, 21, 22];
var n = 0;
for (var f = 0; f < 5; f++) for (var c = 0; c < 5; c++) {
if (f === 2 && c === 2) continue;
var x = CENTROS[c], y = CENTROS[f];
linea(d, "Parteluz " + n, [x, y - 54], [x, y + 54], MARINO, 6);
linea(d, "Travesaño " + n, [x - 54, y], [x + 54, y], MARINO, 6);
var encendida = false;
for (var q = 0; q < encendidas.length; q++) if (encendidas[q] === n) encendida = true;
rect(d, "Vidrio " + n, x - 52, y - 52, x + 52, y + 52, encendida ? "#FFE7A6" : "#3B5566");
rect(d, "Marco " + n, x - 60, y - 60, x + 60, y + 60, MARINO);
n++;
}
rect(d, "Cornisa alta", 0, 196, 1080, 212, "#D9A43A");
rect(d, "Cornisa baja", 0, 868, 1080, 884, "#D9A43A");
rect(d, "Muro", 0, 0, 1080, 1080, "#F2C14E");
});
escena("Escena B · Habitación", function (d) {
rect(d, "Cojín izquierdo", 330, 772, 530, 830, "#2B3A55", 14);
rect(d, "Cojín derecho", 550, 772, 750, 830, "#2B3A55", 14);
rect(d, "Sofá", 300, 760, 780, 880, MARINO, 22);
for (var h = 0; h < 5; h++) elipse(d, "Hoja " + (h + 1), 930 + (h - 2) * 22, 700 - Math.abs(h - 2) * 18, 46, 130, "#3F9B6B", (h - 2) * 24);
poligono(d, "Maceta", [[880, 870], [980, 870], [965, 780], [895, 780]], "#E8473F");
poligono(d, "Pantalla de la lámpara", [[90, 560], [210, 560], [185, 470], [115, 470]], "#F2C14E");
linea(d, "Pie de la lámpara", [150, 870], [150, 560], MARINO, 8);
rect(d, "Marco del cuadro", 387, 387, 693, 693, "#E3B04B");
rect(d, "Sombra del cuadro", 399, 401, 705, 707, "#24495E");
rect(d, "Zoclo", 0, 868, 1080, 884, MARINO);
rect(d, "Piso", 0, 880, 1080, 1080, "#C8724A");
rect(d, "Pared", 0, 0, 1080, 1080, "#2E5E7A");
});
escena("Escena C · Pantalla", function (d) {
linea(d, "Vapor 1", [840, 760], [846, 720], "#F6E7D4", 5);
linea(d, "Vapor 2", [866, 760], [860, 714], "#F6E7D4", 5);
rect(d, "Taza", 820, 780, 890, 860, "#F6E7D4", 8);
linea(d, "Lápiz 1", [190, 770], [176, 700], MARINO, 8);
linea(d, "Lápiz 2", [212, 770], [222, 690], "#F2C14E", 8);
rect(d, "Vaso de lápices", 170, 770, 240, 860, "#3FB7C6", 6);
rect(d, "Base del monitor", 440, 798, 640, 818, MARINO, 8);
poligono(d, "Brazo del monitor", [[505, 715], [575, 715], [590, 800], [490, 800]], MARINO);
rect(d, "Monitor", 365, 365, 715, 715, MARINO, 28);
rect(d, "Borde del escritorio", 0, 860, 1080, 874, "#EAD3BA");
rect(d, "Escritorio", 0, 860, 1080, 1080, "#F6E7D4");
rect(d, "Pared", 0, 0, 1080, 1080, "#E8473F");
});
// ── La composición principal, vacía ──
var comp = app.project.items.addComp("Zoom infinito", 1080, 1080, 1, T, 24);
comp.bgColor = rgb("#F2C14E");
comp.openInViewer();
app.endUndoGroup();
})();
El orden importa: al vincular, After Effects conserva el tamaño en pantalla. Si la escala se pone antes, la hija queda al 100 % de su madre.
// ×4 cada 4 s: el mismo factor que separa una escena de la siguiente var s = 100 * Math.pow(4, time / 4); [s, s]
En 12 s son tres niveles: 100 × 4³ = 6400 %, y la cuarta capa ocupa el lugar de la primera. Los keyframes no sirven aquí: el último tendría que caer en el frame 288, fuera de la composición, y una escala lineal frena.
90 * time / 4.
Cada nivel entra girado un cuarto de vuelta, y el que llena la pantalla queda
derecho.time / 2, y tres escenas más en la
cadena: B, C y A. Si tiene el giro en espiral, también 90 * time / 2.
Seis niveles en 12 s, el doble de rápido; sigue cerrando, porque 6 es múltiplo
de las tres escenas.0 de 7
Cada respuesta explica por qué, acierte o no.
Escenas con luz, sombra y cámara en las que docenas de elementos se mueven con un mismo tiempo. El acabado lleva grano.
Una forma simple clonada en retícula, con el tiempo desfasado según su posición. Pocos colores, para que todo el peso caiga en el movimiento.
Objetos cotidianos convertidos en mecanismos infinitos, que no necesitan a nadie para funcionar, con paletas de mediados del siglo XX.
Sus gifathons: un loop diario durante un mes en una ciudad, y un catálogo de técnicas de bucle —rebote, desplazamiento, isométrico, zoom, giro.
Tutoriales del proceso completo: texturas, falso 3D, transformaciones y el acabado que hace que una pieza plana parezca ilustrada.
El editor de gráficos de After Effects. Las curvas que aquí se pulen, con seis ejercicios.
Los doce principios del motion graphics. Cloning, Offset & Delay y los demás, con sus laboratorios.
Los doce principios de la animación. Arcos, estirar y encoger, anticipación y acción complementaria.
Contadores dinámicos en After Effects. Más expresiones, esta vez aplicadas a cifras.
Adobe. (s. f.). Expression language reference. After Effects User Guide. https://helpx.adobe.com/after-effects/using/expression-language-reference.html
Lodigiani, V. (s. f.). Work [Portafolio]. https://centolodigiani.com/
Motion Design School. (s. f.). Animated loops with James Curran [Curso en línea]. https://motiondesign.school/courses/animated-loops-with-james-curran
School of Motion. (s. f.). Animation Bootcamp [Curso en línea]. https://www.schoolofmotion.com/animation-bootcamp
Thomas, F. y Johnston, O. (1981). The illusion of life: Disney animation. Abbeville Press.
Ward, C. (s. f.). Motion design inspiration: Loops. School of Motion. https://schoolofmotion.com/blog/loop-inspiration-motion-graphics
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.