Fabric.js画布上的多个剪切区域

20

制作照片拼贴制造商时,我使用了具有基于对象的剪切功能的织物js。这个功能非常好,但是剪切区域内的图像无法缩放、移动或旋转。我想要一个固定位置的剪切区域,用户可以将图像放置在固定的剪切区域内。

我谷歌搜索并找到非常接近的解决方案。

var canvas = new fabric.Canvas('c');
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.rect(10,10,150,150);
ctx.rect(180,10,200,200);
ctx.closePath();
ctx.stroke();
ctx.clip();

fabric js画布上的多个裁剪区域

其中一个裁剪区域的图像出现在另一个裁剪区域中。我应该如何避免这种情况,或者是否有其他方法可以使用fabric js完成此操作。


1
如果没有其他答案,并且当然假设它对您有用,我会很感激您选择我的答案。 - natchiketa
@ep4f 这正是我想要的,你是用下面的答案解决了吗?如果是,当你将图像放大时,你是如何停止裁剪区域和位置的变化的?我该如何使裁剪区域始终保持那个大小和位置? - odd_duck
如果有人注意到Fabric.js中剪切到一组对象的问题,那么也应该看看这个答案。链接 - Vito Gentile
7个回答

32
使用Fabric的clipTo属性可以实现此目的,但您必须在clipTo函数中'反转'变换(缩放和旋转)。
当您在Fabric中使用clipTo 属性时,缩放和旋转是在裁剪之后应用的,这意味着裁剪会随图像一起缩放和旋转。您必须通过在clipTo 属性函数中应用确切的反向变换来抵消这种影响。
我的解决方案涉及使用Fabric.Rect作为剪辑区域的“占位符”(这样做有优势,因为您可以使用Fabric移动对象,从而移动剪辑区域)。
请注意,我的解决方案使用Lo-Dash实用程序库,特别是_.bind()(请参阅上下文的代码)。

示例Fiddle


分解

1. 初始化Fabric

首先,我们需要一个画布:

var canvas = new fabric.Canvas('c');

2. 剪辑区域

var clipRect1 = new fabric.Rect({
    originX: 'left',
    originY: 'top',
    left: 180,
    top: 10,
    width: 200,
    height: 200,
    fill: 'none',
    stroke: 'black',
    strokeWidth: 2,
    selectable: false
});

我们给这些Rect对象一个名为clipFor的属性,以便clipTo函数可以通过该属性找到它们想要被裁剪的那个对象:

clipRect1.set({
    clipFor: 'pug'
});
canvas.add(clipRect1);

剪辑区域不一定需要实际对象存在,但这样做可以更容易管理,因为您可以使用Fabric来移动它。

3. 剪切函数

我们单独定义将用于图像的clipTo属性的函数,以避免代码重复:

由于图像对象的angle属性以度数存储,我们将使用它来将其转换为弧度。

function degToRad(degrees) {
    return degrees * (Math.PI / 180);
}

findByClipName() 是一个方便函数,使用 Lo-Dash 来查找要裁剪的图像对象的 clipFor 属性(例如在下面的图像中,name 将是 'pug'):

function findByClipName(name) {
    return _(canvas.getObjects()).where({
            clipFor: name
        }).first()
}

这是负责工作的部分:

var clipByName = function (ctx) {
    var clipRect = findByClipName(this.clipName);
    var scaleXTo1 = (1 / this.scaleX);
    var scaleYTo1 = (1 / this.scaleY);
    ctx.save();
    ctx.translate(0,0);
    ctx.rotate(degToRad(this.angle * -1));
    ctx.scale(scaleXTo1, scaleYTo1);
    ctx.beginPath();
    ctx.rect(
        clipRect.left - this.left,
        clipRect.top - this.top,
        clipRect.width,
        clipRect.height
    );
    ctx.closePath();
    ctx.restore();
}

注意: 请见下文,了解上述函数中this的使用说明。

4. 使用clipByName()功能的fabric.Image对象

最终,可以通过以下方式实例化图像,并使其使用clipByName函数:

var pugImg = new Image();
pugImg.onload = function (img) {    
    var pug = new fabric.Image(pugImg, {
        angle: 45,
        width: 500,
        height: 500,
        left: 230,
        top: 170,
        scaleX: 0.3,
        scaleY: 0.3,
        clipName: 'pug',
        clipTo: function(ctx) { 
            return _.bind(clipByName, pug)(ctx) 
        }
    });
    canvas.add(pug);
};
pugImg.src = 'https://fabricjs.com/lib/pug.jpg';

