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
    / Objects

    Comp

    Reading a composition's identity, size, timing and layers through the handles comp() and thisComp return.

    A composition handle.

    Table of contentsidWorked example: Know which comp you are innameWorked example: The comp's label as datawidthWorked example: A right-hand marginheightWorked example: Sit on the bottom edgefpsWorked example: Frames to seconds, portablydurationFramesWorked example: A last-second warningdurationWorked example: The same cue, in secondsnumLayersWorked example: React to the layer countlayerWorked example: A roll call of the comp's layers

    id#

    Type: string (field).

    The comp id.

    Worked example: Know which comp you are in#

    The scene. A Main comp holding a wide banner, and a second Card comp holding a small badge, so one project script can drive layers in both.

    • The comp Main: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • The comp Card: 320 by 180 at 25 fps, 100 frames (4 seconds).
    • Banner in Main: a solid layer, at [320, 300], 400 by 40, filled #2ec4b6.
    • Badge in Card: a solid layer, at [160, 90], 60 by 60, filled #ff5d73.

    The script. Add a project script named Main only and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame() {
      return thisComp.id === "comp_1" ? 90 : 10;
    }
    

    Line by line:

    • Line 4: Comps carry stable ids just as layers do. The Banner lives in the Main comp, so the check passes and it renders at 90. A script whose outputs span comps runs once per comp - branching on thisComp.id lets each run behave differently.

    What you see. The banner renders at 90 percent - the script's confirmation that it is evaluating inside the Main comp.

    The banner renders at 90 percent - the script's confirmation that it is evaluating inside the Main comp.

    name#

    Type: string (field).

    The display name.

    Worked example: The comp's label as data#

    The scene. A Main comp holding a wide banner, and a second Card comp holding a small badge, so one project script can drive layers in both.

    • The comp Main: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • The comp Card: 320 by 180 at 25 fps, 100 frames (4 seconds).
    • Banner in Main: a solid layer, at [320, 300], 400 by 40, filled #2ec4b6.
    • Badge in Card: a solid layer, at [160, 90], 60 by 60, filled #ff5d73.

    The script. Add a project script named Name drop and paste:

    export const outputs = ["layer_1:transform.position"];
    
    export function frame() {
      return [value[0], 200 + thisComp.name.length * 20];
    }
    

    Line by line:

    • Line 4: name is the comp's display label - "Main", four characters, so the Banner drops to y 280. Like layer names, comp names are for reading; references travel by id, so renaming the comp shifts this read without breaking anything.

    What you see. The banner sits at y 280, twenty pixels of drop per letter of the comp's name - rename the comp and it re-seats itself.

    The banner sits at y 280, twenty pixels of drop per letter of the comp's name - rename the comp and it re-seats itself.

    width#

    Type: number (field).

    Pixel width.

    Worked example: A right-hand margin#

    The scene. A Main comp holding a wide banner, and a second Card comp holding a small badge, so one project script can drive layers in both.

    • The comp Main: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • The comp Card: 320 by 180 at 25 fps, 100 frames (4 seconds).
    • Banner in Main: a solid layer, at [320, 300], 400 by 40, filled #2ec4b6.
    • Badge in Card: a solid layer, at [160, 90], 60 by 60, filled #ff5d73.

    The script. Add a project script named Right margin and paste:

    export const outputs = ["layer_1:transform.position"];
    
    export function frame() {
      return [thisComp.width - 220, value[1]];
    }
    

    Line by line:

    • Line 4: width is the comp's pixel width - 640 here, parking the Banner's centre 120 pixels in from the right edge. Reformat the comp for another placement and the margin holds itself.

    What you see. The banner hugs the right side of the stage, its centre a fixed 220 pixels in from the edge whatever the comp's width becomes - the whole banner staying inside the frame.

    The banner hugs the right side of the stage, its centre a fixed 220 pixels in from the edge whatever the comp's width becomes - the whole banner staying inside the frame.

    height#

    Type: number (field).

    Pixel height.

    Worked example: Sit on the bottom edge#

    The scene. A Main comp holding a wide banner, and a second Card comp holding a small badge, so one project script can drive layers in both.

    • The comp Main: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • The comp Card: 320 by 180 at 25 fps, 100 frames (4 seconds).
    • Banner in Main: a solid layer, at [320, 300], 400 by 40, filled #2ec4b6.
    • Badge in Card: a solid layer, at [160, 90], 60 by 60, filled #ff5d73.

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

    export const outputs = ["layer_1:transform.position"];
    
    export function frame() {
      return [value[0], thisComp.height - 30];
    }
    

    Line by line:

    • Line 4: height is the comp's pixel height - 360 here, so the 40 pixel tall Banner rests with a slim 10 pixel gap under it. Height-relative placement survives any reformat.

    What you see. The banner settles into the bottom band of the stage, 30 pixels above the lower edge, and stays there if the comp is made taller or shorter.

    The banner settles into the bottom band of the stage, 30 pixels above the lower edge, and stays there if the comp is made taller or shorter.

    fps#

    Type: number (field).

    Frames per second.

    Worked example: Frames to seconds, portably#

    The scene. A Main comp holding a wide banner, and a second Card comp holding a small badge, so one project script can drive layers in both.

    • The comp Main: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • The comp Card: 320 by 180 at 25 fps, 100 frames (4 seconds).
    • Banner in Main: a solid layer, at [320, 300], 400 by 40, filled #2ec4b6.
    • Badge in Card: a solid layer, at [160, 90], 60 by 60, filled #ff5d73.

    The script. Add a project script named Portable pace and paste:

    export const outputs = ["layer_1:transform.position"];
    
    export function frame({ frame }) {
      const seconds = frame / thisComp.fps;
      return [220 + seconds * 50, value[1]];
    }
    

    Line by line:

    • Line 4: fps is the comp's frames per second as an exact number. Dividing the frame count by it recovers seconds - the same journey as reading time, spelt out. Motion written in seconds plays at the same real-world speed if the comp is ever retimed to another rate.
    • Line 5: Fifty pixels per second of drift, whatever the frame rate.

    What you see. The banner starts left of centre and drifts right at 50 pixels per second, staying fully inside the frame - a pace that stays true even if the comp's frame rate is later changed.

    The banner starts left of centre and drifts right at 50 pixels per second, staying fully inside the frame - a pace that stays true even if the comp's frame rate is later changed.

    durationFrames#

    Type: number (field).

    Duration in frames.

    Worked example: A last-second warning#

    The scene. A Main comp holding a wide banner, and a second Card comp holding a small badge, so one project script can drive layers in both.

    • The comp Main: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • The comp Card: 320 by 180 at 25 fps, 100 frames (4 seconds).
    • Banner in Main: a solid layer, at [320, 300], 400 by 40, filled #2ec4b6.
    • Badge in Card: a solid layer, at [160, 90], 60 by 60, filled #ff5d73.

    The script. Add a project script named Final second and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame({ frame, fps }) {
      const final_second = thisComp.durationFrames - fps;
      return frame >= final_second ? 30 : 100;
    }
    

    Line by line:

    • Line 4: durationFrames is the comp's length on its own frame grid - 100 frames here. Subtracting one second of frames marks where the ending begins.
    • Line 5: For the comp's last second the Banner dims to 30 - an outro cue that repositions itself if the comp is ever lengthened.

    What you see. The banner holds solid until the comp's final second, then dims to 30 as an outro cue - and the cue moves itself if the comp is re-cut longer.

    The banner holds solid until the comp's final second, then dims to 30 as an outro cue - and the cue moves itself if the comp is re-cut longer.

    duration#

    Type: number (field).

    Duration in seconds.

    Worked example: The same cue, in seconds#

    The scene. A Main comp holding a wide banner, and a second Card comp holding a small badge, so one project script can drive layers in both.

    • The comp Main: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • The comp Card: 320 by 180 at 25 fps, 100 frames (4 seconds).
    • Banner in Main: a solid layer, at [320, 300], 400 by 40, filled #2ec4b6.
    • Badge in Card: a solid layer, at [160, 90], 60 by 60, filled #ff5d73.

    The script. Add a project script named Outro dim and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame() {
      return time >= thisComp.duration - 1 ? 25 : 100;
    }
    

    Line by line:

    • Line 4: duration is the comp's length in seconds - four here, so the dim begins at the three second mark. It is durationFrames translated through the frame rate; use whichever unit reads more naturally for the cue.

    What you see. The banner dims to 25 for the comp's final second - the same outro cue as the frames version, written in seconds.

    The banner dims to 25 for the comp's final second - the same outro cue as the frames version, written in seconds.

    numLayers#

    Type: number (field).

    Layer count.

    Worked example: React to the layer count#

    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 Crowd turn and paste:

    export const outputs = ["layer_3:transform.rotation"];
    
    export function frame() {
      return thisComp.numLayers * 10;
    }
    

    Line by line:

    • Line 4: numLayers counts the comp's layers - three swatches here, so the teal on top twists 30 degrees. A script that reacts to the population keeps working as layers come and go.

    What you see. The teal swatch twists 30 degrees - ten per layer in the comp - revealing the gold and coral corners beneath it.

    The teal swatch twists 30 degrees - ten per layer in the comp - revealing the gold and coral corners beneath it.

    layer#

    layer(ref: number | string): Layer | null
    

    M10 DOM2-1: one layer by 0-BASED index (Layer.index's convention) or by id. A miss answers null (the soft-miss rule - gather loops skip, never throw): comp(id).layer(i).id writes exactly as the feedback asked.

    Determinism: pure.

    layer(ref) takes a 0-based index (Layer.index's convention) or a layer id string and answers a Layer handle. A miss answers null, never a typed error, so gather loops skip gracefully over anything that is not there.

    Worked example: A roll call of the comp's layers#

    The scene. Four crates hang near the top of one Playground comp, each carrying its own Weight slider, so one script can gather every layer's controller and drive them all.

    • The comp Playground: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Crate A: a solid layer, at [120, 40], 56 by 56, filled #2ec4b6.
      • An expression control: Weight, a slider, set to 20.
    • Crate B: a solid layer, at [260, 40], 56 by 56, filled #ffd166.
      • An expression control: Weight, a slider, set to 45.
    • Crate C: a solid layer, at [400, 40], 56 by 56, filled #ff5d73.
      • An expression control: Weight, a slider, set to 70.
    • Crate D: a solid layer, at [540, 40], 56 by 56, filled #9b5de5.
      • An expression control: Weight, a slider, set to 95.

    The script. Add a project script named Roll call and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame() {
      let found = 0;
      for (let i = 0; i < thisComp.numLayers; i += 1) {
        if (comp().layer(i) !== null) found += 1;
      }
      const past_the_end = comp().layer(99);
      const unknown = comp().layer("layer_99");
      const misses_are_null = past_the_end === null && unknown === null;
      set("layer_1:transform.opacity", misses_are_null ? 20 + found * 10 : 0);
    }
    

    Line by line:

    • Lines 5-7: The canonical traversal: indexes 0 to numLayers - 1 answer every layer in paint order, so the count lands on four.
    • Lines 8-10: Both miss forms answer null (the soft-miss rule): an index past the end and an id nothing carries. Gather loops skip, they never throw.
    • Line 11: Crate A's opacity carries the proof: 20 plus ten per found layer, but only if both misses answered null.

    What you see. Crate A settles at 60 percent opacity: the walk found all four crates and both deliberate misses answered null.

    Crate A settles at 60 percent opacity: the walk found all four crates and both deliberate misses answered null.