c2.js
c2.js is a JavaScript library for creative coding based on computational geometry, physics simulation, evolutionary algorithm and other modules.
… from the website.
This library does not have a content delivery network (CDN), so to use in my blog, the first thing I will need to do is download the c2.min.js
file and move it to the /etc/scripts
folder of my blog.
I can now load it into my blog page by referencing it in my markdown like this:
<script src='/etc/scripts/c2.min.js'></script>
Once the library is loaded, we have access to the c2.Renderer
class, which accepts a canvas element as an argument, and subsequently acts as an interface via which we can draw geometric primitives to the canvas:
<canvas id=c2_example_0></canvas>
<script type=module>
// getting the canvas element
let cnv = document.getElementById ('c2_example_0')
// instantiating a new c2.Renderer object
let renderer = new c2.Renderer (cnv)
// calculating width and height
const width = cnv.parentNode.scrollWidth
const height = width * 9 / 16
// setting the size of the canvas
// via the renderer object
renderer.size (width, height)
// setting the background colour
renderer.background ('turquoise')
// no outline
renderer.stroke (false)
// fill colour
renderer.fill (`hotpink`)
// draw circle
renderer.circle (width / 4, height / 2, 50)
// for easier trig:
const TAU = Math.PI * 2
// for convenience
const mid = new c2.Vector (width / 2, height / 2)
// to nudge the triangle down some arbitrary amount
const tri_off = new c2.Vector (0, 17)
// making a new Array with length = 3
const tri_corners = Array (3)
// filling the array with undefined
// this step is necessary for the
// next step to work
.fill ()
// .map will replace each element with the
// return value of the function we pass in
.map ((e, i) => {
// each point to be 66 pixels
// out from the centre
const vec = new c2.Vector (0, 66)
// we rotate 3 times to the circle
// offsetting i by 0.5 to point the
// triangle directly upwards
.rotate (TAU * (i + 0.5) / 3)
// moving the triangle to
// the centre of the canvas
.add (mid)
// nudging it down a bit
.add (tri_off)
// returning the vector places it
// in the array
return vec
})
// using the spread operator to give
// the contents of the tri_corners array
// to the .triangle method, as arguments
renderer.triangle (...tri_corners)
// drawing a square
renderer.rect (width * 3 / 4 - 50, height / 2 - 50, 100, 100)
</script>
Note that c2 has a Vector
class, which is similar to the p5 Vector
class, but with one important differnce. In c2, instance methods, such as .add ()
and .rotate ()
return a new vector object, rather than simply transforming the vector the method was called on.
A useful pattern to know looks like this:
const vec = new c2.Vector (0, 66)
.rotate (TAU * (i + 0.5) / 3)
.add (mid)
.add (tri_off)
Here we are creating a new c2.Vector
object and assigning it to the variable vec
. However, before it is assigned to vec, the .rotate ()
method is called on it, which returns a new, rotated, vector. However, before this new rotated vector is assigned to vec
, the .add ()
method is called on it, returning a new, translated vector, on which .add ()
is called one more time, returning another new translated vector, which is finally assigned to vec
.
So in total four vectors were created, of which only the final vector was then passed to the assignment procedure =
, and stored in the variable vec
.
This block of code uses this type of pattern twice:
const tri_corners = Array (3)
.fill ()
.map ((e, i) => {
const vec = new c2.Vector (0, 66)
.rotate (TAU * (i + 0.5) / 3)
.add (mid)
.add (tri_off)
return vec
})
The array is created with Array ()
, but before it is passed to the assignment procedure on the left, a .fill ()
method is called, which returns an array, on which the .map ()
method is called. Finally once the .map ()
function has exectuted, an array full of vectors is returned to the assignment procedure and stored in the variable tri_corners
.
You can read about .fill ()
here, and about .map ()
here, and watch about them both, here.
Also note my use of the spread operator ...
which allows me to pass the contents of an array into a function, as discrete arguments:
renderer.triangle (...tri_corners)
You can read about the spread operator here, or watch about here.
Random
c2 ships with a c2.Random
class, which instantiates an object capable being a linear congruential generator.
Click to start / stop animation:
<canvas id='c2.Random'></canvas>
<script type=module>
// get the canvas element
const cnv = document.getElementById (`c2.Random`)
// create an instance of the c2.Renderer
// passing in the canvas element
const renderer = new c2.Renderer (cnv)
// calculating and setting the size of the canvas
const width = cnv.parentNode.scrollWidth
const height = width * 9 / 16
renderer.size (width, height)
// we will build the rand_colour function
// with a c2.Random object:
const rand = new c2.Random ()
// giving the Random object a 'seed'
// will swap to using LCG method
const rand_seed = Math.random () * Number.MAX_SAFE_INTEGER
rand.seed (rand_seed)
// defining a function
function rand_colour () {
// calling the .next () method on the rand object
// passing in 360 to be the maximum value
// cutting off the decimal points with Math.floor ()
const h = Math.floor (rand.next (360))
// returning a string housing the random value for hue
return `hsl(${ h }, 100%, 50%)`
}
// this is the size of the "pixel"
// we will use for the static
const l = cnv.width / 256
// draw function
renderer.draw (() => {
// no outlines
renderer.stroke (false)
// go down the canvas
for (let y = 0; y < cnv.height; y+= l) {
// go across the canvas
for (let x = 0; x < cnv.width; x+= l) {
// get a random fill colour
renderer.fill (rand_colour ())
// draw a square there
// of size l
renderer.rect (x, y, l, l)
}
}
})
// the .loop method on the renderer
// sets the ._loop property of the renderer
renderer.loop (false)
// assigning to the .onclick a method that
// toggles the state of the ._loop property
cnv.onclick = e => renderer.loop (!renderer._loop)
</script>
Bubble friends
In this example we will make some pink bubbles that are perhaps a little over-eager to be friends with your cursor. Much of my code is inspired by this example, which uses this code.
We will first define a class called Bubble
. As we want the object we are defining to exhibit many of the properties and methods of an existing class, c2.Circle
, we can use the keyword extends
to automatically give our Bubble
all the properties and methods already contained in the c2.Circle
class. This is a common concept in object oriented programming, called inheritance. You can learn about the extends
keyword here, and here.
This is particularly useful for us, as we want to define custom behavour for our object (we want it to gravitate towards the pointer), but we also want to use c2.Circle.intersection ()
method to find the points the bubbles overlap each other. You can find some rudimentary documentation for the .intersection ()
method of the Circle
class here.
// declaring a Bubble class
// which inherits from c2.Circle
class Bubble extends c2.Circle {
// the constructor takes 5 arguments
// position, velocity, acceleration vectors
// a value for radius, and the renderer
constructor (pos, vel, acc, rad, renderer) {
// super is a keyword that calls
// the constructor of the super class
// c2.Circle in this case
super (pos, rad)
// assigns the vectors passed in
// for velocity and acceleration
this.v = vel
this.a = acc
// stores a reference to the renderer
// on the object, for drawing to
this.renderer = renderer
}
update () {
// the physics engine
// .add () returns a vector in c2
// add acceleration to velocity
this.v = this.v.add (this.a)
// .limit () to keep the velocity
// in a cute range
.limit (5)
// add velocity to position
this.p = this.p.add (this.v)
// bounce off the left
if (this.p.x < 0) {
this.p.x = 0
this.v.x *= -1
}
// bounce off the right
if (this.p.x > this.renderer.width) {
this.p.x = this.renderer.width
this.v.x *= -1
}
// bounce off the top
if (this.p.y < 0) {
this.p.y = 0
this.v.y *= -1
}
// bounce off the bottom
if (this.p.y > this.renderer.height) {
this.p.y = this.renderer.height
this.v.y *= -1
}
}
// method to draw the object
display () {
// no outline
this.renderer.stroke (false)
// pink fill
this.renderer.fill (`hotpink`)
// pass the object itself to
// the .circle method on the
// renderer to draw
this.renderer.circle (this)
}
}
Click on the canvas to toggle animation:.
<canvas id='bubble friends'></canvas>
<script type=module>
// get the canvas element
const cnv = document.getElementById (`bubble friends`)
// create an instance of the c2.Renderer
// passing in the canvas element
const renderer = new c2.Renderer (cnv)
// calculating and setting the size of the canvas
const width = cnv.parentNode.scrollWidth
const height = width * 9 / 16
renderer.size (width, height)
renderer.background (`turquoise`)
// pause the animation
renderer.loop (false)
// clicking on the canvas toggles the animation
cnv.onclick = () => renderer.loop (!renderer._loop)
// instantiate a c2.Random object
const rand = new c2.Random ()
// pass a random value to the rand object to seed
// linear congruential generator mode
const rand_seed = Math.random () * Number.MAX_SAFE_INTEGER
rand.seed (rand_seed)
// instantiating a c2.Vector to house
// the pointer coordinates. Giving an
// arbitrary starting position
const pointer = new c2.Vector (width / 2, height / 2)
// using the .onpointermove onevent listener
// on the document object to keep track of
// where the mouse is
document.onpointermove = e => {
// using the .page properties of the pointerEvent
// and the .offset properties of the canvas
// to calculate position of the mouse
// in relation to the canvas
pointer.x = e.pageX - cnv.offsetLeft
pointer.y = e.pageY - cnv.offsetTop
}
// storing 12 Bubble objects, with random position
// vectors, and a radius of 50, in an array
// assigned to the variable 'bubbles'
const bubbles = Array (12).fill ().map ((e, i) => {
const x = rand.next (width)
const y = rand.next (height)
const p_vec = new c2.Vector (x, y)
const v_vec = new c2.Vector (0, 0)
const a_vec = new c2.Vector (0, 0)
return new Bubble (p_vec, v_vec, a_vec, 50, renderer)
})
// passing an anonymous function in to the .draw method
renderer.draw (() => {
// clear the canvas
renderer.clear ()
// for each Bubble in bubbles
bubbles.forEach (b => {
// create the vector which points to the cursor
// from the bubble, by subtracting the bubble's
// position vector from the pointer vector
const to_pointer = pointer.sub (b.p)
// create a vector which simulates
// a gravity-like force towards the cursor
// .normalize () sets the magnitude to 1
const grav = to_pointer.normalize ()
// using inverse square law
// tinkered with the numerator
// until the result was sane / cute
.mult (5000 / to_pointer.magSq ())
// limited the maximum gravity
// to preserve sanity / cuteness
.limit (0.1)
// set the acceleration vector of the bubble
// to be the grav vector we just made
b.a = grav
// call .update () on the bubble
b.update ()
// display the bubble
b.display ()
})
// go through the bubbles array again
bubbles.forEach ((b, i) => {
// go through the remaining bubbles
// which have not yet been iterated over
for (let j = i + 1; j < bubbles.length; j++) {
// pass those bubbles to the original bubble's
// .intersection () method, which returns
// an array of points, we are assigning to p
let p = b.intersection (bubbles[j])
// if p is not empty
if (p != null) {
// make the line turquoise
renderer.stroke (`turquoise`)
// make the line 2 pixels thick
renderer.lineWidth (2);
// make a line between the two
// points in the array
renderer.line (p[0].x, p[0].y, p[1].x, p[1].y)
}
}
})
})
</script>