Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] animations #1454

Merged
merged 6 commits into from
May 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/compile/Compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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');
Expand Down
11 changes: 11 additions & 0 deletions src/compile/dom/Block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default class Block {
};

maintainContext: boolean;
animation?: string;
hasIntroMethod: boolean;
hasOutroMethod: boolean;
outros: number;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions src/compile/nodes/Animation.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 3 additions & 1 deletion src/compile/nodes/EachBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
35 changes: 17 additions & 18 deletions src/compile/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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>
Expand Down Expand Up @@ -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`);
Expand All @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -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);
Expand Down
11 changes: 10 additions & 1 deletion src/parse/read/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
},
Expand Down
85 changes: 85 additions & 0 deletions src/shared/keyed-each.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { transitionManager, linear, generateRule, hash } from './transitions';

export function destroyBlock(block, lookup) {
block.d(1);
lookup[block.key] = null;
Expand Down Expand Up @@ -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();
}
}
35 changes: 35 additions & 0 deletions src/validate/html/validateElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions src/validate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand All @@ -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>;
};
Expand All @@ -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();
Expand All @@ -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(),
};
Expand Down
Loading