Skip to main content

HTML5 canvas Custom Shape Tutorial

To create a custom shape with Konva, you can use the Konva.Shape() object and define a custom drawing function.

When creating a custom shape, you need to define a drawing function that is passed a Konva.Context renderer and a shape instance. Here's a simple rectangle example:

const rect = new Konva.Shape({
x: 10,
y: 20,
fill: '#00D2FF',
width: 100,
height: 50,
sceneFunc: function (context, shape) {
context.beginPath();
// don't need to set position of rect, Konva will handle it
context.rect(0, 0, shape.getAttr('width'), shape.getAttr('height'));
// (!) Konva specific method, it is very important
// it will apply all required styles
context.fillStrokeShape(shape);
}
});

Konva.Context is a wrapper around native 2d canvas context that has the same properties and methods with some additional API.

There are two properties that can be used for drawing custom shapes:

  • sceneFunc - defines visual appearance of a shape
  • hitFunc - optional function to define custom hit region for events (see Custom Hit Region demo)

Best practices for writing sceneFunc and hitFunc:​

  1. Optimize the function as it can be called many times per second. Avoid creating images or large objects.
  2. The function should not have side effects like moving shapes, attaching events or changing app state.
  3. Define custom hitFunc when applying complex styles or drawing images.
  4. Don't manually apply position and scaling in sceneFunc. Let Konva handle it through shape properties.
  5. Avoid manual styling in sceneFunc. Use context.fillStrokeShape(shape) for styling.
  6. Reference Konva core shapes implementations for more examples.

For full list of properties and methods, see the Shape API Reference.

import Konva from 'konva';

const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});

const layer = new Konva.Layer();
stage.add(layer);

const triangle = new Konva.Shape({
sceneFunc: function (context, shape) {
context.beginPath();
context.moveTo(20, 50);
context.lineTo(220, 80);
context.lineTo(100, 150);
context.closePath();
context.fillStrokeShape(shape);
},
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4
});

layer.add(triangle);

Drawing outside width and height​

Konva measures a custom shape by its width and height. getClientRect(), the Transformer, caching and isolated groups all use that box. If sceneFunc paints outside it, the transformer frame is too small, and cached or isolated drawing can cut that paint off.

Use selfRectFunc (Konva 10.7.0 or newer) to return the local, untransformed rectangle that sceneFunc paints. Konva adds the stroke, shadow and transform on top of it. With selfRectFunc, perfect drawing also uses a buffer the size of the shape instead of the whole canvas:

const bubble = new Konva.Shape({
width: 160,
height: 80,
sceneFunc: drawBubbleWithTail,
// the tail is 30px below the box
selfRectFunc: (shape) => ({
x: 0,
y: 0,
width: shape.width(),
height: shape.height() + 30,
}),
});

Instructions: Both bubbles draw a tail below their height. The left bubble has no selfRectFunc, so its transformer frame leaves the tail out. The right bubble declares the tail with selfRectFunc. Resize both to compare.

import Konva from 'konva';

const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});

const layer = new Konva.Layer();
stage.add(layer);

const TAIL = 30;

// a box of width x height, with a tail below it
function drawBubble(context, shape) {
const width = shape.width();
const height = shape.height();
context.beginPath();
context.moveTo(0, 0);
context.lineTo(width, 0);
context.lineTo(width, height);
context.lineTo(60, height);
context.lineTo(25, height + TAIL);
context.lineTo(30, height);
context.lineTo(0, height);
context.closePath();
context.fillStrokeShape(shape);
}

const bubbleConfig = {
y: 60,
width: 160,
height: 80,
fill: '#ffd166',
stroke: 'black',
strokeWidth: 2,
sceneFunc: drawBubble,
};

const plain = new Konva.Shape({ ...bubbleConfig, x: 40 });
const declared = new Konva.Shape({
...bubbleConfig,
x: 280,
selfRectFunc: (shape) => ({
x: 0,
y: 0,
width: shape.width(),
height: shape.height() + TAIL,
}),
});

layer.add(
new Konva.Text({ x: 40, y: 20, text: 'without selfRectFunc', fontSize: 16 }),
new Konva.Text({ x: 280, y: 20, text: 'with selfRectFunc', fontSize: 16 }),
plain,
declared,
new Konva.Transformer({ nodes: [plain] }),
new Konva.Transformer({ nodes: [declared] })
);