<canvas> 元素中的动画剪辑区域

Posted

技术标签:

【中文标题】<canvas> 元素中的动画剪辑区域【英文标题】:Animating clipping area in <canvas> element 【发布时间】:2012-01-31 22:40:47 【问题描述】:

几个月来,我一直使用 ***.com 作为灵感和解决问题的来源。到目前为止,我从来没有遇到过没有解决方案的问题,这就是为什么我首先要介绍一下自己,并与所有感兴趣的人分享我的问题。

在过去的几周里,我尝试在画布元素上为某些形状和线条设置动画,以创建一些有趣的效果 - 例如手写或类似效果。 为了实现这一点,我使用了一些利用 canvas 元素的 .clip() 命令的技术,来隐藏和逐渐显示预渲染图像(表单、线条......)“等待”的区域。 我在这里遇到的问题与决定画布元素中剪切区域的变量有关。增加(但不减少)动画中的值似乎有一些奇怪的问题。 由于这一切听起来很奇怪,我知道,这里是我正在谈论的代码的相关部分。

$(document).ready(function() 
    var ctx = $( "#canvas" )[0].getContext("2d");
    ctx.fillStyle = "#a00";
    var recW = 200;

    function animate() 
        ctx.clearRect(0,0,400,400);

        ctx.beginPath();
        ctx.rect(50,50,recW,100);
        ctx.clip();

        ctx.beginPath();
        ctx.arc(250,100,90,0,Math.PI*2,true);
        ctx.fill();

        recW--;

        if (recW == 150) clearInterval(run);
    
    var run = setInterval(function()  animate(); ,60);
);

上面的代码工作得很好。它在 400*400 的画布中绘制一个矩形,将其用作剪切区域,然后绘制圆形,然后对这个圆形进行相应的剪切。通过动画间隔,剪裁矩形的长度减少到测试值 150。到目前为止,一切都很好。但让我连续数小时百思不得其解的部分来了:

$(document).ready(function() 
    var ctx = $( "#canvas" )[0].getContext("2d");
    ctx.fillStyle = "#a00";
    var recW = 150;

    function animate() 
        ctx.clearRect(0,0,400,400);

        ctx.beginPath();
        ctx.rect(50,50,recW,100);
        ctx.clip();

        ctx.beginPath();
        ctx.arc(250,100,90,0,Math.PI*2,true);
        ctx.fill();

        recW++;

        if (recW == 200) clearInterval(run);
    
    var run = setInterval(function()  animate(); ,60);
);

如果我翻转整个动画,从剪裁矩形的宽度 150 开始,然后使用 recW++ 将其增加到测试值 200,突然动画不再起作用。变量的逐渐增加没有问题,但可见剪切区域没有增加。 我怀疑我可能只是在这里忽略了明显的问题,但我似乎根本找不到错误,如果有人能指出我正确的方向,我将非常感激 ;)

非常感谢 特里康

【问题讨论】:

在线发布您的完整代码是一个很好的起点。转到jsfiddle 并输入代码,以便我们可以更好地运行它并使用它 不用说,一旦你把它放在那里,给我们一个链接到你的代码的 sn-p =) 解决方案见下文。此外,在回复 cmets 时,请在用户名后面加上与号 @。在这种情况下,您回复的第一行应该是@puk。例外情况是当您回复提出问题/答案的实际人员时,您不需要这样做 @puk 当然,为什么我一开始就没有想到这一点;)这将是整个代码 - html 和 CSS 部分非常枯燥,因为这仅仅是一个实验一个更大的项目。 (jsfiddle.net/tricon/TdVKe/4) 已经感谢您对此进行调查;) 【参考方案1】:

除非您有很多经验(我也没有),否则您的问题很难调试。

您可以使形状变小而不是变大的原因是因为,我怀疑您正在将剪辑组合在一起。因此,随着它们变小,一切看起来都很好,因为您期望剪辑区域最小。但是,当剪辑变大时,您正在与原始的小剪辑区域进行“与”运算,因此看起来好像没有动画。

要解决此问题,您需要在剪辑结尾处调用restore()。但是,要使其正常工作,您还需要在剪辑开头调用save()。最后,我添加了一个边框来指示剪辑的确切位置,由于这是一个填充和一个描边,我放置了另一个 beginPath 语句以不描边剪辑区域之外的圆圈(我们刚刚从中恢复)。

这是完整的jsFiddle code

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

ctx.fillStyle = "#a00";
var recW = 150;

function animate2() 
    ctx.clearRect(50,50,canvas.width,recW - 1);

    ctx.save();

    ctx.beginPath();
    ctx.rect(50, 50, recW, recW);
    ctx.clip();

    ctx.beginPath();
    ctx.arc(250,100,90,0,Math.PI*2,true);
    ctx.fill();

    ctx.restore();
    ctx.beginPath();
    ctx.rect(50 - 1, 50 - 1, recW + 2, recW + 2);
    ctx.lineWidth = 10;
    ctx.stroke();
    console.log(recW);
    recW++;

    if (recW == 300) clearInterval(run);

