Open jtwang7 opened 1 year ago
参考:
🌈 index.tsx
import React, { useEffect, useRef } from "react";
import Konva from "konva";
import { Stage } from "konva/lib/Stage";
import { SpectrumKonvaProps } from "./types";
import _ from "lodash";
const DEGREE = 180 / Math.PI;
const RAD = Math.PI / 180;
export default function SpectrumKonva({
width,
height,
data: odata,
initRotate = 0, // 起始角度:默认十字坐标系y轴正向
radius = 80,
outerRatio = 10,
backgroundColor = "#111",
subCircleStyle = {},
lineStyle = {},
parser = {
rad: "rad",
grid: "grid",
},
}: SpectrumKonvaProps) {
const ref = useRef<HTMLDivElement>(null!);
const stage = useRef<Stage | null>(null);
const {
radius: subCircleRadius = 5,
fill: subCircleFill = "transparent",
stroke: subCircleStroke = "#111",
} = subCircleStyle;
const { width: lineWidth = 1, stroke: lineStroke = "#111" } = lineStyle;
const clockRadius = radius - 6 * subCircleRadius;
// common function
function toRange(num: number, min = 0, max = 360): number {
const r = max - min;
if (num >= min && num < max) return num;
if (num < min) {
num = r - num;
} else if (num >= max) {
num = num - r;
}
return toRange(num, min, max);
}
useEffect(() => {
stage.current = ref.current
? new Konva.Stage({
container: ref.current,
width,
height,
draggable: false,
})
: null;
return () => {
stage.current && stage.current.destroy();
stage.current = null;
};
}, []);
useEffect(() => {
const layer = new Konva.Layer();
if (stage.current && odata.length) {
try {
// 数据格式转换
const data = odata.map((o) =>
_.mapKeys(o, (val, key) => {
if (key === parser?.grid) {
return "grid";
} else if (key === parser?.rad) {
return "rad";
} else {
return key;
}
})
);
const CENTER = [stage.current.width() / 2, stage.current.height() / 2];
const group = new Konva.Group({
x: CENTER[0],
y: CENTER[1],
rotation: initRotate - 90,
});
const rounds = new Array(360).fill(radius + 5);
for (const item of data) {
rounds[toRange(item.grid)] += 1 * outerRatio;
rounds[toRange(item.grid - 1)] += 0.5 * outerRatio;
rounds[toRange(item.grid - 2)] += 0.3 * outerRatio;
rounds[toRange(item.grid + 1)] += 0.5 * outerRatio;
rounds[toRange(item.grid + 2)] += 0.3 * outerRatio;
}
const pts: [number, number][] = [];
const lineList = [];
for (const [i, long] of rounds.entries()) {
// * 绘制频谱线段
const line = new Konva.Line({
points: [0, 0, long, 0],
// x: CENTER[0],
// y: CENTER[1],
strokeWidth: lineWidth,
stroke: lineStroke,
});
pts.push([
Math.cos(i * RAD) * (long + 4),
Math.sin(i * RAD) * (long + 4),
]);
group.add(line);
lineList.push({ node: line, rotation: i });
}
const curve = new Konva.Line({
points: pts.flat(1),
// x: CENTER[0],
// y: CENTER[1],
tension: 0.5,
strokeWidth: lineWidth + 2,
stroke: backgroundColor,
fill: backgroundColor,
closed: true,
opacity: 0,
});
group.add(curve);
const ring = new Konva.Ring({
innerRadius: clockRadius,
outerRadius: radius,
// x: CENTER[0],
// y: CENTER[1],
fill: "#fff",
stroke: lineStroke,
strokeWidth: 2,
});
group.add(ring);
// * 绘制时钟
const clock = new Konva.Circle({
radius: clockRadius,
strokeWidth: 2,
fill: "#fff",
stroke: lineStroke,
});
group.add(clock);
for (let i = 0; i < 360; i++) {
if (i % 3 === 0) {
const subline = new Konva.Line({
points: [clockRadius, 0, clockRadius - 4, 0],
rotation: i,
strokeWidth: 1,
stroke: lineStroke,
});
group.add(subline);
}
if (i % 15 === 0) {
const mainLine = new Konva.Line({
points: [clockRadius, 0, clockRadius - 8, 0],
rotation: i,
strokeWidth: 2,
stroke: lineStroke,
});
group.add(mainLine);
}
}
const subCircleList = [];
for (const o of data) {
// * 绘制小圆点
const subCircle = new Konva.Circle({
radius: subCircleRadius,
// 将操作中心基于默认中心(circle默认中心定位在圆心)偏移
offsetX: -clockRadius,
offsetY: 0,
// 此时 x, y 相对偏移后的中心定位
// x: CENTER[0],
// y: CENTER[1],
fill: subCircleFill,
stroke: subCircleStroke,
strokeWidth: 0.5,
});
group.add(subCircle);
subCircleList.push({ node: subCircle, rotation: o.rad * DEGREE });
}
layer.add(group);
// 动画必须要等待节点添加到 layer 后
lineList.forEach(({ node, rotation }) => {
const tween = new Konva.Tween({
node, // node 节点必须保证已被添加到 layer
duration: 0.5,
rotation,
easing: Konva.Easings.EaseInOut,
});
tween.play();
});
setTimeout(() => {
const curveTween = new Konva.Tween({
node: curve,
opacity: 0.3,
duration: 0.5,
});
curveTween.play();
}, 500);
subCircleList.forEach(({ node, rotation }) => {
const tween = new Konva.Tween({
node, // node 节点必须保证已被添加到 layer
duration: 0.5,
rotation,
easing: Konva.Easings.EaseInOut,
});
tween.play();
setTimeout(() => {
const tween2 = new Konva.Tween({
node, // node 节点必须保证已被添加到 layer
duration: 0.5,
offsetX: -radius + (radius - clockRadius) / 2,
easing: Konva.Easings.EaseInOut,
});
tween2.play();
}, 500);
});
stage.current.add(layer);
} catch (err) {
console.log(err);
}
}
return () => {
// 彻底销毁 layer 图层
// ! 仅通过 stage.clear() 清除所有图层无效,绘制的图层会被缓存
layer.destroy();
};
}, [odata]);
return <div ref={ref}></div>;
}
🌈 index.d.ts
export interface SpectrumKonvaProps {
width: number;
height: number;
data: any[]; // 'grid', 'rad' is needed (or parsed)
initRotate?: number;
radius?: number;
outerRatio?: number;
backgroundColor?: string;
subCircleStyle?: {
radius?: number;
fill?: string;
stroke?: string;
};
lineStyle?: {
width?: number;
stroke?: string;
};
parser?: {
rad?: string;
grid?: string;
};
}
✅ 数据集:同上文
import React, { useEffect, useState } from "react";
import { heatmapBar1, heatmapBar2 } from "../analysis/constants/color";
import SpectrumKonva from "./components/spectrum-konva";
import { data as konvaData } from "./components/spectrum-konva/mocks";
export default function Test() {
const [a, setA] = useState({});
useEffect(() => {
const timer = setInterval(() => {
setA({});
}, 3000);
return () => {
clearInterval(timer);
};
}, []);
return (
<div>
<SpectrumKonva
data={konvaData()}
width={400}
height={400}
backgroundColor="#E85285"
/>
</div>
);
}
数据可视化 - 环形音乐频谱 canvas 绘制
参考文章:
可视化效果
代码
🌈
index.tsx
🌈
index.d.ts
使用
❇️ 数据集
👋 调用