Examples :: Make an animated draw of a curve with random orbits

This script demonstrates the use of arrays for concurrent drawing of multiple clips (brush strokes) on a base clip, thus allowing for interesting effects.

The script draws an animated curve with a family of pens, each with a different color and brush (shape). The pens deviate randomly from the curve's main (x,y) coordinates within a given radius - like the "orbits" setting found in most modern painting programs - while the curve's main (x,y) coordinates advance constantly on each frame.

The result of running the script up to its last frame is presented below.


Figure 1: Last frame of clip produced by example script (version 1)

final frame of first example's clip


Now the script. For reasons of efficiency it uses global canvas-clips for achieving constant draw time per frame. As a consequence, works by design only on a linear pass from frame 0 to the last. That is if one adds filters at the end that request random frames back and forth it will not give the intended results.

LoadModule("avslib", "base", "core")
LoadModule("avslib", "array", "core")
LoadModule("avslib", "array", "operators")
LoadModule("avslib", "numeric", "rounding")
LoadModule("avslib", "string", "sprintf")
LoadModule("avslib", "filters", "utility")

Function ImgSource(string file, clip template, int "target_w", \
	int "target_h", int "frames", bool "pc_range")
	frames = Default(frames, template.Framecount)
	target_w = Default(target_w, template.Width)
	target_h = Default(target_h, template.Height)
	pc_range = Default(pc_range, false)
	ret = ImageSource(file, start=0, end=frames-1).AssumeFPS(template.Framerate)
	ret = ret.ConvertToTarget(template).BilinearResize(target_w, target_h)
	return pc_range ? ret.ScaleToPC() : ret

