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

    Vector maths

    Component by component arithmetic for the [x, y] arrays behind positions and scales, including scalar broadcasting.

    Positions and scales travel as [x, y] arrays, and these helpers do arithmetic on them component by component - lenient like After Effects' vector maths, so a scalar broadcasts where that reads naturally.

    Table of contentsaddWorked example: Hover above the leadersubWorked example: Trail to the leftmulWorked example: A quarter largerdivWorked example: Half strengthdotWorked example: How far along an axislengthWorked example: Fade with distancenormalizeWorked example: Step towards the leader

    add#

    add(a: T, b: T): T
    

    Component-wise a + b.

    Determinism: pure.

    After Effects equivalent: add.

    Worked example: Hover above the leader#

    The scene. A teal leader square keyframed to cross the stage left to right, with an orange follower resting below its start point.

    • The comp Leaders: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Leader: a solid layer, at [80, 140], 40 by 40, filled #2ec4b6.
      • Keyframe transform.position at frame 0: [80, 140] (linear).
      • Keyframe transform.position at frame 96: [560, 140] (linear).
    • Follower: a solid layer, at [80, 240], 40 by 40, filled #f2a833.

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

    export const outputs = ["layer_2:transform.position"];
    
    export function frame() {
      const leader_pos = prop("Leader:transform.position").value;
      return add(leader_pos, [0, -60]);
    }
    

    Line by line:

    • Line 4: The leader's live [x, y] at this frame.
    • Line 5: add() sums vectors component by component - the follower rides exactly 60 pixels above the leader wherever it goes. Positions are plain arrays, so the vector helpers work on them directly.

    What you see. The follower floats a fixed 60 pixels above the marching leader for the whole journey, like a balloon on a short string.

    The follower floats a fixed 60 pixels above the marching leader for the whole journey, like a balloon on a short string.

    sub#

    sub(a: T, b: T): T
    

    Component-wise a - b.

    Determinism: pure.

    After Effects equivalent: sub.

    Worked example: Trail to the left#

    The scene. A teal leader square keyframed to cross the stage left to right, with an orange follower resting below its start point.

    • The comp Leaders: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Leader: a solid layer, at [80, 140], 40 by 40, filled #2ec4b6.
      • Keyframe transform.position at frame 0: [80, 140] (linear).
      • Keyframe transform.position at frame 96: [560, 140] (linear).
    • Follower: a solid layer, at [80, 240], 40 by 40, filled #f2a833.

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

    export const outputs = ["layer_2:transform.position"];
    
    export function frame() {
      const leader_pos = prop("Leader:transform.position").value;
      return sub(leader_pos, [40, 0]);
    }
    

    Line by line:

    • Line 5: sub() subtracts component by component: 40 pixels off the x keeps the follower one square-width behind the leader, at the leader's own height.

    What you see. The follower rides one square-width behind the leader for the whole march, glued to its left flank.

    The follower rides one square-width behind the leader for the whole march, glued to its left flank.

    mul#

    mul(a: T, b: number): T
    

    Scale (a scalar on either side broadcasts).

    Determinism: pure.

    After Effects equivalent: mul.

    Worked example: A quarter larger#

    The scene. One pale 160 by 100 card near the centre of the stage, for anchor, scale and skew reads.

    • The comp Tilt: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Card: a solid layer, at [320, 180], 160 by 100, filled #e8e6e3.

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

    export const outputs = ["layer_1:transform.scale"];
    
    export function frame() {
      return mul(value, 1.25);
    }
    

    Line by line:

    • Line 4: mul() scales a vector by a number - the base [100, 100] becomes [125, 125]. Because it starts from value, scaling the card by hand in the panel multiplies on top.

    What you see. The card renders a quarter larger than its panel scale - resize it by hand and the script keeps multiplying by 1.25.

    The card renders a quarter larger than its panel scale - resize it by hand and the script keeps multiplying by 1.25.

    div#

    div(a: T, b: number): T
    

    Component-wise a / b (0 divisor yields 0).

    Determinism: pure.

    After Effects equivalent: div.

    Worked example: Half strength#

    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 Half teal and paste:

    export const outputs = ["layer_3:transform.opacity"];
    
    export function frame() {
      return div(value, 2);
    }
    

    Line by line:

    • Line 4: div() divides by a number (a zero divisor yields 0 rather than blowing up). The topmost teal swatch drops to half its panel opacity, letting the gold underneath glow through.

    What you see. The teal square turns half transparent, and the gold swatch beneath shows through it as a warm blend.

    The teal square turns half transparent, and the gold swatch beneath shows through it as a warm blend.

    dot#

    dot(a: T, b: T): number
    

    Dot product.

    Determinism: pure.

    After Effects equivalent: dot.

    Worked example: How far along an axis#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Vertical gap and paste:

    export const outputs = ["layer_1:transform.rotation"];
    
    export function frame() {
      const planet = prop("Planet:transform.position").value;
      const sun = prop("Sun:transform.position").value;
      return dot([0, 1], sub(planet, sun));
    }
    

    Line by line:

    • Lines 4-5: Both positions as vectors.
    • Line 6: dot() measures how much one vector points along another. Against the unit y axis [0, 1] it extracts exactly the vertical part of the gap: the planet floats 120 pixels ABOVE the sun, so the answer is -120 and the sun rolls a quarter turn twice over, anticlockwise.

    What you see. The sun square rests rotated -120 degrees - the exact vertical distance in pixels between planet and sun, read off with a dot product.

    The sun square rests rotated -120 degrees - the exact vertical distance in pixels between planet and sun, read off with a dot product.

    length#

    length(a: T, b?: T): number
    

    Vector magnitude, or the distance between two points.

    Determinism: pure.

    After Effects equivalent: length.

    Worked example: Fade with distance#

    The scene. A teal leader square keyframed to cross the stage left to right, with an orange follower resting below its start point.

    • The comp Leaders: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Leader: a solid layer, at [80, 140], 40 by 40, filled #2ec4b6.
      • Keyframe transform.position at frame 0: [80, 140] (linear).
      • Keyframe transform.position at frame 96: [560, 140] (linear).
    • Follower: a solid layer, at [80, 240], 40 by 40, filled #f2a833.

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

    export const outputs = ["layer_2:transform.opacity"];
    
    export function frame() {
      const leader_pos = prop("Leader:transform.position").value;
      const here = prop("Follower:transform.position").value;
      return 100 - clamp(length(leader_pos, here) / 5, 0, 100);
    }
    

    Line by line:

    • Lines 4-5: The two positions at this frame - the leader marches, the follower rests.
    • Line 6: length() with two arguments is the straight-line distance between two points. Divided by five and subtracted from 100, the follower is bright when the leader is near and fades as it walks away. Mid-march the distance is exactly 260 pixels, leaving 48 percent opacity.

    What you see. The resting follower glows brighter as the marching leader approaches and dims as it passes on - a proximity light.

    The resting follower glows brighter as the marching leader approaches and dims as it passes on - a proximity light.

    normalize#

    normalize(a: T): T
    

    The unit vector (a zero vector stays zero).

    Determinism: pure.

    After Effects equivalent: normalize.

    Worked example: Step towards the leader#

    The scene. A teal leader square keyframed to cross the stage left to right, with an orange follower resting below its start point.

    • The comp Leaders: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Leader: a solid layer, at [80, 140], 40 by 40, filled #2ec4b6.
      • Keyframe transform.position at frame 0: [80, 140] (linear).
      • Keyframe transform.position at frame 96: [560, 140] (linear).
    • Follower: a solid layer, at [80, 240], 40 by 40, filled #f2a833.

    The script. Add a project script named Step towards and paste:

    export const outputs = ["layer_2:transform.position"];
    
    export function frame() {
      const leader_pos = prop("Leader:transform.position").value;
      const direction = normalize(sub(leader_pos, value));
      return add(value, mul(direction, 80));
    }
    

    Line by line:

    • Line 5: normalize() shrinks a vector to length one while keeping its direction (a zero vector stays zero) - the pure "which way" with the "how far" stripped out.
    • Line 6: Eighty pixels along that unit direction: the follower always leans a fixed 80 pixels towards the leader, whichever way the leader happens to be.

    What you see. The follower leans a constant 80 pixels towards the marching leader, swivelling its offset direction as the leader passes by.

    The follower leans a constant 80 pixels towards the marching leader, swivelling its offset direction as the leader passes by.