[{"data":1,"prerenderedAt":460},["ShallowReactive",2],{"\u002Fdocs\u002Fexpression-engine-over-time":3},{"id":4,"title":5,"body":6,"category":448,"date":449,"description":450,"extension":451,"heroImage":452,"meta":453,"navigation":454,"path":455,"readingMinutes":456,"seo":457,"stem":458,"tool":448,"__hash__":459},"docs\u002Fdocs\u002Fexpression-engine-over-time.md","The expression engine: animate any property over TIME",{"type":7,"value":8,"toc":439},"minimark",[9,22,27,35,55,73,84,88,98,108,115,143,146,165,169,180,186,205,209,216,222,254,258,261,296,311,315,325,328,420,424,435],[10,11,12],"blockquote",{},[13,14,15,16,21],"p",{},"Looking for the visual, step-by-step version? Start with ",[17,18,20],"a",{"href":19},"\u002Fdocs\u002Fhow-to-video-templates","How to: build a video template in Studio",". This article is the model underneath that workflow, for when you want to understand exactly what the timeline is writing.",[23,24,26],"h2",{"id":25},"properties-are-expressions-not-constants","Properties are expressions, not constants",[13,28,29,30,34],{},"In most template tools a property is a fixed number. ",[31,32,33],"code",{},"x = 750",". That is why those templates cannot move: there is nothing to animate.",[13,36,37,38,42,43,46,47,50,51,54],{},"In a Bluepic template, a property is an ",[39,40,41],"strong",{},"expression"," that is evaluated for every render. ",[31,44,45],{},"x"," might be ",[31,48,49],{},"750",", or ",[31,52,53],{},"750 + logoWidth",", or a function of your data. That is already what makes a template survive real content: when the text is longer than the placeholder, the expressions re-fit the layout instead of overflowing.",[13,56,57,58,61,62,65,66,65,69,72],{},"Animation adds exactly one thing to that model: a variable called ",[31,59,60],{},"TIME",", in seconds. Evaluate the same expressions at ",[31,63,64],{},"TIME = 0",", ",[31,67,68],{},"TIME = 0.04",[31,70,71],{},"TIME = 0.08",", and so on across the clip, and the design moves. A video is the template sampled across a timeline it always had.",[13,74,75,76,83],{},"You build these expressions ",[39,77,78,79],{},"visually in ",[17,80,82],{"href":81},"\u002Fdocs\u002Fbuilding-templates-in-bluepic-studio","Bluepic Studio","; you rarely type them by hand. But understanding the model is what makes the rest of the platform make sense, so here is what is under the hood.",[23,85,87],{"id":86},"animate-a-keyframe-track","ANIMATE: a keyframe track",[13,89,90,91,94,95,97],{},"The workhorse is ",[31,92,93],{},"ANIMATE",". You give it ",[31,96,60],{}," and a list of keyframes, and it returns the value at that moment.",[99,100,105],"pre",{"className":101,"code":103,"language":104},[102],"language-text","ANIMATE(TIME, keyframe, keyframe, keyframe, …)\n","text",[31,106,103],{"__ignoreMap":107},"",[13,109,110,111,114],{},"A keyframe is a ",[31,112,113],{},"(time, value)"," pair. There are two kinds:",[116,117,118,133],"ul",{},[119,120,121,124,125,128,129,132],"li",{},[31,122,123],{},"LINEAR(t, v)"," is a keyframe at time ",[31,126,127],{},"t"," (seconds) with value ",[31,130,131],{},"v",", reached by straight interpolation.",[119,134,135,138,139,142],{},[31,136,137],{},"CUBIC(t, v, x1, y1, x2, y2)"," is the same keyframe, but reached with a cubic-bezier easing curve (the four numbers are its control points, exactly like CSS ",[31,140,141],{},"cubic-bezier",").",[13,144,145],{},"Two rules are worth remembering:",[147,148,149,159],"ol",{},[119,150,151,154,155,158],{},[39,152,153],{},"Interpolation happens between neighbours."," Between two keyframes, the value moves from the earlier one's value to the later one's. The interpolation style (straight or eased) is set by the keyframe you are moving ",[39,156,157],{},"toward",".",[119,160,161,164],{},[39,162,163],{},"The value holds at the ends."," Before the first keyframe it stays at the first value; after the last keyframe it stays at the last value. Nothing extrapolates off into infinity.",[23,166,168],{"id":167},"straight-motion-linear","Straight motion: LINEAR",[13,170,171,172,175,176,179],{},"Here is the headline text in the demo video. It starts off-screen to the left (",[31,173,174],{},"-1500","), slides in, holds, then slides back out. This is its ",[31,177,178],{},"translateX",":",[99,181,184],{"className":182,"code":183,"language":104},[102],"translateX = ANIMATE(TIME,\n  LINEAR(2.31, -1500),   \u002F\u002F holds off-screen until 2.31s\n  LINEAR(2.85, 0),       \u002F\u002F slides in: 2.31s to 2.85s\n  LINEAR(7,    0),       \u002F\u002F holds in place: 2.85s to 7s\n  LINEAR(7.9,  -1500))   \u002F\u002F slides back out: 7s to 7.9s\n",[31,185,183],{"__ignoreMap":107},[13,187,188,189,191,192,194,195,198,199,201,202,204],{},"Read it as a track. Until 2.31 seconds the value is pinned at ",[31,190,174],{}," (the \"before the first keyframe it holds\" rule). From 2.31 to 2.85 it moves ",[31,193,174],{}," to ",[31,196,197],{},"0",". From 2.85 to 7 it stays at ",[31,200,197],{}," (two keyframes with the same value is a hold). From 7 to 7.9 it moves back to ",[31,203,174],{},". After 7.9 it holds off-screen. The intro line above it runs the same track shifted a few tenths of a second earlier, so the two lines slide in one after the other.",[23,206,208],{"id":207},"eased-motion-cubic","Eased motion: CUBIC",[13,210,211,212,215],{},"Straight motion looks mechanical. ",[31,213,214],{},"CUBIC"," gives it weight. Here is the speaker photo scaling up with a slight overshoot, then holding, then shrinking away:",[99,217,220],{"className":218,"code":219,"language":104},[102],"width = 840 * ANIMATE(TIME,\n  LINEAR(0.3, 0),                             \u002F\u002F stays at 0 until 0.3s\n  CUBIC(1.3, 1, 0.5, 0.5, 0.72, 1.66),        \u002F\u002F pops to full size, overshooting past 1\n  LINEAR(7, 1),                               \u002F\u002F holds full size\n  CUBIC(7.9, 0, 0.89, 0.28, 0.89, 0.28))      \u002F\u002F eases back to 0\n",[31,221,219],{"__ignoreMap":107},[13,223,224,225,194,227,230,231,234,235,238,239,194,241,243,244,246,247,249,250,253],{},"The value here runs from ",[31,226,197],{},[31,228,229],{},"1",", and the whole thing is multiplied by ",[31,232,233],{},"840",", so ",[31,236,237],{},"width"," runs from ",[31,240,197],{},[31,242,233],{},". The ",[31,245,214],{}," reaching ",[31,248,229],{}," at 1.3s has a control point with ",[31,251,252],{},"y2 = 1.66",": the easing curve briefly goes past its target, which is what makes the photo \"pop\" and settle instead of arriving flatly. That one number is the difference between a corporate fade and something that feels alive.",[23,255,257],{"id":256},"composition-base-track-or-base-track","Composition: base + track, or base × track",[13,259,260],{},"Notice the two patterns above:",[116,262,263,283],{},[119,264,265,270,271,274,275,179,277],{},[39,266,267],{},[31,268,269],{},"750 + ANIMATE(...)"," offsets a base position. The track oscillates around zero and rides on top of a fixed anchor. That is how the photo gently bobs ",[31,272,273],{},"±15px"," around ",[31,276,33],{},[99,278,281],{"className":279,"code":280,"language":104},[102],"x = 750 + ANIMATE(TIME,\n  LINEAR(0, 0), CUBIC(2, 15, …), CUBIC(4, 0, …),\n  CUBIC(6, -15, …), CUBIC(8, 0, …))\n",[31,282,280],{"__ignoreMap":107},[119,284,285,290,291,194,293,295],{},[39,286,287],{},[31,288,289],{},"840 * ANIMATE(...)"," scales a base size. The track runs from ",[31,292,197],{},[31,294,229],{}," and multiplies a dimension.",[13,297,298,299,303,304,65,307,310],{},"Because these are ordinary expressions, you compose them like any other math. And because ",[300,301,302],"em",{},"every"," property is an expression, the same idea drives ",[31,305,306],{},"opacity",[31,308,309],{},"rotate",", colour, and even text, not just position.",[23,312,314],{"id":313},"from-expressions-to-frames","From expressions to frames",[13,316,317,318,321,322,324],{},"When you request ",[31,319,320],{},"format: \"mp4\"",", the engine samples these expressions at the template's frame rate. An 8-second clip at 24fps is 192 evaluations of the whole template, encoded into a video. Rendering is deterministic: the same ",[31,323,60],{}," always produces the same frame, so the output is stable and cache-friendly at volume.",[13,326,327],{},"Two ways to see it for yourself:",[116,329,330,343],{},[119,331,332,339,340,342],{},[39,333,334,335,158],{},"Scrub the ",[17,336,338],{"href":337},"\u002Fvideo","\u002Fvideo playground"," Dragging the timeline sets ",[31,341,60],{}," and re-evaluates every expression live, in your browser. No render, no credits.",[119,344,345,348,349,351,352,179,355,416,419],{},[39,346,347],{},"Freeze a frame over the API."," Because ",[31,350,60],{}," is just an input, you can render any single moment as a still by passing it in ",[31,353,354],{},"data",[99,356,360],{"className":357,"code":358,"language":359,"meta":107,"style":107},"language-bash shiki shiki-themes github-dark","curl -X POST \"https:\u002F\u002Fapi.bluepic.io\u002Fapi\u002Frender\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{ \"templateId\": \"7ccf097c-10f2-4efc-bd03-8abe967c4711\", \"data\": { \"TIME\": 1.3 }, \"format\": \"png\" }' \\\n  --output pop.png\n","bash",[31,361,362,385,396,407],{"__ignoreMap":107},[363,364,367,371,375,379,382],"span",{"class":365,"line":366},"line",1,[363,368,370],{"class":369},"svObZ","curl",[363,372,374],{"class":373},"sDLfK"," -X",[363,376,378],{"class":377},"sU2Wk"," POST",[363,380,381],{"class":377}," \"https:\u002F\u002Fapi.bluepic.io\u002Fapi\u002Frender\"",[363,383,384],{"class":373}," \\\n",[363,386,388,391,394],{"class":365,"line":387},2,[363,389,390],{"class":373},"  -H",[363,392,393],{"class":377}," \"Content-Type: application\u002Fjson\"",[363,395,384],{"class":373},[363,397,399,402,405],{"class":365,"line":398},3,[363,400,401],{"class":373},"  -d",[363,403,404],{"class":377}," '{ \"templateId\": \"7ccf097c-10f2-4efc-bd03-8abe967c4711\", \"data\": { \"TIME\": 1.3 }, \"format\": \"png\" }'",[363,406,384],{"class":373},[363,408,410,413],{"class":365,"line":409},4,[363,411,412],{"class":373},"  --output",[363,414,415],{"class":377}," pop.png\n",[417,418],"br",{},"That gives you the photo mid-pop, at the 1.3-second mark.",[23,421,423],{"id":422},"why-this-is-the-whole-story","Why this is the whole story",[13,425,426,427,429,430,434],{},"\"Personalized video from a template\" sounds like it needs a video pipeline. It does not. It needs properties that are functions of your data and one more axis, ",[31,428,60],{},", that they can also be functions of. That is the entire mechanism behind ",[17,431,433],{"href":432},"\u002Fdocs\u002Fone-template-two-outputs","one template, two outputs",": the still and the clip come from the same expressions, evaluated at one moment or across many.",[436,437,438],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":107,"searchDepth":387,"depth":387,"links":440},[441,442,443,444,445,446,447],{"id":25,"depth":387,"text":26},{"id":86,"depth":387,"text":87},{"id":167,"depth":387,"text":168},{"id":207,"depth":387,"text":208},{"id":256,"depth":387,"text":257},{"id":313,"depth":387,"text":314},{"id":422,"depth":387,"text":423},"platform","2026-07-24","How a Bluepic template becomes an animation. Every property is an expression, TIME is the only extra input, and ANIMATE turns keyframes into motion with straight or eased interpolation.","md","\u002Fdocs-images\u002Fexpression-engine-time.svg",{},true,"\u002Fdocs\u002Fexpression-engine-over-time",7,{"title":5,"description":450},"docs\u002Fexpression-engine-over-time","DSG65P7Kq7C86LkSzu9me3ZwvErQYidqxkEM2e2dwgc",1785414150520]