From 4e3eb2426080997ca4db20a213b861a2ab177a7f Mon Sep 17 00:00:00 2001 From: nickschot Date: Fri, 27 Jan 2023 15:15:03 +0100 Subject: [PATCH] Properly implement parallel timeline sorting & add some tests --- .../tests/unit/models/orchestration-test.ts | 113 ++++++++++++++++++ .../addon/models/orchestration.ts | 69 ++++++++--- 2 files changed, 163 insertions(+), 19 deletions(-) diff --git a/packages/boxel-motion-test-app/tests/unit/models/orchestration-test.ts b/packages/boxel-motion-test-app/tests/unit/models/orchestration-test.ts index 9a8bba1671..aa34bc8c8f 100644 --- a/packages/boxel-motion-test-app/tests/unit/models/orchestration-test.ts +++ b/packages/boxel-motion-test-app/tests/unit/models/orchestration-test.ts @@ -1,4 +1,5 @@ import { FPS } from '@cardstack/boxel-motion/behaviors/base'; +import SpringBehavior from '@cardstack/boxel-motion/behaviors/spring'; import StaticBehavior from '@cardstack/boxel-motion/behaviors/static'; import TweenBehavior from '@cardstack/boxel-motion/behaviors/tween'; import WaitBehavior from '@cardstack/boxel-motion/behaviors/wait'; @@ -322,4 +323,116 @@ module('Unit | Orchestration', function () { ]) ); }); + + module('Unit | Orchestration | sortParallelTimeline', function () { + test('it sorts properly when all animations have durations', function (assert) { + let sprites = new Set([getMockSprite()]); + + let animation1 = { + sprites, + properties: {}, + timing: { + behavior: new TweenBehavior(), + duration: 2000, + }, + }; + let animation2 = { + sprites, + properties: {}, + timing: { + behavior: new TweenBehavior(), + duration: 150, + }, + }; + let animation3 = { + sprites, + properties: {}, + timing: { + behavior: new TweenBehavior(), + duration: 100, + anchor: 'center', + }, + }; + + let sortedAnimations = OrchestrationMatrix.sortParallelTimeline([ + animation2, + animation1, + animation3, + ]); + + assert.deepEqual(sortedAnimations, [animation1, animation2, animation3]); + }); + + test('it sorts properly when there is an undefined duration', function (assert) { + let sprites = new Set([getMockSprite()]); + + let animation1 = { + sprites, + properties: {}, + timing: { + behavior: new TweenBehavior(), + duration: 2000, + }, + }; + let animation2 = { + sprites, + properties: {}, + timing: { + behavior: new TweenBehavior(), + duration: 150, + }, + }; + let animation3 = { + sprites, + properties: {}, + timing: { + behavior: new TweenBehavior(), + }, + }; + + let sortedAnimations = OrchestrationMatrix.sortParallelTimeline([ + animation2, + animation3, + animation1, + ]); + + assert.deepEqual(sortedAnimations, [animation1, animation2, animation3]); + }); + + test('it sorts animations with SpringBehavior to the front', function (assert) { + let sprites = new Set([getMockSprite()]); + + let animation1 = { + sprites, + properties: {}, + timing: { + behavior: new SpringBehavior(), + }, + }; + let animation2 = { + sprites, + properties: {}, + timing: { + behavior: new TweenBehavior(), + duration: 100, + }, + }; + let animation3 = { + sprites, + properties: {}, + timing: { + behavior: new TweenBehavior(), + anchor: 'center', + }, + }; + + let sortedAnimations = OrchestrationMatrix.sortParallelTimeline([ + animation2, + animation1, + animation3, + ]); + + assert.deepEqual(sortedAnimations, [animation1, animation2, animation3]); + }); + }); }); diff --git a/packages/boxel-motion/addon/models/orchestration.ts b/packages/boxel-motion/addon/models/orchestration.ts index a3dc81d508..15ab867cc6 100644 --- a/packages/boxel-motion/addon/models/orchestration.ts +++ b/packages/boxel-motion/addon/models/orchestration.ts @@ -2,7 +2,6 @@ import Behavior, { FPS } from '@cardstack/boxel-motion/behaviors/base'; import SpringBehavior from '@cardstack/boxel-motion/behaviors/spring'; import StaticBehavior from '@cardstack/boxel-motion/behaviors/static'; -import TweenBehavior from '@cardstack/boxel-motion/behaviors/tween'; import WaitBehavior from '@cardstack/boxel-motion/behaviors/wait'; import Sprite, { MotionOptions, @@ -166,35 +165,61 @@ export class OrchestrationMatrix { return timelineMatrix; } - static fromParallelTimeline( - timeline: AnimationTimeline - ): OrchestrationMatrix { - let timelineMatrix = OrchestrationMatrix.empty(); - let submatrices = []; - - // TODO: add support for nested timelines - timeline.animations.sort((a, b) => { - if ( - 'timing' in b && - (b.timing.duration || !(b.timing.behavior instanceof SpringBehavior)) - ) { + // Probably need to do a 2-pass sort, resolve the springs first and then + // figure out what has the longest duration. + // Sort order generated by this is: Springs, Duration (long -> short), undefined timing + static sortParallelTimeline( + animations: (MotionDefinition | AnimationTimeline)[] + ): (MotionDefinition | AnimationTimeline)[] { + return [...animations].sort((a, b) => { + if (!('timing' in b)) { return -1; - } else if ( - 'timing' in a && - (a.timing.duration || !(a.timing.behavior instanceof SpringBehavior)) - ) { + } else if (!('timing' in a)) { return 1; + } else { + if (b.timing.behavior instanceof SpringBehavior) { + return 1; + } + + if (a.timing.behavior instanceof SpringBehavior) { + return -1; + } + + if ( + a.timing.duration !== undefined && + b.timing.duration !== undefined + ) { + if (a.timing.duration > b.timing.duration) { + return -1; + } else if (a.timing.duration < b.timing.duration) { + return 1; + } + } else if (a.timing.duration !== undefined) { + return -1; + } else if (b.timing.duration !== undefined) { + return 1; + } } return 0; }); + } + + static fromParallelTimeline( + timeline: AnimationTimeline + ): OrchestrationMatrix { + let timelineMatrix = OrchestrationMatrix.empty(); + let submatrices: OrchestrationMatrix[] = []; + + timeline.animations = this.sortParallelTimeline(timeline.animations); // maxLength is for anchoring to the end. let maxLength = 0; - for (let item of timeline.animations) { + timeline.animations.forEach((item, index) => { // If the parallel timeline has an anchor setting but the MotionDefinition // doesn't have one, we take the parent anchor. if ( + index > 0 && timeline.anchor && 'timing' in item && !item.timing.anchor && @@ -203,6 +228,11 @@ export class OrchestrationMatrix { item.timing.anchor = timeline.anchor; } + // after sorting the first item cannot have an anchor defined as it is the reference. + if (index === 0 && 'timing' in item && item.timing.anchor) { + delete item.timing.anchor; + } + // TODO: do we want a different option or more flexibility here? We could for example search for the longest // non-inferred duration already compiled rather than picking the first one. Another option is to explicitly // have to link to a MotionDefinition to infer from. @@ -210,7 +240,7 @@ export class OrchestrationMatrix { maxLength = Math.max(maxLength, submatrix.totalColumns); submatrices.push(submatrix); - } + }); for (let submatrix of submatrices) { timelineMatrix.add(0, submatrix); @@ -229,6 +259,7 @@ export class OrchestrationMatrix { let properties = motionDefinition.properties; let timing = motionDefinition.timing; let rows = new Map(); + for (let sprite of motionDefinition.sprites) { let rowFragments: RowFragment[] = []; let startColumn = 0;