效果概览
支持圆形,矩形,旋转矩形绘制,鼠标像素拾取,图片缩放,图片拖拽,像素测量,roi交集并集补集输出
TODO:实现自由路径绘制,与后台交互数据
实现原理
交集并集差集使用像素做运算,使用0代表没有像素,1代表有像素,然后再做运算
// 计算交集
calculateIntersection(shape1, shape2) {
return shape1.map((pixel, index) => pixel && shape2[index] ? 1 : 0);
}
// 计算并集
calculateUnion(shape1, shape2) {
return shape1.map((pixel, index) => pixel || shape2[index] ? 1 : 0);
}
// 计算差集
calculateDifference(shape1, shape2) {
return shape1.map((pixel, index) => pixel && !shape2[index] ? 1 : 0);
}
canvas事件实现
使用两个canvas,使用隐藏的OffscreenCanvas来判断鼠标命中的是哪个元素,需要把shape同时画到两个canvas中,获取隐藏canvas中的像素值就知道命中哪一个
事件分发器
class EventSimulator {
constructor() {
// 初始化事件监听器映射对象
this.listenersMap = {};
// 初始化最后的鼠标按下和移动的 ID
this.lastDownId = null;
this.lastMoveId = null;
}
// 添加事件动作
addAction(action, evt) {
const { type, id } = action;
// 如果是鼠标移动事件
if (type === ActionType.Move) {
// 触发 mousemove 事件
this.fire(id, EventNames.mousemove, evt);
// 如果存在最后的移动 ID 且与当前 ID 不同
if (this.lastMoveId && this.lastMoveId !== id) {
// 触发 mouseleave 事件
this.fire(this.lastMoveId, EventNames.mouseleave, evt);
// 触发 mouseenter 事件
this.fire(id, EventNames.mouseenter, evt);
}
}
// 如果是鼠标按下事件
if (type === ActionType.Down) {
// 触发 mousedown 事件
this.fire(id, EventNames.mousedown, evt);
}
// 如果是鼠标释放事件
if (type === ActionType.Up) {
// 触发 mouseup 事件
this.fire(id, EventNames.mouseup, evt);
// 如果最后的按下 ID 等于当前 ID,则触发 click 事件
if (this.lastDownId === id) {
this.fire(id, EventNames.click, evt);
}
}
// 更新最后的移动和按下 ID
if (type === ActionType.Move) {
this.lastMoveId = action.id;
} else if (type === ActionType.Down) {
this.lastDownId = action.id;
}
}
// 添加事件监听器
addListeners(id, listeners) {
this.listenersMap[id] = listeners;
}
// 触发事件
fire(id, eventName, evt) {
// 检查是否有对应 ID 和事件名称的监听器,如果有则依次执行监听器函数
if (this.listenersMap[id] && this.listenersMap[id][eventName]) {
this.listenersMap[id][eventName].forEach((listener) => listener(evt));
}
}
}
shape控制点实现
每个shape都有对应的控制点,控制点也绘制在OffscreenCanvas中,通过添加事件来控制shape
旋转矩形的控制点实现
handleCtrlPointMove(x,y,evts){
//判断拖动的哪个点
//顺时针方向
if(evts.activateId==this.ctrlDotId[0]||evts.activateId==this.ctrlDotId[1]||evts.activateId==this.ctrlDotId[2]||evts.activateId==this.ctrlDotId[3]){//左上
let p2 = getNextPoint(this.x,this.y,20,this.phi);
let p1 = {x:this.x,y:this.y};
let p3 = {x:x,y:y};
let dist1=distanceToLine(p3,p1,p2);
let phi2=radianToVerticalAngle(this.phi);
let p22 = getNextPoint(this.x,this.y,20,phi2);
let dist2=distanceToLine(p3,p1,p22);
this.w=dist1*2;
this.h=dist2*2;
}else if(evts.activateId==this.ctrlDotId[4]){//旋转
var dx = x-this.x;
var dy = y-this.y;
this.phi=-Math.atan2(dy,dx)
}
evts.redraw();
}
画布拖拽实现
使用ctx的setTransform实现
画布缩放实现
使用ctx的setTransform实现
this.ctx.setTransform(CanvasStatus.scale,0,0,CanvasStatus.scale,CanvasStatus.offset.x,CanvasStatus.offset.y);
shape移动实现
通过改变shape的内部属性重绘shape实现
class Circle extends BaseShape{
constructor(opts = {}) {
super();
this.type=ShapeType.circle;
Object.assign(this, {
x: 0,
y: 0,
radius: 0,
strokeWidth: 1,
strokeColor: '#e6a23c',
fillColor: '#fff'
}, opts);
}
handleCtrlPointMove(x,y,evts){
//最小10个像素
let diffX=x-this.x;
this.radius=Math.max(diffX,10);
if(diffX>4){
evts.redraw();
}
}
drawMask(ctx){
const x = this.x;
const y = this.y;
const radius = this.radius;
let strokeColor = this.makStrokeColor;
let fillColor = this.maskFillColor;
const strokeWidth = this.strokeWidth;
//ctx.save();
ctx.beginPath();
ctx.fillStyle = fillColor;
ctx.strokeStyle = strokeColor;
ctx.lineWidth = strokeWidth;
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
//ctx.restore();
}
toShape(shapesOperator){
return shapesOperator.createShapeCircle(this.x,this.y,this.radius);
}
}
详细实现
TODO
源码下载
https://download.csdn.net/download/isyoungboy/89072580?spm=1001.2014.3001.5503