import type { Scene } from '@/types/scene' const RUNTIME_VERSION = '7' 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); var pa=merge(layer.base,a.props); var pb=merge(layer.base,b.props); var outSample=lerpProps(pa,pb,t); if(b.ctrlX!=null && b.ctrlY!=null){ var om=1-t; outSample.x=om*om*pa.x+2*om*t*b.ctrlX+t*t*pb.x; outSample.y=om*om*pa.y+2*om*t*b.ctrlY+t*t*pb.y; } return outSample; } } 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; // Honour explicit in/out points; fall back to 0 and the animation's own end. var rangeIn=Math.max(0, Math.min(scene.duration, scene.inTime==null?0:scene.inTime)); var autoEnd=effectiveEnd(scene); var rangeOut=Math.max(rangeIn, Math.min(scene.duration, scene.outTime==null?autoEnd:scene.outTime)); var sceneEnd=rangeOut; var cs=getComputedStyle(container); if(cs.position==='static')container.style.position='relative'; var bgVideo=null; if(scene.backgroundVideo && scene.backgroundVideo.src){ bgVideo=document.createElement('video'); bgVideo.src=scene.backgroundVideo.src; bgVideo.muted=true; bgVideo.playsInline=true; bgVideo.loop=!!loop; bgVideo.preload='auto'; bgVideo.style.cssText='position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none;z-index:0;'; container.appendChild(bgVideo); } var canvas=document.createElement('canvas'); canvas.style.cssText='display:block;width:100%;height:100%;position:relative;z-index:1;'; 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; var bgStarted=false; function syncBgVideo(t){ if(!bgVideo) return; if(!bgStarted){ bgStarted=true; try{ bgVideo.play(); }catch(e){} } if(Math.abs(bgVideo.currentTime-t)>0.2){ bgVideo.currentTime=t; } } var rangeLen=Math.max(0, rangeOut-rangeIn); var runtime={ fps: scene.fps||60, rangeIn: rangeIn, duration: scene.duration, markers: {}, jumpTo: null, forcePause: false, forcePlay: false, warn: function(m){ if(window.console)window.console.warn('[animatr]',m); } }; (scene.markers||[]).forEach(function(mk){ runtime.markers[mk.name]=mk.time; }); function parseScriptSource(source){ var outActs=[]; var outErrs=[]; var lines=String(source||'').split(/\\r?\\n/); for(var pi=0;pi=0) ln=ln.slice(0,ci); ln=ln.trim(); if(!ln) continue; var toks=ln.split(/\\s+/).filter(function(x){return x;}); var cmd=(toks[0]||'').toLowerCase(); var parsed=null, err=null; if(cmd==='play') parsed={type:'play'}; else if(cmd==='pause') parsed={type:'pause'}; else if(cmd==='rewind') parsed={type:'rewindToLoop'}; else if(cmd==='jump'){ if(toks.length<2) err='jump: missing argument'; else { var a=toks[1]; if(a.charAt(0)==='#'){ var fr=Number(a.slice(1)); if(!isFinite(fr)) err='jump: invalid frame '+a; else parsed={type:'jumpToFrame',frame:fr}; } else if(a.charAt(0)==='@'){ var nm=a.slice(1); if(!nm) err='jump: missing marker name'; else parsed={type:'jumpToMarker',name:nm}; } else { var tm=Number(a); if(!isFinite(tm)) err='jump: invalid time '+a; else parsed={type:'jumpTo',time:tm}; } } } else { err='unknown command "'+cmd+'"'; } if(err) outErrs.push({line:pi+1,message:err}); else if(parsed) outActs.push(parsed); } return {actions:outActs, errors:outErrs}; } function evalAction(act){ switch(act.type){ case 'play': runtime.forcePlay=true; runtime.forcePause=false; return; case 'pause': runtime.forcePause=true; runtime.forcePlay=false; return; case 'rewindToLoop': runtime.jumpTo=runtime.rangeIn; return; case 'jumpTo': runtime.jumpTo=Math.max(0,Math.min(runtime.duration,act.time)); return; case 'jumpToFrame': runtime.jumpTo=Math.max(0,Math.min(runtime.duration,act.frame/runtime.fps)); return; case 'jumpToMarker': { var mt=runtime.markers[act.name]; if(mt==null){ runtime.warn('Unknown marker: '+act.name); return; } runtime.jumpTo=Math.max(0,Math.min(runtime.duration,mt)); return; } case 'script': { var parsed=parseScriptSource(act.source); if(parsed.errors.length){ for(var sei=0; seiprev && fr.time<=next) toFire.push(fr); } toFire.sort(function(a,b){return a.time-b.time;}); for(var ffj=0; ffj=rangeOut){ fireFrames(prev, rangeOut); currentT = loop ? rangeIn : rangeOut; } else { currentT=nextT; fireFrames(prev, currentT); } if(runtime.jumpTo!=null){ currentT=runtime.jumpTo; runtime.jumpTo=null; } if(runtime.forcePause){ paused=true; runtime.forcePause=false; } if(runtime.forcePlay){ paused=false; runtime.forcePlay=false; } } draw(currentT); syncBgVideo(currentT); requestAnimationFrame(frame); } function tracePolyPath(tctx,sp,ap,offX,offY,closed){ var n=sp.length; if(n<2) return; tctx.beginPath(); tctx.moveTo(sp[0].x-offX,sp[0].y-offY); var endI=closed?n:n-1; for(var ti=0;ti=3){ tracePolyPath(ctxm,mpPts,asset.points,mpOffX,mpOffY,true); ctxm.fill(); } } ctxm.restore(); } 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; } 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)'; tracePolyPath(ctx,pPts,asset.points,pOffX,pOffY,pClosed); var pFill=p.fill||asset.fill; if(pClosed && pFill && pFill!=='transparent'){ ctx.fillStyle=pFill; ctx.fill(); } if(lHasStroke){ ctx.strokeStyle=lStroke; ctx.lineWidth=lSW; ctx.lineJoin='round'; ctx.lineCap='round'; ctx.stroke(); } ctx.filter='none'; } }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(); if(!scene.backgroundVideo){ ctx.fillStyle=scene.canvas.background; ctx.fillRect(0,0,lw,lh); } var layerById={}; for(var li=0;li=0;oi--){ var id=rawOrder[oi]; var lyr=layerById[id]; if(lyr){ if(lyr.isMask){ ctx.save(); ctx.globalCompositeOperation=(lyr.maskMode==='negative'?'destination-out':'destination-in'); drawMaskShape(ctx,lyr,time); ctx.restore(); continue; } 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) }