Nubu
  • Features
  • How it works
  • Pricing
  • Integrations
  • FAQ
  • Contact
  • Docs
  • Blog
  • Log in
  • Log inSign up
    Nubu

    Creative automation for teams that ship campaigns, not busywork.

    Try for free
    Product
    FeaturesNubu MotionHow it worksPricingIntegrationsFAQ
    Company
    AboutContactDocsBlogLog in
    Legal
    Privacy policyTerms of serviceData deletion
    © 2026 Bear Studios · Nubu
    Introduction
    Templates overviewInstalling Nubu BuilderAdding template compsAdding propertiesAdding footageSetting defaultsExporting your templateUploading a template
    Flows overviewAvailable nodesAdding a nodeConnecting nodesWorking with dataTemplates and outputsUsing AI nodesBuilding a creative
    CampaignsTemplatesAssetsGlossariesRendersSettings
    Connecting Meta AdsConnecting Google AdsConnecting OpenAIConnecting Gemini AI StudioConnecting Anthropic
    AI Assistant overviewPrompt examplesAssistant settings and controls
    Nubu Motion scripts: the reference
    OverviewQuick startDifferences from After EffectsDeterminism
    JavaScript supportReferences and idsProject scriptsState and timeBudgets and errorsFormatting values
    GlobalTime conversionInterpolationVector mathsRandom and noiseColour conversionOther maths
    LayerCompPropertyKeyframePathGeometryProjectComponentConsoleComponentControlExpressionControl
    The property vocabularyAnimators propertiesAudio propertiesContent propertiesContents propertiesEffects propertiesExpression controls propertiesFills propertiesInstance controls propertiesLayer propertiesLayout propertiesMasks propertiesStrokes propertiesText style propertiesTransform propertiesTrim path properties
    Error codesBudgets
    Changelog
    All docs
    Motion Scripting
    / General

    Interpolation

    Mapping values from one range into another with linear() and clamp(), the two workhorses of motion scripting.

    Mapping one range onto another is half of all motion scripting. linear() interpolates with clamping at both ends; clamp() pins a value into a range on its own.

    Table of contentslinearWorked example: Fade in over two secondsWorked example: A colour cycle built from three swatchesWorked example: A colour cycleclampWorked example: March into a wall

    linear#

    linear(t: number, tMin: number, tMax: number, a: T, b: T): T
    linear(t: number, a: T, b: T): T
    

    Clamped linear interpolation, component-wise over vectors.

    Determinism: pure.

    After Effects equivalent: linear.

    Worked example: Fade in over two seconds#

    The scene. One orange square resting on a dark 640 by 360 stage.

    • The comp Drift: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Square: a solid layer, at [320, 180], 60 by 60, filled #f2a833.

    The script. Add a project script named Fade in and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame() {
      return linear(time, 0, 2, 0, 100);
    }
    

    Line by line:

    • Line 4: linear(t, tMin, tMax, a, b) maps t from the range [tMin, tMax] onto [a, b] and CLAMPS outside it - before 0 s the result stays 0, after 2 s it stays 100. One call replaces two keyframes and an ease.

    What you see. The square fades from invisible to fully visible across the first two seconds, then holds - no keyframes involved.

    The square fades from invisible to fully visible across the first two seconds, then holds - no keyframes involved.

    Worked example: A colour cycle built from three swatches#

    The scene. Three 200 by 200 swatches stacked on the same spot - coral at the back, gold in the middle, teal on top - ready for a script to cross-fade between them.

    • The comp Palette: 640 by 360 at 25 fps, 150 frames (6 seconds).
    • Coral: a solid layer, at [320, 180], 200 by 200, filled #ff5d73.
    • Gold: a solid layer, at [320, 180], 200 by 200, filled #f2a833.
    • Teal: a solid layer, at [320, 180], 200 by 200, filled #2ec4b6.

    The script. Add a project script named Colour cycle and paste:

    export const outputs = [
      "layer_1:transform.opacity",
      "layer_2:transform.opacity",
      "layer_3:transform.opacity",
    ];
    
    export function frame() {
      const phase = (time / 2) % 3;
      for (let n = 0; n < 3; n += 1) {
        const away = Math.min(Math.abs(phase - n), 3 - Math.abs(phase - n));
        set("layer_" + (n + 1) + ":transform.opacity", linear(away, 0, 1, 100, 0));
      }
    }
    

    Line by line:

    • Lines 1-5: Three swatches stacked on the same spot; the script owns all three opacities.
    • Line 8: phase crawls 0, 1, 2 and wraps - one whole colour every two seconds.
    • Lines 9-10: For each swatch, away measures how far the phase is from its slot, wrapping round the end so the cycle closes seamlessly.
    • Line 11: linear() turns distance into opacity: a swatch at the phase is solid (100), one a full step away is invisible (0), and anything between cross-fades. The colour appears to melt from coral to gold to teal and back.

    What you see. The square melts through coral, gold and teal on a six second loop, each colour cross-fading smoothly into the next.

    The square melts through coral, gold and teal on a six second loop, each colour cross-fading smoothly into the next.

    Worked example: A colour cycle#

    The scene. One 200 by 200 swatch near the centre of a dark 640 by 360 stage, painted by a single coral fill in its fill stack - the paint model shapes and frames use - ready for a script to drive that fill's colour.

    • The comp Swatch: 640 by 360 at 25 fps, 150 frames (6 seconds).
    • Swatch: a solid layer, at [320, 180], 200 by 200.
      • The stack fill Base (fills.fill_1 in scripts): a colour fill, #ff5d73.

    The script. Add a project script named Colour cycle and paste:

    export const outputs = ["layer_1:fills.fill_1.colour"];
    
    const palette = [
      [1, 0.365, 0.451, 1],
      [0.949, 0.659, 0.2, 1],
      [0.18, 0.769, 0.714, 1],
    ];
    
    export function frame() {
      const phase = (time / 2) % 3;
      const from = palette[Math.floor(phase)];
      const to = palette[(Math.floor(phase) + 1) % 3];
      return linear(phase - Math.floor(phase), 0, 1, from, to);
    }
    

    Line by line:

    • Line 1: One output: the colour of the swatch's STACK FILL - the fill named Base in the scene, addressed by the id the document minted for it (fills.fill_1). Shapes and frames paint through a stack of fills like this one, and the Scripts panel and the @ pick whip write the id for you. Colours are writable like any other animatable property - the script hands back [r, g, b, a] in 0..1 (a "#rrggbb" string works too).
    • Lines 3-7: The three colours of the cycle as [r, g, b, a] arrays: coral, gold and teal.
    • Line 10: phase crawls 0, 1, 2 and wraps - one whole colour every two seconds.
    • Lines 11-12: The colour the phase is leaving and the one it is heading for; the modulo wraps teal back round to coral so the loop closes seamlessly.
    • Line 13: linear() works component by component on arrays: the fraction of the way through the current step blends every channel from one colour to the next, and the single-output return drives the fill directly.

    What you see. The one swatch melts through coral, gold and teal on a six second loop, each colour cross-fading smoothly into the next - its stack fill's colour driven by the script.

    The one swatch melts through coral, gold and teal on a six second loop, each colour cross-fading smoothly into the next - its stack fill's colour driven by the script.

    clamp#

    clamp(v: number, lo: number, hi: number): number
    

    Clamp v to [lo, hi].

    Determinism: pure.

    After Effects equivalent: clamp.

    Worked example: March into a wall#

    The scene. One orange square resting on a dark 640 by 360 stage.

    • The comp Drift: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Square: a solid layer, at [320, 180], 60 by 60, filled #f2a833.

    The script. Add a project script named March and paste:

    export const outputs = ["layer_1:transform.position"];
    
    export function frame({ frame }) {
      return [clamp(value[0] + frame * 8, 0, 480), value[1]];
    }
    

    Line by line:

    • Line 3: frame comes from the function's argument - eight pixels of march per frame.
    • Line 4: clamp(v, lo, hi) pins the result inside [0, 480]: the Square marches right until the invisible wall at x 480, then stops dead while the clock keeps running.

    What you see. The square marches briskly rightwards and stops dead at x 480, as though hitting a wall at the stage's right third.

    The square marches briskly rightwards and stops dead at x 480, as though hitting a wall at the stage's right third.