古希腊哲学家芝诺说:“你知道的越多, 你会发现自己不知道的就会更多”。
以往团队的技术分享不够系统,也没有形成一定的规律和习惯,对于团队成员的技术积累和成果共享达不到很好的效果,为了大家有更快的技术提升和能力提高,今年对团队的技术分享做了相应的规划,每个团队成员都会参与其中,将自己在开发过程中总结的知识点汇总起来,以专题的形式两周一次进行技术交流分享,前期进行各个技术点的积累,逐渐形成体系,也让大家能够不断的丰富自己的技术栈和技术深度。
2022新年伊始,新的起点,后续会不断更新分享的相关文章,关于团队技术提升和团队共建有什么好的实施经验,欢迎评论区交流指导。
本文是团队技术分享的第二篇,针对之前业务开发中关于canvas的两个技术点进行了整理,内容如下:
一、用canvas画布实现鼠标拖拽矩形框
1、实现原理
Canvas是一种非保留性的绘图界面,即不会记录过去执行的绘图操作,而是保持最终结果。如果想让Canvas变得具有交互性,比如用户可以选择、拖动画布上的图形。那么我们必须记录绘制的每一个对象,才能在将来灵活的修改并重绘它们,实现交互。以鼠标的坐标点为基点,改变矩形框的大小,不同的是, 这里每帧都要把画布清空以及重绘 ,这样最终看到的效果就是跟着鼠标的拖动,矩形框也跟着在变化。
2、相关参数
drawer:{
drawType: 1, // 1. 矩形;2.圆形
layers: [], //存储的画框信息
c: null, //获取画布
ctx: null, //获取绘图2D环境
currentR: null,//当前选中的对象
leftDistance: 0, //鼠标点横坐标与当前选中矩形左侧的差值
topDistance: 0, //鼠标点纵坐标与当前选中矩形左侧的差值
flag: 0, //是否点击鼠标的标志,0 不点击,1点击鼠标
op: 0, //op操作类型 0 无操作 1 画矩形框 2 拖动矩形框
x: 0, //鼠标的位置
y: 0, //鼠标的位置
imgUrl: "", //图片地址
elementWidth: 0, //画布的宽度
elementHeight: 0, //画布的高度
}
2.1 isPointInPath:在每次重绘画布的时候,判断鼠标坐标点是否在画布的某一个矩形内部。只有实现了判断方法才可以实现拖动以及改变矩形框的大小。如果指定的点位于当前路径,返回true;否则返回false。
context.isPointInPath(x,y)
2.2 reshow:重绘函数,把已经绘制的矩形的相关属性放在了layers这个数组里,每帧的重绘操作就是根据这个对象的描述重现这个矩形框,然后判断x,y是否在这个矩形框的路径上,由此实现了判断鼠标是否在矩形框内。
reshow: (x, y) => {
let allNotIn = 1;
drawer.layers.forEach((item) => {
drawer.ctx.beginPath();
drawer.ctx.strokeStyle = item.strokeStyle;
drawer.ctx.lineWidth = item.lineWidth;
//矩形
if (item.drawType == 1) {
drawer.ctx.rect(item.x1, item.y1, item.width, item.height);
// 更新大小区域
if (
x >= item.x1 - 10 / drawer.scale &&
x <= item.x1 + 10 / drawer.scale &&
y <= item.y2 - 10 / drawer.scale &&
y >= item.y1 + 10 / drawer.scale
) {
drawer.resizeLeft(item);
} else if (
x >= item.x2 - 10 / drawer.scale &&
x <= item.x2 + 10 / drawer.scale &&
y <= item.y2 - 10 / drawer.scale &&
y >= item.y1 + 10 / drawer.scale
) {
drawer.resizeWidth(item);
} else if (
y >= item.y1 - 10 / drawer.scale &&
y <= item.y1 + 10 / drawer.scale &&
x <= item.x2 - 10 / drawer.scale &&
x >= item.x1 + 10 / drawer.scale
) {
drawer.resizeTop(item);
} else if (
y >= item.y2 - 10 / drawer.scale &&
y <= item.y2 + 10 / drawer.scale &&
x <= item.x2 - 10 / drawer.scale &&
x >= item.x1 + 10 / drawer.scale
) {
drawer.resizeHeight(item);
} else if (
x >= item.x1 - 10 / drawer.scale &&
x <= item.x1 + 10 / drawer.scale &&
y <= item.y1 + 10 / drawer.scale &&
y >= item.y1 - 10 / drawer.scale
) {
drawer.resizeLT(item);
} else if (
x >= item.x2 - 10 / drawer.scale &&
x <= item.x2 + 10 / drawer.scale &&
y <= item.y2 + 10 / drawer.scale &&
y >= item.y2 - 10 / drawer.scale
) {
drawer.resizeWH(item);
} else if (
x >= item.x1 - 10 / drawer.scale &&
x <= item.x1 + 10 / drawer.scale &&
y <= item.y2 + 10 / drawer.scale &&
y >= item.y2 - 10 / drawer.scale
) {
drawer.resizeLH(item);
} else if (
x >= item.x2 - 10 / drawer.scale &&
x <= item.x2 + 10 / drawer.scale &&
y <= item.y1 + 10 / drawer.scale &&
y >= item.y1 - 10 / drawer.scale
) {
drawer.resizeWT(item);
}
}
if (item.drawType == 2) {
drawer.ctx.arc(item.x1, item.y1, item.r, 0, 2 * Math.PI);
let newR = Math.sqrt(
Math.pow(x - item.x1, 2) + Math.pow(y - item.y1, 2)
);
if (
newR >= item.r - 10 / drawer.scale &&
newR <= item.r + 10 / drawer.scale
) {
drawer.resizeCircle(item);
}
}
if (drawer.ctx.isPointInPath(x * drawer.scale, y * drawer.scale)) {
drawer.render(item);
allNotIn = 0;
}
drawer.ctx.stroke();
});
if (drawer.flag && allNotIn && drawer.op < 3) {
drawer.op = 1;
}
},
2.3 render函数:用于修改当前移动的矩形框的坐标
render: (rect) => {
drawer.c.style.cursor = "move";
if (drawer.flag && drawer.op == 0) {
drawer.op = 2;
}
if (drawer.flag && drawer.op == 2) {
if (!drawer.currentR) {
drawer.currentR = rect;
}
drawer.currentR.x2 += drawer.x - drawer.leftDistance - drawer.currentR.x1;
drawer.currentR.x1 += drawer.x - drawer.leftDistance - drawer.currentR.x1;
drawer.currentR.y2 += drawer.y - drawer.topDistance - drawer.currentR.y1;
drawer.currentR.y1 += drawer.y - drawer.topDistance - drawer.currentR.y1;
}
},
2.4 isPointInRetc(x, y):判断当前鼠标是否在矩形框内和圆内
function isPointInRetc(x, y) {
let len = drawer.layers.length;
for (let i = 0; i < len; i++) {
// 矩形区域判断
if (drawer.layers[i].drawType === 1) {
if (
drawer.layers[i].x1 < x &&
x < drawer.layers[i].x2 && drawer.layers[i].y1 < y && y < drawer.layers[i].y2
) {
return drawer.layers[i];
}
}
// 圆形区域判断
if (drawer.layers[i].drawType === 2) {
let oriR =
Math.pow(drawer.layers[i].x1 - drawer.layers[i].x2, 2) +
Math.pow(drawer.layers[i].y1 - drawer.layers[i].y2, 2);
let newR = Math.pow(drawer.layers[i].x1 - x, 2) + Math.pow(drawer.layers[i].y1 - y, 2);
if (newR <= oriR) {
return drawer.layers[i];
}
}
}
}
2.5 mousedown:鼠标按下
let mousedown = function (e) {
//获取鼠标的点
startx =
(e.pageX - drawer.c.offsetLeft + drawer.c.parentElement.scrollLeft)
starty =
(e.pageY - drawer.c.offsetTop + drawer.c.parentElement.scrollTop)
drawer.currentR = isPointInRetc(startx, starty);
if (drawer.currentR) {
drawer.leftDistance = startx - drawer.currentR.x1;
drawer.topDistance = starty - drawer.currentR.y1;
}
drawer.ctx.strokeRect(drawer.x, drawer.y, 0, 0);
drawer.ctx.strokeStyle = "#00ff00";
drawer.ctx.lineWidth = 2;
drawer.flag = 1;
};
2.6 mousemove:鼠标移动
let mousemove = function (e) {
drawer.x =
(e.pageX - drawer.c.offsetLeft + drawer.c.parentElement.scrollLeft)
drawer.y =
(e.pageY - drawer.c.offsetTop + drawer.c.parentElement.scrollTop)
drawer.ctx.save();
drawer.ctx.setLineDash([5]);
drawer.c.style.cursor = "default";
drawer.ctx.lineWidth = 2;
drawer.ctx.clearRect(0, 0, drawer.elementWidth, drawer.elementHeight);
if (drawer.flag && drawer.op == 1) {
if (drawer.drawType == 1) {
drawer.ctx.strokeRect(
startx,
starty,
drawer.x - startx,
drawer.y - starty
);
}
if (drawer.drawType == 2) {
let r = Math.sqrt(
Math.pow(drawer.x - startx, 2) + Math.pow(drawer.y - starty, 2)
);
drawer.ctx.beginPath();
drawer.ctx.arc(startx, starty, r, 0, 2 * Math.PI);
drawer.ctx.stroke();
}
}
drawer.ctx.restore();
drawer.reshow(drawer.x, drawer.y);
};
2.7 mouseup:鼠标弹起
let mouseup = function (e) {
if (drawer.x - startx > 20 || drawer.y - starty > 20) {
drawer.layers.push(
fixPosition({
x1: startx,
y1: starty,
x2: drawer.x,
y2: drawer.y,
strokeStyle: "#00ff00",
lineWidth: 2,
drawType: drawer.drawType,
})
);
}
drawer.currentR = null;
drawer.flag = 0;
drawer.reshow(drawer.x, drawer.y);
drawer.op = 0;
};
3、效果展示:
二、用svg实现实现鼠标拖拽矩形框
SVG 是可缩放的矢量图形,使用XML格式定义图像,可以生成对应的DOM节点,便于对单个图形进行交互操作。比CANVAS更加灵活一点。关于SVG的基础知识,请参考SVG学习地址
1、SVG的图形和基本属性
1.1 基本图形
<rect> 矩形
x y 横坐标和纵坐标(矩形左上角的位置)
width 宽
height 高
rx ry 圆角大小 (只设置rx或ry,则两者的值相同,只有分别设置了不同的值才会各自显示不同的大小)
<circle>圆形
cx cy 横坐标和纵坐标(圆形的中心点)
r 半径
<ellipse>椭圆
cx cy 横坐标和纵坐标
rx ry 横向半径和纵向半径
<line> 线段
x1 y1 端点坐标
x2 y2 端点坐标
<polyline>折线
points="x1 y1 x2 y2 x3 y3.."
多少个节点就设置多少个x y 值
<polygon>多边形
points="x1 y1 x2 y2 x3 y3.."
多少个节点就设置多少个x y 值,第一个节点和最后一个节点会自动连接在一起
1.2 基本属性
fill = “#FFB3AE” 填充颜色
stroke = #971817 描边颜色
stroke-width = 10 描边的粗细
transform = "rotate(30)" 旋转变形
2、案例
<svg
id="svgelem"
style=" position: absolute;"
width="300"
height="200"
xmlns="http://www.w3.org/2000/svg"
@mousemove="mover"
@mousedown="mdown"
@mouseup="mup"
>
<rect
v-for="(tag, index3) in newTags"
:key="'rect' + index3 + '2'"
:id="'rect' + index3"
:x="tag.x"
:y="tag.y"
:width="tag.width"
:height="tag.height"
style="fill-opacity: 0; stroke-width: 2"
:style="{ stroke: tag.color, strokeDasharray: tag.dash }"
@dblclick="handleChooseTag(index3)"
></rect>
<text v-for="(tag, index4) in newTags" :key="'textName' + index4 + '3'" :x="tag.x" :y="tag.y - 5" :fill="tag.color">
{{ tag.tagName }}
</text>
<text
v-for="(tag, index5) in newTags"
:key="'textIndex' + index5 + '4'"
:x="tag.x + tag.width / 2"
:y="tag.y + tag.height / 2"
:fill="tag.color"
>
{{ index5 + 1 }}
</text>
</svg>
2.1 打开图片
这是一个能针对某个元素实行大小、位置变化监听的API,是一个类,它提供了一个观察器,该观察器将在每个 resize 事件上调用,通过实例化一个监听器,然后调用observe方法传入一个要执行监听的DOM元素或SVG元素,那么每次resize事件触发时都会执行ResizeObserver实例化时传入的回调函数,该回调函数默认会返回一个数组,数组里包含了每个被执行了监听器的元素及其可读信息,元素的拜访顺序也是监听器执行的顺序(对,实例化一个监听器,可多次执行observe方法对不同元素进行监听!)
let _that = this;
var ro = new ResizeObserver(entries => {
_that.oriWidthFull = _that.oriWidthFull || oriImg.offsetWidth;
_that.oriHeightFull = _that.oriHeightFull || oriImg.offsetHeight;
_that.resizeDrawSvg(_that.oriWidthFull, _that.oriHeightFull);
_that.oriWidthFull = entries[0].contentRect.width;
_that.oriHeightFull = entries[0].contentRect.height;
});
this.$nextTick(async () => {
ro.observe(document.getElementById('oriImg'));
this.drawSvg();
});
2.2 drawSvg:初始画框
drawSvg() {
this.$nextTick(() => {
let orisize = this.chooseSample.size.split('*');
let oriWidth = +orisize[0];
let oriHeight = +orisize[1];
let imgWidth = oriImg.offsetWidth;
let imgHeight = oriImg.offsetHeight;
svgelem.setAttribute('width', imgWidth);
svgelem.setAttribute('height', imgHeight);
let scale = imgWidth / oriWidth;
// 更改实际的坐标
this.newTags.forEach(tag => {
tag.oriX = tag.x || 0;
tag.oriY = tag.y || 0;
tag.oriWidth = tag.width || 0;
tag.oriHeight = tag.height || 0;
tag.x = Math.round(tag.oriX * scale);
tag.y = Math.round(tag.oriY * scale);
tag.width = Math.round(tag.oriWidth * scale);
tag.height = Math.round(tag.oriHeight * scale);
});
this.currentTags.forEach(tag => {
tag.oriX = tag.x || 0;
tag.oriY = tag.y || 0;
tag.oriWidth = tag.width || 0;
tag.oriHeight = tag.height || 0;
tag.x = Math.round(tag.oriX * scale);
tag.y = Math.round(tag.oriY * scale);
tag.width = Math.round(tag.oriWidth * scale);
tag.height = Math.round(tag.oriHeight * scale);
});
});
},
2.3 resizeDrawSvg:页面变化框重绘
resizeDrawSvg(oriWidth, oriHeight) {
this.$nextTick(() => {
let imgWidth = oriImg.offsetWidth;
let imgHeight = oriImg.offsetHeight;
svgelem.setAttribute('width', imgWidth);
svgelem.setAttribute('height', imgHeight);
let scale = imgWidth / oriWidth;
// 更改实际的坐标
this.newTags.forEach(tag => {
tag.oriX = tag.x || 0;
tag.oriY = tag.y || 0;
tag.oriWidth = tag.width || 0;
tag.oriHeight = tag.height || 0;
tag.x = Math.round(tag.oriX * scale);
tag.y = Math.round(tag.oriY * scale);
tag.width = Math.round(tag.oriWidth * scale);
tag.height = Math.round(tag.oriHeight * scale);
});
this.currentTags.forEach(tag => {
tag.oriX = tag.x || 0;
tag.oriY = tag.y || 0;
tag.oriWidth = tag.width || 0;
tag.oriHeight = tag.height || 0;
tag.x = Math.round(tag.oriX * scale);
tag.y = Math.round(tag.oriY * scale);
tag.width = Math.round(tag.oriWidth * scale);
tag.height = Math.round(tag.oriHeight * scale);
});
});
},
2.4 handleChooseTag:选中某个标注
// 选中某个标注
handleChooseTag(index) {
this.editIndex = index;
let temp = JSON.parse(JSON.stringify(this.newTags));
temp.forEach((tag, ind) => {
tag.isActive = ind == index;
tag.color = ind == index ? 'red' : '#00ff00';
});
this.newTags = JSON.parse(JSON.stringify(temp));
},
3、效果展示
三、基于Vue + fabric.js的图片标注组件搭建
Fabric.js 是一个强大而简单的 Javascript HTML5 画布库 Fabric 在画布元素之上提供交互式对象模型 Fabric 还具有 SVG-to-canvas(和 canvas-to-SVG)解析器。使用Fabric.js,你可以在画布上创建和填充对象; 比如简单的几何形状 - 矩形,圆形,椭圆形,多边形,自定义图片或由数百或数千个简单路径组成的更复杂的形状。 另外,还可以使用鼠标缩放,移动和旋转这些对象; 修改它们的属性 - 颜色,透明度,z-index等。也可以将画布上的对象进行组合。
1、 安装
yarn add fabric -S 或者 npm i fabric -S
2、依赖
下载[customiseControls.min.js](https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fpurestart%2Fvue-fabric%2Fblob%2Fmaster%2Fstatic%2Fjs%2FcustomiseControls.min.js).和 [fabric.min.js](https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fpurestart%2Fvue-fabric%2Fblob%2Fmaster%2Fstatic%2Fjs%2Ffabric.min.js)
到本地 static/js/文件下<br />
本地项目 index.html 引入
<script type="text/javascript" src="./static/js/fabric.min.js"></script>
<script type="text/javascript" src="./static/js/customiseControls.min.js"></script
3、 使用
//在main.js引用
import 'vue-fabric/dist/vue-fabric.min.css';
import { Fabric } from 'vue-fabric';
Vue.use(Fabric);
<vue-fabric ref="canvas" id="canvas" :width="width" :height="height" @selection:created="selected" @selection:updated="selected"></vue-fabric>
3.1 绘制一个简单的图形
3.1.1 Fabric 提供了 7 种基础形状:
- fabric.Circle (圆)
- fabric.Ellipse (椭圆)
- fabric.Line (线)
- fabric.Polyline (多条线绘制成图形)
- fabric.triangle (三角形)
- fabric.Rect (矩形)
- fabric.Polygon (多边形)
3.1.2 效果展示
可支持对图片、矩形、三角形、虚线添加到画布中并且进行旋转。
3.1.2 代码展示
<template>
<div class="container">
<div class="header">
<div class="logo">
LOGO
</div>
</div>
<div class="content-wrapper">
<div class="list-wraper">
<div :key="item.id" v-for="item in list" class="image-wrapper">
<img :src="item.url" />
<i @click="handleAdd(item.url)" class="pt-iconfont icon-plus-circle"></i>
</div>
</div>
<vue-fabric ref="canvas" id="canvas" :width="width" :height="height" @selection:created="selected" @selection:updated="selected"></vue-fabric>
<div class="tool-wrapper">
<i @click="createRect" >创建矩形</i>
<i @click="createCircle" >创建圆</i>
<i @click="createTriangle" >创建三角形</i>
<i @click="createEqualTriangle" >创建等腰三角形</i>
<i @click="createLine" >创建线</i>
<i @click="drawDottedline" >创建虚线</i>
<i @click="handleDelete" class="pt-iconfont icon-delete"></i>
<i @click="rotate" class="pt-iconfont icon-shuaxin"></i>
<i @click="changeDrawMore" class="pt-iconfont icon-crop"></i>
<i @click="selected" class="pt-iconfont icon-crop"></i>
</div>
</div>
<vue-image-model :close="()=>{imgUrl=''}" v-show="imgUrl.length>0" :url="imgUrl"></vue-image-model>
</div>
</template>
<script type='text/ecmascript-6'>
import VueImageModel from '../components/image-model.vue';
export default {
components: {
VueImageModel
},
data () {
return {
// http://data618.oss-cn-qingdao.aliyuncs.com/ys/3524/img/b.jpg
imgUrl: '',
width: 300,
height: 500,
list: [
{
id: 1,
url: '/static/images/sticker1.png'
},
{
id: 2,
url: '/static/images/sticker2.png'
},
{
id: 3,
url: '/static/images/sticker3.png'
},
{
id: 4,
url: '/static/images/sticker4.png'
},
{
id: 5,
url: '/static/images/sticker5.png'
}
],
isDrawingMode: true
};
},
created () {
this.width = document.body.offsetWidth - 200;
this.height = document.body.offsetHeight - 60;
console.log(document.body.offsetWidth);
},
mounted () {
// this.$refs.canvas.createTriangle({ id: 'Triangle', x: 100, y: 100, x1: 150, y1: 200, x2: 180, y2: 190, fill: 'yellow', left: 80 });
// this.$refs.canvas.createImage('/static/images/sticker1.png', { id: 'myImage', width: 100, height: 100, left: 10, top: 10 ,evented:false, selectable: false});
// // this.$refs.canvas.createImage('/static/images/sticker2.png');
// // this.$refs.canvas.createImage('/static/images/sticker3.png');
// let options = {
// x: 100, y: 100, x1: 600, y1: 600, color: '#B2B2B2', drawWidth: 2, id: 'Triangle'
// };
// // this.$refs.canvas.drawDottedline(options);
// // this.$refs.canvas.createEllipse({ rx: 200, ry: 400, left: 300 });
// this.$refs.canvas.createItext(`斯诺伐克两三斯诺伐克两三斯诺伐克两三斯诺伐
// 克两三斯诺伐克两三斯诺伐克两三斯诺伐克两三`, { top: 100, left: 300, width: 50 ,editable:false});
// this.$refs.canvas.setCornerIcons({ size: 20, tl: '/static/images/cow.png' });
// this.$refs.canvas.drawByPath([[50, 50], [120, 120], [80, 160]], {});
var img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
let that = this;
img.onload = function () {
that.$refs.canvas.createImageByImg(img, { id: 'myImage', width: 100, height: 100, left: 10, top: 10, evented: true, selectable: true, crossOrigin: 'anonymous'});
};
img.src = '/static/images/sticker1.png';
this.$refs.canvas.setSelection(false);
let options = {
imgUrl: 'https://weiliicimg9.pstatp.com/weili/l/701712572929933335.webp',
width: this.width,
height: this.height,
opacity: 1,
scaleX: 1.5
};
// this.$refs.canvas.setBackgroundImage(options);
this.$refs.canvas.setBackgroundColor('#ffffff00');
},
methods: {
changeDrawMore () {
this.isDrawingMode = !this.isDrawingMode;
this.$refs.canvas.freeDrawConfig({isDrawingMode: this.isDrawingMode});
},
setErase () {
this.$refs.canvas.eraseDrawConfig({drawWidth: 20});
},
toggleMirror () {
this.$refs.canvas.toggleMirror({flip: 'Y'});
},
discardActive () {
this.$refs.canvas.discardActive();
},
handleAdd (url) {
console.log('handleAdd');
this.$refs.canvas.createImage(url);
},
createRect () {
this.$refs.canvas.createRect({width: 100, height: 20});
},
createCircle () {
this.$refs.canvas.createCircle();
},
createTriangle () {
this.$refs.canvas.createTriangle({x1: 10, y1: 30, x2: 10, y2: 70});
},
createEqualTriangle () {
this.$refs.canvas.createEqualTriangle();
},
createLine () {
this.$refs.canvas.createLine();
},
drawDottedline () {
this.$refs.canvas.drawDottedline();
},
handleDelete () {
this.$refs.canvas.removeCurrentObj();
},
rotate () {
this.$refs.canvas.setRotate();
},
createImg () {
let dataUrl = this.$refs.canvas.toDataUrl();
// console.log(dataUrl);
this.imgUrl = dataUrl;
},
selected (obj, option) {
this.$refs.canvas.setSelection(true);
// console.log(obj);
// console.log(option);
}
}
};
</script>
<template>
<div>
<canvas :id="id" :width="width" :height="height"></canvas>
</div>
</template>
<script type="text/ecmascript-6">
import Utils from '../../utils';
const dotCircleImg = require('../../assets/dot-circle.png');
const rotateMdrImg = require('../../assets/rotate-mdr.png');
export default {
name: 'VueFabric',
props: {
id: {
type: String,
required: false,
default: 'fabricCanvas'
},
width: {
type: Number,
required: true
},
height: {
type: Number,
required: true
}
},
data () {
return {
canvas: null,
currentObj: null
};
},
created () {
},
mounted () {
this.canvas = new fabric.Canvas(this.id, { preserveObjectStacking: true });
let canvas = this.canvas;
// 客製化控制项
fabric.Canvas.prototype.customiseControls({
tl: {
action: 'scale'
// cursor: '../../assets/rotate-mdr.png'
},
tr: {
action: 'scale'
},
bl: {
action: 'scale',
cursor: 'pointer'
},
br: {
action: 'scale',
cursor: 'pointer'
},
mb: {
action: 'scale',
cursor: 'pointer'
},
// mr: {
// // action: function(e, target) {
// // target.set({
// // left: 200,
// // });
// // canvas.renderAll();
// // },
// action: 'scale',
// cursor: 'pointer',
// },
mt: {
// action: {
// rotateByDegrees: 30
// },
action: 'scale',
cursor: 'pointer'
},
// only is hasRotatingPoint is not set to false
mtr: {
action: 'rotate'
// cursor: '../../assets/cow.png',
}
});
this.setCornerIcons({});
// canvas.add(new fabric.Circle({ radius: 30, fill: '#f55', top: 100, left: 100 }));
canvas.backgroundColor = '#ffffff';
// canvas.renderAll();
// this.canvas.push(canvas);
let that = this;
this.canvas.controlsAboveOverlay = false;
this.canvas.skipOffscreen = true;
// this.drawControls();
// 初次选中图层
this.canvas.on('selection:created', function (options) {
that.$emit('selection:created', options);
});
this.canvas.on('mouse:down', function (options) {
that.$emit('mouse:down', options);
});
this.canvas.on('mouse:up', function (options) {
that.$emit('mouse:up', options);
});
this.canvas.on('mouse:move', function (options) {
that.$emit('mouse:move', options);
});
this.canvas.on('mouse:dblclick', function (options) {
that.$emit('mouse:dblclick', options);
});
this.canvas.on('mouse:over', function (options) {
that.$emit('mouse:over', options);
});
this.canvas.on('mouse:out', function (options) {
that.$emit('mouse:out', options);
});
// 添加图层
this.canvas.on('object:added', function (options) {
that.$emit('object:added', options);
});
// 移除图层
this.canvas.on('object:removed', function (options) {
that.$emit('object:removed', options);
});
// 编辑图层
this.canvas.on('object:modified', function (options) {
that.$emit('object:modified', options);
});
this.canvas.on('object:rotating', function (options) {
that.$emit('object:rotating', options);
});
this.canvas.on('object:scaling', function (options) {
that.$emit('object:scaling', options);
});
this.canvas.on('object:moving', function (options) {
that.$emit('object:moving', options);
});
// 图层选择变化
this.canvas.on('selection:updated', function (options) {
that.$emit('selection:updated', options);
});
// 清空图层选中
this.canvas.on('selection:cleared', function (options) {
that.$emit('selection:cleared', options);
});
this.canvas.on('before:selection:cleared', function (options) {
that.$emit('before:selection:cleared', options);
});
},
methods: {
setCornerIcons ({ size = 20, borderColor = '#e4e4e4', cornerBackgroundColor = '#ffffff', cornerShape = 'rect', tl = dotCircleImg, tr = dotCircleImg, bl = dotCircleImg, br = dotCircleImg, ml = dotCircleImg, mr = dotCircleImg, mtr = rotateMdrImg }) {
// basic settings
let that = this;
fabric.Object.prototype.customiseCornerIcons(
{
settings: {
borderColor: borderColor,
cornerSize: size,
cornerShape: cornerShape, // 'rect', 'circle'
cornerBackgroundColor: cornerBackgroundColor
},
tl: {
icon: tl
},
tr: {
icon: tr
},
bl: {
icon: bl
},
br: {
icon: br
},
ml: {
icon: ml
},
mr: {
icon: mr
},
// only is hasRotatingPoint is not set to false
mtr: {
icon: mtr
}
},
function () {
that.canvas.renderAll();
}
);
},
drawDottedline (options) {
options = Object.assign({ x: 0, y: 0, x1: 10, y1: 10, color: '#B2B2B2', drawWidth: 2, offset: 6, empty: 3 }, options);
let canvasObject = new fabric.Line([options.x, options.y, options.x1, options.y1], {
strokeDashArray: [options.offset, options.empty],
stroke: options.color,
strokeWidth: options.drawWidth,
...options
});
this.canvas.add(canvasObject);
this.canvas.renderAll();
},
drawArrowLine (options) {
options = Object.assign({ x: 0, y: 0, x1: 0, y1: 0, color: '#B2B2B2', drawWidth: 2, fillColor: 'rgba(255,255,255,0)', theta: 35, headlen: 35 }, options);
let canvasObject = new fabric.Path(this.drawArrowBase(options.x, options.y, options.x1, options.y1, options.theta, options.headlen), {
stroke: options.color,
fill: options.fillColor,
strokeWidth: options.drawWidth,
...options
});
this.canvas.add(canvasObject);
this.canvas.renderAll();
},
drawArrowBase (fromX, fromY, toX, toY, theta, headlen) {
theta = typeof theta !== 'undefined' ? theta : 30;
headlen = typeof theta !== 'undefined' ? headlen : 10;
// 计算各角度和对应的P2,P3坐标
var angle = (Math.atan2(fromY - toY, fromX - toX) * 180) / Math.PI,
angle1 = ((angle + theta) * Math.PI) / 180,
angle2 = ((angle - theta) * Math.PI) / 180,
topX = headlen * Math.cos(angle1),
topY = headlen * Math.sin(angle1),
botX = headlen * Math.cos(angle2),
botY = headlen * Math.sin(angle2);
var arrowX = fromX - topX,
arrowY = fromY - topY;
var path = ' M ' + fromX + ' ' + fromY;
path += ' L ' + toX + ' ' + toY;
arrowX = toX + topX;
arrowY = toY + topY;
path += ' M ' + arrowX + ' ' + arrowY;
path += ' L ' + toX + ' ' + toY;
arrowX = toX + botX;
arrowY = toY + botY;
path += ' L ' + arrowX + ' ' + arrowY;
return path;
},
freeDrawConfig (options) {
options = Object.assign({color: '#b2b2b2', drawWidth: 2}, options);
this.canvas.isDrawingMode = options.isDrawingMode;
this.canvas.freeDrawingBrush.color = options.color; // 设置自由绘颜色
this.canvas.freeDrawingBrush.width = options.drawWidth;
this.canvas.renderAll();
},
eraseDrawConfig (options) {
options = Object.assign({color: 'white', drawWidth: 2}, options);
this.canvas.freeDrawingBrush.color = options.color; // 设置自由绘颜色
this.canvas.freeDrawingBrush.width = options.drawWidth;
this.canvas.renderAll();
},
removeCurrentObj () {
let obj = this.canvas.getActiveObject();
// console.log(obj);
this.canvas.remove(obj);
this.canvas.renderAll();
},
getEditObj () {
let obj = this.canvas.getActiveObject();
this.removeCurrentObj();
return obj;
},
setEditObj (obj) {
this.canvas.add(obj);
this.canvas.renderAll();
},
setRotate (deg = 36) {
let obj = this.canvas.getActiveObject();
let angle = obj.angle;
obj.rotate(angle + deg);
this.canvas.renderAll();
},
discardActive () {
this.canvas.discardActiveObject();
// this.canvas.discardActiveGroup();
this.canvas.renderAll();
},
deactivateAll () {
// this.canvas.deactivateAll().renderAll();
},
deactivateOne (obj) {
var activeGroup = this.canvas.getActiveGroup();
activeGroup.removeWithUpdate(obj);
this.canvas.renderAll();
},
setSelection (flag) {
this.canvas.selection = flag;
this.canvas.renderAll();
},
moveTo () {
let obj = this.canvas.getActiveObject();
console.log(this.canvas.sendBackwards);
this.canvas.sendBackwards(obj, true);
this.canvas.discardActiveObject();
// this.canvas.discardActiveGroup();
},
createRect (options) {
debugger;
options = Object.assign({ width: 0, height: 0, fillColor: 'red', left: 50, top: 50 }, options);
let rect = new fabric.Rect({
fill: options.fillColor, // 填充的颜色
...options
});
this.canvas.add(rect);
this.canvas.renderAll();
},
createCircle (options) {
options = Object.assign({ left: 0, top: 0, radius: 30, fillColor: 'rgba(255, 255, 255, 0)', color: '#B2B2B2', drawWidth: 2 }, options);
let defaultOption = {
fill: options.fillColor,
strokeWidth: options.drawWidth,
stroke: options.color,
...options
};
let Circle = new fabric.Circle(defaultOption);
this.canvas.add(Circle);
this.canvas.renderAll();
},
createTriangle (options) {
options = Object.assign({ x: 0, y: 0, x1: 0, y1: 0, x2: 0, y2: 0, left: 100, top: 100, color: '#B2B2B2', drawWidth: 2, fillColor: 'rgba(255, 255, 255, 0)', id: 'triangle' }, options);
var path = 'M ' + options.x + ' ' + options.y + ' L ' + options.x1 + ' ' + options.y1 + ' L ' + options.x2 + ' ' + options.y2 + ' z';
let canvasObject = new fabric.Path(path, {
stroke: options.color,
strokeWidth: options.drawWidth,
fill: options.fillColor,
...options
});
this.canvas.add(canvasObject);
this.canvas.renderAll();
},
createEqualTriangle (options) {
options = Object.assign({ left: 100, top: 100, width: 50, height: 80, fillColor: 'rgba(255, 255, 255, 0)', color: '#B2B2B2', drawWidth: 2 }, options);
// console.log(defaultOption);
let triangle = new fabric.Triangle({
fill: options.fillColor,
strokeWidth: options.drawWidth,
stroke: options.color,
...options
});
this.setContronVisibility(triangle);
this.canvas.add(triangle);
this.canvas.renderAll();
},
createLine (options) {
options = Object.assign({ x: 0, y: 0, x1: 10, y1: 10, fillColor: 'rgba(255, 255, 255, 0)', strokeColor: '#B0B0B0' }, options);
let line = new fabric.Line([options.x, options.y, options.x1, options.y1], {
fill: options.fillColor,
stroke: options.strokeColor,
...options
});
this.canvas.add(line);
this.canvas.renderAll();
},
createEllipse (options) {
options = Object.assign({ rx: 100, ry: 200, fillColor: 'rgba(255, 255, 255, 0)', angle: 90, strokeColor: '#B0B0B0', strokeWidth: 3, left: 50, top: 50 }, options);
var ellipse = new fabric.Ellipse({
fill: options.fillColor,
stroke: options.strokeColor,
...options
});
this.canvas.add(ellipse);
this.canvas.renderAll();
},
createText (text, options) {
options = Object.assign({ left: 100, top: 100 }, options);
var canvasObj = new fabric.Text(text, { ...options });
this.canvas.add(canvasObj);
this.canvas.renderAll();
},
createItext (text, options) {
options = Object.assign({ left: 0, top: 0, fill: '#000'}, options);
let IText = new fabric.IText(text, options);
this.canvas.add(IText);
this.canvas.renderAll();
},
createTextbox (text, options) {
// _fontSizeMult: 5,
options.fillColor = options.fillColor ? options.fillColor : options.fill;
options = Object.assign({ fontSize: 14, fillColor: '#000000', registeObjectEvent: false, width: 50, left: 100, top: 100 }, options);
var canvasObj = new fabric.Textbox(text, {
fill: options.fillColor,
...options
});
// let arr = canvasObj._splitTextIntoLines(text);
// console.log(arr);
this.canvas.add(canvasObj);
if (options.registeObjectEvent) {
Utils.registeObjectEvent(this, canvasObj);
}
this.canvas.renderAll();
},
createImageByImg (img, options) {
options = options || {};
let canvas = this.canvas;
let that = this;
// let maxWidth = that.width;
let width = 0;
let height = 0;
width = img.width;
height = img.height;
// if (img.width > img.height) {
// if (img.width > maxWidth) {
// width = maxWidth;
// height = (img.height / img.width) * width;
// } else {
// width = img.width;
// height = img.height;
// }
// } else {
// if (img.height > maxWidth) {
// height = maxWidth;
// width = (img.width / img.height) * height;
// } else {
// width = img.width;
// height = img.height;
// }
// }
if (options && options.width) {
width = options.width;
}
if (options && options.height) {
height = options.height;
}
let leftP = that.width / 2;
let topP = that.height / 2;
if ((options && options.left) || (options && options.left == 0)) {
leftP = options.left + width / 2;
}
if ((options && options.top) || (options && options.top == 0)) {
topP = options.top + height / 2;
}
let imgOptions = Object.assign(options, {
id: (options && options.id) ? options.id : 'image',
left: leftP,
top: topP,
scaleX: width / img.width,
scaleY: height / img.height,
originX: 'center',
originY: 'center',
cornerStrokeColor: 'blue'
});
delete imgOptions.width;
delete imgOptions.height;
var canvasImage = new fabric.Image(img, imgOptions);
canvasImage.hasControls = true;
canvasImage.hasBorders = true;
canvas.add(canvasImage); // 把图片添加到画布上
if (options && options.registeObjectEvent) {
Utils.registeObjectEvent(that, canvasImage);
}
canvas.renderAll.bind(canvas);
},
// 读取图片地址,创建图片
createImage (url, options) {
options = options || {};
let canvas = this.canvas;
let that = this;
fabric.Image.fromURL(url, function (img) {
// 添加过滤器
// img.filters.push(new fabric.Image.filters.Grayscale());
// 应用过滤器并重新渲染画布执行
// img.applyFilters(canvas.renderAll.bind(canvas));
// console.log(img);
let maxWidth = that.width / 2;
let width = 0;
let height = 0;
width = img.width;
height = img.height;
// if (img.width > img.height) {
// if (img.width > maxWidth) {
// width = maxWidth;
// height = (img.height / img.width) * width;
// } else {
// width = img.width;
// height = img.height;
// }
// } else {
// if (img.height > maxWidth) {
// height = maxWidth;
// width = (img.width / img.height) * height;
// } else {
// width = img.width;
// height = img.height;
// }
// }
if (options && options.width) {
width = options.width;
}
if (options && options.height) {
height = options.height;
}
let leftP = that.width / 2;
let topP = that.height / 2;
if ((options && options.left) || (options && options.left == 0)) {
leftP = options.left + width / 2;
}
if ((options && options.top) || (options && options.top == 0)) {
topP = options.top + height / 2;
}
// console.log(options);
let imgOptions = Object.assign(options, {
// ...options,
id: (options && options.id) ? options.id : 'image',
left: leftP,
top: topP,
scaleX: width / img.width,
scaleY: height / img.height,
originX: 'center',
originY: 'center',
cornerStrokeColor: 'blue'
});
delete imgOptions.width;
delete imgOptions.height;
console.log('imgOptions', imgOptions);
img.set(imgOptions);
var oldOriginX = img.get('originX');
var oldOriginY = img.get('originY');
var center = img.getCenterPoint();
img.hasControls = true;
img.hasBorders = true;
canvas.add(img); // 把图片添加到画布上
if (options && options.registeObjectEvent) {
Utils.registeObjectEvent(that, img);
}
canvas.renderAll.bind(canvas);
});
},
toJson () {
let json = this.canvas.toJSON();
return json;
},
toDataUrl () {
let canvas = this.canvas;
let dataURL = canvas.toDataURL({
format: 'jpeg',
quality: 1
});
return dataURL;
},
loadFromJSON (json, cb) {
let canvas = this.canvas;
canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function (
o,
object
) {
// `o` = json object
// `object` = fabric.Object instance
// ... do some stuff ...
cb(o);
object.setControlsVisibility({
bl: true,
br: true,
mb: false,
ml: true,
mr: true,
mt: false,
mtr: true,
tl: true,
tr: true
});
});
},
clear () {
this.canvas.clear();
},
getObjects () {
return this.canvas.getObjects();
},
renderAll () {
this.canvas.renderAll(this.canvas);
},
renderTop () {
this.canvas.renderTop();
},
// 设置背景颜色
setBackgroundColor (color) {
let canvas = this.canvas;
this.canvas.setBackgroundColor(color, canvas.renderAll.bind(canvas));
},
// 设置画布背景
setBackgroundImage (options) {
let canvas = this.canvas;
let opt = {
opacity: 1,
left: 0,
top: 0,
angle: 0,
crossOrigin: null,
originX: 'left',
originY: 'top',
scaleX: 1,
scaleY: 1
};
// console.log(options);
if (Object.prototype.toString.call(options) == '[object String]') {
console.log('字符串兼容');
opt.imgUrl = options;
} else {
opt = Object.assign(opt, options);
}
fabric.Image.fromURL(opt.imgUrl, function (img) {
img.set({width: opt.width ? opt.width : canvas.width, height: opt.height ? opt.height : canvas.height, originX: 'left', originY: 'top', scaleX: opt.scaleX, scaleY: opt.scaleY });
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {...opt});
});
},
toSvg () {
return this.canvas.toSVG();
},
drawControls () {
let canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
ctx.setLineDash([]);
ctx.beginPath();
ctx.ellipse(100, 100, 50, 75, (45 * Math.PI) / 180, 0, 2 * Math.PI); // 倾斜45°角
ctx.stroke();
ctx.setLineDash([5]);
ctx.moveTo(0, 200);
ctx.lineTo(200, 0);
ctx.stroke();
this.canvas.drawControls(ctx);
// this.canvas.controlsAboveOverlay=true;
},
setContronVisibility (obj) {
obj.setControlsVisibility({
bl: true,
br: true,
mb: false,
ml: true,
mr: true,
mt: false,
mtr: true,
tl: true,
tr: true
});
},
// 设置mirror
toggleMirror (options) {
options = options || {};
options = Object.assign({ flip: 'X' }, options);
let img = this.canvas.getActiveObject();
// if (img && img.type == 'image') {
if (options.flip === 'X') {
img.toggle('flipX');
} else {
img.toggle('flipY');
}
this.renderAll();
// }
},
// 设置层级
toNextLayer () {
let obj = this.canvas.getActiveObject();
if (!obj) {
return;
}
obj.sendBackwards(true);
this.renderTop();
// this.canvas.setActiveObject(obj);
},
toBottomLayer () {
let obj = this.canvas.getActiveObject();
if (!obj) {
return;
}
obj.sendToBack();
this.renderTop();
// this.canvas.setActiveObject(obj);
},
toLastLayer () {
let obj = this.canvas.getActiveObject();
if (!obj) {
return;
}
obj.bringForward(true);
this.renderTop();
},
toTopLayer () {
let obj = this.canvas.getActiveObject();
if (!obj) {
return;
}
obj.bringToFront();
this.renderTop();
},
drawByPath (pathArray, options) {
options = Object.assign({ fillColor: 'rgba(255, 255, 255, 0)', left: 150, top: 150, strokeColor: '#B0B0B0', strokeWidth: 3 }, options);
let pathStr = 'M ';
for (let item of pathArray) {
pathStr = pathStr + item[0] + ' ' + item[1] + ' ';
}
pathStr = pathStr + 'z';
console.log(pathStr);
var path = new fabric.Path(pathStr);
path.set({
stroke: options.strokeColor,
fill: options.fillColor,
...options
});
this.canvas.add(path);
}
}
};
</script>
四、基于canvas改色图片
1.图片的颜色组成
RGB色彩是工业界的一种颜色标准,是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是运用最广的颜色系统之一。三原色Red, Green, Blue, 每一种颜色值的范围是0~255,所以每一个颜色用1个字节=8个bit便可完全在计算机内部表示出来。而R, G, B不同的组合几乎产生了所有的颜色,当然自然界中的颜色比这些要远远丰富很多,采用R, G, B的方式,如果以24色深表示的话,在计算机中可表示的颜色数量有2^8 2 ^8 2 ^8 = 16777216种颜色。
2.图片在计算机中的存储
照片、图像等非数字化的图像信息,都是以“像素”的形式,通过按照顺序(矩阵形式)进行有序排列。像素是组成图像的基本单位。每个图片中的像素都会对应一个颜色值的矩阵,如果一个480320像素的黑白图片就会有一个480320的颜色值(0,255)矩阵。下面是入门卷积神经网络经常使用的一张图片。
3.基于canvas的demo
<div>
<img id="imgs" style="display:none"></img>
</div>
<div>
<canvas id='drawing' style="border:1px solid black;" width="640px" height="480px">
</canvas>
</div>
//canvas加载图片
loadImg() {
let img = document.getElementById('imgs');
this.mycanvas = document.getElementById('drawing');//获取dom
let ctx = this.mycanvas.getContext('2d');
img.crossOrigin = '';//配置canvas跨域
img.onload = function() {
ctx.drawImage(img, 0, 0);
console.log(ctx.getImageData(0, 0, img.width, img.height));
};
img.src = 'http://192.168.79.140:80/testN/aaaa/demo.jpg';
},
// 获取图片的像素点数据
getImgPixData() {
let img = document.getElementById('imgs');
var context = this.mycanvas.getContext('2d');
let imageData = context.getImageData(0, 0, img.width, img.height);//获取图像数据
let data = imageData.data;
this.changeImage(context, imageData);
},
//改变图的颜色
changeImage(context, imageData) {
this.times = this.times + 1;
let jtem = 0;
for (let j = 0; j < 10000000; j++) {
jtem = j + jtem;
}
for (let i = 0; i < imageData.data.length; i += 4) {
imageData.data[i + 0] = imageData.data[i + 0] - 2 * this.times;
imageData.data[i + 1] = 255 - imageData.data[i + 1] - 5 * this.times;
imageData.data[i + 2] = imageData.data[i + 2] - 5 * this.times;
// imageData.data[i + 3] = 255;
}
context.putImageData(imageData, 0, 0);//将图像数据画进canvas中
}
canvas返回的图像数据,存在着四方面的信息,即 RGBA 值:
- R - 红色 (0-255)
- G - 绿色 (0-255)
- B - 蓝色 (0-255)
- A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的)
效果展示
本文来自<JSC智数前端团队> 董小胖和中国式苗子