Note: This cartridge's settings do not allow embedded playback. A [Play at lexaloffle] link will be included instead.
Experiments in variable fps animation to simulate drawing / handwriting.
Drawing instructions are encoded and stored as a string in the lua code. Two different encodings ISIDORE and ROXANA were implemented, each with corresponding function used to decode and execute the drawing instructions.
TIP: toggle performance monitor using Ctrl+P to view fps
Motivation
When using animation to simulate something being hand-drawn or handwritten, ability to vary the speed of the "pen stroke" can help make the animation look more realistic and convincing, e.g. faster for straight lines, and slower for tight turns and loops.
How?
With this undocumented function:
_set_fps(rate)
The wiki seems to suggest that 15, 30, or 60 are the only acceptable values, but with a bit of experimentation I found that arbitrary values do work.
There are a few caveats:
custom main loop required
seems limited to maximum of 60 fps or thereabouts
tailoring the fps to whatever is being animated = generally only animating one thing at a time
undocumented feature, so no official support or guarantee it will continue to work
Preliminaries
Before doing anything fancy, it is important to note that on a raster display, animating a line or curve as it is extended pixel-by-pixel will cause the perceived speed of drawing to vary depending on direction. We will need to compensate for this when setting the speed at which a "pen stroke" is animated.
(If you understand why, feel free to skip ahead. Otherwise, open hidden block for explanation.)
A 60px horizontal line takes 1 second to animate at 60fps, and appears to be growing at a rate of 60px/second. A 45° diagonal line made up of 60 pixels also animates in the same time, but it will appear to be drawn at ~40% faster speed due to its length of ~60×sqrt(2).
When drawing curves rather than straight lines this phenomenon can cause the perception of variable speed over the course of the animation. Here is a circle outline that is noticeably slower at the top, bottom and sides. It takes about 4×d/sqrt(2)≈2.828×d frames to complete -- significantly faster than the theoretical circumference pi×d:
One approach to counter this while staying at constant fps would be to interpolate distance traveled by the "pen" over time. For instance, drawing a 45° diagonal line would add a pixel in only about 71% of frames (≈1/sqrt(2)). Applying this principle to draw the same circle outline as before produces a more realistic result:
A comparison with both overlaid shows the naive version clearly pulling ahead in the diagonals:
It is important to keep this phenomenon in mind when setting the speed at which a "pen stroke" is animated.
Bonus content for reading the explanation: circle drawn with variable fps
Encoding schemes
ISIDORE
The basic idea is to specify the predominant axis of motion and increment along it at 1px intervals, then provide the offset (along the other axis) where the pen/brush should be placed.
There are two variants:
Metavlitos: this was the original version which specified the brush size to use at every increment
Konstantinos: added later for more efficient encoding when brush size does not change often (=in most cases)
Details inside spoiler block:
Header
The first one or two characters initialize some settings:
0xFF-- initial speed in fps (1-256)
0x00FF initial brush diameter (1-256) <-- Konstantinos only
Increment
A byte <128 indicates data for the next increment along the current axis of drawing
0x80-- indicates a non-control token
0x7F-- offset (0-127)
0x00FF brush diameter (1-256) <-- Metavlitos only
Control codes
A byte >127 indicates upcoming data to be interpreted as a control code
0xF0 =0xF: invalid <-- deliberately not used
0x0F unused
0xF000 =0xE: sub-header
0x0400 axis (x/y)
0x0200 direction: forward/reverse
0x0100 movement (0-1) -- allows remaining stationary
0x007F skip ahead (0-127) increments
0x0880 unused bits
0xF000 =0xD: skip ahead
0x007F skip ahead (0-127) increments
0x0F80 unused bits
0xF00000 =0xC: change speed
0x00FF00 target speed (1-256) fps -- max 60 in practice
0x0000FF number of increments (1-256) to take to lerp toward target speed
0x0F0000 unused bits
0xF000 =0xB: change brush size <-- Konstantinos only
0x00FF brush diameter (1-256)
0x0F00 unused bits
0xF000 =0xA: pause
0x00FF number of frames (1-256) to advance without drawing (at current fps)
0x0F00 unused bits
0xF000 =0x9: no animate
0x00FF number of increments (1-256) to draw without advancing frames
0x0F00 unused bits
0xF000 =0x8: repeat
0x007F number of times (1-128) to repeat the previous increment data
0x0F80 unused bits
ROXANA
The basic idea is to specify the coordinates of a starting point, and subsequently move the pen/brush by small increments, each given as x- and y- offsets from the preceding position.
Like Isidore, Roxana has two variants:
Metavlitos: brush size specified at every increment
Konstantinos: changes in brush size requires control code
Details inside spoiler block:
Header
The first one or two characters initialize some settings (identical to Isidore):
0xFF-- initial speed in fps (1-256)
0x00FF initial brush diameter (1-256) <-- Konstantinos only
Increment
A byte <128 indicates data for the next increment, moving the pen/brush by an offset from its previous position.
A byte >127 indicates upcoming data to be interpreted as a control code
0xF0 =0xF: invalid <-- deliberately not used
0x0F unused
0xF00000-- =0xE: sub-header (reposition)
0X010000-- should initial position be drawn? (false/true)
0x00FF00-- x-position (-64-191)
0x0000FF-- y-position (-64-191)
0x000000FF brush diameter for initial position (1-256) <-- Metavlitos only
0x0E0000-- unused bits
0xF0 =0xD: invalid <-- "skip" in Isidore, irrelevant for Roxana
The remaining codes function identically as Isidore.
0xC: change speed
0xB: change brush size
0xA: pause
0x9: no animate
0x8: repeat
changelog
2021-04-09 - Roxana v1.0 initial version
2021-04-05 - Isidore v1.0 initial version