import type { Scene } from '@/types/scene' const RUNTIME_VERSION = '1' const ENGINE_SOURCE = `(function(){ if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return; function easing(name, t){ if(name==='easeIn')return t*t; if(name==='easeOut')return 1-(1-t)*(1-t); if(name==='easeInOut')return t<0.5?2*t*t:1-Math.pow(-2*t+2,2)/2; return t; } function merge(base,partial){ var o={};for(var k in base)o[k]=base[k]; if(partial)for(var k2 in partial)o[k2]=partial[k2]; return o; } function lerp(a,b,t){return a+(b-a)*t;} function parseHex(hex){ if(!hex)return null; var h=String(hex).trim().replace('#',''); if(h.length===3)h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2]; if(h.length<6)return null; var n=parseInt(h.slice(0,6),16); if(isNaN(n))return null; return [(n>>16)&255,(n>>8)&255,n&255]; } function formatHex(r,g,b){ function c(v){v=Math.round(v);if(v<0)v=0;if(v>255)v=255;var s=v.toString(16);return s.length<2?'0'+s:s;} return '#'+c(r)+c(g)+c(b); } function lerpColor(a,b,t){ var pa=parseHex(a),pb=parseHex(b); if(!pa||!pb)return t<0.5?a:b; return formatHex(lerp(pa[0],pb[0],t),lerp(pa[1],pb[1],t),lerp(pa[2],pb[2],t)); } function lerpProps(a,b,t){ var out={ x:lerp(a.x,b.x,t), y:lerp(a.y,b.y,t), scaleX:lerp(a.scaleX,b.scaleX,t), scaleY:lerp(a.scaleY,b.scaleY,t), rotation:lerp(a.rotation,b.rotation,t), opacity:lerp(a.opacity,b.opacity,t) }; if(a.fill&&b.fill)out.fill=lerpColor(a.fill,b.fill,t); else if(b.fill)out.fill=b.fill; else if(a.fill)out.fill=a.fill; return out; } function sample(layer,time){ var kfs=layer.keyframes.slice().sort(function(a,b){return a.time-b.time;}); if(!kfs.length)return merge(layer.base); if(time<=kfs[0].time)return merge(layer.base,kfs[0].props); var last=kfs[kfs.length-1]; if(time>=last.time)return merge(layer.base,last.props); for(var i=0;i=a.time&&time<=b.time){ if(a.hold){ if(time>=b.time)return merge(layer.base,b.props); return merge(layer.base,a.props); } var raw=(time-a.time)/(b.time-a.time); var t=easing(b.easing,raw); return lerpProps(merge(layer.base,a.props),merge(layer.base,b.props),t); } } return merge(layer.base); } function loadImages(assets){ var imgs={}; var list=assets.filter(function(a){return a.kind==='image';}); return Promise.all(list.map(function(a){ return new Promise(function(resolve){ var img=new Image(); img.onload=function(){imgs[a.id]=img;resolve();}; img.onerror=function(){resolve();}; img.src=a.src; }); })).then(function(){return imgs;}); } function effectiveEnd(scene){ var max=0; for(var i=0;imax) max=kfs[j].time; } } var instances=scene.clipInstances||[]; var clips=scene.clips||[]; var clipById={}; for(var ci=0;cimax) max=end; } if(max<=0) return scene.duration; return Math.min(max,scene.duration); } function mount(container,scene,opts){ if(!container)return; opts=opts||{}; var loop=opts.loop!==false; var sceneEnd=effectiveEnd(scene); var cs=getComputedStyle(container); if(cs.position==='static')container.style.position='relative'; var canvas=document.createElement('canvas'); canvas.style.cssText='display:block;width:100%;height:100%;'; container.appendChild(canvas); var ctx=canvas.getContext('2d'); var dpr=window.devicePixelRatio||1; var size={w:0,h:0}; function resize(){ var r=container.getBoundingClientRect(); size.w=r.width;size.h=r.height; canvas.width=Math.max(1,Math.round(r.width*dpr)); canvas.height=Math.max(1,Math.round(r.height*dpr)); } var ro=new ResizeObserver(resize); ro.observe(container); resize(); var assetMap={}; scene.assets.forEach(function(a){assetMap[a.id]=a;}); var imgs={}; var start=0; function frame(now){ if(!start)start=now; var t=(now-start)/1000; if(sceneEnd<=0){ draw(0); requestAnimationFrame(frame); return; } if(loop) t=t%sceneEnd; else if(t>sceneEnd) t=sceneEnd; draw(t); requestAnimationFrame(frame); } function drawLayer(layer,time){ var asset=assetMap[layer.assetId]; if(!asset||!layer.visible)return; var p=sample(layer,time); ctx.save(); ctx.globalAlpha=ctx.globalAlpha*p.opacity; ctx.translate(p.x,p.y); ctx.rotate(p.rotation*Math.PI/180); ctx.scale(p.scaleX,p.scaleY); var sOp=p.shadowOpacity||0; var sBl=p.shadowBlur||0; var sDi=p.shadowDistance||0; if(sOp>0 && (sBl>0 || sDi>0)){ var sAn=(p.shadowAngle==null?90:p.shadowAngle)*Math.PI/180; var sHex=(p.shadowColor||'#000000').replace('#',''); if(sHex.length===3){sHex=sHex[0]+sHex[0]+sHex[1]+sHex[1]+sHex[2]+sHex[2];} var sNum=parseInt(sHex.slice(0,6),16); if(isNaN(sNum))sNum=0; ctx.shadowColor='rgba('+((sNum>>16)&255)+','+((sNum>>8)&255)+','+(sNum&255)+','+sOp+')'; ctx.shadowBlur=sBl; ctx.shadowOffsetX=Math.cos(sAn)*sDi; ctx.shadowOffsetY=Math.sin(sAn)*sDi; } if(asset.kind==='rect'){ var rOffX=layer.offsetX!=null?layer.offsetX:asset.width/2; var rOffY=layer.offsetY!=null?layer.offsetY:asset.height/2; var rBlur=(p.blur!=null?p.blur:(asset.blur||0)); ctx.fillStyle=p.fill||asset.fill; if(rBlur>0) ctx.filter='blur('+rBlur+'px)'; ctx.fillRect(-rOffX,-rOffY,asset.width,asset.height); ctx.filter='none'; }else if(asset.kind==='circle'){ var cOffX=layer.offsetX!=null?layer.offsetX:0; var cOffY=layer.offsetY!=null?layer.offsetY:0; var cBlur=(p.blur!=null?p.blur:(asset.blur||0)); ctx.fillStyle=p.fill||asset.fill; if(cBlur>0) ctx.filter='blur('+cBlur+'px)'; ctx.beginPath(); ctx.arc(-cOffX,-cOffY,asset.radius,0,Math.PI*2); ctx.fill(); ctx.filter='none'; }else if(asset.kind==='text'){ var tOffX=layer.offsetX!=null?layer.offsetX:0; var tOffY=layer.offsetY!=null?layer.offsetY:0; ctx.fillStyle=p.fill||asset.fill; ctx.font=asset.fontSize+'px sans-serif'; ctx.textBaseline='top'; ctx.fillText(asset.text,-tOffX,-tOffY); }else if(asset.kind==='image'){ var img=imgs[asset.id]; if(img){ var iOffX=layer.offsetX!=null?layer.offsetX:asset.width/2; var iOffY=layer.offsetY!=null?layer.offsetY:asset.height/2; ctx.drawImage(img,-iOffX,-iOffY,asset.width,asset.height); } } ctx.shadowColor='transparent'; ctx.shadowBlur=0; ctx.shadowOffsetX=0; ctx.shadowOffsetY=0; ctx.restore(); } function draw(time){ var lw=scene.canvas.width,lh=scene.canvas.height; var cw=size.w,ch=size.h; if(!cw||!ch)return; var s=Math.min(cw/lw,ch/lh); var ox=(cw-lw*s)/2; var oy=(ch-lh*s)/2; ctx.setTransform(dpr,0,0,dpr,0,0); ctx.clearRect(0,0,cw,ch); ctx.save(); ctx.translate(ox,oy); ctx.scale(s,s); ctx.beginPath(); ctx.rect(0,0,lw,lh); ctx.clip(); ctx.fillStyle=scene.canvas.background; ctx.fillRect(0,0,lw,lh); var layerById={}; for(var li=0;li=0;oi--){ var id=order[oi]; var lyr=layerById[id]; if(lyr){ drawLayer(lyr,time); continue; } var tInsts=instByTrack[id]||[]; for(var ti=0;tivisible)continue; var localT=tS+offset; for(var cl2=clip2.layers.length-1;cl2>=0;cl2--){ drawLayer(clip2.layers[cl2],localT); } } } ctx.restore(); } loadImages(scene.assets).then(function(loaded){ imgs=loaded; requestAnimationFrame(frame); }); } var api={ __v: '${RUNTIME_VERSION}', mount: mount }; window.AnimatrPlayer = api; var pending = window.__animatrPending; window.__animatrPending = null; if (pending) for (var i=0;i Animatr — Example

Animatr Export

Load the runtime once, then embed as many scenes on the page as you like.

Embed on your own page

<!-- once per page -->
<script src="animatr-runtime.js"></script>

<!-- per animation: one container + scene script -->
<div style="width: 800px; height: 450px;">
  <script src="animation.js"></script>
</div>

<div style="width: 400px; height: 225px;">
  <script src="animation-2.js"></script>
</div>
` } export function downloadExampleHtml(scene: Scene, filename = 'animation-example.html') { const source = generateExampleHtml(scene) triggerDownload(source, filename, 'text/html') } function triggerDownload(source: string, filename: string, mime: string) { const blob = new Blob([source], { type: mime }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename document.body.appendChild(a) a.click() document.body.removeChild(a) setTimeout(() => URL.revokeObjectURL(url), 1000) }