svg-filter-animation
Version:
Plays given SVG-filter animations.
206 lines (180 loc) • 6.79 kB
JavaScript
const $ = require('jquery');
const tween = require('./tween-functions');
const SVGFilterAnimation = {};
SVGFilterAnimation.module = (function () {
let keyframePath = '';
let filterContainerId = '';
let keyframeTracks = null;
let $filter;
/**
* Read keyframes from JSON file
* @returns {*}
* @private
*/
function _loadJSON () {
return (function() {
let jsonData = null;
$.ajax({
'async': false,
'global': false,
'url': keyframePath,
'dataType': 'json',
'success': function (data) {
jsonData = data;
}
});
return jsonData;
})();
}
/**
* Trigger animation playback based on loaded keyframes
* @private
*/
function _triggerAnimation () {
if(keyframeTracks.length > 0) {
for (let i = 0; i < keyframeTracks.length; i++) {
filterContainerId = keyframeTracks[i].filterContainerId;
$filter = $('#' + filterContainerId);
_playTrack(keyframeTracks[i]);
}
}
}
/**
* Play an animation track
* @param track
* @private
*/
function _playTrack (track) {
let start = null;
let firstKey = track.keyframes[0];
let lastKey = track.keyframes[track.keyframes.length - 1];
let trackDuration = lastKey.timestamp - firstKey.timestamp;
let keyframes = track.keyframes;
function trackLoop (timestamp) {
if (!start) start = timestamp;
let progress = timestamp - start;
// Iterate keyframes
for (let keyframeIndex = 0; keyframeIndex < keyframes.length; keyframeIndex++) {
// Trigger keyframe playback based on timestamp
if (keyframes[keyframeIndex].timestamp <= progress) {
// Get start- and end-key pair
let startKey = keyframes[keyframeIndex];
let endKey = undefined;
if (keyframeIndex + 1 < keyframes.length) {
endKey = keyframes[keyframeIndex + 1]
}
// Trigger animation playback for current pair
if (startKey && endKey) {
let duration = endKey.timestamp - startKey.timestamp;
_playAnimation(parseInt(track.filterId), track.propertyName, track.propertyIndex, duration, startKey.propertyValue, endKey.propertyValue);
keyframes.shift(); // Remove current start key from list
keyframeIndex = -1; // Reset key index
}
}
}
if (progress < trackDuration) {
window.requestAnimationFrame(trackLoop);
}
}
window.requestAnimationFrame(trackLoop);
}
/**
* Play a single animation
* @param filterId
* @param propertyName
* @param propertyIndex
* @param duration
* @param startValue
* @param endValue
* @private
*/
function _playAnimation (filterId, propertyName, propertyIndex, duration, startValue, endValue) {
let start = null;
let $filterNode = $filter.find('#' + filterContainerId + filterId.toString());
function step (timestamp) {
if (!start) start = timestamp;
let progress = timestamp - start;
if ($filterNode.length) {
_setPropertyValue($filterNode, propertyName, propertyIndex, tween.easeInOutCubic(progress, startValue, endValue, duration));
_updateContainer();
}
if (progress < duration) {
window.requestAnimationFrame(step);
} else {
// Clean value
if ($filterNode.length) {
_setPropertyValue($filterNode, propertyName, propertyIndex, endValue);
}
}
}
window.requestAnimationFrame(step);
}
/**
* Set property value at given positon to given value
* @param $filterNode
* @param propertyName
* @param propertyIndex
* @param propertyValue
* @private
*/
function _setPropertyValue ($filterNode, propertyName, propertyIndex, propertyValue) {
/** Special nodes **/
switch ($filterNode[0].tagName) {
case 'feComponentTransfer': {
// Extract color channel from property name
let channel = propertyName.charAt(4);
let $childNode = $filterNode.find('feFunc' + channel);
// Extract final property attribute name
const allowedPropertyNames = ['TableValues', 'Slope', 'Intercept', 'Amplitude', 'Exponent', 'Offset'];
let childPropertyName = '';
for (let i = 0; i < allowedPropertyNames.length; i++) {
if (propertyName.includes(allowedPropertyNames[i])) {
childPropertyName = allowedPropertyNames[i];
childPropertyName = childPropertyName.charAt(0).toLowerCase() + childPropertyName.slice(1);
}
}
$filterNode = $childNode;
propertyName = childPropertyName;
break;
}
case 'feColorMatrix': {
propertyName = 'values';
break;
}
case 'feFlood': {
propertyName = 'flood-opacity';
break;
}
}
let properties = $filterNode.attr(propertyName).split(' ');
properties[propertyIndex] = propertyValue;
let outputString = properties.join(' ');
$filterNode.attr(propertyName, outputString);
}
/**
* Trigger container update
* @private
*/
function _updateContainer () {
$filter.redraw();
}
$.fn.redraw = function() {
return this.hide(0, function() {
$(this).show();
});
};
function init (keyframeFile) {
// Set keyframe file path
keyframePath = keyframeFile;
// Load JSON data
keyframeTracks = _loadJSON();
// Start animation
_triggerAnimation();
}
return {
init: init
};
})();
exports.SVGFilterAnimation = function (keyframeFile) {
SVGFilterAnimation.module.init(keyframeFile);
};