Chapter III.

SVG

ear.svg

SVGs are vector-based, look great at any resolution, and have fantastic interoperability with plotters, cutters, and web browsers.

Introduction

var svg = ear.svg()

This SVG initializes with a 300x150 blank canvas, but isn't mounted yet to the document. Everything adheres to the HTML DOM standard with methods like appendChild and setAttribute.

Your first task is to append it to the DOM.

var svg = ear.svg()
document.body.appendChild(svg)

Elements made with this library are HTML DOM Elements and work with DOM level 1 methods.

Or, more conveniently:

var svg = ear.svg(document.body)

the svg initializer doesn't require any arguments, but optionally can accept:

The function parameter is an extremely convenient way of creating an SVG.

ear.svg((svg) => {
svg.circle(0, 1, 2);
});

Combining both of these looks like

ear.svg(document.body, (svg) => {
svg.circle(0, 1, 2);
});

This is a very convenient way to wait for page load and encapsulate all your code within a function-scope.

Drawing Shapes

Drawing a shape is a method called on the shape's parent:

var svg = ear.svg();
svg.circle(0, 1, 2);

These are the simple primitives:

svg.circle(x, y, radius)
svg.ellipse(x, y, rx, ry)
svg.line(x1, y1, x2, y2)
svg.rect(x, y, width, height)
svg.text(string, x, y)

The type of all arguments are numbers, except for "string" on text.

All primitives are initially black fill with no stroke, causing a line to be invisible. If you draw a line remember it needs to be styled.

Polygon, Polyline

svg.polygon(points)
svg.polyline(points)

Polygon and polyline take a series of points, which can come in the form of a list of arrays of numbers, or an array of x,y objects.

[ [4, 5], [1, 1], [9, 12] ]
[ {x:5, y:2}, {x:1, y:1} ]

Paths

Paths are the most powerful SVG drawing tool. Think of them like a pen tool; drawing is done by stacking functions one after another.

svg.path()
  .moveTo(50, 50)
  .LineTo(100, 150)
  .curveTo(200, 200, 300, 0)

Path command names follow the spec, including capitalization for absolute/relative.
("lineTo" takes relative coordinates, "LineTo" absolute).

Groups

So far, we have only drawn shapes onto the SVG, or the base layer. If you create a group, you can draw shapes into it instead and manage the layer order of shapes.

var group = svg.g()
svg.rect(0, 0, 20, 20)
group.circle(10, 10, 20)

In this code example, this circle will appear underneath the rectangle.

Viewbox

The actual size on screen is managed by the DOM. The viewbox is our tool to set the size and scale of our drawing canvas.

svg.size(-1, -1, 2, 2)  // x, y, width, height

The first two parameters define the top left corner; followed by the width and height.

svg.onMove = function (mouse) { }
{ x: , y: }

We can easily zoom into unit-space where drawing a unit-circle fills the canvas. This makes it especially great playing with math.

Notice the equations being rendered, notice how they appear upside-down.

Y-Axis

Because of a computer standard, the SVG y-axis increases downwards.

It's very easy to invert the y-axis if you want.

svg.scale(1, -1) // no Safari support
layer.scale(1, -1) // full support

Safari does not support transforms on <svg> elements (Chrome and Firefox do). However, all browsers support transforms on <g> elements.

Style

Style is applied by chaining methods, each name is an SVG attribute where kebab-case becomes camelCase.

svg.ellipse(40, 30, 20, 10)
  .fill("crimson")
  .stroke("#fcd")
  .strokeDasharray("5 10")
  .strokeWidth(5);

Interactivity

draw

var points = []
var shape = svg.polygon() svg.onMove = function (mouse) { points.push(mouse); if (points.length > 100) { points.shift(); } shape.setPoints(points); };

An SVG created with this library comes with 4 touch methods.

These are basically wrappers around the standard Web API MouseEvent but with viewBox coordinates instead of pixels.

{
  x: 0.000, // viewBox coordinate space
  y: 0.000,
  position: {x: 0.000, y: 0.000},
  pressX: undefined,
  pressY: undefined,
  press: {x: undefined, y: undefined},
  buttons: 0,
}

The event object is the MouseEvent object, but with additional properties included (i.e., buttons is one of the default standard properties).

Controls

svg.controls(4)

A control point is like a touch event that remains in the place where you left it.

svg.controls(4)
  .onChange((point, i, points) => {
    // point will be an x,y coordinate
    console.log(point)
  })

The onChange handler fires every time a point is moved. The three arguments (item, iterator, array) reflect the same found in familiar Javascript methods like map, filter.

