Drive animation with time, parameters and a few functions.

A property script sets layer, camera or shape values from an expression that is evaluated on every frame. This reference covers the script format, the values you can set, the functions available, and the request that adds a script to a project.

When to use a script

Use keyframes when you know the important moments and values. Use a live driver when one value should follow another with spring or pendulum physics. Use a property script when the movement follows a rule: a handheld drift, a bob that repeats every two seconds, a focus pull mapped from one range to another.

The parameters you declare become controls in Properties, so the rule stays fixed while its amounts are adjusted or keyed.

Script format

A script imports grapple, defines one function, looks up its targets and assigns values to them:

import grapple as gp

def drift():
  layer = gp.target("layer")
  layer.position.x = gp.wiggle(gp.time(), 7, gp.param("frequency"), gp.param("amount"))
  layer.rotation_degrees = gp.wiggle(gp.time(), 8, gp.param("frequency"), gp.param("tilt"))
def drift():
The entry point. Its name is declared in the request.
gp.target("layer")
Returns the property named by the binding with the handle layer. The local variable is that property, so you write layer.position.x, not layer.transform.position.x.
Assignments
Each line sets one value. The right-hand side is an expression.

The binding also lists the channels the script controls. Assign every listed channel; channels you don't list keep their own values and keyframes.

Values a script can set

BindingAssignUnits
transformposition.x, position.y, scale.x, scale.y, rotation_degrees, opacityPosition in composition space; rotation in degrees; scale and opacity as factors, where 1 is 100%
transform on a 3D layerposition.z, scale.z, rotation.x_degrees, rotation.y_degrees, extrusion.depth, extrusion.bevel, material.base_color.r / .g / .b, material.roughness, material.metallic, material.specularDepth in scene units, where a full-frame plane is 1
camera.motionmotion.position.x, motion.position.y, motion.position.z, motion.zoom, motion.rotation_degrees, motion.rotation.x_degrees, motion.rotation.y_degrees, lens.focal_length, lens.sensor_width, lens.focus_distance, lens.apertureFocal length and sensor width in millimetres; aperture as an f-stop
shape.geometrywidth, height, corner_radius, line_start.x / .y, line_end.x / .y, path_trim_start, path_trim_end, path_trim_offsetPath trim from 0 to 1

Composition space puts (0, 0) at the centre of the frame, with the edges at −0.5 and 0.5. Positive x is to the right, positive y is up, and positive rotation is anticlockwise. A wiggle amount of 0.01 moves a layer by one percent of the frame. Corner radius applies to rounded rectangles, line ends to lines, and path trim to lines and paths.

Functions

gp.time()
The composition time in seconds.
gp.param("name")
The current value of a declared parameter, including its keyframes.
gp.ease(v, in0, in1, out0, out1)
Maps v from the input range to the output range with a smooth start and finish, and holds at the ends outside the range.
gp.remap(v, in0, in1, out0, out1)
Maps v linearly and keeps going past the ends of the range.
gp.loop(v, period)
Wraps v into the range from 0 up to period. The period must be greater than zero.
gp.wiggle(v, seed, frequency, amplitude)
Smooth noise between −amplitude and +amplitude. The seed is a whole number; the same seed always gives the same motion. Frequency sets how many new random targets the value moves through per unit of v, so with gp.time() it is per second. It can't be negative.

Expressions can also use numbers, parentheses, +, -, * and /. Division by zero, an empty ease or remap range and non-finite results are reported as errors on the property.

Examples

A bob that repeats every two seconds

import grapple as gp

def bob():
  badge = gp.target("badge")
  badge.position.y = gp.param("rest_y") + gp.param("height") * (gp.ease(gp.loop(gp.time(), 2), 0, 1, 0, 1) - gp.ease(gp.loop(gp.time(), 2), 1, 2, 0, 1))

The first ease rises from 0 to 1 over the first second of each loop and holds; the second rises over the next second. Subtracting one from the other gives a smooth rise and fall. Binding property transform, controlling position_y.

A focus pull between two times

import grapple as gp

def pull():
  cam = gp.target("cam")
  cam.lens.focus_distance = gp.ease(gp.time(), gp.param("start"), gp.param("end"), gp.param("near"), gp.param("far"))

Binding property camera.motion, controlling focus_distance. Give the camera an aperture above zero, or there is no depth of field to see.

Draw a path on

import grapple as gp

def draw_on():
  route = gp.target("route")
  route.path_trim_end = gp.ease(gp.time(), gp.param("start"), gp.param("end"), 0, 1)

Binding property shape.geometry on a path shape, controlling path_trim_end.

Declare parameters

Each parameter becomes a control in Properties. Give it a name used in the script, a label, a short description, a starting value and an editor:

{
  "name": "amount",
  "label": "Drift amount",
  "description": "How far the layer drifts, as a fraction of the frame width.",
  "value": 0.01,
  "editor": { "family": "scalar", "min": 0, "max": 0.1, "step": 0.001 }
}

Keep creative values such as ranges, periods, seeds, frequencies and amounts in parameters rather than as numbers in the source, so they can be changed and keyed without editing the script.

Add a script to a project

Ape adds scripts with the property.author_controller operation. Give it the source and the parameters, and it looks up the layer and fills in the rest. This is the request it sends for the drift example:

{
  "displayName": "Handheld drift",
  "entrypoint": "drift",
  "source": "import grapple as gp\n\ndef drift():\n  layer = gp.target(\"layer\")\n  ...",
  "bindings": [{
    "targetNodeId": "<layer id>",
    "property": "transform",
    "handle": "layer",
    "controlledChannels": ["position_x", "rotation_degrees"]
  }],
  "params": [ ...parameter declarations... ],
  "activeRange": { "start": 0, "end": 12 }
}

The active range is in seconds of composition time. For scripted workflows, the command-line tool can send the same request to a project folder:

grapple-cli --open-package "C:\Users\you\Documents\Grapple\Night walk" --invoke-tool property.author_controller --arguments-file drift.json

To find layer IDs, run the same command with --invoke-tool project.inspect and an arguments file containing {}.