【问题标题】:Fabric.js - How to animate from a sprite sheetFabric.js - 如何从精灵表制作动画
【发布时间】:2016-11-09 13:22:57
【问题描述】:

JavaScript 语法:

context.drawImage(img,srcX,srcY,srcWidth,srcHeight,x,y,width,height);

在 Javascript 中,如果我想为下面的 spritesheet 设置动画,我只需更新每个动画帧的 srcXsrcY 以捕获图像的片段。

这会导致每一帧被剪辑并单独显示在canvas 上,当以固定帧速率更新时会产生流畅的精灵动画,如下所示:

如何使用“Fabric.js”库来做到这一点?

注意:实现此目的的一种方法是设置canvasSize = frameSize,以便在任何给定时间只能看到一帧。然后通过移动图像,可以在画布内放置不同的帧以模拟动画。但是,这不适用于大画布或可变帧大小。

【问题讨论】:

标签: javascript animation html5-canvas fabricjs sprite-sheet


【解决方案1】:

看看这个,它做同样的事情。

一个行走的人形。 Fabric.js,图像动画。

        var URL = 'http://i.stack.imgur.com/M06El.jpg';
        var canvas = new fabric.Canvas('canvas');
        var positions = {
            topSteps:2,
            leftSteps:4
        };
        canWalk(URL,positions);
        function canWalk(URL,positions){
            var myImage = new Image();
            myImage.src = URL;
    
            myImage.onload = function() {
                var topStep = myImage.naturalHeight/positions.topSteps;
                var leftStep = myImage.naturalWidth/positions.leftSteps;
    
                var docCanvas = document.getElementById('canvas');
                docCanvas.height = topStep;
                docCanvas.width = leftStep;
                fabricImageFromURL(0,0);
                var y = 0;
                var x = 0;
                setInterval(function(){
                    if(x == positions.leftSteps)
                    {
                        x = 0;
                        y++;
                        if(y==positions.topSteps)
                        {
                            y=0;
                        }
                    }
                    fabricImageFromURL(-y*topStep,-x*leftStep);
                    x++;
                },100);
            };
        }
    
	function fabricImageFromURL(top, left)
	{
		console.log(top, left);
		fabric.Image.fromURL(URL, function (oImg) {
			oImg.set('left', left).set('top',top);
			oImg.hasControls = false;
			oImg.hasBorders  = false;
			oImg.selectable  = false;
			canvas.add(oImg);
			canvas.renderAll();
		}, {"left": 0, "top": 0, "scaleX": 1, "scaleY": 1});
	}
<canvas id="canvas"></canvas>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.3/fabric.min.js"></script>

【讨论】:

  • 不错的 hack 方式 ;) 不幸的是,我将使用大画布,所以这种方法对我不起作用。我希望从源代码中剪辑图像。
  • @sookie 我回答了你的问题。没有被告知“剪辑图像”。但原则上并不难。这不是黑客,这是逻辑
  • 我使用srcXsrcY(等)描述的问题是为了从源中剪辑每一帧。我正在寻找使用fabricjs的等效解决方案
【解决方案2】:

默认情况下,Fabric 不会这样做。您需要在 fabric.Image 对象中显示“源”属性并扩展 fabric.Image _render 方法。原来是这样的:

/**
     * @private
     * @param {CanvasRenderingContext2D} ctx Context to render on
     * @param {Boolean} noTransform
     */
    _render: function(ctx, noTransform) {
      var x, y, imageMargins = this._findMargins(), elementToDraw;

      x = (noTransform ? this.left : -this.width / 2);
      y = (noTransform ? this.top : -this.height / 2);

      if (this.meetOrSlice === 'slice') {
        ctx.beginPath();
        ctx.rect(x, y, this.width, this.height);
        ctx.clip();
      }

      if (this.isMoving === false && this.resizeFilters.length && this._needsResize()) {
        this._lastScaleX = this.scaleX;
        this._lastScaleY = this.scaleY;
        elementToDraw = this.applyFilters(null, this.resizeFilters, this._filteredEl || this._originalElement, true);
      }
      else {
        elementToDraw = this._element;
      }
      elementToDraw && ctx.drawImage(elementToDraw,
                                     x + imageMargins.marginX,
                                     y + imageMargins.marginY,
                                     imageMargins.width,
                                     imageMargins.height
                                    );

      this._stroke(ctx);
      this._renderStroke(ctx);
    },

你需要改变它:

fabric.util.object.extend(fabric.Image.prototype, {
    _render: function(ctx, noTransform) {
        // ...
        
        elementToDraw && ctx.drawImage(
            elementToDraw,
            this.source.x,
            this.source.y,
            this.source.width,
            this.source.height,
            x + imageMargins.marginX,
            y + imageMargins.marginY,
            imageMargins.width,
            imageMargins.height
        );
        this._renderStroke(ctx);
    }
});

