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

    Random and noise

    Seeded randomness and smooth noise: motion that looks organic in the editor and renders identically on every machine.

    Every random source is SEEDED: the numbers derive from the document seed, the script and the frame, so scrubbing, replaying and farm rendering all see identical values. Nothing here can make a render differ between machines.

    Table of contentsrandomWorked example: Deterministic scattergaussRandomWorked example: Bell-curved jitterseedRandomWorked example: Replay the same numbersnoiseWorked example: Smooth wanderingwiggleWorked example: A gentle drift

    random#

    random(): number
    

    A seeded pseudo-random number in [0, 1), deterministic per (document seed, script, frame). Math.random is the same stream.

    Determinism: seeded.

    Worked example: Deterministic scatter#

    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 Scatter and paste:

    export const outputs = ["layer_1:transform.position"];
    
    export function frame() {
      const jx = (random() - 0.5) * 200;
      const jy = (random() - 0.5) * 120;
      return [value[0] + jx, value[1] + jy];
    }
    

    Line by line:

    • Lines 4-5: random() answers a number in [0, 1) - a fresh one per call, but the SEQUENCE is seeded per (document, script, frame). Frame 7 always produces the same two numbers, on your machine and on the render farm alike.
    • Line 6: Centred jitter: up to 100 pixels sideways and 60 vertically, snapping to a new spot each frame.

    What you see. The square teleports to a new nearby spot every frame - jittery and chaotic to watch, yet identical on every play, scrub and render.

    The square teleports to a new nearby spot every frame - jittery and chaotic to watch, yet identical on every play, scrub and render.

    gaussRandom#

    gaussRandom(): number
    

    A seeded standard-normal random number (Box-Muller over random()).

    Determinism: seeded.

    Worked example: Bell-curved jitter#

    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 Nervous and paste:

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

    Line by line:

    • Line 4: gaussRandom() draws from a standard bell curve: usually near zero, occasionally further out. Multiplied by 12, the Square mostly trembles close to home with the odd larger twitch - more natural than the flat spread of random().

    What you see. The square trembles around its resting spot - small nervous movements most frames, an occasional larger twitch, identical on every playback.

    The square trembles around its resting spot - small nervous movements most frames, an occasional larger twitch, identical on every playback.

    seedRandom#

    seedRandom(offset: number, timeless?: boolean): void
    

    Reseed the random()/gaussRandom() stream for this frame (timeless omits the frame from the seed - the AE switch).

    Determinism: seeded.

    Worked example: Replay the same numbers#

    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 Two dice, one seed and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame() {
      seedRandom(3);
      const first = random();
      seedRandom(3);
      const second = random();
      return first === second ? 88 : 0;
    }
    

    Line by line:

    • Lines 4-5: seedRandom(offset) restarts the random stream for this frame from a seed you choose. The first draw after it is fully determined.
    • Lines 6-7: Reseeding with the SAME offset rewinds the stream - the second draw repeats the first exactly.
    • Line 8: The equality proves it, and the Square rests at 88 as the visible receipt. Pass true as the second argument to drop the frame from the seed, freezing the numbers across time - the AE timeless switch.

    What you see. The square rests steadily at 88 percent opacity - the proof that reseeding with the same offset replays exactly the same random numbers.

    The square rests steadily at 88 percent opacity - the proof that reseeding with the same offset replays exactly the same random numbers.

    noise#

    noise(t?: number): number
    

    Seeded value noise in [-1, 1] - a PURE function of (seed, t), random-access safe.

    Determinism: seeded.

    Worked example: Smooth wandering#

    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 Wander and paste:

    export const outputs = ["layer_1:transform.position"];
    
    export function frame() {
      return [value[0] + noise(time) * 60, value[1] + noise(time + 50) * 40];
    }
    

    Line by line:

    • Line 4: noise(t) is smooth value noise in [-1, 1]: neighbouring times give neighbouring values, so the motion flows instead of teleporting (compare random()). It is a pure function of its input - the same t always answers the same number - which is why offsetting the second axis by 50 seconds makes it wander independently.

    What you see. The square roams smoothly around its resting spot - up to 60 pixels sideways and 40 vertically - drifting rather than jumping, identical on every playback.

    The square roams smoothly around its resting spot - up to 60 pixels sideways and 40 vertically - drifting rather than jumping, identical on every playback.

    wiggle#

    wiggle(freq: number, amp: number, t?: number): number
    

    amp x seeded value noise at t x freq - time-pure, random-access safe.

    Determinism: seeded.

    wiggle() is a pure function of the frame being evaluated: ask for the same frequency, amplitude and time twice and you get the same number back. That is what makes scrubbing safe - the drift at frame 40 is the same whether you played into it or jumped straight there. It also means two calls with the same arguments return the SAME value, so to wiggle two axes independently you offset the time of one call, as the example shows.

    Worked example: A gentle drift#

    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 Drift and paste:

    export const outputs = ["layer_1:transform.position"];
    
    export function frame() {
      const x = value[0] + wiggle(0.8, 24);
      const y = value[1] + wiggle(0.8, 24, time + 100);
      return [x, y];
    }
    

    Line by line:

    • Line 1: Declare what this script is allowed to drive: the Square layer's position. The Scripts panel and the pick whip write these ids for you.
    • Line 3: frame() runs once for every frame the comp shows. Whatever it returns becomes the value of the script's one declared output for that frame.
    • Line 4: value is the position the Square would have without the script - here its resting [320, 180]. wiggle(0.8, 24) adds a smooth wander that changes about 0.8 times per second and strays up to 24 pixels.
    • Line 5: The second axis offsets the wiggle's clock by 100 seconds. Without the offset both axes would get the SAME number and the Square would only slide along a diagonal.
    • Line 6: Return the new position as an [x, y] array - positions are arrays in scripts, exactly as the panel shows them as X and Y.

    What you see. The square drifts gently around its resting spot, never straying more than about 24 pixels, and the motion loops nowhere - it just keeps wandering.

    The square drifts gently around its resting spot, never straying more than about 24 pixels, and the motion loops nowhere - it just keeps wandering.