Function MakeSolidPen(clip brush, int pen_color) { 
	return brush.BlankClip(color=pen_color)

# create a clip (640x480, 5 sec)

global clp = BlankClip(length=120, color=color_darkslateBlue, \
	fps=24, width=640, height=480, pixel_type="YV12")

global pen_canvas = clp.BlankClip(color=color_black, length=1)  # pens draw here
global brs_canvas = pen_canvas.ScaleToPC()                      # brushes draw here

# create solid color brushes

global brushes = ArrayCreate( \
	ImgSource("brush1.jpg", clp, 24, 24, pc_range=true), \
	ImgSource("brush2.jpg", clp, 24, 24, pc_range=true), \
	ImgSource("brush3.jpg", clp, 24, 24, pc_range=true) \
colors = "color_gold, color_beige, color_crimson"

global pens = ArrayOpArrayFunc(brushes, colors, "MakeSolidPen")

# define the curves (they assume a 640x480 canvas)

Function NormDist(float x, float m, float s) {
	return (1.0/(Sqrt(2*Pi)*s))*Exp(-Pow(x-m,2)/(2*Pow(s,2)))

Function SinPulse(float x, float xc, float peak, float spread) {
	return peak*NormDist(x, xc, spread)*Sin((x-xc)/(2*Pi)) 

Function Curve(float x) { 
	return 240 - 120 * Sin(0.65*x*Pi/320) + SinPulse(x, 520, 14000, 120)

Up to this point the script loads required modules and declares the curve-generating functions and a utility function - ImgSource - that loads an image and converts it to a clip with the same characteristics as a template clip. In addition it creates the arrays with pens and brushes that will be used for drawing.

Because here different brush shapes and pen colors are drawn, the script uses two global canvas-like clips to hold brush strokes of the previous frames. One for the actual color (pen) strokes and one for the opacity mask (brush) strokes.

The actual drawing part of the script code follows:

# Draw on x,y; x belongs to [x_start, x_end)
# assumes brush, bmask have same dimensions

Function DrawItems(clip pen, clip brush, int x_start, int y_start) {
	dx = Round(brush.Width / 2.0)
	dy = Round(brush.Height / 2.0)
	# shuffle x,y based on radius
	xr = x_start + Round(Rand(StrokeRadius*2) - StrokeRadius)
	yr = y_start + Round(Rand(StrokeRadius*2) - StrokeRadius)
	global pen_canvas = Overlay(pen_canvas, pen, x=xr-dx, y=yr-dy, mask=brush, \
	global brs_canvas = Overlay(brs_canvas, brush, x=xr-dx, y=yr-dy, mask=brush, \
		opacity=StrokeOpacity, pc_range=true)
	return 1 # return value doesn't really matters

Function DrawCurve(clip c, string curve, int x_start, int x_end, int step)
	dummy2 = (x_start < x_end && x_start > xlast) ? Eval("""
		y_start = Round(Apply(curve, x_start))
		dummy = ArrayOpArrayFunc(pens, brushes, "DrawItems", \
			String(x_start) + "," + String(y_start))
		global xlast = x_start
	""") : (x_start <= xlast ? Eval("""
		x_start = IntBs(xlast, step) + step
	""") : NOP)
	return x_start < x_end \
		? DrawCurve(c, curve, x_start + step, x_end, step) \
		: Overlay(c, pen_canvas, mask=brs_canvas, opacity=1.0, \

The heart of the drawing procedure is DrawItems, which is called by ArrayOpArrayFunc for every defined pen-brush pair. The caller simply ensures that drawing occures only after the last x-point processed by the previous frames and, at the end of the recursion, overlays the pens' canvas on top of the base clip, using the brushes' canvas as a mask.

The if...elseif..else block inside DrawCurve (implemented with Eval and triply quoted strings) is used to quickly advance x_start in one step and avoid the cost of unneeded recursive function calls. IntBs(xlast, step) ensures that x_start will remain a multiple of step.

# begin draw
global xlast = 0  
global xs = 0
global xe = 0
xd = Int(clp.Width / clp.Framecount) 
global xdelta = clp.Framecount * xd < clp.Width ? xd + 1 : xd
global StrokeRadius = 20
global StrokeOpacity = 1.0

ScriptClip(clp, """
global xe = Min2(xe + xdelta, last.Width)
DrawCurve(last, "Curve", xs, xe, 4)
ConditionalReader("radius.txt", "StrokeRadius")
ConditionalReader("opac.txt", "StrokeOpacity")

The rest of the code is simply the setup of global variables needed for proper execution and the actual calls to ScriptClip and ConditionalReader.

The later is to allow the radius and opacity of the pen strokes to be controlled by external files; this gives greater flexibility for determining the final shape of the pen strokes. The links following give the specific files used in this example: i.radius.txt and ii.opac.txt.

The assignment to xe global is used to advance the pen strokes towards the right of the clip on each frame. It could be done in a separate FrameEvaluate call, but also in a separate line of the script passed to SriptClip (with triple quotes) as well. Here the later is selected for no special reason.


As is the case always, there is space for improvement in the script; the next example script, version 2 introduces improvements regarding the loading of brushes and pens by creating more generic and easy to adapt code.

The result of running the modified script up to its last frame is presented below.


Figure 2: Last frame of clip produced by example script (version 2)

final frame of second example's clip


The differences with the first version are briefly outlined below.

# create solid color brushes

Function LoadBrush(int idx, clip tpl, int w, int h) { 
	fname = "brush" + String(idx) + ".jpg"
	return ImgSource(fname, tpl, w, h, pc_range=true)

global brushes = ArrayRange(1, 6).ArrayOpFunc("LoadBrush", "clp, 24, 24")

colors = "color_gold, color_beige, color_crimson, color_forestgreen, " + \
	"color_lightskyblue, color_firebrick"

Brushes are loaded by a custom function that calculates the filename based on an index. This allows to load a sequence of brushes with a simple ArrayRange call followed by ArrayOpFunc generated calls to the custom function in the same script line, thanks to OOP notation.

Here since clp is a global we simply pass its name to the LoadBrush's arguments inside ArrayOpFunc call; else (if clp was not a global) we would have to use either ArrayCreate(clp) or StrPrint("%g", clp) to create a global and get its name as a string.

The colors array is updated also, to accommodate for the three new brushes used: brush4.jpg, brush5.jpg and brush6.jpg.