From 82c385eb6259f29a31f533e16ed61b67dc2c76ec Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Fri, 7 Apr 2017 19:43:47 -0500 Subject: [PATCH] [#770] Fix drawings on rotated and scaled actors Closes #770 ## Changes: - Update the order of the affine transformations, if you scale before rotating the scale will not be applied along the new rotated coordinates. Rotations must occur before scaling so that the x,y scale are applied in the correct direction. Unfortunately this was a very subtle bug thanks to @HSAR for bringing this to our attention - Added visual test to the sandbox/tests/scale - Added ScaleSpec to our test suite to do image diff testing --- sandbox/index.html | 1 + sandbox/tests/scale/logo.png | Bin 0 -> 2745 bytes sandbox/tests/scale/scale.html | 13 +++++++ sandbox/tests/scale/scale.ts | 39 +++++++++++++++++++ src/engine/Actor.ts | 2 +- src/spec/ScaleSpec.ts | 56 ++++++++++++++++++++++++++++ src/spec/images/ScaleSpec/logo.png | Bin 0 -> 2745 bytes src/spec/images/ScaleSpec/scale.png | Bin 0 -> 4482 bytes src/spec/logo.png | Bin 0 -> 2745 bytes 9 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 sandbox/tests/scale/logo.png create mode 100644 sandbox/tests/scale/scale.html create mode 100644 sandbox/tests/scale/scale.ts create mode 100644 src/spec/ScaleSpec.ts create mode 100644 src/spec/images/ScaleSpec/logo.png create mode 100644 src/spec/images/ScaleSpec/scale.png create mode 100644 src/spec/logo.png diff --git a/sandbox/index.html b/sandbox/index.html index a7b5a95b..8852c3bb 100644 --- a/sandbox/index.html +++ b/sandbox/index.html @@ -27,6 +27,7 @@
  • Label - Fonts
  • Collision
  • Rotation
  • +
  • Scale
  • Sprite Font
  • Label Fonts
  • Anchors
  • diff --git a/sandbox/tests/scale/logo.png b/sandbox/tests/scale/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..aada9f8e5527d4c27adc31c2c6b99d169a1b8b35 GIT binary patch literal 2745 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 + + + + + + Document + + + + + + \ No newline at end of file diff --git a/sandbox/tests/scale/scale.ts b/sandbox/tests/scale/scale.ts new file mode 100644 index 00000000..2709e03e --- /dev/null +++ b/sandbox/tests/scale/scale.ts @@ -0,0 +1,39 @@ +/// + +var game = new ex.Engine({ + width: 800, + height: 600 +}); + + +// create an asset loader +var loader = new ex.Loader(); +var resources = { + txPlayer: new ex.Texture('logo.png') +}; + +// queue resources for loading +for (var r in resources) { + loader.addResource(resources[r]); +} + +var target = new ex.Actor(500, 500, 20, 20, ex.Color.Red); +game.add(target); + +var aim = new ex.Actor(game.getDrawWidth() / 2 , game.getDrawHeight() / 2, 100, 100, ex.Color.Black); +aim.addDrawing(resources.txPlayer); +aim.setHeight(10); +aim.scale.setTo(1, .2); +game.add(aim); + +// uncomment loader after adding resources +game.start(loader).then(() => { + + game.input.pointers.primary.on('move', (ev: ex.Input.PointerEvent) => { + target.pos.setTo(ev.x, ev.y); + + var aimVec = target.pos.sub(aim.pos); + aim.rotation = aimVec.toAngle(); + }); + +}); \ No newline at end of file diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index f2986005..fa085c79 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -981,8 +981,8 @@ export class Actor extends Class implements IActionable, IEvented { public draw(ctx: CanvasRenderingContext2D, delta: number) { ctx.save(); ctx.translate(this.pos.x, this.pos.y); - ctx.scale(this.scale.x, this.scale.y); ctx.rotate(this.rotation); + ctx.scale(this.scale.x, this.scale.y); // translate canvas by anchor offset ctx.save(); diff --git a/src/spec/ScaleSpec.ts b/src/spec/ScaleSpec.ts new file mode 100644 index 00000000..ab5eb5bd --- /dev/null +++ b/src/spec/ScaleSpec.ts @@ -0,0 +1,56 @@ +/// +/// +/// +/// +/// + +describe('A scaled and rotated actor', () => { + var actor: ex.Actor; + var engine: ex.Engine; + var scene: ex.Scene; + var mock = new Mocks.Mocker(); + + beforeEach(() => { + jasmine.addMatchers(imagediff.jasmine); + + actor = new ex.UIActor(50, 50, 100, 50); + actor.color = ex.Color.Blue; + actor.collisionType = ex.CollisionType.Active; + engine = TestUtils.engine({ width: 800, height: 600 }); + engine.setAntialiasing(false); + + scene = new ex.Scene(engine); + engine.currentScene = scene; + + spyOn(scene, 'draw').and.callThrough(); + spyOn(actor, 'draw').and.callThrough(); + + }); + + afterEach(() => { + engine.stop(); + }); + + + it('is drawn correctly scaled at 90 degrees', (done) => { + + let bg = new ex.Texture('src/spec/images/ScaleSpec/logo.png', true); + + engine.start(new ex.Loader([bg])).then(() => { + let actor = new ex.Actor(engine.getDrawWidth() / 2 , engine.getDrawHeight() / 2, 100, 100, ex.Color.Black); + actor.addDrawing(bg); + actor.setHeight(10); + actor.scale.setTo(1, .2); + engine.add(actor); + + actor.rotation = Math.PI / 2; + + actor.on('postdraw', (ev: ex.PostDrawEvent) => { + engine.stop(); + imagediff.expectCanvasImageMatches('ScaleSpec/scale.png', engine.canvas, done); + }); + }); + + }); + +}); \ No newline at end of file diff --git a/src/spec/images/ScaleSpec/logo.png b/src/spec/images/ScaleSpec/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..aada9f8e5527d4c27adc31c2c6b99d169a1b8b35 GIT binary patch literal 2745 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 Pfq}u()z4*}Q$iB}gK2Y0 literal 0 HcmV?d00001 diff --git a/src/spec/logo.png b/src/spec/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..aada9f8e5527d4c27adc31c2c6b99d169a1b8b35 GIT binary patch literal 2745 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