Search Docs
Loading...
Skip to content

Markers and Frame Rate

Set the frame rate of a scene and name points and spans on its timeline with markers.

A ball 40 percent of its way across the page, at the beat marker at 4 seconds

5 mins
estimated time
GitHub

The frame rate of a scene sets how many frames a second of its timeline has. Markers give names to points and spans on that timeline, such as chapters or beats, and travel with the scene. The editor has no controls for either, so you set them with the API.

This guide covers how to set the frame rate, how to add, read and remove markers, how to jump to a marker, and the errors to expect. It ends with a frame export.

Creating the Ball#

The example works on scene and on page, a page of 10 seconds. A small local function adds a ball that crosses the page in 10 seconds, so the playback time is easy to see.

// Creates a ball that crosses the page in 10 seconds
func createBall() throws {
let ball = try engine.block.create(.graphic)
try engine.block.setShape(ball, shape: engine.block.createShape(.ellipse))
let fill = try engine.block.createFill(.color)
try engine.block.setColor(fill, property: "fill/color/value", color: .rgba(r: 0.2, g: 0.4, b: 0.9, a: 1))
try engine.block.setFill(ball, fill: fill)
try engine.block.setWidth(ball, value: 160)
try engine.block.setHeight(ball, value: 160)
try engine.block.setPositionY(ball, value: 460)
try engine.block.appendChild(to: page, child: ball)
try engine.block.setDuration(ball, duration: 10)
try engine.block.setKeyframes(ball, property: "position/x", keyframes: [
Keyframe(time: 0, value: .float(100)),
Keyframe(time: 1, value: .float(1660)),
])
}

The example calls it once.

// A ball crosses the page over the 10 seconds of the page
try createBall()

Setting the Frame Rate#

scene/frameRate is a number property of the scene, in frames per second. It is 30 by default and accepts values greater than 0 and up to 240. getFloat(_:property:) reads it and setFloat(_:property:value:) sets it. The preview motion blur uses it, and so does a video export that passes no frame rate of its own.

// 30 frames per second by default
let defaultFrameRate = try engine.block.getFloat(scene, property: "scene/frameRate")
try engine.block.setFloat(scene, property: "scene/frameRate", value: 25)
let frameRate = try engine.block.getFloat(scene, property: "scene/frameRate")
print(defaultFrameRate, frameRate) // 30.0 25.0

Adding Markers#

Markers name points or spans on the scene timeline. engine.scene.addMarker(name:time:duration:) appends a marker with a name, a start time and a duration in seconds. A duration of 0, the default, marks a point. Several markers may share a name.

// A marker has a name, a start time and a duration in seconds.
// A duration of 0 marks a point.
try engine.scene.addMarker(name: "intro", time: 0, duration: 2.5)
try engine.scene.addMarker(name: "beat", time: 4)
try engine.scene.addMarker(name: "beat", time: 6)
try engine.scene.addMarker(name: "outro", time: 8, duration: 2)

Reading Markers#

engine.scene.getMarkers() returns the markers in the order they were added, as SceneMarker values with a name, a time and a duration.

let markers = try engine.scene.getMarkers()
let markerNames = markers.map(\.name)
print(markerNames) // ["intro", "beat", "beat", "outro"]

Jumping to a Marker#

Markers do not change the playback on their own. To jump to one, find it by name and pass its time to setPlaybackTime(_:time:) of the page.

// Jump to the first marker named "beat"
if let beat = markers.first(where: { $0.name == "beat" }) {
try engine.block.setPlaybackTime(page, time: beat.time)
}
let playbackTimeAtBeat = try engine.block.getPlaybackTime(page)
print(playbackTimeAtBeat) // 4.0

Removing a Marker#

engine.scene.removeMarker(at:) removes a marker by its index in the list that engine.scene.getMarkers() returns. The markers after it move up by one.

// Remove the second "beat" by its index in the list
try engine.scene.removeMarker(at: 2)
let markerCountAfterRemove = try engine.scene.getMarkers().count
print(markerCountAfterRemove) // 3

