遇到一个需求是,给fabric的图片加一个相框,而且支持拖拽变换。查了下没查到,自己想了个蠢方法:
- 图片对象在canvas里可以拿到位置,在
aCroods
里有tl
,tr
,bl
,br
代表四角坐标,根据这个可以知道四个边框作为四个Rect
的长宽,这个先直接框住图片。 - 但是在四边会有四个正方形的空白,相框都是要由斜角组成的,所以这里要给四个角画8个三角形,并根据图片坐标定位。
- 所有这些都需要在同一个选择集里面,否则无法一起跟随变形,用group组成一个新的整体对象,然后将group加回canvas
/**
* 将边框添加到当前活动对象
*/
addFrameToActive(url) {
url = loadFromRemote(url);
var us = [
"https://img.alicdn.com/imgextra/i1/O1CN01g0WBpQ1rpwhu9wD8U_!!6000000005681-2-tps-104-104.png",
"https://img.alicdn.com/imgextra/i4/O1CN01innORS1WrDsaz9ElQ_!!6000000002841-2-tps-105-105.png",
"https://img.alicdn.com/imgextra/i3/O1CN01MxntTp1WGaC8FV5kR_!!6000000002761-2-tps-130-130.png",
]
url = us[Math.floor(Math.random() * 3)];
let object = this.getActiveObject()
if (!object) {
console.log('no active object detected.'); return;
}
fabric.util.loadImage(url, (img) => {
if (img.width !== img.height) {
console.error(`this frame\'s texture is not square, please connect with administrator. url info: [${url}]`);
return;
}
let frameParts, originObj = object;
if (object.isFrame) {
[object, ...frameParts] = object.getObjects();
}
// width of a frame
let w = 20;
// t b l r : triangle
/**
* x1,y1 ------- x2,y2
* | |
* | |
* | |
* x3,y3 ------- x4,y4
*/
let {
tl: { x: x1, y: y1 },
tr: { x: x2, y: y2 },
bl: { x: x3, y: y3 },
br: { x: x4, y: y4 }
} = object.aCoords
let bar = [
// M L L top, left
[x1, y1, x1 - w, y1 - w, x1, y1 - w, x1 - w, y1 - w], // t
[x2, y2, x2 + w, y2 - w, x2, y2 - w, x2, y2 - w], // t
[x3, y3, x3 - w, y3 + w, x3, y3 + w, x3 - w, y3], // b
[x4, y4, x4 + w, y4 + w, x4, y4 + w, x4, y4], // b
[x1, y1, x1 - w, y1 - w, x1 - w, y1, x1 - w, y1 - w], // l
[x3, y3, x3 - w, y3 + w, x3 - w, y3, x3 - w, y3], // l
[x2, y2, x2 + w, y2 - w, x2 + w, y2, x2, y2 - w], // r
[x4, y4, x4 + w, y4 + w, x4 + w, y4, x4, y4], // r
]
// t b l r : rect
let foo = [
{
width: object.aCoords.tr.x - object.aCoords.tl.x,
height: w,
top: object.aCoords.tl.y - w,
left: object.aCoords.tl.x,
rotate: 180,
repeat: 'x'
},
{
width: object.aCoords.br.x - object.aCoords.bl.x,
height: w,
top: object.aCoords.bl.y,
left: object.aCoords.bl.x,
rotate: 0,
repeat: 'x'
},
{
width: w,
height: object.aCoords.bl.y - object.aCoords.tl.y,
top: object.aCoords.tl.y,
left: object.aCoords.tl.x - w,
rotate: 90,
repeat: 'y'
},
{
width: w,
height: object.aCoords.br.y - object.aCoords.tr.y,
top: object.aCoords.tr.y,
left: object.aCoords.tr.x,
rotate: 270,
repeat: 'y'
},
].map((i, idx) => ({
...i,
triangles: bar.slice(2*idx, 2*idx+2),
}));
const all = foo
.map(i => {
const o = new fabric.Image(img, {});
o.scaleToHeight(10, true) // 縮小成 height = 50
o.rotate(i.rotate);
const patternSourceCanvas = new fabric.StaticCanvas();
patternSourceCanvas.add(o);
patternSourceCanvas.renderAll();
const pattern = new fabric.Pattern({
source: function () {
patternSourceCanvas.setDimensions({
width: o.getScaledWidth(),
height: o.getScaledHeight(),
});
patternSourceCanvas.renderAll();
// html element
return patternSourceCanvas.getElement();
},
repeat: `repeat-${i.repeat}`,
});
const r = new fabric.Rect({
width: i.width,
height: i.height,
top: i.top,
left: i.left,
fill: pattern,
});
const ts = i.triangles.map(tri => {
const p = new fabric.Path(`M ${tri[0]} ${tri[1]} L ${tri[2]} ${tri[3]} L ${tri[4]} ${tri[5]} Z`)
p.set({
left: tri[6],
top: tri[7],
fill: pattern,
});
return p;
})
return [ r, ...ts ];
})
.reduce((acc, curr) => curr.concat(acc), []);
if (originObj.isFrame) {
originObj.remove(...frameParts);
all.forEach(i => originObj.addWithUpdate(i));
// originObj.render();
this.canvas.setActiveObject(originObj);
this.canvas.renderAll();
} else {
const g = new fabric.Group([
object,
...all, // t, b, l, r, and triangles
]);
g.isFrame = true;
this.canvas.add(g).setActiveObject(g, { add: true });
}
this.updateState();
})
}
另外几个需要考虑的点是:
- 相框添加后如何切换相框,要判断是否是group后者group是否是加了相框的,如果添加相框同时活动对象已经有相框,那执行的是替换而不是再加一层;
- 去掉相框,这个比较简单,拆分group后保留主体
- 相框本身不能成比例缩放,这个还没加入,因为就目前的用例来讲,相框被拖拽的很浮夸的情况不太可能发生;而解决的方案应该是监听拖拽结束事件,当拖拽结束去掉原相框,再重新计算后添加新相框,当然也可以在拖拽实时计算相框本身的改动,只是会无谓的消耗性能。