HTML5 Canvas 自定义图形教程
要使用 Konva 创建自定义图形,可以使用 Konva.Shape() 对象并定义自定义绘制函数。
创建自定义图形时,需要定义一个绘制函数。该函数接收一个 Konva.Context 渲染器和一个图形实例。下面是一个简单的矩形示例:
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 是原生 2D Canvas 上下文的封装。它具有相同的属性和方法,还提供了一些附加 API。
有两个属性可用于绘制自定义图形:
sceneFunc- 定义图形的视觉外观hitFunc- 可选函数,用于定义事件的自定义命中区域(请参阅自定义命中区域示例)
编写 sceneFunc 和 hitFunc 的最佳实践:
- 优化该函数,因为它每秒可能调用多次。不要创建图像或大型对象。
- 该函数不得产生移动图形、绑定事件或更改应用的 state 等副作用。
- 应用复杂样式或绘制图像时,请定义自定义
hitFunc。 - 不要在
sceneFunc中手动应用位置和缩放。让 Konva 通过图形属性处理这些变换。 - 不要在
sceneFunc中手动设置样式。使用context.fillStrokeShape(shape)设置样式。 - 有关更多示例,请参阅 Konva 核心图形实现。
有关完整的属性和方法列表,请参阅 Shape API 参考。
- Vanilla
- React
- Vue
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);
import { Stage, Layer, Shape } from 'react-konva';
const App = () => {
return (
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer>
<Shape
sceneFunc={(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>
</Stage>
);
};
export default App;
<template>
<v-stage :config="stageSize">
<v-layer>
<v-shape :config="shapeConfig" />
</v-layer>
</v-stage>
</template>
<script setup>
const stageSize = {
width: window.innerWidth,
height: window.innerHeight
};
const shapeConfig = {
sceneFunc: (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
};
</script>
在 width 和 height 之外绘制
Konva 根据自定义图形的 width 和 height 测量其大小。getClientRect()、Transformer、缓存和隔离组都会使用这个框。如果 sceneFunc 在框外绘制内容,Transformer 的边框就会太小,缓存绘制或隔离绘制也可能裁剪掉这部分内容。
使用 selfRectFunc(需要 Konva 10.7.0 或更高版本)返回 sceneFunc 绘制的局部、未变换的矩形区域。Konva 会在其基础上加上描边、阴影和变换。设置 selfRectFunc 后,完美绘制也会使用与图形一样大的缓冲区,而不是与整个 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,
}),
});
操作说明:两个气泡都在其 height 下方绘制了一个尾巴。左侧气泡没有设置 selfRectFunc,因此其 Transformer 边框没有包含尾巴。右侧气泡使用 selfRectFunc 声明了尾巴。调整两者的大小进行比较。
- Vanilla
- React
- Vue
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] })
);
import { Stage, Layer, Shape, Text, Transformer } from 'react-konva';
import { useEffect, useRef } from 'react';
const TAIL = 30;
// a box of width x height, with a tail below it
const 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 bubbleSelfRect = (shape) => ({
x: 0,
y: 0,
width: shape.width(),
height: shape.height() + TAIL,
});
const bubbleProps = {
y: 60,
width: 160,
height: 80,
fill: '#ffd166',
stroke: 'black',
strokeWidth: 2,
sceneFunc: drawBubble,
};
const App = () => {
const plainRef = useRef(null);
const declaredRef = useRef(null);
const plainTrRef = useRef(null);
const declaredTrRef = useRef(null);
useEffect(() => {
plainTrRef.current.nodes([plainRef.current]);
declaredTrRef.current.nodes([declaredRef.current]);
}, []);
return (
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer>
<Text x={40} y={20} text="without selfRectFunc" fontSize={16} />
<Text x={280} y={20} text="with selfRectFunc" fontSize={16} />
<Shape {...bubbleProps} x={40} ref={plainRef} />
<Shape
{...bubbleProps}
x={280}
selfRectFunc={bubbleSelfRect}
ref={declaredRef}
/>
<Transformer ref={plainTrRef} />
<Transformer ref={declaredTrRef} />
</Layer>
</Stage>
);
};
export default App;
<template>
<v-stage :config="stageSize">
<v-layer>
<v-text :config="{ x: 40, y: 20, text: 'without selfRectFunc', fontSize: 16 }" />
<v-text :config="{ x: 280, y: 20, text: 'with selfRectFunc', fontSize: 16 }" />
<v-shape :config="plainConfig" ref="plainRef" />
<v-shape :config="declaredConfig" ref="declaredRef" />
<v-transformer ref="plainTrRef" />
<v-transformer ref="declaredTrRef" />
</v-layer>
</v-stage>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const stageSize = {
width: window.innerWidth,
height: window.innerHeight,
};
const TAIL = 30;
// a box of width x height, with a tail below it
const 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 plainConfig = { ...bubbleConfig, x: 40 };
const declaredConfig = {
...bubbleConfig,
x: 280,
selfRectFunc: (shape) => ({
x: 0,
y: 0,
width: shape.width(),
height: shape.height() + TAIL,
}),
};
const plainRef = ref(null);
const declaredRef = ref(null);
const plainTrRef = ref(null);
const declaredTrRef = ref(null);
onMounted(() => {
plainTrRef.value.getNode().nodes([plainRef.value.getNode()]);
declaredTrRef.value.getNode().nodes([declaredRef.value.getNode()]);
});
</script>