Merge pull request #1454 from sveltejs/animations

[WIP] animations
pull/1464/head
Rich Harris 6 years ago committed by GitHub
commit 45f88ade2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -99,6 +99,7 @@ export default class Compiler {
components: Set<string>;
events: Set<string>;
methods: Set<string>;
animations: Set<string>;
transitions: Set<string>;
actions: Set<string>;
importedComponents: Map<string, string>;
@ -149,6 +150,7 @@ export default class Compiler {
this.components = new Set();
this.events = new Set();
this.methods = new Set();
this.animations = new Set();
this.transitions = new Set();
this.actions = new Set();
this.importedComponents = new Map();
@ -475,7 +477,7 @@ export default class Compiler {
templateProperties[getName(prop.key)] = prop;
});
['helpers', 'events', 'components', 'transitions', 'actions'].forEach(key => {
['helpers', 'events', 'components', 'transitions', 'actions', 'animations'].forEach(key => {
if (templateProperties[key]) {
templateProperties[key].value.properties.forEach((prop: Node) => {
this[key].add(getName(prop.key));
@ -685,6 +687,12 @@ export default class Compiler {
});
}
if (templateProperties.animations) {
templateProperties.animations.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'animations');
});
}
if (templateProperties.actions) {
templateProperties.actions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'actions');

@ -40,6 +40,7 @@ export default class Block {
};
maintainContext: boolean;
animation?: string;
hasIntroMethod: boolean;
hasOutroMethod: boolean;
outros: number;
@ -77,6 +78,7 @@ export default class Block {
destroy: new CodeBuilder(),
};
this.animation = null;
this.hasIntroMethod = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros
this.hasOutroMethod = false;
this.outros = 0;
@ -127,6 +129,10 @@ export default class Block {
this.outros += 1;
}
addAnimation(name) {
this.animation = name;
}
addVariable(name: string, init?: string) {
if (this.variables.has(name) && this.variables.get(name) !== init) {
throw new Error(
@ -183,6 +189,11 @@ export default class Block {
this.builders.hydrate.addLine(`this.first = ${this.first};`);
}
if (this.animation) {
properties.addBlock(`node: null,`);
this.builders.hydrate.addLine(`this.node = ${this.animation};`);
}
if (this.builders.create.isEmpty() && this.builders.hydrate.isEmpty()) {
properties.addBlock(`c: @noop,`);
} else {

@ -0,0 +1,18 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
export default class Animation extends Node {
type: 'Animation';
name: string;
expression: Expression;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.name = info.name;
this.expression = info.expression
? new Expression(compiler, this, scope, info.expression)
: null;
}
}

@ -315,10 +315,12 @@ export default class EachBlock extends Node {
const dynamic = this.block.hasUpdateMethod;
block.builders.update.addBlock(deindent`
var ${this.each_block_value} = ${snippet};
const ${this.each_block_value} = ${snippet};
${this.block.hasOutroMethod && `@transitionManager.groupOutros();`}
${this.block.animation && `const rects = @measure(${blocks});`}
${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});
${this.block.animation && `@animate(${blocks}, rects, %animations-${this.children[0].animation.name}, {});`}
`);
if (this.compiler.options.nestedTransitions) {

@ -13,6 +13,7 @@ import Attribute from './Attribute';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Transition from './Transition';
import Animation from './Animation';
import Action from './Action';
import Text from './Text';
import * as namespaces from '../../utils/namespaces';
@ -30,8 +31,9 @@ export default class Element extends Node {
actions: Action[];
bindings: Binding[];
handlers: EventHandler[];
intro: Transition;
outro: Transition;
intro?: Transition;
outro?: Transition;
animation?: Animation;
children: Node[];
ref: string;
@ -54,6 +56,7 @@ export default class Element extends Node {
this.intro = null;
this.outro = null;
this.animation = null;
if (this.name === 'textarea') {
// this is an egregious hack, but it's the easiest way to get <textarea>
@ -113,6 +116,10 @@ export default class Element extends Node {
if (node.outro) this.outro = transition;
break;
case 'Animation':
this.animation = new Animation(compiler, this, scope, node);
break;
case 'Ref':
// TODO catch this in validation
if (this.ref) throw new Error(`Duplicate refs`);
@ -126,8 +133,6 @@ export default class Element extends Node {
}
});
// TODO break out attributes and directives here
this.children = mapChildren(compiler, this, scope, info.children);
compiler.stylesheet.apply(this);
@ -142,6 +147,10 @@ export default class Element extends Node {
this.cannotUseInnerHTML();
}
this.var = block.getUniqueName(
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
);
this.attributes.forEach(attr => {
if (attr.dependencies.size) {
this.parent.cannotUseInnerHTML();
@ -180,19 +189,13 @@ export default class Element extends Node {
block.addDependencies(handler.dependencies);
});
if (this.intro) {
this.parent.cannotUseInnerHTML();
block.addIntro();
}
if (this.outro) {
if (this.intro || this.outro || this.animation || this.ref) {
this.parent.cannotUseInnerHTML();
block.addOutro();
}
if (this.ref) {
this.parent.cannotUseInnerHTML();
}
if (this.intro) block.addIntro();
if (this.outro) block.addOutro();
if (this.animation) block.addAnimation(this.var);
const valueAttribute = this.attributes.find((attribute: Attribute) => attribute.name === 'value');
@ -229,10 +232,6 @@ export default class Element extends Node {
component._slots.add(slot);
}
this.var = block.getUniqueName(
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
);
if (this.children.length) {
if (this.name === 'pre' || this.name === 'textarea') stripWhitespace = false;
this.initChildren(block, stripWhitespace, nextSibling);

@ -63,8 +63,17 @@ const DIRECTIVES: Record<string, {
error: 'Transition argument must be an object literal, e.g. `{ duration: 400 }`'
},
Animation: {
names: ['animate'],
attribute(start, end, type, name, expression) {
return { start, end, type, name, expression };
},
allowedExpressionTypes: ['ObjectExpression'],
error: 'Animation argument must be an object literal, e.g. `{ duration: 400 }`'
},
Action: {
names: [ 'use' ],
names: ['use'],
attribute(start, end, type, name, expression) {
return { start, end, type, name, expression };
},

@ -1,3 +1,5 @@
import { transitionManager, linear, generateRule, hash } from './transitions';
export function destroyBlock(block, lookup) {
block.d(1);
lookup[block.key] = null;
@ -95,4 +97,87 @@ export function updateKeyedEach(old_blocks, component, changed, get_key, dynamic
while (n) insert(new_blocks[n - 1]);
return new_blocks;
}
export function measure(blocks) {
const measurements = {};
let i = blocks.length;
while (i--) measurements[blocks[i].key] = blocks[i].node.getBoundingClientRect();
return measurements;
}
export function animate(blocks, rects, fn, params) {
let i = blocks.length;
while (i--) {
const block = blocks[i];
const from = rects[block.key];
if (!from) continue;
const to = block.node.getBoundingClientRect();
if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom) continue;
const info = fn(block.node, { from, to }, params);
const duration = 'duration' in info ? info.duration : 300;
const delay = 'delay' in info ? info.delay : 0;
const ease = info.easing || linear;
const start = window.performance.now() + delay;
const end = start + duration;
const program = {
a: 0,
t: 0,
b: 1,
delta: 1,
duration,
start,
end
};
const animation = {
pending: delay ? program : null,
program: delay ? null : program,
running: !delay,
start() {
if (info.css) {
const rule = generateRule(program, ease, info.css);
program.name = `__svelte_${hash(rule)}`;
transitionManager.addRule(rule, program.name);
block.node.style.animation = (block.node.style.animation || '')
.split(', ')
.filter(anim => anim && (program.delta < 0 || !/__svelte/.test(anim)))
.concat(`${program.name} ${program.duration}ms linear 1 forwards`)
.join(', ');
}
},
update: now => {
const p = now - program.start;
const t = program.a + program.delta * ease(p / program.duration);
if (info.tick) info.tick(t, 1 - t);
},
done() {
if (info.css) {
transitionManager.deleteRule(block.node, program.name);
}
if (info.tick) {
info.tick(1, 0);
}
animation.running = false;
}
};
transitionManager.add(animation);
if (info.tick) info.tick(0, 1);
if (!delay) animation.start();
}
}

@ -78,6 +78,7 @@ export default function validateElement(
let hasIntro: boolean;
let hasOutro: boolean;
let hasTransition: boolean;
let hasAnimation: boolean;
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Ref') {
@ -228,6 +229,40 @@ export default function validateElement(
message: `Missing transition '${attribute.name}'`
});
}
} else if (attribute.type === 'Animation') {
validator.used.animations.add(attribute.name);
if (hasAnimation) {
validator.error(attribute, {
code: `duplicate-animation`,
message: `An element can only have one 'animate' directive`
});
}
if (!validator.animations.has(attribute.name)) {
validator.error(attribute, {
code: `missing-animation`,
message: `Missing animation '${attribute.name}'`
});
}
const parent = stack[stack.length - 1];
if (!parent || parent.type !== 'EachBlock' || !parent.key) {
// TODO can we relax the 'immediate child' rule?
validator.error(attribute, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the immediate child of a keyed each block`
});
}
if (parent.children.length > 1) {
validator.error(attribute, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the sole child of a keyed each block`
});
}
hasAnimation = true;
} else if (attribute.type === 'Attribute') {
if (attribute.name === 'value' && node.name === 'textarea') {
if (node.children.length) {

@ -21,6 +21,7 @@ export class Validator {
components: Map<string, Node>;
methods: Map<string, Node>;
helpers: Map<string, Node>;
animations: Map<string, Node>;
transitions: Map<string, Node>;
actions: Map<string, Node>;
slots: Set<string>;
@ -29,6 +30,7 @@ export class Validator {
components: Set<string>;
helpers: Set<string>;
events: Set<string>;
animations: Set<string>;
transitions: Set<string>;
actions: Set<string>;
};
@ -47,6 +49,7 @@ export class Validator {
this.components = new Map();
this.methods = new Map();
this.helpers = new Map();
this.animations = new Map();
this.transitions = new Map();
this.actions = new Map();
this.slots = new Set();
@ -55,6 +58,7 @@ export class Validator {
components: new Set(),
helpers: new Set(),
events: new Set(),
animations: new Set(),
transitions: new Set(),
actions: new Set(),
};

@ -89,7 +89,7 @@ export default function validateJs(validator: Validator, js: Node) {
}
});
['components', 'methods', 'helpers', 'transitions', 'actions'].forEach(key => {
['components', 'methods', 'helpers', 'transitions', 'animations', 'actions'].forEach(key => {
if (validator.properties.has(key)) {
validator.properties.get(key).value.properties.forEach((prop: Node) => {
validator[key].set(getName(prop.key), prop.value);

@ -0,0 +1,21 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
export default function transitions(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
code: `invalid-transitions-property`,
message: `The 'transitions' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach(() => {
// TODO probably some validation that can happen here...
// checking for use of `this` etc?
});
}

@ -1,5 +1,6 @@
import data from './data';
import actions from './actions';
import animations from './animations';
import computed from './computed';
import oncreate from './oncreate';
import ondestroy from './ondestroy';
@ -23,6 +24,7 @@ import immutable from './immutable';
export default {
data,
actions,
animations,
computed,
oncreate,
ondestroy,

@ -0,0 +1,3 @@
{#each things as thing (thing)}
<div animate:flip>flips</div>
{/each}

@ -0,0 +1,59 @@
{
"html": {
"start": 0,
"end": 70,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 70,
"type": "EachBlock",
"expression": {
"type": "Identifier",
"start": 7,
"end": 13,
"name": "things"
},
"children": [
{
"start": 33,
"end": 62,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 38,
"end": 50,
"type": "Animation",
"name": "flip",
"expression": null
}
],
"children": [
{
"start": 51,
"end": 56,
"type": "Text",
"data": "flips"
}
]
}
],
"context": {
"start": 17,
"end": 22,
"type": "Identifier",
"name": "thing"
},
"key": {
"type": "Identifier",
"start": 24,
"end": 29,
"name": "thing"
}
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,62 @@
export default {
data: {
things: [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' },
{ id: 5, name: 'e' }
]
},
html: `
<div>a</div>
<div>b</div>
<div>c</div>
<div>d</div>
<div>e</div>
`,
test(assert, component, target, window, raf) {
let divs = document.querySelectorAll('div');
divs.forEach(div => {
div.getBoundingClientRect = function() {
const index = [...this.parentNode.children].indexOf(this);
const top = index * 30;
return {
left: 0,
right: 100,
top,
bottom: top + 20
}
};
})
const bcr1 = divs[0].getBoundingClientRect();
const bcr2 = divs[4].getBoundingClientRect();
component.set({
things: [
{ id: 5, name: 'e' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' },
{ id: 1, name: 'a' }
]
});
divs = document.querySelectorAll('div');
assert.ok(~divs[0].style.animation.indexOf('__svelte'));
assert.equal(divs[1].style.animation, undefined);
assert.equal(divs[2].style.animation, undefined);
assert.equal(divs[3].style.animation, undefined);
assert.ok(~divs[4].style.animation.indexOf('__svelte'));
raf.tick(100);
assert.deepEqual([
divs[0].style.animation,
divs[4].style.animation
], ['', '']);
}
};

@ -0,0 +1,19 @@
{#each things as thing (thing.id)}
<div animate:flip>{thing.name}</div>
{/each}
<script>
export default {
animations: {
flip(node, animation, params) {
const dx = animation.from.left - animation.to.left;
const dy = animation.from.top - animation.to.top;
return {
duration: 100,
css: (t, u) => `transform: translate(${u + dx}px, ${u * dy}px)`
};
}
}
};
</script>

@ -0,0 +1,61 @@
export default {
data: {
things: [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' },
{ id: 5, name: 'e' }
]
},
html: `
<div>a</div>
<div>b</div>
<div>c</div>
<div>d</div>
<div>e</div>
`,
test(assert, component, target, window, raf) {
let divs = document.querySelectorAll('div');
divs.forEach(div => {
div.getBoundingClientRect = function() {
const index = [...this.parentNode.children].indexOf(this);
const top = index * 30;
return {
left: 0,
right: 100,
top,
bottom: top + 20
}
};
})
const bcr1 = divs[0].getBoundingClientRect();
const bcr2 = divs[4].getBoundingClientRect();
component.set({
things: [
{ id: 5, name: 'e' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' },
{ id: 1, name: 'a' }
]
});
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);
assert.equal(divs[4].dy, -120);
raf.tick(50);
assert.equal(divs[0].dy, 60);
assert.equal(divs[4].dy, -60);
raf.tick(100);
assert.equal(divs[0].dy, 0);
assert.equal(divs[4].dy, 0);
}
};

@ -0,0 +1,22 @@
{#each things as thing (thing.id)}
<div animate:flip>{thing.name}</div>
{/each}
<script>
export default {
animations: {
flip(node, animation, params) {
const dx = animation.from.left - animation.to.left;
const dy = animation.from.top - animation.to.top;
return {
duration: 100,
tick: (t, u) => {
node.dx = u * dx;
node.dy = u * dy;
}
};
}
}
};
</script>

@ -0,0 +1,15 @@
[{
"code": "duplicate-animation",
"message": "An element can only have one 'animate' directive",
"start": {
"line": 2,
"column": 18,
"character": 50
},
"end": {
"line": 2,
"column": 29,
"character": 61
},
"pos": 50
}]

@ -0,0 +1,17 @@
{#each things as thing (thing)}
<div animate:foo animate:bar></div>
{/each}
<script>
export default {
animations: {
foo(node, animation, params) {
// ...
},
bar(node, animation, params) {
// ...
}
}
};
</script>

@ -0,0 +1,15 @@
[{
"code": "missing-animation",
"message": "Missing animation 'foo'",
"start": {
"line": 2,
"column": 6,
"character": 38
},
"end": {
"line": 2,
"column": 17,
"character": 49
},
"pos": 38
}]

@ -0,0 +1,3 @@
{#each things as thing (thing)}
<div animate:foo></div>
{/each}

@ -0,0 +1,15 @@
[{
"code": "invalid-animation",
"message": "An element that use the animate directive must be the immediate child of a keyed each block",
"start": {
"line": 1,
"column": 5,
"character": 5
},
"end": {
"line": 1,
"column": 16,
"character": 16
},
"pos": 5
}]

@ -0,0 +1,11 @@
<div animate:foo></div>
<script>
export default {
animations: {
foo(node, animation, params) {
// ...
}
}
};
</script>

@ -0,0 +1,15 @@
[{
"code": "invalid-animation",
"message": "An element that use the animate directive must be the immediate child of a keyed each block",
"start": {
"line": 2,
"column": 6,
"character": 30
},
"end": {
"line": 2,
"column": 17,
"character": 41
},
"pos": 30
}]

@ -0,0 +1,14 @@
{#each things as thing}
<div animate:foo></div>
<div animate:foo></div>
{/each}
<script>
export default {
animations: {
foo(node, animation, params) {
// ...
}
}
};
</script>

@ -0,0 +1,15 @@
[{
"code": "invalid-animation",
"message": "An element that use the animate directive must be the sole child of a keyed each block",
"start": {
"line": 2,
"column": 6,
"character": 38
},
"end": {
"line": 2,
"column": 17,
"character": 49
},
"pos": 38
}]

@ -0,0 +1,14 @@
{#each things as thing (thing)}
<div animate:foo></div>
<div animate:foo></div>
{/each}
<script>
export default {
animations: {
foo(node, animation, params) {
// ...
}
}
};
</script>
Loading…
Cancel
Save