immaterial-design-ripple
Version:
HTML5 Canvas based pixelated ripple effect
276 lines (237 loc) • 8.85 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.requestAnimationFrame = requestAnimationFrame;
exports.promiseEvent = promiseEvent;
exports.createContext2d = createContext2d;
exports.getImageData = getImageData;
exports.transparentize = transparentize;
exports.getTimingFunction = getTimingFunction;
exports.createRenderSchedule = createRenderSchedule;
exports.getPixelColor = getPixelColor;
var _bluebird = require('bluebird');
var _bluebird2 = _interopRequireDefault(_bluebird);
var _easingJs = require('easing-js');
var _easingJs2 = _interopRequireDefault(_easingJs);
var _objectAssign = require('object-assign');
var _objectAssign2 = _interopRequireDefault(_objectAssign);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* 利用可能な非同期関数でcallbackを実行する
*
* @function requestAnimationFrame
* @param {Function} [callback]
* @return undefined
*/
function requestAnimationFrame(callback) {
return window.requestAnimationFrame(callback);
}
/**
* 指定した要素のイベントを待つプロミスを返す
*
* @function promiseEvent
* @param {Element} target イベントを取得する要素
* @param {String} eventName 取得するイベント名
* @return {Promise<EventTarget>} deferredEvent 取得したイベント
*/
function promiseEvent(target, eventName) {
return new _bluebird2.default(function (resolve) {
var onceListener = function onceListener(event) {
target.removeEventListener(eventName, onceListener);
resolve(event);
};
target.addEventListener(eventName, onceListener);
});
}
/**
* 指定した大きさのcontext2dを返す
*
* @function createContext2d
* @param {Number} width contextの幅
* @param {Number} height contextの高さ
* @param {Object} [options]
* @param {Object} [options.pixelated=true] アンチエイリアスを切る
* @return {CanvasRenderingContext2D}
*/
function createContext2d(width, height) {
var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.right = 0;
canvas.style.bottom = 0;
canvas.style.left = 0;
var context = canvas.getContext('2d');
if (options.pixelated) {
context.mozImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
}
return context;
}
/**
* contextと同じ大きさの空のimageDataを返す
*
* @function getImageData
* @param {HTMLCanvasElement} canvas 大きさの基準となるcanvas
* @return {ImageData}
*/
function getImageData(canvas) {
var width = canvas.width;
var height = canvas.height;
var newContext = document.createElement('canvas').getContext('2d');
newContext.canvas.width = width;
newContext.canvas.height = height;
return newContext.getImageData(0, 0, width, height);
}
/**
* canvasを透明化、opacity:0でcanvasを破棄
*
* @function transparentize
* @param {Element} element 透明化させ、破棄する要素
* @param {Object} [options]
* @param {Number} [options.opacityStep=0.02] 1フレームの透明化進行度
* @return {Promise<null>} animation 要素破棄時にfullfill
*/
function transparentize(element) {
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
var elementStyle = element.style;
var opts = (0, _objectAssign2.default)({
opacityStep: 0.02
}, options);
return new _bluebird2.default(function (resolve) {
var opacity = 1;
var render = function render() {
opacity -= opts.opacityStep;
if (opacity <= 0) {
elementStyle.opacity = 0;
return resolve();
}
elementStyle.opacity = opacity;
return requestAnimationFrame(render);
};
requestAnimationFrame(render);
}).then(function () {
if (element.parentNode) {
element.parentNode.removeChild(element);
}
});
}
/**
* easingJsで定義された関数名であれば、その関数を返し
* 引数が関数であれば、そのまま返す
* それ以外はnull
*
* @function getTimingFunction
* @param {String|Function} name EasingJsの関数名か、独自で関数を定義
* @return {Function|null} timingFunction (t,b,c,d)を受け取るイージング関数。未定義の関数名ならnull
*/
function getTimingFunction() {
var name = arguments.length <= 0 || arguments[0] === undefined ? 'easeInBack' : arguments[0];
if (typeof name === 'function') {
return name;
}
if (_easingJs2.default[name]) {
return _easingJs2.default[name];
}
return null;
}
/**
* 指定した大きさのimageDataを作成し
* 波形アニメーションとして表示するフレーム番号を計算する
* x,yを始点とする
*
* 返される配列の値は大きさからpixelSizeを割ったもの。
*
* @function createRenderSchedule
* @param {Number} x 波形アニメーションの始点x
* @param {Number} y 波形アニメーションの始点y
* @param {Number} width 波形アニメーションの幅
* @param {Number} height 波形アニメーションの高さ
* @param {Object} [options]
* @param {Number} [options.pixelSize] ピクセル1粒の大きさ
* @param {Number} [options.bitCrash=null] 境界にノイズを入れる、値はノイズの強さ
* @param {String|Function} [options.timingFunction='easeInQuint'] フレーム番号のイージング関数名
* @return {Object} RenderSchedule
* @return {Array} RenderSchedule.data yとxからなる二次元配列。表示するフレーム番号を値に持つ
* @return {Number} RenderSchedule.width ピクセルの横の個数
* @return {Number} RenderSchedule.height ピクセルの縦の個数
* @return {Number} RenderSchedule.pixelSize ピクセル1粒の大きさ
* @return {Function|null} RenderSchedule.easedBy フレーム番号の調整に使用した関数
*/
function createRenderSchedule(x, y, width, height) {
var options = arguments.length <= 4 || arguments[4] === undefined ? {} : arguments[4];
var opts = Object.create(options);
if (opts.pixelSize === undefined) {
opts.pixelSize = 1;
}
var dataWidth = Math.ceil(width / opts.pixelSize);
var dataHeight = Math.ceil(height / opts.pixelSize);
var data = [];
// ピクセルごとの表示を開始するフレーム番号を定義する
var c = 0; // maxFrame
for (var i = 0; i < dataHeight; i++) {
if (data[i] === undefined) {
data[i] = [];
}
for (var j = 0; j < dataWidth; j++) {
var originalX = opts.pixelSize * j;
var originalY = opts.pixelSize * i;
// x, yからの距離をピクセル基準で求める
var distanceX = Math.abs(originalX - x);
var distanceY = Math.abs(originalY - y);
var distance = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2));
var showFrame = Math.floor(distance / opts.pixelSize);
if (c < showFrame) {
c = showFrame;
}
data[i].push(showFrame);
}
}
// アニメーションの緩急を変更する
// http://d.hatena.ne.jp/nakamura001/20111117/1321539246
var timingFunction = getTimingFunction(opts.timingFunction);
if (timingFunction) {
var d = 1;
for (var _i = 0; _i < data.length; _i++) {
for (var _j = 0; _j < data[_i].length; _j++) {
var b = data[_i][_j]; // showFrame
var t = c > 0 ? b / c : 0; // distanceRate
data[_i][_j] = Math.floor(timingFunction(t, b, c, d));
// 5フレーム以降は境界部分のジャギーを目立たせる(ささくれさせる)
if (opts.bitCrash > 1 && b > 5) {
data[_i][_j] += Math.floor(opts.bitCrash * Math.random());
}
}
}
}
return {
data: data,
width: dataWidth,
height: dataHeight,
pixelSize: opts.pixelSize,
easedBy: timingFunction
};
}
/**
* 指定したcolorNameのrgbaを返す(CanvasRenderingContext2D経由)
*
* @function getPixelColor
* @param {String} colorName CanvasRenderingContext2D.fillStyleの値
* @return {Array} color [r,g,b,a]
*/
function getPixelColor() {
var colorName = arguments.length <= 0 || arguments[0] === undefined ? 'rgba(0,0,0,.3)' : arguments[0];
var context = document.createElement('canvas').getContext('2d');
context.canvas.width = 1;
context.canvas.width = 1;
context.fillStyle = colorName;
context.fillRect(0, 0, 1, 1);
// splat構文を使用するとエラーになるので、配列に変換する
// const [r,g,b,a] = document.createElement('canvas').getContext('2d').getImagedata(...).data
// => TypeError: Invalid attempt to destructure non-iterable instance
return [].slice.call(context.getImageData(0, 0, 1, 1).data);
}