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.
falseskips 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
Make the background listen
In the second example, change
{ listening: false }to{ listening: true }. The log now saysdots 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
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();. Thenstage.getIntersection({ x: 100, y: 195 })isnull(5 pixels below a 1-pixel line) and at y 190 it is theLine. Now callthin.hitStrokeWidth(20); layer.draw();: the point 5 pixels off finds theLine. A thin line needs a wider invisible hit area, as a finger needs a bigger reach in Topic 12.3. - 3
Refresh a stale cache
At the end of the second example, add a group with one dark circle,
cache()it, then callfill("red")on the circle. Reading the cached bitmap's centre pixel (group.toCanvas()) still gives38,70,83, the old dark colour. Callgroup.cache()again and it gives255,0,0. A cache is a photograph: retake it after you change what it shows.
Code & diagrams
Drag a circle, or click one. The Console shows the scene answering questions, the hit canvas that updates only on draw, and bubbling.
Preview
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: 7The 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
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: 2Break 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.
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: falseon shapes with fill and stroke skips an extra offscreen draw, andlayer.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 withhitStrokeWidth.
Remember this
- 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
How Konva is organised. A
StageholdsLayers, and each layer is its own<canvas>. Layers hold shapes (Circle,Rect,Line,Text) andGroups. 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: trueneeds no more code, events bubble up the tree, andshape.fire("click")triggers a handler from code, which is how you test. - 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 givesnull, and afterlayer.draw()it gives the top one. The shape added last is on top until you callmoveToTop(). - 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. AGroupcan becache()d: Konva draws it once into a bitmap and reuses it. After changing a child, callcache()again or the bitmap shows the old picture. - 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
What does a scene graph give you that plain canvas does not?
Why did getIntersection return null right after adding shapes, and what does listening: false save?
Practice
In the first example, make clicking an empty spot reset every circle's colour. Which object do you listen on?
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: falseandcache()where they help.I can say when raw canvas is a better choice than Konva.