整体的绘画界面大致如下:
该画板主要是用来给碎片化组件来进行绘画的,碎片化大致效果如下图。具体可以看 上期文章 。
Canvas 画板的实现
码上掘金简单 Demo:
创建 Canvas 实时绘画
useRTDraw 是一个以每秒60帧进行实时绘画的钩子,具体可以看 往期文章 这里不过多赘述。
绑定获取 Canvas 信息后,当调用 startAnimation ,就开始触发 onDraw 的回调了,大概每秒会触发60次,此时只需要调用 drawAllPolygon ,保持多边形的绘画即可。
export default function Page() {
// ...
const {drawState, canvasRef, canvasInfo, setCanvasInfo, startAnimation} = useRTDraw({
onDraw() {
drawState.ctx.clearRect(0, 0, canvasInfo.w * drawState.ratio, canvasInfo.h * drawState.ratio);
drawAllPolygon()
},
})
useEffect(() => {
// 根据浏览器宽高,获取 canvas 宽高的,可看码上掘金就不细讲了
getCardInfo()
setTimeout(() => {
setDrawPolygonData(firstShard)
startAnimation()
}, 200);
}, [])
// ...
return (
<canvas
ref={canvasRef}
width={canvasInfo.w * drawState.ratio}
height={canvasInfo.h * drawState.ratio}
style={{
display: 'block',
width: canvasInfo.w + 'px',
height: canvasInfo.h + 'px',
background: '#1f1f1f'
}}
/>
)
}
拖拽多边形到 Canvas
这里借助 ahooks 的 useDrop 和 useDrag 来实现 dom 元素的拖拽效果。
当拖拽一个 triangle 到 canvas 中时,会触发 onDom 回调,此时在对应的位置生成一份三角形的数据即可。
const selectBlock: SelectBlock[] = [
{
key: 'triangle', // 三角形
children: <IoTriangle />
},
{
key: 'square', // 四边形
children: <FaSquareFull />
},
// ...
]
const DragItem = ({ item, style }: {
item: SelectBlock
style?: React.CSSProperties
}) => {
const dragRef = useRef(null);
useDrag(item.key, dragRef);
return (
<div ref={dragRef} className="item" style={style}>
{item.children}
</div>
);
};
export default function Page() {
// ...
useDrop(dropRef, {
onDom(type, event) {
// 当图形拖拽到 canvas 中,触发的回调
}
}
return (<>
// ...
{selectBlock.map((item, i) => (
<DragItem
key={i}
item={item}
style={{color: curColor}}
/>
))}
<div ref={dropRef}>
<canvas
// ...
/>
</div>
</>)
}
生成三角形
根据鼠标拖拽三角形到 Canvas 上的位置,将此刻鼠标的位置当成该三角形的中心,同时基于该中心点生成三个顶点对应的位置。
顶点主要是用于后续的拖拽,以及生成 clip-path: polygon(x y) 对应的路径数据。
// 多边形的类型
type Polygon = {
// 多边形中心 x y
x: number
y: number
color: string
pointColor: string
pointList: Point[]
}
type Point = {
x: number
y: number
}
// ...
const polygonListRef = useRef<Polygon[]>([])
// ...
useDrop(dropRef, {
onDom(type, event) {
// 当图形拖拽到 canvas 中,触发的回调
const e = event as any
const x = e.offsetX;
const y = e.offsetY;
const distance = 15;
pList = [
{x: x, y: y - distance},
{x: x + distance, y: y + distance},
{x: x - distance, y: y + distance}
]
const p: Polygon = {
x,
y,
color: curColor,
pointColor: pointColor,
pointList: pList,
}
polygonListRef.current.push(p)
}
}
// ...
drawAllPolygon 绘画多边形
用 canvas 绘画多边形,相信大家都会,这里就不细说了。只需把多边形和对应的顶点绘画出来即可了。
function drawAllPolygon() {
for(let i = 0; i < polygonListRef.current.length; i++) {
drawPolygon(i)
}
}
// 绘画一个多边形
function drawPolygon(pI: number) {
const p = polygonListRef.current[pI];
const ctx = drawState.ctx!
const ratio = drawState.ratio
ctx.beginPath();
// 以一个顶点开始
ctx.moveTo((p.pointList[0].x) * ratio, (p.pointList[0].y) * ratio);
// 顶点连接到该点
for(let i = 1; i < p.pointList.length; i++) {
ctx.lineTo((p.pointList[i].x) * ratio, (p.pointList[i].y) * ratio);
}
// 连回顶点
ctx.lineTo((p.pointList[0].x) * ratio, (p.pointList[0].y) * ratio);
ctx.fillStyle = p.color;
ctx.fill();
// 拖拽时增加一些样式做区分
if(pI === polygonIndex.current) {
ctx.strokeStyle = HOVER_COLOR;
ctx.lineWidth = 2 * ratio;
ctx.stroke()
}
ctx.closePath();
// 绘画点
for(let i = 0; i < p.pointList.length; i++) {
ctx.beginPath();
ctx.arc((p.pointList[i].x) * ratio, (p.pointList[i].y) * ratio, POINT_R * ratio, 0, 2 * Math.PI);
ctx.fillStyle = p.pointColor;
ctx.fill();
// 拖拽时增加一些样式做区分
if(pI === polygonIndex.current && pointIndex.current === i) {
ctx.strokeStyle = HOVER_COLOR;
ctx.lineWidth = 2 * ratio;
ctx.stroke()
}
ctx.closePath();
}
}
点击 Canvas 中对应的多边形。
当鼠标点击 Canvas 时,首先通过 isPointInsideCircle 判断是否点击到多边形的点上,然后再通过 isPointInsidePolygon 判断是否点到多边形上。紧接着调用各自的 Move 函数根据鼠标进行移动即可实现拖拽效果。
// 记录拖拽时选中的多边形索引
const polygonIndex = useRef(-1)
// 记录拖拽时选中的多边形的顶点索引
const pointIndex = useRef(-1)
const onMouseDown = (e: React.MouseEvent<HTMLCanvasElement, MouseEvent>) => {
if(e.button !== 0) return
const polygonList = polygonListRef.current;
const x = e.clientX - canvasRef.current!.offsetLeft;
const y = e.clientY - canvasRef.current!.offsetTop;
// 是否选中了多边形的点
let polygonI = polygonList.findIndex((item) => {
pointIndex.current = item.pointList.findIndex((i) => (
isPointInsideCircle({x, y}, {x: i.x, y: i.y, r: POINT_R})
))
return pointIndex.current >= 0
})
// 是否选中了多边形整体
if(pointIndex.current < 0) {
polygonI = polygonList.findIndex((item) => (
isPointInsidePolygon({x: x, y: y}, item)
))
}
polygonIndex.current = polygonI
if(pointIndex.current >= 0) {
document.addEventListener('mousemove', onPolygonPointTouchMove)
document.addEventListener('mouseup', onTouchEnd);
} else {
if(polygonI >= 0) {
document.addEventListener('mousemove', onPolygonTouchMove)
document.addEventListener('mouseup', onTouchEnd);
}
}
}
//...
<canvas
// ...
// 长按拖拽移动多边形
onMouseDown={onMouseDown}
// 右键删除
onContextMenu={(e) => {
e.preventDefault()
onDeleteItem(e.clientX, e.clientY)
}}
/>
判断一个点是否在圆上的函数:
function isPointInsideCircle(point: Point, circle: { x: number, y: number, r: number }) {
return Math.sqrt(Math.pow(point.x - circle.x, 2) + Math.pow(point.y - circle.y, 2)) < circle.r
}
判断一个点是否在多边形上的函数:(从 该博客 找来的)
function isPointInsidePolygon(point: Point, polygon: Polygon) {
const pointList = polygon.pointList
var iSum = 0
var iCount = pointList.length
if (iCount < 3) {
return false
}
// 待判断的点(x, y) 为已知值
var y = point.y
var x = point.x
for (var i = 0; i < iCount; i++) {
var y1 = pointList[i].y
var x1 = pointList[i].x
if (i == iCount - 1) {
var y2 = pointList[0].y
var x2 = pointList[0].x
} else {
var y2 = pointList[i + 1].y
var x2 = pointList[i + 1].x
}
// 当前边的 2 个端点分别为 已知值(x1, y1), (x2, y2)
if (((y >= y1) && (y < y2)) || ((y >= y2) && (y < y1))) {
// y 界于 y1 和 y2 之间
// 假设过待判断点(x, y)的水平直线和当前边的交点为(x_intersect, y_intersect),有y_intersect = y
// 则有(2个相似三角形,公用顶角,宽/宽 = 高/高):|x1 - x2| / |x1 - x_intersect| = |y1 - y2| / |y1 - y|
if (Math.abs(y1 - y2) > 0) {
var x_intersect = x1 - ((x1 - x2) * (y1 - y)) / (y1 - y2);
if (x_intersect < x) {
iSum += 1
}
}
}
}
if (iSum % 2 != 0) {
return true
} else {
return false
}
}
拖拽多边形
根据上面的点击判断出,需要拖拽的是顶点还是多边形整体。
拖拽多边形整体时,记录原来各顶点和中心点的距离,重新赋加到鼠标位置即可,将中心点位置和鼠标位置保持同步。
拖拽某个顶点时,让顶点的位置和鼠标位置保持同步,然后重新计算出中心点位置。
// 拖拽多边形整体
const onPolygonTouchMove = (e: MouseEvent) => {
const x = e.clientX - canvasRef.current!.offsetLeft;
const y = e.clientY - canvasRef.current!.offsetTop;
const item = polygonListRef.current[polygonIndex.current]
item.pointList.forEach(p => {
const changeX = p.x - item.x
const changeY = p.y - item.y
p.x = x + changeX
p.y = y + changeY
})
item.x = x
item.y = y
}
// 拖拽某一个顶点
const onPolygonPointTouchMove = (e: MouseEvent) => {
const x = e.clientX - canvasRef.current!.offsetLeft;
const y = e.clientY - canvasRef.current!.offsetTop;
const item = polygonListRef.current[polygonIndex.current]
item.pointList[pointIndex.current].x = x
item.pointList[pointIndex.current].y = y
const centerPoint = getPolygonCenterPoint(item.pointList)
item.x = centerPoint.x
item.y = centerPoint.y
}
根据多边形的各个顶点,找到在多边形中心的点的位置。
function getPolygonCenterPoint(pointList: Point[]) {
let x = 0
let y = 0
for(let i = 0; i < pointList.length; i++) {
x += pointList[i].x
y += pointList[i].y
}
return {
x: x / pointList.length,
y: y / pointList.length
}
}
松开鼠标的点击,移除拖拽事件,至此一个拖拽过程大致完成。
onst onTouchEnd = () => {
document.removeEventListener('mousemove', pointIndex.current < 0 ? onPolygonTouchMove : onPolygonPointTouchMove);
document.removeEventListener('mouseup', onTouchEnd);
pointIndex.current = -1;
polygonIndex.current = -1;
}
画板的主要拖拽图像功能大致完成。紧接着导出数据即可。
导出数据
根据当前 Canvas 中的多边形数据导出一份用于碎片化图形绘画的数据。
const getShardData = (polygonList: Polygon[], canvasInfo: { w: number, h: number }) => {
return polygonList.map((p) => {
return {
color: p.color,
polygon: p.pointList.map((item) => (
`${Math.round(item.x / canvasInfo.w * 10000) / 100}% ${Math.round(item.y / canvasInfo.h * 10000) / 100}%`
)).join(', ')
}
})
}
const onExport = () => {
const res = getShardData(polygonListRef.current, canvasInfo)
console.log('res: ', res);
alert(JSON.stringify(res))
}
最后将导出的数据放到碎片化组件中即可