react-spark-scroll
Version:
Scroll-based animation and actions for React
423 lines (357 loc) • 12.4 kB
JavaScript
;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var AnimationFrame = require('animation-frame');
var EventEmitter = require('events').EventEmitter;
var _isArray = function _isArray(x) {
return x instanceof Array;
};
function _isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}
function sparkFactory(_ref4) {
var animator = _ref4.animator;
var formulas = _ref4.formulas;
var actionProps = _ref4.actionProps;
var setup = _ref4.setup;
var invalidateAutomatically = _ref4.invalidateAutomatically;
var allowAnimation = !!animator;
var sparkFormulas = formulas ? _extends({}, _sparkFormulas, formulas) : _sparkFormulas;
var sparkActionProps = actionProps ? _extends({}, _sparkActionProps, actionProps) : _sparkActionProps;
var sparkSetup = setup ? _extends({}, _sparkSetup, setup) : _sparkSetup;
var eventEmitter = new EventEmitter();
eventEmitter.setMaxListeners(0);
var spark = function spark(element, proxyElementFn, timeline, options) {
var callback = options.callback;
var prevRatio = 0;
var minScrollY = 0;
var maxScrollY = 0;
var sparkAnimator = allowAnimation && animator.instance();
var actor = sparkAnimator && sparkAnimator.addActor({
context: element
});
var isAnimated = true;
var actionFrames = [];
var actionFrameIdx = -1;
var y = 0;
var prevy = 0;
var scrollY = 0;
var animationFrame = new AnimationFrame();
var updating = false;
var data = null;
var sparkData = null;
var container = document.documentElement;
var actionsUpdate = function actionsUpdate() {
var a, actionProp, c, d, idx, o, prop, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
d = y - prevy;
if (d < 0 && actionFrameIdx >= 0) {
idx = actionFrameIdx >= actionFrames.length ? actionFrameIdx - 1 : actionFrameIdx;
while (idx >= 0 && y < actionFrames[idx]) {
c = sparkData[actionFrames[idx]];
_ref = c.actions;
for (a in _ref) {
o = _ref[a];
_ref1 = o.props;
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
prop = _ref1[_i];
actionProp = sparkActionProps[prop];
if (actionProp.up) {
actionProp.up.call(c, o);
}
}
}
actionFrameIdx = --idx;
}
}
if (d >= 0 && actionFrameIdx < actionFrames.length) {
idx = actionFrameIdx < 0 ? 0 : actionFrameIdx;
while (idx < actionFrames.length && y > actionFrames[idx]) {
c = sparkData[actionFrames[idx]];
_ref2 = c.actions;
for (a in _ref2) {
o = _ref2[a];
_ref3 = o.props;
for (_j = 0, _len1 = _ref3.length; _j < _len1; _j++) {
prop = _ref3[_j];
actionProp = sparkActionProps[prop];
if (actionProp.down) {
actionProp.down.call(c, o);
}
}
}
actionFrameIdx = ++idx;
}
}
prevy = y;
updating = false;
};
var update = function update() {
y = scrollY;
sparkAnimator.update(y);
callback && doCallback();
actionsUpdate(); // sets updating = false
};
var recalcMinMax = function recalcMinMax() {
var scrY,
idx = 0;
for (scrY in sparkData) {
scrY = ~ ~scrY;
if (idx++) {
if (scrY > maxScrollY) maxScrollY = scrY;else if (scrY < minScrollY) minScrollY = scrY;
} else {
maxScrollY = minScrollY = scrY;
}
}
};
var doCallback = function doCallback() {
var ratio = Math.max(0, Math.min((y - minScrollY) / (maxScrollY - minScrollY), 1));
if (ratio !== prevRatio) {
callback(ratio);
}
prevRatio = ratio;
};
var recalcFormulas = function recalcFormulas() {
var changed, containerRect, keyFrame, kf, newScrY, rect, scrY;
if (!sparkData) {
parseData();
if (!sparkData) return;
}
changed = false;
rect = proxyElementFn().getBoundingClientRect();
containerRect = container.getBoundingClientRect();
for (scrY in sparkData) {
keyFrame = sparkData[scrY];
if (!keyFrame.formula) continue;
newScrY = keyFrame.formula.fn(proxyElementFn(), container, rect, containerRect, keyFrame.formula.offset);
if (newScrY !== ~ ~scrY) {
changed = true;
if (keyFrame.anims && allowAnimation) {
actor.moveKeyframe(~ ~scrY, newScrY);
}
sparkData[newScrY] = keyFrame;
delete sparkData[scrY];
}
}
if (changed) {
if (callback) {
recalcMinMax();
}
actionFrames = [];
for (scrY in sparkData) {
kf = sparkData[scrY];
if (kf.actionCount) {
actionFrames.push(~ ~scrY);
}
}
actionFrames.sort(function (a, b) {
return a > b;
});
onScroll();
}
};
var parseData = function parseData(data) {
var c, ease;
var k, o, parts, rect, scrY, formula;
if (!data) {
return;
}
data = _extends({}, data);
if (allowAnimation) {
actor.removeAllKeyframes();
}
var elmEase = data.ease || 'linear';
delete data.ease;
var animCount = 0;
sparkData = {};
actionFrames = [];
rect = proxyElementFn().getBoundingClientRect();
var containerRect = container.getBoundingClientRect();
for (scrY in data) {
var keyFrame = data[scrY] || {};
keyFrame = _extends({}, keyFrame);
var actionCount = 0;
c = scrY.charCodeAt(0);
if (c < 48 || c > 57) {
parts = scrY.match(/^(\w+)(.*)$/);
formula = {
fn: sparkFormulas[parts[1]],
offset: ~ ~parts[2]
};
scrY = formula.fn(proxyElementFn(), container, rect, containerRect, formula.offset);
if (sparkData[scrY]) {
if (sparkSetup.debug) {
console.log("warning: spark-scroll failed to calculate formulas", data);
}
sparkData = null;
return;
}
}
ease = {};
var kfEase = elmEase;
if (keyFrame.ease != null) {
if (_isObject(keyFrame.ease)) {
ease = keyFrame.ease;
} else {
kfEase = keyFrame.ease;
}
delete keyFrame.ease;
}
for (k in keyFrame) {
var v = keyFrame[k];
var ksplit = k.split(',');
if (sparkActionProps[ksplit[0]]) {
keyFrame.actions || (keyFrame.actions = {});
keyFrame.actions[k] = {
props: ksplit,
val: v
};
delete keyFrame[k];
actionCount++;
} else {
keyFrame.anims || (keyFrame.anims = {});
if (!_isArray(v)) {
v = [v, kfEase];
}
o = {};
o[k] = v[1];
Object.assign(ease, o);
keyFrame.anims[k] = v[0];
delete keyFrame[k];
}
}
if (keyFrame.anims && allowAnimation) {
actor.keyframe(scrY, keyFrame.anims, ease);
animCount++;
}
keyFrame.formula = formula;
keyFrame.element = element;
keyFrame.actionCount = actionCount;
sparkData[scrY] = keyFrame;
if (actionCount) {
actionFrames.push(~ ~scrY);
}
}
isAnimated = !!animCount;
if (isAnimated) {
actor.finishedAddingKeyframes && actor.finishedAddingKeyframes();
}
actionFrames.sort(function (a, b) {
return a > b;
});
if (callback) {
recalcMinMax();
}
y = prevy = scrollY = window.pageYOffset;
if (isAnimated) {
update();
}
return actionsUpdate();
};
var nonAnimatedUpdate = function nonAnimatedUpdate() {
if (callback) {
doCallback();
}
return actionsUpdate();
};
var onScroll = function onScroll() {
scrollY = window.pageYOffset;
if (!updating) {
updating = true;
if (isAnimated) {
return animationFrame.request(update);
} else {
y = scrollY;
return animationFrame.request(nonAnimatedUpdate);
}
}
};
var onInvalidate = debounce(recalcFormulas, 100);
window.addEventListener('scroll', onScroll, false);
window.addEventListener('resize', onInvalidate, false);
eventEmitter.on('invalidate', onInvalidate);
spark.cleanup = function () {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onInvalidate);
eventEmitter.removeListener('invalidate', onInvalidate);
};
// delay parse a frame to allow proxy to render
animationFrame.request(parseData.bind(null, timeline));
};
spark.invalidate = function () {
return eventEmitter.emit('invalidate');
};
var invalidationInterval;
spark.enableInvalidationInterval = function (delay) {
delay = delay || 1000;
clearInterval(invalidationInterval);
invalidationInterval = setInterval(spark.invalidate, delay);
};
spark.disableInvalidationInterval = function (delay) {
clearInterval(invalidationInterval);
};
if (invalidateAutomatically) {
spark.enableInvalidationInterval(invalidateAutomatically > 1 ? invalidateAutomatically : null);
}
return spark;
}
function debounce(fn, wait) {
var t;
return function () {
clearTimeout(t);
t = setTimeout(fn, wait);
};
}
var _sparkSetup = {
debug: true
};
var _sparkFormulas = {
// top of the element hits the top of the viewport
topTop: function topTop(element, container, rect, containerRect, offset) {
return ~ ~(rect.top - containerRect.top + offset);
},
// top of the element hits the center of the viewport
topCenter: function topCenter(element, container, rect, containerRect, offset) {
return ~ ~(rect.top - containerRect.top - container.clientHeight / 2 + offset);
},
// top of the element hits the bottom of the viewport
topBottom: function topBottom(element, container, rect, containerRect, offset) {
return ~ ~(rect.top - containerRect.top - container.clientHeight + offset);
},
// center of the element hits the top of the viewport
centerTop: function centerTop(element, container, rect, containerRect, offset) {
return ~ ~(rect.top + rect.height / 2 - containerRect.top + offset);
},
// center of the element hits the center of the viewport
centerCenter: function centerCenter(element, container, rect, containerRect, offset) {
return ~ ~(rect.top + rect.height / 2 - containerRect.top - container.clientHeight / 2 + offset);
},
// center of the element hits the bottom of the viewport
centerBottom: function centerBottom(element, container, rect, containerRect, offset) {
return ~ ~(rect.top + rect.height / 2 - containerRect.top - container.clientHeight + offset);
},
// bottom of the element hits the top of the viewport
bottomTop: function bottomTop(element, container, rect, containerRect, offset) {
return ~ ~(rect.bottom - containerRect.top + offset);
},
// bottom of the element hits the bottom of the viewport
bottomBottom: function bottomBottom(element, container, rect, containerRect, offset) {
return ~ ~(rect.bottom - containerRect.top - container.clientHeight + offset);
},
// bottom of the element hits the center of the viewport
bottomCenter: function bottomCenter(element, container, rect, containerRect, offset) {
return ~ ~(rect.bottom - containerRect.top - container.clientHeight / 2 + offset);
}
};
var _sparkActionProps = {
onDown: {
down: function down(o) {
return o.val('onDown', this, o);
}
},
onUp: {
up: function up(o) {
return o.val('onUp', this, o);
}
}
};
module.exports = sparkFactory;