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

    PathGeometry

    Sampling points, tangents and normals along a path's outline by arc length, the tool behind point-on-path setups.

    M10 DOM2-3: a read-only arc-length sampler over the EFFECTIVE outline, piecewise-linear over the E27 chord flatten (16 stops per cubic - deterministic, cross-host bit-stable; the dossier's veto D14-a). Fractions clamp 0..1; multiple subpaths concatenate by arc length (samplers never lerp across the gaps); an empty geometry answers {0, 0} with length 0.

    Table of contentslengthWorked example: Measuring the railclosedWorked example: Open wave, closed rimnumSubpathsWorked example: Counting drawable subpathspointAtFractionWorked example: Walking the outline by fractiontangentAtFractionWorked example: Riding the wave, facing the travelnormalAtFractionWorked example: Standing off the rail on the normal

    length#

    Type: number (accessor).

    The total flattened arc length (px).

    Determinism: pure.

    PathGeometry samples the flattened effective outline by ARC LENGTH: sixteen deterministic stops per segment (the same chord flatten the feather band and the outliner walk), fractions clamped to 0..1, multiple subpaths concatenated end to end with zero-length jumps the samplers never lerp across. An empty geometry answers a zero length and every sampler answers the origin point.

    Worked example: Measuring the rail#

    The scene. A Shoreline comp holding an open two-segment Wave path at the origin, a small Chip ready to ride it, and a Plate solid wearing a rectangular mask whose rim a script can sample.

    • The comp Shoreline: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Wave: a path layer, at [0, 0].
    • Chip: a solid layer, at [320, 180], 24 by 24, filled #ffd166.
    • Plate: a solid layer, at [480, 80], 160 by 40, filled #1a1e26.

    The script. Add a project script named Rail length and paste:

    export const outputs = ["layer_2:transform.opacity"];
    
    export function frame() {
      const rail = layer("Wave").path();
      set("layer_2:transform.opacity", rail.length / 4);
    }
    

    Line by line:

    • Lines 4-5: The wave's two straight runs measure 200 and 100 px, so the flattened arc length is exactly 300 and the chip settles at 75 percent.

    What you see. The chip fades to 75 percent opacity: one quarter of the rail's 300 px flattened length.

    The chip fades to 75 percent opacity: one quarter of the rail's 300 px flattened length.

    closed#

    Type: boolean (accessor).

    The FIRST drawable subpath's closed flag (pinned).

    Determinism: pure.

    Worked example: Open wave, closed rim#

    The scene. A Shoreline comp holding an open two-segment Wave path at the origin, a small Chip ready to ride it, and a Plate solid wearing a rectangular mask whose rim a script can sample.

    • The comp Shoreline: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Wave: a path layer, at [0, 0].
    • Chip: a solid layer, at [320, 180], 24 by 24, filled #ffd166.
    • Plate: a solid layer, at [480, 80], 160 by 40, filled #1a1e26.

    The script. Add a project script named Closed flags and paste:

    export const outputs = ["layer_2:transform.opacity"];
    
    export function frame() {
      const wave = layer("Wave").path();
      const rim = layer("Plate").maskPath("mask_1");
      set("layer_2:transform.opacity", !wave.closed && rim.closed ? 65 : 5);
    }
    

    Line by line:

    • Lines 4-6: closed reports the FIRST drawable subpath's flag: the wave is an open polyline, the mask rim a closed rectangle. Closed rails wrap for the samplers; open rails clamp.

    What you see. The chip rests at 65 percent opacity: the wave read open and the rim read closed.

    The chip rests at 65 percent opacity: the wave read open and the rim read closed.

    numSubpaths#

    Type: number (accessor).

    The drawable subpath count.

    Determinism: pure.

    Worked example: Counting drawable subpaths#

    The scene. A Shoreline comp holding an open two-segment Wave path at the origin, a small Chip ready to ride it, and a Plate solid wearing a rectangular mask whose rim a script can sample.

    • The comp Shoreline: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Wave: a path layer, at [0, 0].
    • Chip: a solid layer, at [320, 180], 24 by 24, filled #ffd166.
    • Plate: a solid layer, at [480, 80], 160 by 40, filled #1a1e26.

    The script. Add a project script named Subpath count and paste:

    export const outputs = ["layer_2:transform.opacity"];
    
    export function frame() {
      const rail = layer("Wave").path();
      set("layer_2:transform.opacity", 50 + rail.numSubpaths * 10);
    }
    

    Line by line:

    • Lines 4-5: The wave draws one contour, so numSubpaths answers 1. A path layer with several contents leaves concatenates them all and counts each drawable subpath.

    What you see. The chip lands at 60 percent opacity: fifty plus ten for the wave's single drawable subpath.

    The chip lands at 60 percent opacity: fifty plus ten for the wave's single drawable subpath.

    pointAtFraction#

    pointAtFraction(f: number): {x, y}
    

    The outline point at the arc-length fraction (layer-local px).

    Determinism: pure.

    Worked example: Walking the outline by fraction#

    The scene. A Shoreline comp holding an open two-segment Wave path at the origin, a small Chip ready to ride it, and a Plate solid wearing a rectangular mask whose rim a script can sample.

    • The comp Shoreline: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Wave: a path layer, at [0, 0].
    • Chip: a solid layer, at [320, 180], 24 by 24, filled #ffd166.
    • Plate: a solid layer, at [480, 80], 160 by 40, filled #1a1e26.

    The script. Add a project script named Fraction walker and paste:

    export const outputs = ["layer_2:transform.position"];
    
    export function frame() {
      const rail = layer("Wave").path();
      const p = rail.pointAtFraction(0.75);
      set("layer_2:transform.position", [p.x, p.y]);
    }
    

    Line by line:

    • Lines 4-5: Three quarters of 300 px is 225 px of arc: past the 200 px horizontal run and 25 px down the vertical one, the layer-local point (300, 225). Fractions outside 0..1 clamp.
    • Line 6: The chip takes the sample directly; the wave sits at the origin so local px read as comp coordinates.

    What you see. The chip parks a quarter of the way down the wave's vertical drop at (300, 225).

    The chip parks a quarter of the way down the wave's vertical drop at (300, 225).

    tangentAtFraction#

    tangentAtFraction(f: number): {x, y}
    

    The unit tangent along the walk at the fraction.

    Determinism: pure.

    Worked example: Riding the wave, facing the travel#

    The scene. A Shoreline comp holding an open two-segment Wave path at the origin, a small Chip ready to ride it, and a Plate solid wearing a rectangular mask whose rim a script can sample.

    • The comp Shoreline: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Wave: a path layer, at [0, 0].
    • Chip: a solid layer, at [320, 180], 24 by 24, filled #ffd166.
    • Plate: a solid layer, at [480, 80], 160 by 40, filled #1a1e26.

    The script. Add a project script named Orient to path and paste:

    export const outputs = [
      "layer_2:transform.position",
      "layer_2:transform.rotation",
    ];
    
    export function frame() {
      const rail = layer("Wave").path();
      const p = rail.pointAtFraction(0.75);
      const t = rail.tangentAtFraction(0.75);
      set("layer_2:transform.position", [p.x, p.y]);
      set("layer_2:transform.rotation", t.x === 0 && t.y === 1 ? 90 : 0);
    }
    

    Line by line:

    • Lines 7-9: The point and the unit tangent at the same fraction: on the wave's vertical drop the tangent points straight down, (0, 1) in y-down space.
    • Lines 10-11: The chip parks on the rail and turns to face the travel, the auto-orient-to-path pattern the feedback asked for. The proof drives 90 degrees only while the tangent reads exactly straight down.

    What you see. The chip sits on the vertical drop rotated 90 degrees, facing along the rail like a wave rider.

    The chip sits on the vertical drop rotated 90 degrees, facing along the rail like a wave rider.

    normalAtFraction#

    normalAtFraction(f: number): {x, y}
    

    The unit normal - the tangent rotated +90 degrees in y-down space (the E15 side convention).

    Determinism: pure.

    Worked example: Standing off the rail on the normal#

    The scene. A Shoreline comp holding an open two-segment Wave path at the origin, a small Chip ready to ride it, and a Plate solid wearing a rectangular mask whose rim a script can sample.

    • The comp Shoreline: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Wave: a path layer, at [0, 0].
    • Chip: a solid layer, at [320, 180], 24 by 24, filled #ffd166.
    • Plate: a solid layer, at [480, 80], 160 by 40, filled #1a1e26.

    The script. Add a project script named Normal offset and paste:

    export const outputs = ["layer_2:transform.position"];
    
    export function frame() {
      const rail = layer("Wave").path();
      const p = rail.pointAtFraction(0.5);
      const n = rail.normalAtFraction(0.5);
      set("layer_2:transform.position", [p.x + n.x * 40, p.y + n.y * 40]);
    }
    

    Line by line:

    • Lines 4-6: The normal is the tangent rotated ninety degrees in y-down space: on the horizontal run the tangent reads (1, 0) and the normal (0, 1), pointing below the rail.
    • Line 7: The chip hovers 40 px off the rail along the normal, the offset-rider pattern.

    What you see. The chip floats 40 px beneath the middle of the wave's horizontal run, at (250, 240).

    The chip floats 40 px beneath the middle of the wave's horizontal run, at (250, 240).