var run = setInterval(function()  animate2(); ,5);

【讨论】:

非常感谢您的快速回复。我目前正在检查代码、save() 和 restore() 命令,因为我之前没有积极使用过它们;) @tricon 我已经在 Canvas 中编写了 50,000 多行代码,即使是最简单的事情,我仍然需要用谷歌搜索(即是 ctx.lineWidth 还是 ctx.linewidth 还是 ctx.lw... ) @tricon 除了clipglobalCompositeOperation 也很酷。 See here 我相信我明白了。我读过的文档(好吧,无论如何,大部分文档)都没有解释,剪裁操作保存在绘图状态堆栈中(我个人觉得有点……烦人;))。这就解释了为什么剪辑区域的增加没有被动画正确地转换,因为不断增长的剪辑区域总是受到其最小部分的限制(这有意义吗?^^)。因此,保存完全空的画布(即还没有剪辑操作),并在绘制下一帧之前恢复它是唯一的解决方案 - 我希望我做对了;D @tricon 是的,这就是我所说的 ANDing。我想这与剪辑和 beginPath 以及一大堆其他操作的设计使它们可以组合在一起这一事实有关。合并剪辑是否是一个好主意值得商榷,我想不出最好做2个剪辑的情况,但也许一些复杂的剪辑不能单独使用路径。【参考方案2】:

这是一个很好的指南,告诉你如何animate a clipping path. 你的问题是,一旦剪辑完成,你必须把它拿走才能让它生长,所以你使用保存状态和画布擦除来产生这种效果。

这是一些示例代码。

<canvas  id="slide29Canvas2"  ></canvas>

<script>
    // Grabs the canvas element we made above
var ca1=document.getElementById("slide29Canvas1");

// Defines the 2d thing, standard for making a canvas
var c1=ca1.getContext("2d");

// Creates an image variable to hold and preload our image (can't do animations on an image unless its fully loaded)
var img1 = document.createElement('IMG');

// Loads image link into the img element we created above
img1.src = "http://tajvirani.com/wp-content/uploads/2012/03/slide29-bg_1.png";

// Creates the first save event, this gives us a base to clear our clipping / mask to since you can't just delete elements.
c1.save();

// Our function for when the image loads
img1.onload = function () 

    // First call to our canvas drawing function, the thing that is going to do all the work for us.
        // You can just call the function but I did it through a timer
    setTimeout(function()  drawc1r(0); ,5);

        // The function that is doing all the heavy lifting. The reason we are doing a function is because
        // to make an animation we have to draw the circle (or element) frame by frame, to do this manually would be to time
        // intensive so we are just going to create a loop to do it. 'i' stands for the radius of our border
        // so over time our radius is going to get bigger and bigger.
    function drawc1r(i) 

        // Creates a save state. Imagine a save state like an array, when you clear one it pops last element in the array off the stack
        // When you save, it creates an element at the top of the stack. So if we cleared without making new ones, we would end up with nothing on our stage.
    c1.save();

        // This clears everything off the stage, I do this because our image has transparency, and restore() (the thing that pops one off the stack)
        // Doesn't clear off images, and so if we stick an image on the stage over and over, the transparency will stack on top of each other and
        // That isn't quite what we want.
    c1.clearRect(0, 0, ca1.width, ca1.height);

        // Adds one to the radius making the circle a little bigger with every step
    i++;

        // Tells canvas we are going to start creating an item on the stage - it can be a line, a rectangle or any shape you draw, but whatever
        // after this path will be added to the clip when its called. I can have 3 rectangles drawn and that would make a clip.
    c1.beginPath();

        // Can't make a true circle, so we make an arced line that happens to trace a circle - 'i' is used to define our radius.
    c1.arc(853, 320, i, 0, 2 * Math.PI, false);

        // After everything is defined, we make a clip area out of it.
    c1.clip();

        // Now that we have the clip added to it, we are going to add the image to the clip area.
    c1.drawImage(img1, 0, 0);

        // This pops one off the stack which gets rid of the clip so we can enlarge it and make it again on the next pass
    c1.restore();

        // Here is the final size of the circle, I want it to grow the circle until it hits 800 so we set a timeout to run this function again
        // until we get the size we want. The time in milliseconds pretty much defines your framerate for growing the circle. There are other
        // methods for doing this that give you better frame rates, but I didn't have much luck with them so I'm not going to include them.
    if(i < 800) 
        setTimeout(function()  drawc1r(i); ,5);
    


【讨论】:

以上是关于<canvas> 元素中的动画剪辑区域的主要内容,如果未能解决你的问题,请参考以下文章

html5 canvas - 合并两个剪辑区域 - James Bond Gunbarrel

HTML Canvas 剪辑区域 - 上下文恢复?

JS之使用Canvas绘图

canvas

Canvas绘图基本用法

canvas和svg小记