We are all in the gutter, but some of us are looking at the stars, said Oscar Wilde. To him, and to many of us, a sky full of stars moves us to look into that vast darkness and, with imagination, connect those scattered and flickering dots. What would a sky be without stars, and what would a starry night be without us looking and imagining!
A group of points is never just a mathematical concept. We actively connect the dots and find strange forms and humanistic meanings in them.
Pts, these acts of imagination are known as "Op". They transform a static point into an active one, a noun into a verb, a vector space into an expressive canvas.
Let's begin with an example. Suppose there are 100 points randomly placed on a canvas, and a pointer moves randomly about. A simple (indeed boring) act of imagination might be to ask: which point is closest to the pointer?
We can draw this whole scene a few lines of code:
// make 100 pts and pointer var pts = Create.distributeRandom( space.innerBound, 100 ); let t = space.pointer; // sort the pts pts.sort( (a,b) => a.$subtract(t).magnitudeSq() - b.$subtract(t).magnitudeSq() ); // draw the pts form.fillOnly("#123").points( pts, 2, "circle" ); form.fill("#f03").point( pts, 5, "circle" ); form.strokeOnly("#f03", 2).line( [pts, space.pointer] );
sort function to rearrange the
pts group by comparing two points' distance to
space.pointer. Then we draw the first point in the group (the closest) in red.
sort function is essentially an "Op" by our definition. It transforms a group of points and makes it meaningful. From here on, it's easy to imagine what other structures we can derive from this simple sketch. For example, what if we visualize all points' distances to the pointer in different ways:
pts.forEach( (p, i) => form.point( p, 20 - 20*i/pts.length, "circle" ) )
The group of points becomes active. It's somewhat interesting and kind of a mess -- a starting point for further experimentation.
Pts includes many different Ops to help you make the points meaningful. Next we will look at them in more details.
The Op module includes various static functions that deal with specific forms such as
Curve, and Num module includes utility classes like
Geom for numeric and geometric calculations. Most of them are straightforward and easy to use.
It's time to let our imaginary forces work on these functions. Keeping most of the code from above, let's try the
perpendicularFromPt function. Given a line and a point, this Op finds a perpendicular line (ie, shortest distance) between the point and the line.
let path = [new Pt(), space.pointer]; let perpends = pts.map( (p) => [p, Line.perpendicularFromPt(path, p)] );
First we create a line by joining the pointer and the top-left corner at (0,0), and then we convert the set of random points on canvas to perpendicular lines. Also note that, since we don't need additional features from
Group, we can just use Array to store the Pts for drawing. Pretty fun and simple, right?
Ops can also construct shapes out of points. Creating a rectangle or a line from 2 points is obvious, so let's get a bit more elaborate.
// create a group of 4 Pts from rectangle let c = space.center; let corners = Rectangle.corners( Rectangle.fromCenter( c, space.height ) ); // interpolate with time to make them move let cycle = (t, i) => Num.cycle( (t+i*500)%3000/3000 ); let pts = corners.map( (p, i) => Geom.interpolate( p, c, cycle(time, i)) ); // close the B-spline by adding first 3 anchors at the end pts.push( space.pointer ); pts = pts.concat( pts.slice(0, 3) ); // draw the B-spline curve let curve = Curve.bspline( pts ); form.fill("#f03").stroke("#fff", 3).polygon( curve );
What's going on here? First, we use
Rectangle.fromCenter to make a rectangle and then get its 4 corners as a group.
Then, we define a function called
cycle to get a value between 0 to 1 for interpolation. The function takes two parameters
t (for time) and
i (for index). And we map the 4 corners into 4 interpolated points, making use of
Next, we add the first 3 points again to the end of the
pts group, which is a quick way to close a b-spline curve. Finally, we get the curve from
Curve.bspline and just draw it.
Knowing how bspline works, we can easily apply it to our 100 random points. The following sketch also uses
Polygon.convexHull: think of it like a rubber band that wraps around a group of points.
That was quick! By combining different ops together, you can quickly try out and compare different options in forms and interactions.
If you think of code like a narrative, then the static ops are like monologues — telling the story in a dull way.
// dull Polygon.convexHull( pts ) // meh pts.convexHull() // fun makeRubberBand()
op function in both Pt and Group enables you to turn your dull code into an expressive one. Let's see how it works:
let makeRubberBand = pts.op( Polygon.convexHull ); makeRubberBand();
When you supply
op with a function, it applies the Pt or Group as a parameter to that function, and returns a new function with one less parameter. That's all. So here
pts is applied as the first parameter
group. If it's still confusing, think of it as a noun ("the
pts") turning into a verb ("make rubber band using the
Let's illustrate this with a concrete example. Suppose we want to make 50 lines by pairing the 100 random points, and then find out which lines intersect with another line drawn by the pointer. What's the code?
It only takes 3 lines:
let pairs = pts.segments(2, 2); let hit = new Group(space.center, space.pointer).op( Line.intersectLine2D ); let hitPts = pairs.map( (pa) => hit( pa ) );
First, we take every 2 points in
pts to make 50 lines. Next, we make a line from space's center to pointer, and immediately turn it into an op of
Line.intersectLine2D. Lastly we just apply the
hit function to each pair and get its intersection point.
This approach works best if the op will be re-used in different scenarios, or if it can make the code easier to read. Of course, you can always use the static intersectLine2D function inside the
map(...), or even create a custom function and call it
hit. Just like there're many ways to tell a story, there're many ways to write code.
Num from "Num" module includes helper functions to simplify numeric calculations.
Num.cycle( 0.3 ); // cycle between 0...1...0 Num.mapToRange( 5, 1,100, 0, 2 ); // map a value to new range Num.lerp( 1, 100, 0.2 ); // linear interpolation
Geom from "Num" module includes helper functions to simplify geometric calculations.
Geom.boundAngle( 361 ); // bound between 0 to 360 Geom.withinBound( p1, top_left, bottom_right ); Geom.interpolate( p1, p2, 0.3 );
Line from "Op" module helps you create and work with lines.
Line.fromAngle( p1, Math.PI/3, 10 ); // create with angle and distance Line.collinear( p1, p2, p3 ); Line.intersectRay2D( ln1, ln2 ); Line.subpoints( 5 ); // get 5 evenly distributed pts on the line
Rectangle from "Op" module helps you create and work with rectangles.
Rectangle.fromCenter( center, 100, 50 ); Rectangle.corners(); Rectangle.sides(); Rectangle.quadrants(); // get 4 inner rectangles Rectangle.intersectRect2D( rect1, rect2 );
Circle from "Op" module helps you create and work with circles.
Circle.fromCenter( center, 10 ); Circle.fromRect( rect ); Circle.toRect(); Circle.intersectCircle2D( c1, c2 );
Triangle from "Op" module helps you create and work with triangles.
Triangle.fromCircle( c ); // equilateral triangle Triangle.fromRect( rect ); Triangle.incircle( tri ); Triangle.orthocenter( tri ); Triangle.medial( tri );
Polygon from "Op" module helps you create and work with polygons.
Polygon.centroid( poly ); Polygon.convexHull( poly ); Polygon.lines( poly ); // get line segments Polygon.intersectPolygon2D( poly, lines );
Curve from "Op" module helps you create and work with curves.
Curve.catmullRom( pts ); Curve.cardinal( pts ); Curve.cardinal( pts, 20, 0.3 ); // step and tension parameters Curve.bezier( pts ); Curve.bspline( pts );
Check out the full documentation too.