【讨论】:

    【解决方案3】:

    您可以结合使用设置 clipTo 函数和在循环中调用 setLeft / setTop。在 fabric.Image 构造函数的选项中,您传递属性 clipTo 并告诉fabric 剪切图像的特定部分。然后,使用 setTop / setLeft 在循环内触发重绘,从而调用 clipTo 并同时重新定位剪切图像,使其始终保持在同一个位置。

    我遇到了同样的问题,并将逻辑提取到两个函数中。选项概览:
    spriteWidth - 精灵一帧动画的宽度
    spriteHeight - 精灵一帧动画的高度
    totalWidth - 整个精灵图像的宽度
    totalHeight - 整个精灵图像的高度
    animationFrameDuration - 一个精灵帧应该多长显示
    startRandom - 如果您不想立即开始动画,而是在 1 秒内随机开始
    left - 就像 fabric 的正常左侧选项一样.Image
    top - 就像 fabric.Image 的正常顶部选项

    同步版本(通过 HTMLImageElement):

    /**
     * @param imgObj HTMLImageElement
     * @param options {
     *                  spriteWidth: number
     *                  spriteHeight: number
     *                  totalWidth: number
     *                  totalHeight: number
     *                  animationFrameDuration: number
     *                  startRandom: boolean (optional)
     *                  left: number (optional)
     *                  top: number (optional)
     *                }
     * @returns fabric.Image
     */
    function animateImg(imgObj, options) {
        const left = options.left || 0;
        const top = options.top || 0;
        let x = 0;
        let y = 0;
        const image = new fabric.Image(imgObj, {
            width: options.totalWidth,
            height: options.totalHeight,
            left: left,
            top: top,
            clipTo: ctx => {
                ctx.rect(-x - options.totalWidth / 2, -y - options.totalHeight / 2, options.spriteWidth, options.spriteHeight);
            }
        });
        setTimeout(() => {
            setInterval(() => {
                x = (x - options.spriteWidth) % options.totalWidth;
                if (x === 0) {
                    y = (y - options.spriteHeight) % options.totalHeight;
                }
                image.setLeft(x + left);
                image.setTop(y + top);
            }, options.animationFrameDuration)
        }, options.startRandom ? Math.random() * 1000 : 0);
        return image;
    }
    

    异步版本(传递图片 URL):

    /**
     * @param imgURL string
     * @param options {
     *                  spriteWidth: number
     *                  spriteHeight: number
     *                  totalWidth: number
     *                  totalHeight: number
     *                  animationFrameDuration: number
     *                  startRandom: boolean (optional)
     *                  left: number (optional)
     *                  top: number (optional)
     *                }
     * @param callback (image : fabric.Image) => void
     */
    function animateImgFromURL(imgURL, options, callback) {
        const left = options.left || 0;
        const top = options.top || 0;
        let x = 0;
        let y = 0;
        fabric.Image.fromURL(
            imgURL,
            image => {
                setTimeout(() => {
                    setInterval(() => {
                        x = (x - options.spriteWidth) % options.totalWidth;
                        if (x === 0) {
                            y = (y - options.spriteHeight) % options.totalHeight;
                        }
                        image.setLeft(x);
                        image.setTop(y);
                    }, options.animationFrameDuration)
                }, options.startRandom ? Math.random() * 1000 : 0);
                callback(image);
            }, {
                width: options.totalWidth,
                height: options.totalHeight,
                left: 0,
                top: 0,
                left: left,
                top: top,
                clipTo: ctx => {
                    ctx.rect(-x - options.totalWidth / 2, -y - options.totalHeight / 2, options.spriteWidth, options.spriteHeight);
                }
            });
    

    请注意,上述函数不会重新渲染画布,您必须自己进行。
    您可以像这样使用上面的代码将您的精灵并排设置两次动画(一次同步版本,一次异步版本):

    // Assuming:
    // 1. canvas was created
    // 2. Sprite is in html with id 'walking'
    // 3. Sprite is within folder 'images/walking.jpg'
    const img1 = animateImg(document.getElementById('walking'), {
        spriteWidth: 125,
        spriteHeight: 125,
        totalWidth: 500,
        totalHeight: 250,
        startRandom: true,
        animationFrameDuration: 150,
        left: 125,
        top: 0
    });
    canvas.add(img1);
    animateImgFromURL('images/walking.jpg', {
        spriteWidth: 125,
        spriteHeight: 125,
        totalWidth: 500,
        totalHeight: 250,
        startRandom: true,
        animationFrameDuration: 150
    }, image => canvas.add(image));
    // hacky way of invoking renderAll in a loop:
    setInterval(() => canvas.renderAll(), 10);
    

    【讨论】:

      猜你喜欢
      • 2019-03-10
      • 1970-01-01
      • 2013-11-10
      • 1970-01-01
      • 2013-02-12
      • 2017-06-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多