Handling Errors#

A finite frame rate outside its range, a negative or non-finite marker time or duration, and an index outside the marker list are refused, and nothing changes. The call throws an EngineError whose catalogCode names the cause.

var frameRateErrorCode: EngineErrorCode?
do {
// A frame rate is greater than 0 and at most 240
try engine.block.setFloat(scene, property: "scene/frameRate", value: 0)
} catch let error as EngineError {
frameRateErrorCode = error.catalogCode
print(frameRateErrorCode == .sceneFrameRateOutOfRange) // true
}
var markerTimeErrorCode: EngineErrorCode?
do {
try engine.scene.addMarker(name: "before", time: -1)
} catch let error as EngineError {
markerTimeErrorCode = error.catalogCode
print(markerTimeErrorCode == .sceneMarkerInvalidTime) // true
}
var markerIndexErrorCode: EngineErrorCode?
do {
try engine.scene.removeMarker(at: 10)
} catch let error as EngineError {
markerIndexErrorCode = error.catalogCode
print(markerIndexErrorCode == .sceneMarkerIndexOutOfRange) // true
}

Exporting a Frame#

The playback still stands at the beat marker, so we export the page as PNG at 4 seconds, with the ball 40 percent of its way across the page.

// The playback time is at the "beat" marker, 4 seconds into the page
let pngData = try await engine.block.export(page, mimeType: .png)
print(!pngData.isEmpty) // true

Markers, Frame Rate and Other Operations#

  • Saving and loading keep the markers and the frame rate. Undo and redo restore edits to them.
  • A scene load drops markers with a negative time or duration, and loads a frame rate above 240 as 240.
  • A scene imported from a Lottie file on Web or Node carries the Lottie frame rate and markers, so they are already set when you load that scene.

Troubleshooting#

  • The frame rate is not a number (EngineErrorCode.blockValueNotFinite): pass a finite value.
  • The frame rate is refused (EngineErrorCode.sceneFrameRateOutOfRange): pass a value greater than 0 and up to 240.
  • The marker is refused (EngineErrorCode.sceneMarkerInvalidTime): pass a time and a duration that are finite and not negative.
  • The marker cannot be removed (EngineErrorCode.sceneMarkerIndexOutOfRange): pass an index from 0 to the number of markers minus 1.
  • The wrong marker is removed: indexes follow the order of the list, and removing a marker moves the later ones up. Read the markers again after you remove one.

API Reference#

Methods#

Method Description
engine.block.getFloat(_:property:) Read the frame rate of the scene
engine.block.setFloat(_:property:value:) Set the frame rate, greater than 0 and up to 240
engine.scene.addMarker(name:time:duration:) Add a marker; duration defaults to 0
engine.scene.getMarkers() Read the markers in the order they were added
engine.scene.removeMarker(at:) Remove the marker at an index
engine.block.setPlaybackTime(_:time:) Jump to a time, for example a marker’s
engine.block.getPlaybackTime(_:) Read the playback time
engine.block.create(_:) Create the ball
engine.block.createShape(_:) Create the ellipse shape
engine.block.setShape(_:shape:) Give the ball its shape
engine.block.createFill(_:) Create a color fill
engine.block.setColor(_:property:color:) Set the fill color
engine.block.setFill(_:fill:) Give the ball its fill
engine.block.setWidth(_:value:) Set the width of the ball
engine.block.setHeight(_:value:) Set the height of the ball
engine.block.setPositionY(_:value:) Set the y position of the ball
engine.block.appendChild(to:child:) Add the ball to the page
engine.block.setDuration(_:duration:) Set the duration of the ball
engine.block.setKeyframes(_:property:keyframes:mode:) Move the ball
engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:) Export the frame as PNG

Properties#

Property Type Description
scene/frameRate Float Frames per second of the scene, greater than 0 and up to 240. Default 30

Next Steps#

  • Scenes: the other properties and methods of a scene.
  • Motion Blur: blur moving blocks over one frame of the scene frame rate.