Search code examples
javascriptphaser-frameworktween

How to animate a line scaling in Phaser 3 with a tween?


Imagine a very simple scene:

  • canvas of 800x640
  • red circle in the center
  • two blue lines in a 9:00 position
    private create(): void {
        const circ = this.add.circle(
            400, 320, 200, 0xff0000
        );

        const l1 = this.add.line(
            0, 0, 400, 320, 400, 100, 0x0000ff
        ).setOrigin(0);

        const l2 = this.add.line(
            0, 0, 400, 320, 200, 320, 0x0000ff
        ).setOrigin(0);
    }

So far so good.

Now I want to scale this simple figure, same config 1.5x its current size:

    private create(): void {
        const circ = this.add.circle(
            400, 320, 200, 0xff0000
        );

        const l1 = this.add.line(
            0, 0, 400, 320, 400, 100, 0x0000ff
        ).setOrigin(0);

        const l2 = this.add.line(
            0, 0, 400, 320, 200, 320, 0x0000ff
        ).setOrigin(0);

        this.tweens.add({
            targets: [circ, l1, l2],
            scale: 1.5,
            yoyo: false,
            duration: 2000,
            ease: 'Sine.easeInOut'
        });
    }

Expected behavior:

  • the circle expands from the center
  • the lines expand as well, ideally where they meet

Actual behavior:

As things stand, only #1 fit my expectations. The lines, however, translate as opposed to merely scaling. And the translation seems affected by the scale parameter passed to tweens.add. What gives? What am I missing here?

Given the various configurations for "origin" wrt lines in Phaser 3, the worst I was expecting was that the lines would emanate/grow differently than the circle (which emanates from the center/origin). But I definitely expect the lines to stay still/keep their intersection at the circle's center.

Can you explain what exactly Phaser is doing here and what might I do to get my desired effect?


Solution

  • There are a bunch of things that might be unintuitive with the Line GameObject in Phaser 3, all contributing to this behavior. Let's break them down.

    Lines and Origins

    First of all, make the distinction between line as a geometric entity and line as a Phaser 3 Game Object. They are related but the confusion stems from the subtleties.

    For the Phaser 3 line Game Object, there are three ordered pairs p (x, y) you need to keep in mind:

    • P (X, Y), the origin of the line, relative to the game world (and we are going to define the term in a while); in the game world, (0, 0) is at the upper-left corner.
    • P1 (X1, Y1), the first point that defines a line, relative to P
    • P2 (X2, Y2), the second point that defines a line, relative to P

    Another important thing is that the game world is basically a Cartesian coordinate system flipped around the x-axis. So negative x values still go to the left but negative y values go up and positive y values go down.

    Now let's define the origin, which I will just paraphrase from the official docs:

    An object's origin is a normalized value in the range [0, 1].

    For the x-axis, 0 means the left of the Game Object and 1 means the right.

    For the y-axis, 0 means the top of the Game Object and 1 means the bottom.

    By default, the origin is set at 0.5, the center of the object.

    Interpreting the example and the tween

    Let's take the horizontal line l2 in the example and figure out how it's being drawn.

    const l2 = this.add.line(
        0, 0, 400, 320, 200, 320, 0x0000ff
    ).setOrigin(0);
    
    • The origin is to the left of the game object.
    • With respect to the game world, this origin lies at 0, 0 or the upper-left corner.
    • The line is defined by two points relative to the origin, (400, 320) and (200, 320).

    The important observation here is that the origin is completely outside the geometric line defined. In effect, there is a considerable invisible space in the rectangular area between your origin (at (0, 0)), left of the game object and the right, lowermost corner (at (400, 320)).

    Now, when the tween is invoked, it actually scales the game object rather than just the geometric line. And since most of the game object is "invisible" (the geometric line being the only part visible), the effect is as if the geometric line is translating!

    Solution

    To get the desired effect, constrain the game object to exactly the area taken up by the geometric line. One such way to do this would be:

    • since we want the lines to "radiate" in the same way as the circle, set P at (400, 320), the center of the circle. Additionally, set P1 to (0, 0)---remember this is relative to P, so in the game world, we just made P = P1.
    • let's leave the lines' origins at 0 but now we have to redefine their P2 values to (0, -100) and (0, -200) respectively.

    Putting it all together,

        private create(): void {
            const circ = this.add.circle(
                400, 320, 200, 0xff0000
            );
    
            const l1 = this.add.line(
                400, 320, 0, 0, 0, -100, 0x0000ff
            ).setOrigin(0);
    
            const l2 = this.add.line(
                400, 320, 0, 0, -200, 0, 0x0000ff
            ).setOrigin(0);
    
            this.tweens.add({
                targets: [circ, l1, l2],
                scale: 1.5,
                yoyo: false,
                duration: 2000,
                ease: 'Sine.easeInOut'
            });
        }