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.

In 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[0], 5, "circle" );
form.strokeOnly("#f03", 2).line( [pts[0], space.pointer] );


Demo: Finding the closest pt to pointer and paint it red

Notice we just used the javascript 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.

The humbe 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" ) )


Demo: Relate a circle's size to its proximity to pointer

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.

Static Op

The Op module includes various static functions that deal with specific forms such as Rectangle and Curve, and the Num module includes static functions for simple 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)] );


Demo: Find a perpendicular line from a point onto an infinite path.

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 );


Demo: Drawing a B-spline using 5 anchor points.

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 Geom.interpolate op.

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.


Demo: Find 10 points closest to pointer and use them as anchor points to draw a B-spline.

That was quick! By combining different ops together, you can quickly try out and compare different options in forms and interactions.

Pt to Op

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

// fun

The 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 );

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 Polygon.convexHull(group) becomes makeRubberBand() since 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 pts").

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 ) );


Demo: Creating line segments from a sorted array of points, and then check their intersections with another line drawn by pointer.

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.

Cheat sheet

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.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.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.intersect2D( 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.