Skip to content

Commit 59455b6

Browse files
committed
boxel-motion improvements
- Add Split View example to boxel-motion test app as a study for the AI Assistant Panel animation - Incorporate boxel-motion builds into CI - Generalize fill behavior of behaviors rather than special casing StaticBehavior treatment - Default StaticBehavior to a duration of 1, since you can now opt into forward fill - when using StaticBehavior with SpringBehavior, you don't know how long the animation will be, and you may want a static animation to persist for the duration of the animation - Use context-relative bounds for removed sprites
1 parent f76bbd3 commit 59455b6

24 files changed

+278
-39
lines changed

.github/workflows/ci.yaml

+19
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ jobs:
5252
if: always()
5353
run: pnpm run lint
5454
working-directory: packages/boxel-ui/test-app
55+
- name: Lint Boxel Motion
56+
if: always()
57+
run: pnpm run lint
58+
working-directory: packages/boxel-motion/addon
59+
- name: Build Boxel Motion
60+
# To faciliate linting of projects that depend on Boxel Motion
61+
if: always()
62+
run: pnpm run build
63+
working-directory: packages/boxel-motion/addon
64+
- name: Lint Boxel Motion Test App
65+
if: always()
66+
run: pnpm run lint
67+
working-directory: packages/boxel-motion/test-app
5568
- name: Lint Host
5669
if: always()
5770
run: pnpm run lint
@@ -159,6 +172,9 @@ jobs:
159172
- name: Build boxel-ui
160173
run: pnpm build
161174
working-directory: packages/boxel-ui/addon
175+
- name: Build boxel-motion
176+
run: pnpm build
177+
working-directory: packages/boxel-motion/addon
162178
- name: Build host dist/ for fastboot
163179
run: pnpm build
164180
env:
@@ -192,6 +208,9 @@ jobs:
192208
- name: Build boxel-ui
193209
run: pnpm build
194210
working-directory: packages/boxel-ui/addon
211+
- name: Build boxel-motion
212+
run: pnpm build
213+
working-directory: packages/boxel-motion/addon
195214
- name: Build host dist/ for fastboot
196215
run: pnpm build
197216
env:

QUICKSTART.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ To build the entire repository and run the application, follow these steps:
2121
pnpm install
2222
```
2323

24-
4. Build the boxel-ui:
24+
4. Build the boxel-ui and boxl-motion addons:
2525

2626
```zsh
2727
cd ./packages/boxel-ui/addon
2828
pnpm rebuild:icons
2929
pnpm build
30+
cd ../../boxel-motion/addon
31+
pnpm build
3032
```
3133

3234
5. Build the host:

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ Make sure that you have created a matrix user for the base realm, drafts realm,
4545
In order to run the ember-cli hosted app:
4646

4747
1. `pnpm build` in the boxel-ui/addon workspace to build the boxel-ui addon.
48-
2. `pnpm start` in the host/ workspace to serve the ember app. Note that this script includes the environment variable `OWN_REALM_URL=http://localhost:4201/draft/` which configures the host to point to the draft realm's cards realm by default.
49-
3. `pnpm start:all` in the realm-server/ to serve the base realm, draft realm and published realm -- this will also allow you to switch between the app and the tests without having to restart servers)
48+
2. `pnpm build` in the boxel-motion/addon workspace to build the boxel-motion addon.
49+
3. `pnpm start` in the host/ workspace to serve the ember app. Note that this script includes the environment variable `OWN_REALM_URL=http://localhost:4201/draft/` which configures the host to point to the draft realm's cards realm by default.
50+
4. `pnpm start:all` in the realm-server/ to serve the base realm, draft realm and published realm -- this will also allow you to switch between the app and the tests without having to restart servers)
5051

5152
The app is available at http://localhost:4200. It will serve the draft realm (configurable with OWN_REALM_URL, as mentioned above). You can open the base and draft cards workspace directly by entering http://localhost:4201/base or http://localhost:4201/draft in the browser (and additionally the published realm by entering http://localhost:4201/published).
5253

