Skip to content
garciadelcastillo edited this page Jan 16, 2015 · 41 revisions

Introduction

Make sure you have read Basic Setup and Hello World before continuing this walkthrough.

Setup

To start, create a basic HTML file that incorporates a Canvas element with an unique id, and import the latest stable version of Sketchpad.js. Sketchpad relies on jQuery for DOM manipulation, so make sure it is available in scope.

A basic HTML scaffolding for a full-page canvas could look as follows:

<!DOCTYPE html>
<meta charset="utf-8">
<html>
  <head>
  </head>
  <style>
    body {
      margin: 0;
      padding: 0;
    }

    #sketchpadDiv {
      position: absolute; 
      width: 100%; 
      height: 100%;
    }

    #sketchpadCanvas {
      position: absolute;
    }

  </style>
  <body>
    <div id="sketchpadDiv">
      <canvas id="sketchpadCanvas"></canvas>       
    </div>
  </body>
  <script type="text/javascript" src="jquery-2.1.1.min.js"></script>
  <script type="text/javascript" src="sketchpad.js"></script>
  <script>

    // Sketchpad code goes here

  </script>
</html>

Initialization

Once the main structure is in place, the first thing is to create a new instance of a Sketchpad object, passing in the id of the Canvas DOM element as a string (with no CSS selectors):

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');  

pad becomes the namespace containing accessors to Sketchpad's functionality and storing all Geometry, parent-children relational tree and updating engine for that particular Canvas. This is particularly useful if several instances of Sketchpad are running for separate Canvases in the same document concurrently.

Primitives

Sketchpad incorporates a set of common Geometry primitives for vector drawing. These can be constructed with absolute numeric values (i.e. with no associativity to other objects) by instantiating them with the built-in constructors:

// Unconstrained Geometry
var point = new pad.Point(100, 100),          // Point(x, y)
    line = new pad.Line(100, 150, 300, 150),  // Line(x0, y0, x1, y1)
    circle = new pad.Circle(100, 200, 25);    // Circle(x, y, radius)

// Points are invisible by default
point.setVisible(true);                       

At the moment, only Point, Line and Circle object are fully supported, but many more are on the way!

Nodes

Nodes are a special subset of Point element. They are visible by default, and allow mouse interactivity by drag & dropping. Nodes can move freely on the Canvas, or have their movement constrained in certain directions:

// A free unconstrained Node, created with constructor
var node = new pad.Node(160, 200);  

// A Node with constrained Y movement
var nodeX = pad.Node.horizontal(320, 200);  

// A Node with constrained X movement
var nodeY = pad.Node.vertical(480, 200);  

Nodes inherit from the Point class, and share most properties and methods (see exceptions below).

Constructivity and Associativity

Ever felt frustrated by wanting to draw hierarchical geometry, and having to manually implement complicated update cycles? Pain is over.

Constructivity and associativity are the main features of Sketchpad. When you create geometry, using other elements as primitives, Sketchpad keeps track of the parent-children relations, and takes care of maintaining them consistently through the life of the sketch. This makes it very easy to create Geometry constructively in an intuitive way, and not needing to worry about managing update and render cycles.

Let's look at the most basic example, a Line between two Points:

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');  

// Create two unconstrained Nodes
var A = new pad.Node(160, 200),
	B = new pad.Node(480, 200);

// Now create a Line, passing the Nodes as arguments
var AB = pad.Line.between(A, B);

// Let's display some automatic tags on current sketch elements
pad.tagElementNames();

The Line object maintains the relation with its parent Nodes after manipulation.

Several methods allow creating Geometry constructively, based on parent objects. Let's look at an example with Point.along(Geometry, relativeLength):

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// Create a Line between two Nodes
var A = new pad.Node(160, 200),
	B = new pad.Node(480, 200),
	AB = pad.Line.between(A, B);