_.bind()是什么?

请注意,引用被包装在_.bind()函数中。

我使用_.bind()有以下两个原因:

  1. 我们需要传递一个Image对象的引用给clipByName()
  2. clipTo属性传递的是画布上下文,而不是对象。

基本上,_.bind()让你创建一个版本的函数,该函数使用你指定的对象作为this上下文。

来源
  1. https://lodash.com/docs#bind
  2. https://fabricjs.com/docs/fabric.Object.html#clipTo
  3. https://html5.litten.com/understanding-save-and-restore-for-the-canvas-context/

2
JSFiddle需要更新(因为fabricjs的位置已更改):http://jsfiddle.net/32Acb/ - basarat
1
@natchiketa 这正是我想要的,除了一个问题 - 我可以问一下为什么实际图像和剪切区域与黑色矩形不匹配吗?当你将图像放大时,为什么剪切区域和位置会改变?如何使剪切区域始终保持那个大小和位置? - odd_duck
Fabric有一个utils类,其中包含一个degreesToRadians函数。 fabric.util.degreesToRadians(value) - Prescol
类型错误:_(canvas.getObjects()).where不是一个函数。(在“_(canvas.getObjects()).where({ clipFor: name })”中,“_(canvas.getObjects()).where”未定义) - Glen Elkins

11

我根据 @natchiketa 的解决方案进行了微调,因为夹子区域的定位不正确,在旋转时表现异常。但是现在一切似乎都很好。请查看这个修改后的 fiddle: https://jsfiddle.net/PromInc/ZxYCP/

唯一真正的更改发生在由 @natchiketa 提供的代码中第三步的 clipByName 函数中。 这是更新后的函数:

var clipByName = function (ctx) {
    this.setCoords();

    var clipRect = findByClipName(this.clipName);

    var scaleXTo1 = (1 / this.scaleX);
    var scaleYTo1 = (1 / this.scaleY);
    ctx.save();

    var ctxLeft = -( this.width / 2 ) + clipRect.strokeWidth;
    var ctxTop = -( this.height / 2 ) + clipRect.strokeWidth;
    var ctxWidth = clipRect.width - clipRect.strokeWidth + 1;
    var ctxHeight = clipRect.height - clipRect.strokeWidth + 1;

    ctx.translate( ctxLeft, ctxTop );

    ctx.rotate(degToRad(this.angle * -1));
    ctx.scale(scaleXTo1, scaleYTo1);
    ctx.beginPath();

    ctx.rect(
        clipRect.left - this.oCoords.tl.x,
        clipRect.top - this.oCoords.tl.y,
        ctxWidth,
        ctxHeight
    );
    ctx.closePath();
    ctx.restore();
}

我发现了两个小问题:

  1. 给剪切对象添加描边似乎会使事物偏移几个像素。我尝试通过调整位置来补偿,但旋转后,它会在底部和右侧添加2个像素。因此,我选择完全将其删除。
  2. 有时当您旋转图像时,它会在剪辑的随机一侧留下1像素的间距。

1
注意 - 我发现我的原始Fiddle在FireFox中无法正常工作。我能够通过使用FabricJS 1.4.8而不是1.4.0来解决这个问题。似乎存在某些与FF和crossOrigin图像的错误/冲突。 https://github.com/kangax/fabric.js/issues/903我已经更新了上面帖子中的Fiddle链接。 - PromInc
请注意,需要保持图像的纵横比。如果不这样做,将会产生一些奇怪的效果。这可能是转换矩阵的某些问题。 - l00k
在我的情况下,裁剪区域没有采用矩形给出的角度。prntscr.com/8gw2qi 裁剪区域总是保持直线。它应该完全适合此框架部分,但是它被90度剪辑。有什么想法吗? - devesh singhal

8

更新@Promlnc的答案。

为了进行正确的剪裁,您需要更改上下文转换的顺序。

  1. 平移
  2. 缩放
  3. 旋转

否则,当您在不保持纵横比(仅更改一个维度)的情况下进行缩放时,将会得到错误剪裁的对象。

bad

代码 (69-72):

ctx.translate( ctxLeft, ctxTop );

ctx.rotate(degToRad(this.angle * -1));
ctx.scale(scaleXTo1, scaleYTo1);

替换为:

ctx.translate( ctxLeft, ctxTop );
ctx.scale(scaleXTo1, scaleYTo1);
ctx.rotate(degToRad(this.angle * -1));

看这个: https://jsfiddle.net/ZxYCP/185/

正确的结果:

good

更新 1:

我已经开发了一个可以通过多边形剪切的功能: https://jsfiddle.net/ZxYCP/198/

poly


在我的情况下,剪辑区域没有采用矩形给定的角度。剪辑区域总是保持直线状态。它应该完全适合这个框架部分,但是它被90度剪辑了。有什么想法吗? - devesh singhal
@deveshsinghal 提出新问题。我会回答你的。这不是这个问题的主题。 - l00k
好的 @Luk,但我在https://dev59.com/m47ea4cB1Zd3GeqPIfsE找到了我的问题的解决方案。 - devesh singhal
1
很抱歉,我已经开始编写我的解决方案了 :P 不过你可以试一下,或许它会在某种程度上对你有所帮助。 - l00k
我还有一个问题要问你。;) :P 请看一下,如果您能帮忙。 http://stackoverflow.com/questions/32626889/multiple-object-in-a-clipping-section-in-fabric-js-canvas - devesh singhal

4
这可以更轻松地完成。 Fabric提供了渲染方法,通过另一个对象的上下文进行裁剪。
请查看这个代码片段。我在这里的评论中看到过这个。
obj.clipTo = function(ctx) {
    ctx.save();
    ctx.setTransform(1, 0, 0, 1, 0, 0);

    clippingRect.render(ctx);

    ctx.restore();
};

3

当我测试上面所有的fiddles时,它们都有一个bug。问题出现在当您同时翻转X和Y值时,剪辑边界将会出错。此外,为了不进行所有计算以将图像放置在正确的位置,您需要为它们指定originX='center'originY='center'

这是一个剪辑函数更新自 @natchiketa 的原始代码:

var clipByName = function (ctx) {
    var clipRect = findByClipName(this.clipName);
    var scaleXTo1 = (1 / this.scaleX);
    var scaleYTo1 = (1 / this.scaleY);
    ctx.save();
    ctx.translate(0,0);
        
    //logic for correct scaling
    if (this.getFlipY() && !this.getFlipX()){
        ctx.scale(scaleXTo1, -scaleYTo1);
    } else if (this.getFlipX() && !this.getFlipY()){
        ctx.scale(-scaleXTo1, scaleYTo1);
    } else if (this.getFlipX() && this.getFlipY()){
        ctx.scale(-scaleXTo1, -scaleYTo1);
    } else {
        ctx.scale(scaleXTo1, scaleYTo1);
    }

    //IMPORTANT!!! do rotation after scaling
    ctx.rotate(degToRad(this.angle * -1));
    ctx.beginPath();
    ctx.rect(
        clipRect.left - this.left,
        clipRect.top - this.top,
        clipRect.width,
        clipRect.height
    );
    ctx.closePath();
    ctx.restore();
}

请检查更新后的 fiddle

这个工作得很好,我的物体有原点中心,之前我没有使用原点,现在我在动态计算顶部和左侧时有问题。 - phpnerd

1
随着 fabric 1.6.0-rc.1 的最新更新,您可以通过按住 shift 键并拖动中轴来扭曲图像。
我不知道如何将扭曲反转以使剪裁区域保持不变。我尝试了以下代码来尝试将其反转回来,但没有成功。
var skewXReverse = - this.skewX;
var skewYReverse = - this.skewY;

ctx.translate( ctxLeft, ctxTop );
ctx.scale(scaleXTo1, scaleYTo1);
ctx.transform(1, skewXReverse, skewYReverse, 1, 0, 0);
ctx.rotate(degToRad(this.angle * -1));

演示:https://jsfiddle.net/uimos/bntepzLL/5/


-1

更新之前的答案。

ctx.rect(
    clipRect.oCoords.tl.x - this.oCoords.tl.x - clipRect.strokeWidth,
    clipRect.oCoords.tl.y - this.oCoords.tl.y - clipRect.strokeWidth,
    clipRect.oCoords.tr.x - clipRect.oCoords.tl.x,
    clipRect.oCoords.bl.y - clipRect.oCoords.tl.y
);

现在我们毫无疑问地能够缩放剪辑区域。


1
如果你要更新别人的答案,请直接更新它们,而不是发布另一个答案。 - SOFe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接