packages/boxel-motion/addon/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
"types": "./declarations/*.d.ts",
118118
"default": "./dist/*.js"
119119
},
120+
"./styles/*.css": "./dist/styles/*.css",
120121
"./addon-main.js": "./addon-main.cjs"
121122
},
122123
"files": [

packages/boxel-motion/addon/rollup.config.mjs

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export default {
5151
// Ensure that .gjs files are properly integrated as Javascript
5252
addon.gjs(),
5353

54+
// addons are allowed to contain imports of .css files, which we want rollup
55+
// to leave alone and keep in the published output.
56+
addon.keepAssets(['styles/*']),
57+
5458
// Remove leftover build artifacts when starting a new build.
5559
addon.clean({ runOnce: true }),
5660

packages/boxel-motion/addon/src/behaviors/base.ts

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type Frame = {
3636
export type FrameGenerator = Generator<Frame | void, void, never>;
3737

3838
export default interface Behavior {
39+
fill: boolean;
40+
3941
/**
4042
* Calculates the frames for the given parameters.
4143
*

packages/boxel-motion/addon/src/behaviors/spring.ts

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type SpringValues = {
3636
};
3737

3838
export default class SpringBehavior implements Behavior {
39+
fill = true;
3940
private options: SpringOptions;
4041

4142
constructor(options?: SpringOptionsArgument) {

packages/boxel-motion/addon/src/behaviors/static.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1-
import type Behavior from './base.ts';
2-
import {
1+
import Behavior, {
32
type Frame,
43
type StaticToFramesArgument,
54
timeToFrame,
65
} from './base.ts';
76

7+
export interface StaticBehaviorOptions {
8+
fill?: boolean;
9+
}
810
export default class StaticBehavior implements Behavior {
11+
fill: boolean;
12+
constructor(options?: StaticBehaviorOptions) {
13+
this.fill = options?.fill ?? false;
14+
}
15+
916
*getFrames(options: StaticToFramesArgument) {
1017
let frameCount = timeToFrame(options.duration) + 1;
1118

1219
for (let i = 0; i < frameCount; i++) {
13-
// TODO: this can explicitly be non-numeric, fix TS
1420
yield { value: options.value, velocity: 0 } as Frame;
1521
}
1622
}

packages/boxel-motion/addon/src/behaviors/tween.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface TweenBehaviorOptions {
1515
}
1616

1717
export default class TweenBehavior implements Behavior {
18+
fill = true;
1819
easing: Easing;
1920

2021
constructor(options?: TweenBehaviorOptions) {

packages/boxel-motion/addon/src/behaviors/wait.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type Behavior from './base.ts';
22
import { type WaitToFramesArgument, timeToFrame } from './base.ts';
33

44
export default class WaitBehavior implements Behavior {
5+
fill = false;
56
*getFrames(options: WaitToFramesArgument) {
67
let frameCount = timeToFrame(options.duration) + 1;
78

packages/boxel-motion/addon/src/components/animation-context.gts

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { assert } from '@ember/debug';
22
import { action } from '@ember/object';
33
import { inject as service } from '@ember/service';
4+
import { htmlSafe } from '@ember/template';
45
import Component from '@glimmer/component';
56
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
67
// @ts-ignore
@@ -19,6 +20,7 @@ const { VOLATILE_TAG, consumeTag } =
1920
Ember.__loader.require('@glimmer/validator');
2021

2122
interface AnimationContextArgs {
23+
debugging?: boolean;
2224
id?: string;
2325
use: ((changeset: Changeset) => AnimationDefinition) | undefined;
2426
}
@@ -37,7 +39,7 @@ export default class AnimationContextComponent
3739
{
3840
<template>
3941
{{this.renderDetector}}
40-
<div class='animation-context' {{registerContext this}} ...attributes>
42+
<div class={{this.cssClasses}} {{registerContext this}} ...attributes>
4143
<div
4244
{{registerContextOrphansEl this}}
4345
data-animation-context-orphan-element='true'
@@ -52,6 +54,13 @@ export default class AnimationContextComponent
5254
get id(): string | undefined {
5355
return this.args.id;
5456
}
57+
get cssClasses() {
58+
let result = 'animation-context';
59+
if (this.args.debugging) {
60+
result += ' debugging';
61+
}
62+
return htmlSafe(result);
63+
}
5564

5665
element!: HTMLElement; //set by template
5766
orphansElement: HTMLElement | null = null; //set by template
@@ -67,6 +76,17 @@ export default class AnimationContextComponent
6776
);
6877
}
6978

79+
constructor(owner: unknown, args: AnimationContextArgs) {
80+
super(owner, args);
81+
if (!this.animations) {
82+
throw new Error(
83+
`Expected to find "animations" service in app.
84+
Add 'app/services/animations.ts' with
85+
\`export { AnimationsService as default } from '@cardstack/boxel-motion';\``,
86+
);
87+
}
88+
}
89+
7090
willDestroy(): void {
7191
super.willDestroy();
7292
this.animations.unregisterContext(this);

packages/boxel-motion/addon/src/index.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import StaticBehavior from './behaviors/static.ts';
44
import TweenBehavior from './behaviors/tween.ts';
55
import WaitBehavior from './behaviors/wait.ts';
66
import AnimationContext from './components/animation-context.gts';
7-
import { type Changeset } from './models/animator.ts';
7+
import { type Changeset, type IContext } from './models/animator.ts';
88
import {
99
type AnimationDefinition,
1010
type AnimationTimeline,
1111
OrchestrationMatrix,
1212
} from './models/orchestration.ts';
1313
import Sprite, { type ISpriteModifier, SpriteType } from './models/sprite.ts';
14+
import sprite from './modifiers/sprite.ts';
1415
import AnimationsService from './services/animations.ts';
1516

1617
export {
@@ -20,10 +21,12 @@ export {
2021
AnimationTimeline,
2122
Changeset,
2223
FPS,
24+
IContext,
2325
ISpriteModifier,
2426
OrchestrationMatrix,
2527
SpringBehavior,
26-
Sprite,
28+
Sprite, // model
29+
sprite, // modifier
2730
SpriteType,
2831
StaticBehavior,
2932
TweenBehavior,

packages/boxel-motion/addon/src/models/orchestration.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type Behavior from '../behaviors/base.ts';
2-
import StaticBehavior from '../behaviors/static.ts';
32
import WaitBehavior from '../behaviors/wait.ts';
43
import generateFrames from '../utils/generate-frames.ts';
54
import { type Keyframe, type Value } from '../value/index.ts';
@@ -8,9 +7,9 @@ import { type MotionOptions, type MotionProperty } from './motion.ts';
87
import type Sprite from './sprite.ts';
98

109
interface RowFragment {
10+
fill: boolean;
1111
frames: Frame[];
1212
startColumn: number;
13-
static: boolean;
1413
}
1514

1615
export class OrchestrationMatrix {
@@ -39,8 +38,8 @@ export class OrchestrationMatrix {
3938
fragmentsByColumn[rowFragment.startColumn] =
4039
fragmentsByColumn[rowFragment.startColumn] ?? [];
4140
fragmentsByColumn[rowFragment.startColumn]!.push(rowFragment);
42-
// don't backfill static frames, they're intended to only be set for their duration
43-
if (rowFragment.static === false && rowFragment.frames[0]) {
41+
// some frames (where fill == false) are intended to only be set for their duration
42+
if (rowFragment.fill && rowFragment.frames[0]) {
4443
baseFrames.push(rowFragment.frames[0] as Frame);
4544
}
4645
}
@@ -80,8 +79,8 @@ export class OrchestrationMatrix {
8079
if (frame) {
8180
frames.push(frame);
8281

83-
// Detect the final frame for static behaviors, so we can exclude it from future frames (no forward-fill).
84-
if (!fragment.frames.length && fragment.static) {
82+
// Detect the final frame for behaviors that should not fill, so we can exclude it from future frames (no forward-fill).
83+
if (!fragment.frames.length && !fragment.fill) {
8584
propertiesToRemoveFromPreviousKeyframe.push(frame.property);
8685
}
8786
} else {
@@ -195,7 +194,7 @@ export class OrchestrationMatrix {
195194
rowFragments.push({
196195
frames,
197196
startColumn: 0,
198-
static: true,
197+
fill: timing.behavior.fill,
199198
});
200199
maxLength = Math.max(frames.length, maxLength);
201200
}
@@ -213,7 +212,7 @@ export class OrchestrationMatrix {
213212
rowFragments.push({
214213
frames,
215214
startColumn: 0,
216-
static: timing.behavior instanceof StaticBehavior,
215+
fill: timing.behavior.fill,
217216
});
218217
maxLength = Math.max(frames.length, maxLength);
219218
}

packages/boxel-motion/addon/src/models/sprite.ts

+15-10
Original file line numberDiff line numberDiff line change
@@ -163,24 +163,29 @@ export default class Sprite {
163163

164164
get initial(): { [k in string]: Value } {
165165
let initialBounds = {};
166+
let boundsRect: DOMRect;
166167
if (this.initialBounds) {
167-
let { x, y, width, height, top, right, bottom, left } =
168-
this.initialBounds.relativeToParent;
168+
if (this.type == SpriteType.Removed) {
169+
// because removed sprites are moved to the orphans container under the AnimationContext
170+
boundsRect = this.initialBounds.relativeToContext;
171+
} else {
172+
boundsRect = this.initialBounds.relativeToParent;
173+
}
169174

170175
initialBounds = {
171176
// TODO: maybe also for top/left?
172177
// TODO: figure out if we want the boundsDelta to be under these properties
173178
'translate-x': `${-(this.boundsDelta?.x ?? 0)}px`,
174179
'translate-y': `${-(this.boundsDelta?.y ?? 0)}px`,
175180

176-
x: `${x}px`,
177-
y: `${y}px`,
178-
width: `${width}px`,
179-
height: `${height}px`,
180-
top: `${top}px`,
181-
right: `${right}px`,
182-
bottom: `${bottom}px`,
183-
left: `${left}px`,
181+
x: `${boundsRect.x}px`,
182+
y: `${boundsRect.y}px`,
183+
width: `${boundsRect.width}px`,
184+
height: `${boundsRect.height}px`,
185+
top: `${boundsRect.top}px`,
186+
right: `${boundsRect.right}px`,
187+
bottom: `${boundsRect.bottom}px`,
188+
left: `${boundsRect.left}px`,
184189
};
185190
}
186191

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.animation-context {
2+
position: relative;
3+
}
4+
.animation-context.debugging {
5+
border: 1px dashed blue;
6+
}
7+
.animation-context.debugging .sprite {
8+
border: 1px dotted green;
9+
}

packages/boxel-motion/addon/src/utils/generate-frames.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ export default function generateFrames(
7575

7676
return resolveFrameGenerator(normalizedProperty, generator);
7777
}
78-
7978
if (typeof options !== 'object') {
8079
if (!(timing.behavior instanceof StaticBehavior)) {
8180
throw new Error(
@@ -84,7 +83,7 @@ export default function generateFrames(
8483
}
8584

8685
if (!timing.duration) {
87-
throw new Error('Static behavior requires a duration');
86+
timing.duration = 1;
8887
}
8988

9089
// todo maybe throw error if options is not numeric or string

packages/boxel-motion/test-app/app/app.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Application from '@ember/application';
22
import config from 'boxel-motion-test-app/config/environment';
33
import loadInitializers from 'ember-load-initializers';
44
import Resolver from 'ember-resolver';
5+
import '@cardstack/boxel-motion/styles/addon.css';
56

67
export default class App extends Application {
78
modulePrefix = config.modulePrefix;

0 commit comments

Comments
 (0)