// Create a Point at 3/4 the length of the Line, 
// and use it as the center of a Circle
var C = pad.Point.along(AB, 0.75),
    bigCircle = pad.Circle.centerRadius(C, 51); 

// Create a Point at 1/4 the length of that Circle, 
// and draw another Circle
var D = pad.Point.along(bigCircle, 0.25),
	smallCircle = pad.Circle.centerRadius(D, 26);

// Display Tags only on Node elements
pad.tagNodes();

As you can see above, hierarchical relations between Geometry persist during Node manipulation, implicitly declared on construction.

In the previous example, we have used Points as anchors for the Circle elements, resulting in a totally constrained relation with its parent parameters. Using Node instead would allow for that element to still be constrained to the parent object, while having a degree of freedom based on the former:

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// Create a Line between two Nodes, and two nested Circles
var A = new pad.Node(160, 120),
	B = new pad.Node(480, 120),
	AB = pad.Line.between(A, B);

var C = pad.Point.along(AB, 0.75),
    circleC = pad.Circle.centerRadius(C, 51); 

var D = pad.Point.along(circleC, 0.25),
	circleD = pad.Circle.centerRadius(D, 26);

// Now create the same setup, using Node elements as anchors instead
var M = new pad.Node(160, 280),
	N = new pad.Node(480, 280),
	MN = pad.Line.between(M, N);

var O = pad.Node.along(MN, 0.75, {clamp: true}),
	circleO = pad.Circle.centerRadius(O, 51);

var P = pad.Node.along(circleO, 0.25),
	circleP = pad.Circle.centerRadius(P, 26);

// Display Tags only on Node elements
pad.tagNodes();

As a rule of thumb, static methods for constructing any type of Geometry can be accessed throught the Class to which the expected return type will belong. For example, a Line to Line intersection yields a Point in return, therefore the static method Point.intersection(line0, line1):

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// Create some Nodes and join them with Lines
var A = new pad.Node(100, 100),
    B = new pad.Node(540, 300),
    C = new pad.Node(540, 100),
    D = new pad.Node(100, 300),
    AB = pad.Line.between(A, B),
    CD = pad.Line.between(C, D);

// Compute the intersection of the Lines, and attach a Circle to it
var X = pad.Point.intersection(AB, CD),
	circleX = pad.Circle.centerRadius(X, 101);

// Compute intersections between Circle and Lines
var XAB = pad.Point.intersection(AB, circleX),
	XCD = pad.Point.intersection(CD, circleX);

// Display and Tag all Points
pad.showPoints();
pad.tagPoints();

Similarly, for Circle-Circle intersections:

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// Create some Nodes and attach some Circles to them
var A = new pad.Node(270, 150),
    B = new pad.Node(370, 150),
    C = new pad.Node(320, 250),
    circleA = new pad.Circle.centerRadius(A, 51),
    circleB = new pad.Circle.centerRadius(B, 76),
    circleC = new pad.Circle.centerRadius(C, 101);

// Compute intersections between circles
var ABX = pad.Point.intersection(circleA, circleB),
	BCX = pad.Point.intersection(circleB, circleC),
	CAX = pad.Point.intersection(circleC, circleA);

// Display and Tag all Points
pad.showPoints();
pad.tagPoints();

You can also project Points on Geometry:

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// Create some Nodes, Lines and Circles
var A = new pad.Node(80, 100),
    B = new pad.Node(280, 300),
    C = new pad.Node(460, 200),
	P = new pad.Node(280, 100),
    AB = pad.Line.between(A, B),
    circleC = new pad.Circle.centerRadius(C, 101);

// Compute the projection of P on the Line and Circle
var PAB = pad.Point.projection(P, AB),
	PC = pad.Point.projection(P, circleC);

// Create a Line between the projections
var PABC = pad.Line.between(PAB, PC);

// Display and Tag all Points
pad.showPoints();
pad.tagPoints();

Tags and Labels

