What happened when we ran it
We sent
Act as a motion designer who builds short animated films entirely in code, with no image or video generators.
Task: Build a 10-second animated film in the style of flat vector with bold colour blocks, rounded shapes and bouncy, springy easing for Greenfold, a small Bristol company that sells refillable cleaning products by subscription, as a single web page whose frame can be drawn for any moment in time.
Steps:
1. Write a style note listing the palette, type, shapes and movement rules that define flat vector with bold colour blocks, rounded shapes and bouncy, springy easing.
2. Plan the film as a beat sheet, giving each scene a start time, end time and one clear idea, based on this message: One bottle, refilled again and again, replaces dozens of plastic bottles thrown away each year, saving households money and cutting waste..
3. Write one self-contained HTML file with a function renderAt(t) that draws every element using SVG or Canvas, so any frame can be captured on demand.
4. Describe a soundtrack and sound effects that could be synthesised in code, matched to each beat.
5. Review your own work against the style note, name two weaknesses, then fix them.
Output: the style note, the beat sheet as a table, the complete HTML code in one block, the audio plan as a short list, and a two-line review summary.
The model replied
# Greenfold: "One Bottle" (10 s)
I haven't opened this in a browser, so it is untested. It is a pure function of `t`, so you can check any frame with `?t=4.2`.
## 1. Style note
- **Palette:**
- Coral `#FF6B4A`, the problem colour.
- Cream `#FFF3DC`, lime `#C6F26B` and deep green `#0E4D32`, the Greenfold colours.
- Leaf green `#1F8A4C` and `#7ED957`, yellow `#FFC93C`, sky `#5CC8FF`, ink `#14301F`.
- Dull grey-blue `#9DB4C6` is reserved for plastic, so waste always looks lifeless.
- **Type:** One heavy rounded sans (Nunito, then Arial Rounded or Trebuchet, weight 900). Headlines are 46–120 px, with at most two lines at a time and ink on light or cream on dark.
- **Shapes:**
- Everything is a flat fill with no outlines, gradients or shadows.
- Every corner is rounded (rx ≥ 8).
- Circles act as colour blocks and wipe transitions.
- Bottles, pouches, coins and leaves are built from rounded rectangles and one leaf path.
- **Movement:**
- Objects arrive with a damped spring that overshoots by about 15% and settles.
- Falling things use a bounce.
- Impacts add squash and stretch.
- Scene changes are circle wipes that overshoot.
- Nothing moves linearly, and nothing sits completely still.
## 2. Beat sheet
| # | Start | End | Idea |
|---|-------|-----|------|
| 1 | 0.0 s | 2.0 s | **Problem:** a dozen plastic bottles bounce into a pile. "Dozens of plastic bottles binned every year." |
| 2 | 2.0 s | 3.0 s | **Hero:** a cream wipe lands, one green Greenfold bottle drops in, and "One bottle, refilled again and again." appears. |
| 3 | 3.0 s | 6.0 s | **Loop:** the bottle is used, a pouch tips in to refill it, and a ×1, ×2, ×3 counter pops, one cycle per second. |
| 4 | 6.0 s | 8.0 s | **Payoff:** a lime wipe. A coin stack builds under "Save money. Cut waste." A long grey "Dozens" bar sits against a tiny green "1" bar. |
| 5 | 8.0 s | 10.0 s | **Brand:** a deep-green wipe. A logo, "Greenfold", "Refill. Repeat." and "Bristol" appear, with two bottles hopping. |
## 3. HTML
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Greenfold - One Bottle</title>
<style>
html,body{margin:0;height:100%;background:#0b1a12;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:system-ui,sans-serif;color:#fff}
svg{width:min(100vw,calc((100vh - 50px)*16/9));aspect-ratio:16/9;display:block;
font-family:'Nunito','Baloo 2','Arial Rounded MT Bold','Trebuchet MS',system-ui,sans-serif;font-weight:900}
#ui{display:flex;gap:12px;align-items:center;padding:10px;width:min(90vw,700px)}
#ui input{flex:1}
body.clean #ui{display:none}
</style>
</head>
<body>
<svg id="stage" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg"></svg>
<div id="ui"><button id="pb">Pause</button><input id="sc" type="range" min="0" max="10" step="0.01" value="0"><span id="tl">0.00s</span></div>
<script>
const W=1280,H=720,DUR=10;
const C={cream:'#FFF3DC',coral:'#FF6B4A',coralD:'#E5502F',green:'#1F8A4C',deep:'#0E4D32',lime:'#C6F26B',
leaf:'#7ED957',yellow:'#FFC93C',yellowD:'#E8A100',sky:'#5CC8FF',skyD:'#2E9FD6',ink:'#14301F',
plastic:'#9DB4C6',plasticD:'#7C97AB'};
/* ---------- maths & easing ---------- */
const clamp=(x,a=0,b=1)=>Math.min(b,Math.max(a,x));
const lerp=(a,b,k)=>a+(b-a)*k;
const sm=(a,b,x)=>{const k=clamp((x-a)/(b-a));return k*k*(3-2*k)};
const spring=p=>p<=0?0:p>=1?1:1-Math.exp(-5.5*p)*Math.cos(9*p); // ~15% overshoot, settles
const pop=(t,s,d=0.8)=>spring((t-s)/d);
const eob=p=>{p=clamp(p);const c1=1.70158,c3=c1+1;return 1+c3*Math.pow(p-1,3)+c1*Math.pow(p-1,2)};
const bounce=x=>{const n=7.5625,d=2.75;x=clamp(x);
if(x<1/d)return n*x*x; if(x<2/d){x-=1.5/d;return n*x*x+.75}
if(x<2.5/d){x-=2.25/d;return n*x*x+.9375} x-=2.625/d;return n*x*x+.984375};
const rnd=i=>{const x=Math.sin(i*127.1+311.7)*43758.5453;return x-Math.floor(x)};
/* ---------- primitives ---------- */
const T=(s,x,y,size,fill,o={})=>{
const k=o.k===undefined?1:o.k; if(k<=0.001)return '';
const op=o.op===undefined?clamp(k*4):o.op;
return `<text transform="translate(${x} ${y}) rotate(${o.r||0}) scale(${k})" font-size="${size}" fill="${fill}" text-anchor="${o.a||'middle'}" opacity="${op}">${s}</text>`;
};
const leaf=(x,y,s,rot,fill,vein)=>`<g transform="translate(${x} ${y}) rotate(${rot}) scale(${s})">
<path d="M-26 28 C-30 -10 0 -34 30 -32 C34 0 10 30 -26 28Z" fill="${fill}"/>
<path d="M-22 24 L16 -16" stroke="${vein}" stroke-width="4" stroke-linecap="round"/></g>`;
const bottle=(x,y,s,rot,body,cap)=>`<g transform="translate(${x} ${y}) rotate(${rot}) scale(${s})">
<rect x="-15" y="-44" width="30" height="40" rx="9" fill="${body}"/>
<rect x="-18" y="-62" width="36" height="24" rx="9" fill="${cap}"/>
<rect x="-38" y="-12" width="76" height="116" rx="26" fill="${body}"/>
<rect x="-26" y="4" width="10" height="60" rx="5" fill="#fff" opacity=".5"/></g>`;
function hero(x,y,s,lvl,q,sq,id,body=C.green){
const sx=s*(1-sq), sy=s*(1+sq);
const capT=`translate(${q*80} ${-q*70}) rotate(${q*28} 0 -50)`;
return `<g transform="translate(${x} ${y-104*s*sq}) scale(${sx} ${sy})">
<defs><clipPath id="${id}"><rect x="-24" y="0" width="48" height="64" rx="18"/></clipPath></defs>
<rect x="-15" y="-44" width="30" height="40" rx="9" fill="${body}"/>
<rect x="-38" y="-12" width="76" height="116" rx="26" fill="${body}"/>
<rect x="-24" y="0" width="48" height="64" rx="18" fill="${C.cream}"/>
<rect x="-24" y="${64*(1-lvl)}" width="48" height="${64*lvl+1}" fill="${C.leaf}" clip-path="url(#${id})"/>
${leaf(0,86,.42,0,C.cream,body)}
<g transform="${capT}"><rect x="-18" y="-62" width="36" height="24" rx="9" fill="${C.yellow}"/></g></g>`;
}
function pouch(cx,cy,a){return `<g transform="translate(${cx} ${cy}) rotate(${a})">
<rect x="-55" y="-80" width="110" height="160" rx="30" fill="${C.yellow}"/>
<rect x="-53" y="-100" width="26" height="30" rx="9" fill="${C.yellowD}"/>
<rect x="-56" y="-110" width="32" height="16" rx="7" fill="${C.coral}"/>
<circle cx="0" cy="12" r="34" fill="${C.coral}"/>${leaf(0,12,.55,0,C.cream,C.coral)}
<rect x="-44" y="-62" width="9" height="90" rx="4.5" fill="#fff" opacity=".45"/></g>`}
const coin=(x,y)=>`<rect x="${x-90}" y="${y-8}" width="180" height="44" rx="22" fill="${C.yellowD}"/>
<rect x="${x-90}" y="${y-22}" width="180" height="44" rx="22" fill="${C.yellow}"/>
<rect x="${x-58}" y="${y-12}" width="116" height="8" rx="4" fill="#FFE08A"/>`;
function wipe(t,start,color,cx,cy){
const p=(t-start)/0.9; if(p<=0)return '';
return `<circle cx="${cx}" cy="${cy}" r="${1500*eob(sm(0,1,p))}" fill="${color}"/>`; // eased start, then overshoot
}
/* ---------- scene 1: the problem (0-2s) ---------- */
function s1(t){
let o=`<rect x="-20" y="676" width="1320" height="80" rx="30" fill="${C.coralD}"/>`;
const rows=[5,4,3]; let i=0;
for(let r=0;r<3;r++)for(let k=0;k<rows[r];k++,i++){
const d=0.15+i*0.1; if(t<d)continue;
const p=clamp((t-d)/0.75);
const rx=640+(k-(rows[r]-1)/2)*84+(rnd(i)-.5)*14, ry=616-r*66;
const y=lerp(-140,ry,bounce(p));
const rot=(rnd(i+3)-.5)*110*sm(0,.8,p)+(1-p)*160*(rnd(i)>.5?1:-1);
o+=bottle(rx,y,.62,rot,i%3==0?C.sky:C.plastic,i%3==0?C.skyD:C.plasticD);
}
o+=T('Dozens of plastic bottles',640,160,66,C.cream,{k:pop(t,.3)});
o+=T('binned every year.',640,240,66,C.yellow,{k:pop(t,.55)});
return o;
}
/* ---------- scene 2: hero + refill loop (2-6s) ---------- */
function s2(t){
let o='';
o+=`<circle cx="1150" cy="70" r="${80*pop(t,2.3)}" fill="${C.coral}"/>`;
o+=`<circle cx="1090" cy="190" r="${34*pop(t,2.45)}" fill="${C.sky}"/>`;
o+=`<circle cx="430" cy="470" r="${250*pop(t,2.15,.9)}" fill="${C.lime}"/>`;
o+=`<rect x="-20" y="640" width="1320" height="100" rx="40" fill="${C.green}"/>`;
// cycle state: 1.0 s per refill cycle (120 bpm, 2 beats)
const c=(t-3)/1.0;
let lvl=1,f=0,q=0,n=0,sq=0,amt=0;
if(t>=3&&c<3){
f=c%1;
lvl=f<.5?1-.8*sm(.05,.45,f):.2+.8*sm(.58,.93,f);
q=f<.42?0:eob((f-.42)/.16)*(1-sm(.9,1,f));
amt=sm(.56,.6,f)*(1-sm(.92,.95,f));
}
n=c>=1?Math.min(3,Math.floor(c)):0;
if(n>=1){const f2=c-n; sq=0.09*Math.exp(-7*f2)*Math.cos(22*f2);}
// pouch pose
const ox=600*(1-eob((t-2.9)/0.7));
const a=lerp(-12,-135,q), ar=a*Math.PI/180;
const pcx=lerp(780+ox,475,q), pcy=lerp(330,225,q);
const spx=pcx+(-40)*Math.cos(ar)-(-104)*Math.sin(ar);
const spy=pcy+(-40)*Math.sin(ar)+(-104)*Math.cos(ar);
// pour stream sits BEHIND the bottle so it never crosses the (popped-off) cap
if(amt>0.01){
const surf=440+64*(1-lvl)*1.9;
o+=`<line x1="${spx}" y1="${spy}" x2="430" y2="${surf}" stroke="${C.leaf}" stroke-width="${14*amt}" stroke-linecap="round"/>`;
}
// bubbles while the bottle is being used
if(t>=3&&c<3&&f<.5){
for(let i=0;i<5;i++){
const b=clamp((f-.03-i*.06)/.3);
if(b>0&&b<1){
o+=`<circle cx="${500+(rnd(i)-.2)*150*b}" cy="${330-b*150}" r="${(10+12*rnd(i+9))*Math.sin(Math.PI*b)}" fill="${C.sky}"/>`;
}
}
}
// hero bottle drops in with a real bounce
if(t>=2.3){
const y=lerp(-260,440,bounce((t-2.3)/0.8));
o+=hero(430,y,1.9,lvl,q,sq,'w');
}
if(t>=2.9) o+=pouch(pcx,pcy,a);
o+=T('One bottle, refilled again and again.',640,92,46,C.ink,{k:pop(t,2.4)});
if(n>=1){
const k=pop(t,3+n,.7);
o+=`<circle cx="1030" cy="430" r="${100*k}" fill="${C.yellow}"/>`;
o+=T('\u00d7'+n,1030,458,88,C.ink,{k});
o+=T('refills',1030,510,32,C.ink,{k});
}
return o;
}
/* ---------- scene 3: payoff (6-8s) ---------- */
function s3(t){
let o='';
o+=T('Save money. Cut waste.',640,110,70,C.ink,{k:pop(t,6.3)});
for(let i=0;i<6;i++){
const d=6.5+i*.15; if(t<d)continue;
o+=coin(300,lerp(-80,598-i*30,bounce((t-d)/.55)));
}
const kc=pop(t,7.4,.8);
o+=`<g transform="translate(300 ${598-6*30-26}) scale(${kc})"><circle r="56" fill="${C.yellowD}"/><circle r="48" fill="${C.yellow}"/></g>`;
o+=T('\u00a3',300,598-6*30-6,64,C.ink,{k:kc});
o+=T('Save money',300,682,40,C.ink,{k:pop(t,7.0)});
o+=T('Plastic bottles bought a year',600,250,30,C.ink,{a:'start',k:pop(t,6.6),op:.8});
const w1=560*pop(t,6.7,.8), w2=100*pop(t,7.1,.8);
if(w1>1)o+=`<rect x="600" y="285" width="${w1}" height="84" rx="42" fill="${C.plastic}"/>`;
o+=T('Dozens',640,339,40,C.cream,{a:'start',k:pop(t,6.9)});
if(w2>1)o+=`<rect x="600" y="405" width="${w2}" height="84" rx="42" fill="${C.green}"/>`;
o+=T('1 refillable bottle',600+w2+24,459,36,C.ink,{a:'start',k:pop(t,7.3)});
const kl=pop(t,7.5,.8);
o+=`<circle cx="1100" cy="590" r="${72*kl}" fill="${C.green}"/>`;
o+=leaf(1100,590,1.5*kl,10,C.lime,C.green);
o+=`<circle cx="1210" cy="520" r="${26*pop(t,7.65)}" fill="${C.coral}"/>`;
return o;
}
/* ---------- scene 4: brand card (8-10s) ---------- */
function s4(t){
let o='';
o+=`<circle cx="1130" cy="110" r="${90*pop(t,8.1)}" fill="${C.coral}"/>`;
o+=`<circle cx="130" cy="130" r="${50*pop(t,8.2)}" fill="${C.sky}"/>`;
o+=`<rect x="${580}" y="${650}" width="120" height="40" rx="20" fill="${C.yellow}" transform="scale(${pop(t,8.3)})" style="transform-origin:640px 670px"/>`;
const kk=pop(t,8.15,.9);
o+=`<g transform="translate(640 250) rotate(${Math.sin(t*3)*3}) scale(${kk})"><circle r="110" fill="${C.cream}"/>${leaf(0,0,3,0,C.green,C.cream)}</g>`;
o+=T('Greenfold',640,480,124,C.cream,{k:pop(t,8.4)});
o+=T('Refill. Repeat.',640,555,58,C.lime,{k:pop(t,8.65)});
o+=T('Refillable cleaning by subscription \u00b7 Bristol',640,615,28,C.cream,{k:pop(t,8.9),op:.9});
[[220,8.6,0],[1060,8.75,.4]].forEach(([x,st,off],i)=>{
const k=pop(t,st,.8); if(k<=0.001)return;
let hop=0,sq=0;
if(t>=9.0){
const ph=((t-9.0+off)%0.8+0.8)%0.8;
hop=-50*Math.sin(Math.PI*clamp(ph/.5));
if(ph>.5){const g=ph-.5; sq=0.09*Math.exp(-9*g)*Math.cos(26*g);}
}
o+=hero(x,440+hop,1.3*k,1,0,sq,'b'+i,C.leaf);
});
return o;
}
/* ---------- the frame function ---------- */
const svg=document.getElementById('stage');
function renderAt(t){
t=clamp(t,0,DUR);
let o=`<rect width="${W}" height="${H}" fill="${C.coral}"/>`;
if(t<2.7) o+=s1(t);
o+=wipe(t,2.0,C.cream,640,400);
if(t>=2.0&&t<6.7) o+=s2(t);
o+=wipe(t,6.0,C.lime,1030,430);
if(t>=6.0&&t<8.7) o+=s3(t);
o+=wipe(t,8.0,C.deep,640,300);
if(t>=8.0) o+=s4(t);
svg.innerHTML=o;
}
window.renderAt=renderAt;
/* ---------- player (not part of the frame logic) ---------- */
const qs=new URLSearchParams(location.search);
let playing=!qs.has('t'), clock=qs.has('t')?+qs.get('t'):0, last=performance.now();
if(qs.has('clean'))document.body.classList.add('clean');
const sc=document.getElementById('sc'),tl=document.getElementById('tl'),pb=document.getElementById('pb');
pb.onclick=()=>{playing=!playing;pb.textContent=playing?'Pause':'Play'};
sc.oninput=()=>{clock=+sc.value;playing=false;pb.textContent='Play'};
function loop(now){
if(playing)clock=(clock+(now-last)/1000)%DUR;
last=now; renderAt(clock); sc.value=clock; tl.textContent=clock.toFixed(2)+'s';
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>
```
## 4. Audio plan
All of it can be made with the Web Audio API or an OfflineAudioContext. The tempo is 120 bpm, so the 2 s, 3 s, 6 s and 8 s cuts land on beats.
- **0–2 s, problem:**
- A muted, slightly detuned square-wave bass pulse.
- A short noise burst through a band-pass filter plus a sine thump (pitch 300→120 Hz) for each bottle landing. Pitch each one a little differently.
- A low rising filter sweep into the cut.
- **2–3 s, hero:**
- A cream-wipe "whoosh" from white noise with a rising band-pass.
- A marimba-style drop (sine plus a fast-decaying 2× partial) when the bottle lands.
- A bright major chord on a plucked-synth patch.
- **3–6 s, loop:**
- A bouncy bass: a four-note pattern on beats 1 and 3, using triangle waves with a short pitch glide.
- A hi-hat from filtered noise on the off-beats.
- A "pssst" noise burst plus rising bubble blips (sine glides) during use.
- A cap pop (a 600→900 Hz sine click).
- A glug sound during the pour: a low sine with fast vibrato, rising in pitch as the bottle fills.
- A coin-style ping at each ×1, ×2 and ×3, one step higher each time (C5, E5, G5).
- **6–8 s, payoff:**
- A swoosh on the lime wipe.
- A coin clink for each coin landing: two high sines (about 2.1 kHz and 3.2 kHz) with a quick decay.
- A sparkle arpeggio when the £ coin lands.
- A soft whoomp as the bars slide out, then a bright "ding" on the small green bar.
- **8–10 s, brand:**
- A deep-green swoosh into a big major chord (pad plus pluck).
- A rising three-note motif synced to the wordmark and tagline.
- A boing at each bottle hop (sine glide 200→400 Hz).
- A tail with a 1 s filter-sweep release.
## 5. Review
**Review:** Against the style note, the layout, palette, rounded shapes and spring easing hold up. Two weaknesses came out of the first pass:
1. The pour stream crossed the bottle cap, which made the refill read as a glitch. I fixed it by popping the cap off with a bounce and drawing the stream behind the bottle.
2. The circle wipes snapped open too abruptly for the springy feel. I fixed it by easing the wipe's start with smoothstep and then letting it overshoot.
Run on Claude Sonnet 5.5, 30 Oct 2026. Automatic check: 1 out of 5.