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; if(a.points&&b.points&&a.points.length===b.points.length){ var lp=[]; for(var lpi=0;lpi=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 drawMaskShape(ctxm,layer,time){ var asset=assetMap[layer.assetId]; if(!asset) return; if(asset.kind!=='rect' && asset.kind!=='circle' && asset.kind!=='text' && asset.kind!=='polygon') return; var p=sample(layer,time); ctxm.save(); ctxm.translate(p.x,p.y); ctxm.rotate(p.rotation*Math.PI/180); ctxm.scale(p.scaleX,p.scaleY); ctxm.fillStyle='#000000'; if(asset.kind==='rect'){ var mrOff=layer.offsetX!=null?layer.offsetX:asset.width/2; var mrOffY=layer.offsetY!=null?layer.offsetY:asset.height/2; ctxm.fillRect(-mrOff,-mrOffY,asset.width,asset.height); } else if(asset.kind==='circle'){ var mcOff=layer.offsetX!=null?layer.offsetX:0; var mcOffY=layer.offsetY!=null?layer.offsetY:0; ctxm.beginPath(); ctxm.arc(-mcOff,-mcOffY,asset.radius,0,Math.PI*2); ctxm.fill(); } else if(asset.kind==='text'){ var mtOff=layer.offsetX!=null?layer.offsetX:0; var mtOffY=layer.offsetY!=null?layer.offsetY:0; ctxm.font=asset.fontSize+'px sans-serif'; ctxm.textBaseline='top'; ctxm.fillText(asset.text,-mtOff,-mtOffY); } else { var mpOffX=layer.offsetX!=null?layer.offsetX:0; var mpOffY=layer.offsetY!=null?layer.offsetY:0; var mpPts=p.points||asset.points||[]; var mpClosed=asset.closed!==false; if(mpClosed && mpPts.length>=3){ ctxm.beginPath(); ctxm.moveTo(mpPts[0].x-mpOffX,mpPts[0].y-mpOffY); for(var mpI=1;mpI0 && (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; } var lRawStroke=p.stroke||asset.stroke; var lSW=(p.strokeWidth!=null?p.strokeWidth:(asset.strokeWidth||0)); var lStroke=lSW>0?(lRawStroke||'#000000'):null; var lHasStroke=!!lStroke && lStroke!=='transparent' && lSW>0; 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)); if(rBlur>0) ctx.filter='blur('+rBlur+'px)'; var rFill=p.fill||asset.fill; if(rFill && rFill!=='transparent'){ ctx.fillStyle=rFill; ctx.fillRect(-rOffX,-rOffY,asset.width,asset.height); } if(lHasStroke){ ctx.strokeStyle=lStroke; ctx.lineWidth=lSW; ctx.strokeRect(-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)); if(cBlur>0) ctx.filter='blur('+cBlur+'px)'; ctx.beginPath(); ctx.arc(-cOffX,-cOffY,asset.radius,0,Math.PI*2); var cFill=p.fill||asset.fill; if(cFill && cFill!=='transparent'){ ctx.fillStyle=cFill; ctx.fill(); } if(lHasStroke){ ctx.strokeStyle=lStroke; ctx.lineWidth=lSW; ctx.stroke(); } 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.font=asset.fontSize+'px sans-serif'; ctx.textBaseline='top'; var tFill=p.fill||asset.fill; if(tFill && tFill!=='transparent'){ ctx.fillStyle=tFill; ctx.fillText(asset.text,-tOffX,-tOffY); } if(lHasStroke){ ctx.strokeStyle=lStroke; ctx.lineWidth=lSW; ctx.strokeText(asset.text,-tOffX,-tOffY); } }else if(asset.kind==='polygon'){ var pOffX=layer.offsetX!=null?layer.offsetX:0; var pOffY=layer.offsetY!=null?layer.offsetY:0; var pBlur=(p.blur!=null?p.blur:(asset.blur||0)); var pPts=p.points||asset.points||[]; var pClosed=asset.closed!==false; var pNeeded=pClosed?3:2; if(pPts.length>=pNeeded){ if(pBlur>0) ctx.filter='blur('+pBlur+'px)'; ctx.beginPath(); ctx.moveTo(pPts[0].x-pOffX,pPts[0].y-pOffY); for(var pI=1;pI=0;oi--){ var id=rawOrder[oi]; var lyr=layerById[id]; if(lyr){ drawLayer(lyr,time); continue; } var tInsts=instByTrack[id]||[]; if(tInsts.length){ for(var ti=0;tivisibleClip)continue; var localT=tS+offset; for(var cl2=clip2.layers.length-1;cl2>=0;cl2--){ drawLayer(clip2.layers[cl2],localT); } } continue; } var grp=groupById2[id]; if(grp){ var gContent=[], gMasks=[]; for(var ch=grp.childIds.length-1;ch>=0;ch--){ var cl0=layerById[grp.childIds[ch]]; if(!cl0) continue; if(cl0.isMask) gMasks.push(cl0); else gContent.push(cl0); } if(!gMasks.length){ for(var gi3=0;gi3 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) }