Sketchpad's Labels represent textual information. They can be linked to other objects by means of composition. They are rendered through the Tag object, which can also be declared as a primitive.

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// Render a fixed text Tag on XY position
var title = new pad.Tag('This is a fixed Tag', 320, 40);

// Create a text Label linked to Node P
// Note how Label.compose takes as arguments all the elements
// the Label is child to, plus a callback function 
// returning the Label text string
var P = new pad.Node(320, 200);
var posLabel = pad.Label.compose(P, function() {
    return '[' + Math.round(P.x) + ', ' + Math.round(P.y) + ']';
});

// Now render a text Tag on Node P with text Label
var positionTag = pad.Tag.on(P, posLabel);

// Similarly, display Node distance to [0, 0]
var distLabel = pad.Label.compose(P, function() {
    var d = Math.sqrt(P.x * P.x + P.y * P.y);
    return 'This is a dynamic Tag. D = ' + Math.round(d) + ' px';
});

var anchor = new pad.Point(320, 375),
    distTag = pad.Tag.on(anchor, distLabel);

Measures

Associativity in Sketchpad is not only reduced to Geometry. Numeric relations can also be constructed associatively by means of the Measure object. Measures can be passed as arguments pretty much anywhere that would accept a plain numeric input.

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

var C = new pad.Node(320, 200),
    P = new pad.Node(495, 200),
    CP = pad.Line.between(C, P);

// Create a Measure representing the distance between C and P
var R = pad.Measure.distance(C, P);

// Use the distance to create a Circle between the Points
var circle = pad.Circle.centerRadius(C, R);

// Compose an area Measure by passing parent references and callback
// Note how numeric measures are accesssed through the .value property
var area = pad.Measure.compose(R, function () {
    return Math.PI * R.value * R.value;
});

// Add some Tags
var labelR = pad.Label.compose(R, function() {
        return 'R = ' + Math.round(R.value) + ' px';
    }),
    tagR = pad.Tag.on(CP, labelR);

var labelArea = pad.Label.compose(area, function() {
        return 'Area = ' + Math.round(area.value) + ' px';
    }),
    tagArea = pad.Tag.on(C, labelArea);

Any instance of a Sketchpad will have some default (environment) Measures representing the current state of the sketch, most prominently Sketchpad.width and Sketchpad.height. Centering an element on the Canvas object becomes as easy as deriving half measures from these:

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// .width and .height properties are accessible as Measures to inherit from
var label = pad.Label.compose(pad.width, pad.height, function() {
    return 'Canvas dimensions: ' + pad.width.value 
            + 'x' + pad.height.value + ' px';
});

// Compose half Measures and attach a Circle on Canvas center
var halfWidth = pad.Measure.compose(pad.width, function () {
        return pad.width.value / 2;
    }),
    halfHeight = pad.Measure.compose(pad.height, function() {
        return pad.height.value / 2;
    });
var C = pad.Point.fromMeasures(halfWidth, halfHeight),
    circle = pad.Circle.centerRadius(C, 101);

// Display dimension on the center
var tag = pad.Tag.on(C, label);

Styling

Sketchpad accepts CSS-like style declarations via the Style model. This includes strokes, fills, fonts and alignments.

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// Draw a circle with current default Style
var C1 = new pad.Node(128, 200),
    C1c = pad.Circle.centerRadius(C1, 100);

// Create a Style object
var beige = new pad.Style({
    stroke: 'rgb(224, 228, 204)',
    strokeWidth: 3.0,
    fill: 'rgba(224, 228, 204, 0.5)'
});

// From now on, draw elements with new Style
pad.currentStyle(beige);

// These new elements will use the new default style
var C2 = new pad.Node(256, 200),
    C2c = pad.Circle.centerRadius(C2, 100);

// More Styles
var aqua = new pad.Style({
        stroke: 'rgb(167, 219, 216)',
        strokeWidth: 3.0,
        fill: 'rgba(167, 219, 216, 0.5)'
    }),
    cyan = new pad.Style({
        stroke: 'rgb(105, 210, 231)',
        strokeWidth: 3.0,
        fill: 'rgba(105, 210, 231, 0.5)'
    });

