refactor(ripple): web animations api

This commit is contained in:
Adam Bradley
2015-09-23 11:31:31 -05:00
parent 8f76057147
commit 1d275497f1
8 changed files with 127 additions and 141 deletions

View File

@@ -1,8 +1,6 @@
import {CSS} from '../util/dom';
import {extend} from '../util/util';
const RENDER_DELAY = 36;
let AnimationRegistry = {};
/**
Animation Steps/Process
@@ -26,10 +24,13 @@ let AnimationRegistry = {};
export class Animation {
constructor(ele) {
constructor(ele, opts={}) {
this._el = [];
this._chld = [];
this._ani = [];
this._opts = extend({
renderDelay: 36
}, opts);
this._bfAdd = [];
this._bfSty = {};
@@ -238,10 +239,10 @@ export class Animation {
});
}
if (this._duration > RENDER_DELAY) {
if (self._duration > this._opts.renderDelay) {
// begin each animation when everything is rendered in their starting point
// give the browser some time to render everything in place before starting
setTimeout(kickoff, RENDER_DELAY);
setTimeout(kickoff, this._opts.renderDelay);
} else {
// no need to render everything in there place before animating in
@@ -294,7 +295,8 @@ export class Animation {
this._to,
this.duration(),
this.easing(),
this.playbackRate() );
this.playbackRate(),
this._opts.renderDelay );
if (animation.shouldAnimate) {
this._ani.push(animation);
@@ -510,7 +512,7 @@ export class Animation {
class Animate {
constructor(ele, fromEffect, toEffect, duration, easingConfig, playbackRate) {
constructor(ele, fromEffect, toEffect, duration, easingConfig, playbackRate, renderDelay) {
// https://w3c.github.io/web-animations/
// not using the direct API methods because they're still in flux
// however, element.animate() seems locked in and uses the latest
@@ -522,7 +524,7 @@ class Animate {
this.toEffect = parseEffect(toEffect);
this.shouldAnimate = (duration > RENDER_DELAY);
this.shouldAnimate = (duration > renderDelay);
if (!this.shouldAnimate) {
return inlineStyle(ele, this.toEffect);
@@ -864,3 +866,5 @@ const EASING_FN = {
}
};
let AnimationRegistry = {};

View File

@@ -5,8 +5,9 @@
$button-md-font-size: 1.4rem !default;
$button-md-min-height: 3.6rem !default;
$button-md-padding: 0 1.1em !default;
$button-md-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12) !default;
$button-md-box-shadow-active: 0 4px 5px 0 rgba(0, 0, 0, 0.14),0 1px 10px 0 rgba(0, 0, 0, 0.12),0 2px 4px -1px rgba(0, 0, 0, 0.2); //0 1px 3px 0 rgba(0, 0, 0, 0.3); //0 2px 5px 0 rgba(0, 0, 0, 0.26) !default;
$button-md-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12) !default;
$button-md-box-shadow-active: 0 3px 5px rgba(0, 0, 0, 0.14), 0 3px 5px rgba(0, 0, 0, 0.21) !default;
$button-md-border-radius: 2px !default;
$button-md-animation-curve: cubic-bezier(0.4, 0, 0.2, 1) !default;
$button-md-transition-duration: 300ms !default;

View File

@@ -5,53 +5,34 @@ export class Activator {
constructor(app, config) {
this.app = app;
this.id = 0;
this.queue = {};
this.active = {};
this.active = [];
this.clearStateTimeout = 180;
this.clearAttempt = 0;
this.activatedClass = config.setting('activatedClass') || 'activated';
this.x = 0;
this.y = 0;
this.lastActivate = 0;
}
downAction(targetEle, pointerX, pointerY, callback) {
// the user just pressed down
// throttle how many activates fire off in XXms
let now = Date.now();
if (this.lastActivate + 50 > now) return;
this.lastActivate = now;
// remember where they pressed
this.x = pointerX;
this.y = pointerY;
// remember this is the active element
let id = ++this.id;
this.queue[id] = targetEle;
if (this.id > 9) this.id = 0;
// activate targetEle
raf(() => {
for (let eleId in this.queue) {
this.queue[eleId].classList.add(this.activatedClass);
this.active[eleId] = this.queue[eleId];
}
callback && callback(targetEle);
this.queue = {};
});
targetEle.classList.add(this.activatedClass);
this.active.push(targetEle);
}
upAction(ev) {
upAction() {
// the user was pressing down, then just let up
setTimeout(() => {
this.clearState();
}, this.clearStateTimeout);
}
clearState(ev) {
clearState() {
// all states should return to normal
if (!this.app.isEnabled() && this.clearAttempt < 30) {
@@ -63,18 +44,17 @@ export class Activator {
} else {
// not actively transitioning, good to deactivate any elements
this.queue = {};
// remove the active class from all active elements
for (var key in this.active) {
if (this.active[key]) {
this.active[key].classList.remove(this.activatedClass);
delete this.active[key];
}
}
this.deactivate();
this.clearAttempt = 0;
}
}
deactivate() {
// remove the active class from all active elements
for (let i = 0; i < this.active.length; i++) {
this.active[i].classList.remove(this.activatedClass);
}
this.active = [];
}
}

View File

@@ -2,8 +2,7 @@
// Material Design Ripple Effect
// --------------------------------------------------
$ripple-background-color: #fff !default;
$ripple-animation-curve: cubic-bezier(0, 0, 0.2, 1) !default;
$ripple-background-color: rgba(0, 0, 0, 0.1) !default;
md-ripple {
@@ -13,20 +12,9 @@ md-ripple {
border-radius: 50%;
background: $ripple-background-color;
opacity: 0.3;
overflow: hidden;
pointer-events: none;
transform: scale(0.001) translateZ(0);
transition: transform 280ms $ripple-animation-curve, opacity 400ms ease;
}
md-ripple.ripple-expand {
transform: scale(1) translateZ(0);
}
md-ripple.ripple-fade-out {
transform: scale(1) translateZ(0);
opacity: 0;
}

View File

@@ -1,108 +1,117 @@
import {Activator} from './activator';
import {raf, transitionEnd} from '../../util/dom';
import {raf, removeElement} from '../../util/dom';
import {Animation} from '../../animations/animation';
export class RippleActivator extends Activator {
constructor(app, config) {
super(app, config);
this.ripples = {};
}
downAction(targetEle, pointerX, pointerY) {
super.downAction(targetEle, pointerX, pointerY, (targetEle) => {
super.downAction(targetEle, pointerX, pointerY);
if (!targetEle || !targetEle.parentNode || NO_RIPPLE_TAGNAMES.test(targetEle.tagName)) return;
if (!isRippleElement(targetEle)) return;
// clean out any existing ripple elements
removeAll(targetEle);
// create a new ripple element
let r = targetEle.getBoundingClientRect();
let x = Math.max(Math.abs(r.width - pointerX), pointerX) * 2;
let y = Math.max(Math.abs(r.height - pointerY), pointerY) * 2;
let size = (Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))) - 10;
// create a new ripple element
let r = targetEle.getBoundingClientRect();
let rippleSize = Math.sqrt(r.width * r.width + r.height * r.height) * 2 + 2;
let rippleEle = document.createElement('md-ripple');
let eleStyle = rippleEle.style;
eleStyle.width = size + 'px';
eleStyle.height = size + 'px';
eleStyle.marginTop = -(size / 2) + 'px';
eleStyle.marginLeft = -(size / 2) + 'px';
eleStyle.left = (pointerX - r.left) + 'px';
eleStyle.top = (pointerY - r.top) + 'px';
let rippleEle = document.createElement('md-ripple');
rippleEle.style.width = rippleSize + 'px';
rippleEle.style.height = rippleSize + 'px';
rippleEle.style.marginTop = -(rippleSize / 2) + 'px';
rippleEle.style.marginLeft = -(rippleSize / 2) + 'px';
rippleEle.style.left = (pointerX - r.left) + 'px';
rippleEle.style.top = (pointerY - r.top) + 'px';
targetEle.appendChild(rippleEle);
targetEle.appendChild(rippleEle);
let ripple = this.ripples[Date.now()] = { ele: rippleEle };
transitionEnd(rippleEle).then(ev => {
// the ripple animation has ended
ev.target.dataset.expanded = true;
fadeOutRipple(ev.target);
});
// expand the circle from the users starting point
// start slow, and when they let up, then speed up the animation
ripple.expand = new Animation(rippleEle, {renderDelay: 0});
ripple.expand
.fromTo('scale', '0.001', '1')
.duration(300)
.playbackRate(0.35)
.onFinish(()=> {
// finished expanding
ripple.expand && ripple.expand.dispose();
ripple.expand = null;
ripple.expanded = true;
this.next();
})
.play();
// kick off animaiton
raf(() => {
rippleEle.classList.add('ripple-expand');
});
});
this.next();
}
upAction(ev) {
// the user was pressing down, then just let up
super.upAction(ev);
upAction() {
this.deactivate();
// immediately remove the activated css class and clear the queue
// this stops the background from changing colors, not stop the ripple
this.queue = {};
let ripple;
for (let rippleId in this.ripples) {
ripple = this.ripples[rippleId];
if (ev && ev.target) {
raf(() => {
let targetEle = ev.target;
if (targetEle && targetEle.parentNode) {
targetEle.classList.remove(this.activatedClass);
if (!ripple.fade) {
// ripple has not been let up yet
// spped up the rate if the animation is still going
setTimeout(() => {
ripple.expand && ripple.expand.playbackRate(1);
ripple.fade = new Animation(ripple.ele);
ripple.fade
.fadeOut()
.duration(750)
.onFinish(() => {
ripple.fade && ripple.fade.dispose();
ripple.fade = null;
ripple.faded = true;
this.next();
})
.play();
let rippleElements = getRippleElements(targetEle);
for (let i = 0, l = rippleElements.length; i < l; i++) {
rippleElements[i].dataset.pointerUp = true;
fadeOutRipple(rippleElements[i]);
}
}
});
}
}
clearState(ev) {
super.clearState(ev);
if (ev && ev.target) {
removeAll(ev.target);
}
}
}
function fadeOutRipple(rippleEle) {
if (rippleEle && rippleEle.dataset.pointerUp && rippleEle.dataset.expanded && !rippleEle.dataset.removing) {
transitionEnd(rippleEle).then(ev => {
// the ripple has faded out completely
let rippleEle = ev.target;
if (rippleEle.parentNode) {
rippleEle.parentNode.removeChild(rippleEle);
}, 16);
}
});
}
// start fading out the ripple
rippleEle.classList.add('ripple-fade-out');
rippleEle.dataset.removing = true;
this.next();
}
next(forceComplete) {
let ripple, rippleEle;
for (let rippleId in this.ripples) {
ripple = this.ripples[rippleId];
if ((ripple.expanded && ripple.faded && ripple.ele) ||
forceComplete ||
parseInt(rippleId) + 5000 < Date.now()) {
// finished expanding and the user has lifted the pointer
ripple.expand && ripple.expand.dispose();
ripple.fade && ripple.fade.dispose();
removeElement(ripple.ele);
ripple.ele = ripple.expand = ripple.fade = null;
delete this.ripples[rippleId];
}
}
}
clearState() {
this.deactivate();
this.next(true);
}
}
function removeAll(targetEle) {
let rippleElements = getRippleElements(targetEle);
for (let i = 0; i < rippleElements.length; i++) {
rippleElements[i].dataset.pointerUp = rippleElements[i].dataset.expanded = true;
fadeOutRipple(rippleElements[i]);
}
}
function getRippleElements(containerEle) {
return containerEle && containerEle.querySelectorAll('md-ripple');
function isRippleElement(targetEle) {
return (targetEle && targetEle.parentNode && !(NO_RIPPLE_TAGNAMES.test(targetEle.tagName)));
}
const NO_RIPPLE_TAGNAMES = /BACKDROP/;

View File

@@ -157,7 +157,7 @@ export class TapClick {
* TODO
*/
pointerEnd(ev) {
this.activator.upAction(ev);
this.activator.upAction();
this.moveListeners(false);
}
@@ -166,7 +166,7 @@ export class TapClick {
*/
pointerCancel(ev) {
console.debug('pointerCancel')
this.activator.clearState(ev);
this.activator.clearState();
this.moveListeners(false);
this.disableClick = Date.now();
}

View File

@@ -195,7 +195,7 @@ export function hasFocusedTextInput() {
return false;
}
export function closest(el, selector) {
export function closest(ele, selector) {
var matchesFn;
// find vendor prefix
@@ -208,17 +208,21 @@ export function closest(el, selector) {
})
// traverse parents
while (el!==null) {
parent = el.parentElement;
while (ele !== null) {
parent = ele.parentElement;
if (parent!==null && parent[matchesFn](selector)) {
return parent;
}
el = parent;
ele = parent;
}
return null;
}
export function removeElement(ele) {
ele && ele.parentNode && ele.parentNode.removeChild(ele);
}
/**
* Get the element offsetWidth and offsetHeight. Values are cached

View File

@@ -27,7 +27,7 @@
@if ($lightness-percent < 35) {
// dark foreground color, so light it up
@return lighten($color-value, $amount);
@return lighten($color-value, $amount * 2);
}
// default to darken