Skip to content

Commit 45f88ad

Browse files
authored
Merge pull request #1454 from sveltejs/animations
[WIP] animations
2 parents 6d00f2b + 94206ca commit 45f88ad

File tree

28 files changed

+576
-22
lines changed

28 files changed

+576
-22
lines changed

src/compile/Compiler.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export default class Compiler {
9999
components: Set<string>;
100100
events: Set<string>;
101101
methods: Set<string>;
102+
animations: Set<string>;
102103
transitions: Set<string>;
103104
actions: Set<string>;
104105
importedComponents: Map<string, string>;
@@ -149,6 +150,7 @@ export default class Compiler {
149150
this.components = new Set();
150151
this.events = new Set();
151152
this.methods = new Set();
153+
this.animations = new Set();
152154
this.transitions = new Set();
153155
this.actions = new Set();
154156
this.importedComponents = new Map();
@@ -475,7 +477,7 @@ export default class Compiler {
475477
templateProperties[getName(prop.key)] = prop;
476478
});
477479

478-
['helpers', 'events', 'components', 'transitions', 'actions'].forEach(key => {
480+
['helpers', 'events', 'components', 'transitions', 'actions', 'animations'].forEach(key => {
479481
if (templateProperties[key]) {
480482
templateProperties[key].value.properties.forEach((prop: Node) => {
481483
this[key].add(getName(prop.key));
@@ -685,6 +687,12 @@ export default class Compiler {
685687
});
686688
}
687689

690+
if (templateProperties.animations) {
691+
templateProperties.animations.value.properties.forEach((property: Node) => {
692+
addDeclaration(getName(property.key), property.value, false, 'animations');
693+
});
694+
}
695+
688696
if (templateProperties.actions) {
689697
templateProperties.actions.value.properties.forEach((property: Node) => {
690698
addDeclaration(getName(property.key), property.value, false, 'actions');

src/compile/dom/Block.ts

+11
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default class Block {
4040
};
4141

4242
maintainContext: boolean;
43+
animation?: string;
4344
hasIntroMethod: boolean;
4445
hasOutroMethod: boolean;
4546
outros: number;
@@ -77,6 +78,7 @@ export default class Block {
7778
destroy: new CodeBuilder(),
7879
};
7980

81+
this.animation = null;
8082
this.hasIntroMethod = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros
8183
this.hasOutroMethod = false;
8284
this.outros = 0;
@@ -127,6 +129,10 @@ export default class Block {
127129
this.outros += 1;
128130
}
129131

132+
addAnimation(name) {
133+
this.animation = name;
134+
}
135+
130136
addVariable(name: string, init?: string) {
131137
if (this.variables.has(name) && this.variables.get(name) !== init) {
132138
throw new Error(
@@ -183,6 +189,11 @@ export default class Block {
183189
this.builders.hydrate.addLine(`this.first = ${this.first};`);
184190
}
185191

192+
if (this.animation) {
193+
properties.addBlock(`node: null,`);
194+
this.builders.hydrate.addLine(`this.node = ${this.animation};`);
195+
}
196+
186197
if (this.builders.create.isEmpty() && this.builders.hydrate.isEmpty()) {
187198
properties.addBlock(`c: @noop,`);
188199
} else {

src/compile/nodes/Animation.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Node from './shared/Node';
2+
import Expression from './shared/Expression';
3+
4+
export default class Animation extends Node {
5+
type: 'Animation';
6+
name: string;
7+
expression: Expression;
8+
9+
constructor(compiler, parent, scope, info) {
10+
super(compiler, parent, scope, info);
11+
12+
this.name = info.name;
13+
14+
this.expression = info.expression
15+
? new Expression(compiler, this, scope, info.expression)
16+
: null;
17+
}
18+
}

src/compile/nodes/EachBlock.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,12 @@ export default class EachBlock extends Node {
315315
const dynamic = this.block.hasUpdateMethod;
316316

317317
block.builders.update.addBlock(deindent`
318-
var ${this.each_block_value} = ${snippet};
318+
const ${this.each_block_value} = ${snippet};
319319
320320
${this.block.hasOutroMethod && `@transitionManager.groupOutros();`}
321+
${this.block.animation && `const rects = @measure(${blocks});`}
321322
${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.get_each_context});
323+
${this.block.animation && `@animate(${blocks}, rects, %animations-${this.children[0].animation.name}, {});`}
322324
`);
323325

324326
if (this.compiler.options.nestedTransitions) {

src/compile/nodes/Element.ts

+17-18
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Attribute from './Attribute';
1313
import Binding from './Binding';
1414
import EventHandler from './EventHandler';
1515
import Transition from './Transition';
16+
import Animation from './Animation';
1617
import Action from './Action';
1718
import Text from './Text';
1819
import * as namespaces from '../../utils/namespaces';
@@ -30,8 +31,9 @@ export default class Element extends Node {
3031
actions: Action[];
3132
bindings: Binding[];
3233
handlers: EventHandler[];
33-
intro: Transition;
34-
outro: Transition;
34+
intro?: Transition;
35+
outro?: Transition;
36+
animation?: Animation;
3537
children: Node[];
3638

3739
ref: string;
@@ -54,6 +56,7 @@ export default class Element extends Node {
5456

5557
this.intro = null;
5658
this.outro = null;
59+
this.animation = null;
5760

5861
if (this.name === 'textarea') {
5962
// this is an egregious hack, but it's the easiest way to get <textarea>
@@ -113,6 +116,10 @@ export default class Element extends Node {
113116
if (node.outro) this.outro = transition;
114117
break;
115118

119+
case 'Animation':
120+
this.animation = new Animation(compiler, this, scope, node);
121+
break;
122+
116123
case 'Ref':
117124
// TODO catch this in validation
118125
if (this.ref) throw new Error(`Duplicate refs`);
@@ -126,8 +133,6 @@ export default class Element extends Node {
126133
}
127134
});
128135

129-
// TODO break out attributes and directives here
130-
131136
this.children = mapChildren(compiler, this, scope, info.children);
132137

133138
compiler.stylesheet.apply(this);
@@ -142,6 +147,10 @@ export default class Element extends Node {
142147
this.cannotUseInnerHTML();
143148
}
144149

150+
this.var = block.getUniqueName(
151+
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
152+
);
153+
145154
this.attributes.forEach(attr => {
146155
if (attr.dependencies.size) {
147156
this.parent.cannotUseInnerHTML();
@@ -180,19 +189,13 @@ export default class Element extends Node {
180189
block.addDependencies(handler.dependencies);
181190
});
182191

183-
if (this.intro) {
184-
this.parent.cannotUseInnerHTML();
185-
block.addIntro();
186-
}
187-
188-
if (this.outro) {
192+
if (this.intro || this.outro || this.animation || this.ref) {
189193
this.parent.cannotUseInnerHTML();
190-
block.addOutro();
191194
}
192195

193-
if (this.ref) {
194-
this.parent.cannotUseInnerHTML();
195-
}
196+
if (this.intro) block.addIntro();
197+
if (this.outro) block.addOutro();
198+
if (this.animation) block.addAnimation(this.var);
196199

197200
const valueAttribute = this.attributes.find((attribute: Attribute) => attribute.name === 'value');
198201

@@ -229,10 +232,6 @@ export default class Element extends Node {
229232
component._slots.add(slot);
230233
}
231234

232-
this.var = block.getUniqueName(
233-
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
234-
);
235-
236235
if (this.children.length) {
237236
if (this.name === 'pre' || this.name === 'textarea') stripWhitespace = false;
238237
this.initChildren(block, stripWhitespace, nextSibling);

src/parse/read/directives.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,17 @@ const DIRECTIVES: Record<string, {
6363
error: 'Transition argument must be an object literal, e.g. `{ duration: 400 }`'
6464
},
6565

66+
Animation: {
67+
names: ['animate'],
68+
attribute(start, end, type, name, expression) {
69+
return { start, end, type, name, expression };
70+
},
71+
allowedExpressionTypes: ['ObjectExpression'],
72+
error: 'Animation argument must be an object literal, e.g. `{ duration: 400 }`'
73+
},
74+
6675
Action: {
67-
names: [ 'use' ],
76+
names: ['use'],
6877
attribute(start, end, type, name, expression) {
6978
return { start, end, type, name, expression };
7079
},

src/shared/keyed-each.js

+85
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { transitionManager, linear, generateRule, hash } from './transitions';
2+
13
export function destroyBlock(block, lookup) {
24
block.d(1);
35
lookup[block.key] = null;
@@ -95,4 +97,87 @@ export function updateKeyedEach(old_blocks, component, changed, get_key, dynamic
9597
while (n) insert(new_blocks[n - 1]);
9698

9799
return new_blocks;
100+
}
101+
102+
export function measure(blocks) {
103+
const measurements = {};
104+
let i = blocks.length;
105+
while (i--) measurements[blocks[i].key] = blocks[i].node.getBoundingClientRect();
106+
return measurements;
107+
}
108+
109+
export function animate(blocks, rects, fn, params) {
110+
let i = blocks.length;
111+
while (i--) {
112+
const block = blocks[i];
113+
const from = rects[block.key];
114+
115+
if (!from) continue;
116+
const to = block.node.getBoundingClientRect();
117+
118+
if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom) continue;
119+
120+
const info = fn(block.node, { from, to }, params);
121+
122+
const duration = 'duration' in info ? info.duration : 300;
123+
const delay = 'delay' in info ? info.delay : 0;
124+
const ease = info.easing || linear;
125+
const start = window.performance.now() + delay;
126+
const end = start + duration;
127+
128+
const program = {
129+
a: 0,
130+
t: 0,
131+
b: 1,
132+
delta: 1,
133+
duration,
134+
start,
135+
end
136+
};
137+
138+
const animation = {
139+
pending: delay ? program : null,
140+
program: delay ? null : program,
141+
running: !delay,
142+
143+
start() {
144+
if (info.css) {
145+
const rule = generateRule(program, ease, info.css);
146+
program.name = `__svelte_${hash(rule)}`;
147+
148+
transitionManager.addRule(rule, program.name);
149+
150+
block.node.style.animation = (block.node.style.animation || '')
151+
.split(', ')
152+
.filter(anim => anim && (program.delta < 0 || !/__svelte/.test(anim)))
153+
.concat(`${program.name} ${program.duration}ms linear 1 forwards`)
154+
.join(', ');
155+
}
156+
},
157+
158+
update: now => {
159+
const p = now - program.start;
160+
const t = program.a + program.delta * ease(p / program.duration);
161+
if (info.tick) info.tick(t, 1 - t);
162+
},
163+
164+
done() {
165+
if (info.css) {
166+
transitionManager.deleteRule(block.node, program.name);
167+
}
168+
169+
if (info.tick) {
170+
info.tick(1, 0);
171+
}
172+
173+
animation.running = false;
174+
}
175+
};
176+
177+
transitionManager.add(animation);
178+
179+
if (info.tick) info.tick(0, 1);
180+
181+
if (!delay) animation.start();
182+
}
98183
}

src/validate/html/validateElement.ts

+35
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export default function validateElement(
7878
let hasIntro: boolean;
7979
let hasOutro: boolean;
8080
let hasTransition: boolean;
81+
let hasAnimation: boolean;
8182

8283
node.attributes.forEach((attribute: Node) => {
8384
if (attribute.type === 'Ref') {
@@ -228,6 +229,40 @@ export default function validateElement(
228229
message: `Missing transition '${attribute.name}'`
229230
});
230231
}
232+
} else if (attribute.type === 'Animation') {
233+
validator.used.animations.add(attribute.name);
234+
235+
if (hasAnimation) {
236+
validator.error(attribute, {
237+
code: `duplicate-animation`,
238+
message: `An element can only have one 'animate' directive`
239+
});
240+
}
241+
242+
if (!validator.animations.has(attribute.name)) {
243+
validator.error(attribute, {
244+
code: `missing-animation`,
245+
message: `Missing animation '${attribute.name}'`
246+
});
247+
}
248+
249+
const parent = stack[stack.length - 1];
250+
if (!parent || parent.type !== 'EachBlock' || !parent.key) {
251+
// TODO can we relax the 'immediate child' rule?
252+
validator.error(attribute, {
253+
code: `invalid-animation`,
254+
message: `An element that use the animate directive must be the immediate child of a keyed each block`
255+
});
256+
}
257+
258+
if (parent.children.length > 1) {
259+
validator.error(attribute, {
260+
code: `invalid-animation`,
261+
message: `An element that use the animate directive must be the sole child of a keyed each block`
262+
});
263+
}
264+
265+
hasAnimation = true;
231266
} else if (attribute.type === 'Attribute') {
232267
if (attribute.name === 'value' && node.name === 'textarea') {
233268
if (node.children.length) {

src/validate/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class Validator {
2121
components: Map<string, Node>;
2222
methods: Map<string, Node>;
2323
helpers: Map<string, Node>;
24+
animations: Map<string, Node>;
2425
transitions: Map<string, Node>;
2526
actions: Map<string, Node>;
2627
slots: Set<string>;
@@ -29,6 +30,7 @@ export class Validator {
2930
components: Set<string>;
3031
helpers: Set<string>;
3132
events: Set<string>;
33+
animations: Set<string>;
3234
transitions: Set<string>;
3335
actions: Set<string>;
3436
};
@@ -47,6 +49,7 @@ export class Validator {
4749
this.components = new Map();
4850
this.methods = new Map();
4951
this.helpers = new Map();
52+
this.animations = new Map();
5053
this.transitions = new Map();
5154
this.actions = new Map();
5255
this.slots = new Set();
@@ -55,6 +58,7 @@ export class Validator {
5558
components: new Set(),
5659
helpers: new Set(),
5760
events: new Set(),
61+
animations: new Set(),
5862
transitions: new Set(),
5963
actions: new Set(),
6064
};

0 commit comments

Comments
 (0)