svg.controls(4)
  .svg(() => svg.circle(svg.getWidth() / 20).fill("red"))
  .position(() => [random(svg.getWidth()), random(svg.getHeight())])
  .onChange((point, i, points) => {
    l1.setPoints(points[0], points[1]);
    l2.setPoints(points[3], points[2]);
    curve.clear()
      .moveTo(points[0])
      .curveTo(points[1], points[2], points[3]);
  }, true);

control points is the most usage-specific feature in the library, but its usefulness proved itself so it's worth being included.

Animation

svg.play = function (e) {
// animation code here
}

The play function uses the DOM method requestAnimationFrame and will fire with as little delay as your display allows, typically around 60 fps.

If you come from a background using Processing or openFrameworks, the following code might look fine, but it contains a serious issue in the case of this SVG library.

// bad code!
svg.play = (e) => {
// each frame, a new circle is
// added without removing the previous
svg.circle(50, 50, 10)
// after a few seconds, your svg
// will contain hundreds of circles
// slowing down the renderer
}

the SVG primitives are actual objects that get appended to the SVG. There are no pixels, and there is no automatic screen clearing. At the very least, call removeChildren somewhere in your animation loop.

svg.removeChildren()

The code above can be corrected to:

svg.play = (e) => {
svg.removeChildren()
svg.circle(50, 50, 10)
}

Better yet, consider this approach to animation.

  1. create a shape outside the animation loop
  2. update the shape's attributes inside the animation loop
var circle = svg.circle(50, 50, 1)

svg.play = (e) => {
circle.setRadius(e.time)
}

And at any time, you can stop the animation loop.

svg.stop()

Rabbit Ear Integration

Everything up until this point is available as a fully independent library (~35kb), hosted here,

https://robbykraft.github.io/SVG/svg.js

but it's also included in Rabbit Ear, an integration which unlocks additional features.

Origami

The SVG library can create renderings of FOLD objects.

svg.origami(fold)

The renderer is able to differentiate between crease patterns and folded origami, including known and unknown layer order, and style them accordingly.

The renderer assumes a graph is a crease pattern unless frame_classes contains "foldedState". The layer order of folded models is determined by "faces_layer".

The element itself is a g element with components nested inside their own subgroups.

<g> // the origami drawing
 ┣━<g> // boundary group
 ┃  ┗━<polygon> // each boundary
 ┣━<g> // faces group
 ┃  ┣━<polygon> // each face
 ┃  ┣━<polygon> // each face
 ┃  ┗━ ...
 ┣━<g> // edges group
 ┃  ┣━<path> // mountain creases
 ┃  ┣━<path> // valley creases
 ┃  ┗━ ...   // flat, unassigned ...
 ┗━<g> // vertices group
    ┣━<circle> // each vertex
    ┣━<circle> // each vertex
    ┗━ ...

Alternatively, you can draw these individual component groups directly.

svg.origami.vertices(fold)
svg.origami.edges(fold)
svg.origami.faces(fold)
svg.origami.boundaries(fold)

Or if no SVG exists, the methods are still accessible from the main Rabbit Ear library.

ear.svg.origami.drawInto(svg, fold)

One rendering challenge is to make FOLD objects of all scales appear similar.

vertices_coords: [
[0, 0],
[1, 0],
[1, 1],
[0, 1]
],
vertices_coords: [
[0, 0],
[1000, 0],
[1000, 1000],
[0, 1000]
],

These two crease pattern renderings have vastly different viewBoxes, but because stroke-width is calculated dynamically, they appear similar.

svg.origami(fold, {
strokeWidth: 0.02,
viewBox: true,
radius: 0.02,
})

The second parameter is the style options. The value of strokeWidth is not absolute, it is multiplied by the width or the height of the FOLD object (whichever is larger).

By setting viewBox to true, the renderer will aspect-fit a viewBox around all vertices.

And if you choose to render vertices, the radius will be set in a similar manner to strokeWidth.

If the viewBox option is true, even deeply-nested origami drawings will find the parent SVG and cause it to reset its viewBox.

There are two approaches to styling: style before render, or modify the style after render.

var style = {
  faces: {
    front: { fill: "blue" },
    back: { fill: "white" },
  },
  edges: {
    mountain: { stroke: "purple" },
  }
}

svg.origami(fold, style)
var drawing = svg.origami(fold)

drawing
.vertices
.childNodes
.forEach(v => v.fill("yellow")) drawing
.edges
.mountain
.stroke("blue") drawing.boundaries.fill("linen")

For post-render style, components are accessible on the return object, each of which is a g element or childNodes.

For pre-render style, this style object is the same options object which should include viewBox and strokeWidth if you so choose.