Command Palette

Search for a command to run...

Hectal
PHASE 12Advanced ~22 min· topic 6 of 7

Topic 12.6

When You Need a Scene Graph: Konva

In one line

A scene graph keeps shapes as objects with events, dragging and layers, and draws them onto canvas for you. Konva is one. It saves the hit-testing and redraw code you wrote by hand and costs memory per shape. Choose it when users edit the picture.

Think of it like this

A magnetic whiteboard. Each magnet is its own object: you can pick it up, move it, stack it over another. Plain canvas is a painted wall: once the colour is on, there is no magnet to pick up. A scene graph gives you the magnets and keeps the wall painted behind them.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

scene graph
A tree of drawable objects (shapes and groups) that the library draws for you.
Stage and Layer
Konva's top object, and one canvas inside it. Shapes on a layer are drawn together.
Group
A shape that holds other shapes, so they move and cache together.
listening
Whether a layer or shape takes part in events. false skips building its hit canvas.

Step by step

01Build a scene and ask it questions

The first example makes a Stage, one Layer and six draggable circles from the station file. Drag one on the page. The Console shows the tree answering: 1 layer, 6 shapes, #S02 at x 112, and getIntersection at S01's centre returns S01 while empty space returns null.

02Fire events, watch them bubble

first.fire("click") runs a handler without a pointer, and a listener on the layer hears a click fired on rectangle B.

03Draw before you hit-test

Two added rectangles hit-test as null until layer.draw(); then B is on top, and after A.moveToTop() and a draw, A is.

04Make it cheap

The second example draws 5,000 background circles on a layer with listening: false and one draggable handle on another: 5,001 shapes, two canvases. A hit-test on a dot gives null; the handle answers. A cached group's isCached() goes false, true, and false after clearCache().

Try it yourself

  1. 1

    Make the background listen

    In the second example, change { listening: false } to { listening: true }. The log now says dots layer listening: true | a hit-test on a dot -> and an empty name: the dot is found (it has no name), and the layer builds a hit canvas for 5,000 shapes.

  2. 2

    Click a thin line

    At the end of the first example add const thin = new Konva.Line({ points: [20, 190, 200, 190], stroke: "#264653", strokeWidth: 1 }); layer.add(thin); layer.draw();. Then stage.getIntersection({ x: 100, y: 195 }) is null (5 pixels below a 1-pixel line) and at y 190 it is the Line. Now call thin.hitStrokeWidth(20); layer.draw();: the point 5 pixels off finds the Line. A thin line needs a wider invisible hit area, as a finger needs a bigger reach in Topic 12.3.

  3. 3

    Refresh a stale cache

    At the end of the second example, add a group with one dark circle, cache() it, then call fill("red") on the circle. Reading the cached bitmap's centre pixel (group.toCanvas()) still gives 38,70,83, the old dark colour. Call group.cache() again and it gives 255,0,0. A cache is a photograph: retake it after you change what it shows.

Code & diagrams

A draggable station scene in Konva 10 New tab

Drag a circle, or click one. The Console shows the scene answering questions, the hit canvas that updates only on draw, and bubbling.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Konva 10.7.1
layers: 1 | shapes in the layer: 6
find by name: 6 | find by id: 112
hit-test at the centre of S01 -> S01
hit-test on empty space -> null
fired a click from code, handler ran: true
hit-test before the new shapes are drawn -> null
on top at the overlap: B
after A.moveToTop(): A
the layer heard a click from: B
after destroy, shapes in the layer: 7
5,000 shapes: layers, listening and cache New tab

The first-draw time is shown on the page, not logged. The background dots are on a layer that takes no events: a hit-test there finds nothing.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

nodes in the scene graph: 5,001 - canvas elements Konva made: 2
dots layer listening: false | a hit-test on a dot -> null
hit-test on the handle -> handle
before cache, group cached: false
after cache, group cached: true
the cached bitmap still shows the old colour until you call cache() again: true
after clearCache(): false
empty layer children: 0 | hit-test there: null
layers now: 2

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Hit-test before drawing

In the first example, delete the line layer.draw(); // the hit canvas is redrawn with the layer.

terminal
$ Console
── what you'll see ──
hit-test before the new shapes are drawn -> null
Uncaught TypeError: Cannot read properties of null (reading 'id')

Myth vs fact

Myth

Konva is a chart library.

Fact

It draws shapes and handles events. It has no scales, axes or data logic: bring D3 for those and use Konva for the interactive shapes.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    perfectDrawEnabled: false on shapes with fill and stroke skips an extra offscreen draw, and layer.batchDraw() merges many changes into one frame. Thin lines are hard to click for the same reason as in Topic 12.3: give them a wider invisible hit area with hitStrokeWidth.

Remember this

  1. 1

    The problem this solves: Topic 12.3 made you build hit-testing, and Topic 12.2 a redraw loop. For a chart that is looked at, that is fine. For a diagram editor, a whiteboard or a map where stations can be dragged, you would rebuild a small SVG on top of canvas. A scene graph is that, already built: a tree of shape objects you can click, drag and restack, drawn onto canvas.

  2. 2

    How Konva is organised. A Stage holds Layers, and each layer is its own <canvas>. Layers hold shapes (Circle, Rect, Line, Text) and Groups. The example finds 1 layer and 6 shapes, and searches the tree by name (stage.find(".station")) or id (stage.findOne("#S02")). shape.on("click tap", ...) works for mouse and touch, draggable: true needs no more code, events bubble up the tree, and shape.fire("click") triggers a handler from code, which is how you test.

  3. 3

    It hit-tests by colour picking (Topic 12.3): each layer keeps a hidden canvas, and stage.getIntersection({ x, y }) reads it. That canvas is updated only when the layer is drawn: a hit-test right after adding two rectangles gives null, and after layer.draw() it gives the top one. The shape added last is on top until you call moveToTop().

  4. 4

    Layers and caching keep it fast. Put things that rarely change on one layer with listening: false, so it builds no hit canvas and takes no events, and the interactive things on another. A Group can be cache()d: Konva draws it once into a bitmap and reuses it. After changing a child, call cache() again or the bitmap shows the old picture.

  5. 5

    What it costs. Every shape is a JavaScript object. The second example keeps 5,000 circles as 5,001 nodes, and one machine took about 41 milliseconds for the first draw. Changing N to 100,000 on that machine took about 680 milliseconds for the first draw and the page's script memory went from about 10 to about 68 megabytes (single readings, on one machine, in Chromium): every later full redraw costs about as much again, and a phone is slower. A scene graph is for thousands of objects people interact with, not a hundred thousand dots people look at: for those, raw canvas and a quadtree.

Explain it without notes

01

What does a scene graph give you that plain canvas does not?

02

Why did getIntersection return null right after adding shapes, and what does listening: false save?

Practice

01

In the first example, make clicking an empty spot reset every circle's colour. Which object do you listen on?

02

Add a dragmove listener that keeps a station inside the 420 by 200 stage.

Trade-offs

  • ↔

    A scene graph removes a lot of hit-testing and redraw code, and costs memory and speed per shape. More layers keep redraws small, at the price of more canvases to composite.

Done when you can

  • I can name Konva's Stage, Layer, Group and shapes and how they nest.

  • I can drag, click and find shapes, and fire an event from code.

  • I draw before I hit-test, and use listening: false and cache() where they help.

  • I can say when raw canvas is a better choice than Konva.