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] );
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 humble 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 Rectangle
and Curve
, and Num module includes utility classes like Num
and 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 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.
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()
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 );
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 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 ) );
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.