// Still using 'reddish' for these
var C3 = new pad.Node(384, 200),
    C3c = pad.Circle.centerRadius(C3, 100),
    C4 = new pad.Node(512, 200),
    C4c = pad.Circle.centerRadius(C4, 100);

// Styles can be applied indivually to elements
aqua.applyTo(C3, C3c);
cyan.applyTo(C4, C4c);

// Font styling can also be defined, see docs for options
var arial12black = new pad.Style({
    stroke: 'black',
    fontFamily: 'Courier',
    fontSize: '9pt',
    fontStyle: 'bold',
    textHAlign: 'left', 
    textVAlign: 'top'
});

// Use new Style
pad.currentStyle(arial12black);
var title = new pad.Tag("Topleft aligned bold 9pt Courier", 10, 10);

Sets

Sketchpad implements the possibility of working with lists of Objects as operators.

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// Create some Lines and Circles
var A = new pad.Node(128, 100),
    B = new pad.Node(256, 300),
    C = new pad.Node(512, 100),
    D = new pad.Node(384, 300),
    E = new pad.Node(320, 100),
    AB = pad.Line.between(A, B),
    DC = pad.Line.between(D, C),  // note orientation change here
    circleE = pad.Circle.centerRadius(E, 50);

// Create a Set of numbers with 100 steps from 0 to 1
var divs = pad.Set.range(0, 1, 100);

// Use those values to create Point Sets along Lines and Circles
var ABpts = pad.Point.along(AB, divs),
    DCpts = pad.Point.along(DC, divs),
    circleEpts = pad.Point.along(circleE, divs);

// Apply new Style
pad.currentStyle( new pad.Style({ stroke: 'rgba(127, 127, 127, 0.3' }) );

// Create Line Sets between Point Sets
var lineToLine = pad.Line.between(ABpts, DCpts),
    lineToCircle = pad.Line.between(DCpts, circleEpts);

Please note Sets are currently under severe redevelopment, and behavior may change soon.

Animation

Scripted changes can be added to the sketch by overriding its update function. This process would be analogous to Processing's draw() function:

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// Construct a circle with a rotating line inside
var center = new pad.Node(pad.width.value / 2, pad.height.value / 2),
	circle = pad.Circle.centerRadius(center, 100);
	tip = pad.Point.along(circle, 0.0),
	hand = pad.Line.between(center, tip);

// Override sketch's update function with a custom updater
pad.update = function () {
	// Make sure to use object's methods to modify values
	// or changes won't replicate to children elements
	tip.setParameter(pad.frameCount / 500); 
	// tip.parameter = pad.frameCount / 500;  // this won't work
};

UI

The nature of Sketchpad makes it very easy to create UI elements such as sliders or knobs, which stream their values directly to Measure objects and trigger update chains.

// Create new instance of Sketchpad in target Canvas
var pad = new Sketchpad('sketchpadCanvas');

// Create a Measure object through the Slider interface
var radius = Slider(pad, 25, 25, 150, 0, 300, 150);

// Now use the slider's Measure as radius for a circle
var center = new pad.Node(pad.width.value / 2, pad.height.value / 2),
    circle = pad.Circle.centerRadius(center, radius);

/**
 * A quick implementation of a Slider class
 * Returns a Measure object defined by the slider's state
 */
function Slider(pad, x, y, width, minValue, maxValue, startValue) {
  var axis = new pad.Line(x, y, x + width, y),
      t = (startValue - minValue) / (maxValue - minValue),
      handle = pad.Node.along(axis, t, {clamp: true});
  var measure = pad.Measure.compose(handle, function() {
    return minValue + (maxValue - minValue) * (handle.x - x) / width;
  });
  var label = pad.Label.from(measure, {round: true}),
      tag = new pad.Tag.on(handle, label);
  return measure;
};

Check another example here.