使用 Konva 创建 HTML5 Canvas 图形探戈
使用 Konva 创建 HTML5 Canvas 图形探戈
此示例展示如何创建触发后在 Canvas 上舞动的动画图形。它演示以下内容:
- 创建具有不同属性的随机图形
- 使用 Konva 的补间系统创建平滑动画
- 处理用户交互(拖放和单击按钮)
- 同时管 理多个动画
操作说明: 拖放图形以确定其位置,然后单击“Tango!”按钮,让它们在 Canvas 上舞动。每个图形都会移动到随机位置,并改变旋转角度、大小和颜色。刷新页面以生成新的随机图形。
- Vanilla
- React
- Vue
import Konva from 'konva';
// Create button
const button = document.createElement('button');
button.textContent = 'Tango!';
button.style.position = 'absolute';
button.style.top = '10px';
button.style.left = '10px';
button.style.padding = '10px';
document.body.appendChild(button);
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
function getRandomColor() {
return colors[Math.floor(Math.random() * colors.length)];
}
function tango(layer) {
layer.getChildren().forEach((shape) => {
const radius = Math.random() * 100 + 20;
new Konva.Tween({
node: shape,
duration: 1,
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
rotation: Math.random() * 360,
radius: radius,
opacity: (radius - 20) / 100,
easing: Konva.Easings.EaseInOut,
fill: getRandomColor(),
}).play();
});
}
// Create initial shapes
for (let n = 0; n < 10; n++) {
const radius = Math.random() * 100 + 20;
const shape = new Konva.RegularPolygon({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
sides: Math.ceil(Math.random() * 5 + 3),
radius: radius,
fill: getRandomColor(),
opacity: (radius - 20) / 100,
draggable: true,
});
layer.add(shape);
}
button.addEventListener('click', () => tango(layer));
import React from 'react';
import Konva from 'konva';
import { Stage, Layer, RegularPolygon } from 'react-konva';
const COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
const NUM_SHAPES = 10;
const getRandomColor = () => COLORS[Math.floor(Math.random() * COLORS.length)];
const getRandomShapeProps = (id, width, height) => {
const radius = Math.random() * 100 + 20;
return {
id,
x: Math.random() * width,
y: Math.random() * height,
sides: Math.ceil(Math.random() * 5 + 3),
radius,
fill: getRandomColor(),
opacity: (radius - 20) / 100,
};
};
const App = () => {
const [shapes, setShapes] = React.useState([]);
const [isAnimating, setIsAnimating] = React.useState(false);
const stageRef = React.useRef();
const isAnimatingRef = React.useRef(false);
const tweensRef = React.useRef([]);
React.useEffect(() => {
const initialShapes = Array.from({ length: NUM_SHAPES }, (_, index) =>
getRandomShapeProps(`shape-${index}`, window.innerWidth, window.innerHeight)
);
setShapes(initialShapes);
return () => {
tweensRef.current.forEach((tween) => tween.destroy());
tweensRef.current = [];
isAnimatingRef.current = false;
};
}, []);
const handleDragEnd = (e, id) => {
const { x, y } = e.target.position();
setShapes(currentShapes => currentShapes.map(shape =>
shape.id === id ? { ...shape, x, y } : shape
));
};
const handleTango = () => {
if (isAnimatingRef.current) return;
const layer = stageRef.current.findOne('Layer');
const shapeNodes = layer.find('RegularPolygon');
if (!shapeNodes.length) return;
isAnimatingRef.current = true;
setIsAnimating(true);
const nextShapes = new Map();
let remaining = shapeNodes.length;
shapeNodes.forEach((node) => {
const id = node.id();
const radius = Math.random() * 100 + 20;
const nextShape = {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
rotation: Math.random() * 360,
radius: radius,
opacity: (radius - 20) / 100,
fill: getRandomColor(),
};
nextShapes.set(id, nextShape);
const tween = new Konva.Tween({
node,
...nextShape,
duration: 1,
easing: Konva.Easings.EaseInOut,
onFinish: () => {
tween.destroy();
tweensRef.current = tweensRef.current.filter(
(activeTween) => activeTween !== tween
);
remaining -= 1;
if (remaining > 0) return;
setShapes(currentShapes => currentShapes.map(shape => ({
...shape,
...nextShapes.get(shape.id),
})));
isAnimatingRef.current = false;
setIsAnimating(false);
},
});
tweensRef.current.push(tween);
tween.play();
});
};
return (
<>
<Stage
width={window.innerWidth}
height={window.innerHeight}
ref={stageRef}
>
<Layer>
{shapes.map((shape) => (
<RegularPolygon
key={shape.id}
{...shape}
draggable={!isAnimating}
onDragEnd={(e) => handleDragEnd(e, shape.id)}
/>
))}
</Layer>
</Stage>
<button
onClick={handleTango}
disabled={isAnimating}
style={{
position: 'absolute',
top: '10px',
left: '10px',
padding: '10px',
}}
>
{isAnimating ? 'Tangoing…' : 'Tango!'}
</button>
</>
);
};
export default App;
<template>
<div>
<v-stage
:config="stageConfig"
ref="stageRef"
>
<v-layer>
<v-regular-polygon
v-for="(shape, i) in shapes"
:key="i"
:config="{ ...shape, draggable: true }"
@dragend="(e) => handleDragEnd(e, i)"
/>
</v-layer>
</v-stage>
<button
@click="handleTango"
style="position: absolute; top: 10px; left: 10px; padding: 10px"
>
Tango!
</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
const NUM_SHAPES = 10;
const shapes = ref([]);
const stageRef = ref(null);
const stageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
const getRandomColor = () => COLORS[Math.floor(Math.random() * COLORS.length)];
const getRandomShapeProps = () => {
const radius = Math.random() * 100 + 20;
return {
x: Math.random() * stageConfig.width,
y: Math.random() * stageConfig.height,
sides: Math.ceil(Math.random() * 5 + 3),
radius,
fill: getRandomColor(),
opacity: (radius - 20) / 100,
};
};
const handleDragEnd = (e, index) => {
const newShapes = [...shapes.value];
newShapes[index] = {
...newShapes[index],
x: e.target.x(),
y: e.target.y(),
};
shapes.value = newShapes;
};
const handleTango = () => {
const stage = stageRef.value.getStage();
const layer = stage.findOne('Layer');
const shapeNodes = layer.find('RegularPolygon');
shapeNodes.forEach((node, i) => {
const radius = Math.random() * 100 + 20;
const newProps = {
duration: 1,
x: Math.random() * stageConfig.width,
y: Math.random() * stageConfig.height,
rotation: Math.random() * 360,
radius: radius,
opacity: (radius - 20) / 100,
easing: Konva.Easings.EaseInOut,
fill: getRandomColor(),
};
node.to(newProps);
// Update state after animation
setTimeout(() => {
const newShapes = [...shapes.value];
newShapes[i] = { ...newShapes[i], ...newProps };
shapes.value = newShapes;
}, 1000);
});
};
onMounted(() => {
shapes.value = Array.from({ length: NUM_SHAPES }, getRandomShapeProps);
});
</script>