The drawn look

draw() draws in pencil, pen and ballpoint, stroke by stroke, the way a hand does.

You plan the drawing as strokes: planning lines, outlines, hatching, a firm last line and written text. draw() lays each one with a hand that speeds up and slows down, wanders a little, and presses in and lifts off. It draws them on one after another, or shows the drawing already drawn.

draw(canvas, options)

draw(canvas, { scene, inks, …, strokes }) draws on its own canvas. Pencil catches only the peaks of the paper's tooth unless it is pressed hard, a ballpoint blobs where it starts and skips where it moves fast, and a pen lays an even line. Where inks cross, they darken as real ink does.

A cube drawn in one ink. Change the tool, the hand and the paper.

Use it when the drawing is the look the piece asks for, such as a sketchbook, a lesson worked through on paper, or an engineer's notes. A drawing already drawn suits a lesson's figure, and a drawing drawn on suits a concept explained one step at a time.

Inks

inks names each ink, up to 16 on one canvas, as { tool, color }:

  • 'pencil': graphite, #4a4b51, broken by the paper's tooth.
  • 'pen': an even line, #222222.
  • 'ballpoint': #1c2f7a, with blobs and skips.

color gives a colored pencil or pen, such as { tool: 'pencil', color: '#c4452f' }.

Strokes

strokes(s) plans the drawing, and each call returns s:

  • construct({ d | points }): light, fast planning lines, drawn first as a person would.
  • contour({ d | points, twice }): an outline, drawn twice a little apart and never quite closed. twice: false draws it once.
  • hatch({ clip, angle, spacing, cross }): parallel lines across a clip, in degrees (-50) and px apart (7), with a little drift. cross: true adds a second layer. The clip is a shape as shade() takes it.
  • accent({ d | points }): one firm line, usually the last.
  • text({ text, font, x, y, letter }): display text written letter by letter, each along its center line, letter seconds a letter (0.11).
  • pause(s): a wait before the next stroke.

Each takes ink (an ink's name), pressure, at (its start in seconds, on the same clock as start) and group (a name for timeOf()). d is SVG path data, and each of its subpaths is a stroke.

Here is a pear drawn on in five inks: graphite planning lines and a cast shadow, a pen outline, ochre and red pencil on its shadow side, and a green leaf.

<div id="stage" data-composition-id="pear" data-start="0" data-duration="10" data-width="1280" data-height="720" data-fps="30">
    <div data-component="pear" data-start="0" data-duration="10"><canvas></canvas></div>
</div>
const { scene, draw } = wildflower.kinetic;

const PEAR = 'M640 190 C 600 190 592 250 600 300 C 556 340 520 410 540 490 C 562 570 718 570 740 490 C 760 410 724 340 680 300 C 688 250 680 190 640 190 Z';
// The shadow side, away from a light at the upper left.
const SHADOW = 'M668 210 C 690 250 684 290 690 310 C 736 350 762 420 742 490 C 716 556 650 562 610 552 C 690 510 712 420 668 210 Z';
const LEAF = 'M664 150 C 700 108 764 104 800 124 C 768 156 712 168 664 150 Z';

scene('pear', {
    init() {
        draw(this.element.querySelector('canvas'), {
            scene: this, start: 0.4, end: 8,
            inks: {
                lead: { tool: 'pencil' },
                pen: { tool: 'pen' },
                ochre: { tool: 'pencil', color: '#d39a2c' },
                red: { tool: 'pencil', color: '#c4452f' },
                green: { tool: 'pencil', color: '#4f8a4a' },
            },
            strokes: (s) => s
                .construct({ points: [[640, 150], [640, 600]], ink: 'lead' })
                .construct({ points: [[380, 560], [940, 560]], ink: 'lead' })
                .contour({ d: PEAR, ink: 'pen' })
                .accent({ d: 'M640 190 C 644 160 652 140 664 150', ink: 'pen' })
                .hatch({ clip: { path: SHADOW }, ink: 'ochre', angle: -60, spacing: 8, pressure: 1.1 })
                .hatch({ clip: { path: SHADOW }, ink: 'red', angle: 25, spacing: 10, pressure: 1 })
                .hatch({ clip: { path: 'M600 560 A 180 26 0 0 0 960 560 A 180 26 0 0 0 600 560 Z' }, ink: 'lead', angle: -8, spacing: 6 })
                .contour({ d: LEAF, ink: 'green' })
                .hatch({ clip: { path: LEAF }, ink: 'green', angle: -30, spacing: 8, pressure: 0.5 }),
        });
    },
});
html, body { margin: 0; background: #0a0b10; }

#stage { position: relative; overflow: hidden; background: #f3efe6; }

[data-component="pear"], canvas { position: absolute; inset: 0; width: 100%; height: 100%; }

[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
A pear in five inks

Timing and moving

  • The strokes follow one another from start, in seconds of the scene's local time with scene: this, else the composition time, with a short pen lift between them. end paces the hand so the last of those strokes ends then, while a stroke pinned with at keeps its time, and drawn: true shows the drawing already drawn.
  • The handle's timeOf(group) gives { start, end } of a group's strokes, or of the whole drawing without a group, from its earliest stroke, so the rest of the piece can be timed to the drawing. duration is when the last stroke ends.
  • The drawing is baked once, the first time it is drawn, and every frame after that shows the ink laid by its time, so the same time always draws the same frame.
  • The paper's tooth belongs to the frame. A drawing moved by CSS, on its canvas or on an element around it, catches the paper anew as it moves, and moving it costs nothing.

The hand and the paper

  • wobble (1): how far the hand wanders, from 0 for a ruler's line.
  • pressure (0.85): how hard the hand presses, so how dark a line is and how far a pencil reaches into the tooth.
  • speed (1): how fast the hand moves, which also thins and lightens fast strokes.
  • tooth (0 to 1; 0.7): how much the paper breaks a line.
  • lead (1): the size of the tip. A tip thinner than a pixel is drawn a pixel wide and a little lighter, so a small drawing keeps whole lines.
  • seed (7): the hand's noise.
  • paper: a color paints the sheet, blended normally. Without it, the ink is multiplied onto what is under the canvas.
  • The hand's sizes and speeds are for a composition 1280 px wide and scale with its data-width. Coordinates are px of the canvas.
  • set() changes any option but scene, redraw() plans the strokes again, and either is drawn at the next seek, as an effect's changes are; on a paused page, seek to the current time to see it. dispose() takes the drawing off.
  • A bad option, ink or stroke throws KN-100, and so does a 17th ink, a font CSS cannot read (a family with no size), or a canvas with no WebGL 2 context. A context the browser loses draws nothing until it is restored (KN-191 in development builds), and the drawing is drawn again then.