src/utility.js
import Promise from 'bluebird';
import easingJs from 'easing-js';
import objectAssign from 'object-assign';
/**
* 利用可能な非同期関数でcallbackを実行する
*
* @function requestAnimationFrame
* @param {Function} [callback]
* @return undefined
*/
export function requestAnimationFrame(callback) {
return window.requestAnimationFrame(callback);
}
/**
* 指定した要素のイベントを待つプロミスを返す
*
* @function promiseEvent
* @param {Element} target イベントを取得する要素
* @param {String} eventName 取得するイベント名
* @return {Promise<EventTarget>} deferredEvent 取得したイベント
*/
export function promiseEvent(target, eventName) {
return new Promise((resolve) => {
const 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}
*/
export function createContext2d(width, height, options = {}) {
const 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;
const 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}
*/
export function getImageData(canvas) {
const { width, height } = canvas;
const 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
*/
export function transparentize(element, options = {}) {
const elementStyle = element.style;
const opts = objectAssign({
opacityStep: 0.02,
}, options);
return new Promise((resolve) => {
let opacity = 1;
const render = () => {
opacity -= opts.opacityStep;
if (opacity <= 0) {
elementStyle.opacity = 0;
return resolve();
}
elementStyle.opacity = opacity;
return requestAnimationFrame(render);
};
requestAnimationFrame(render);
}).then(() => {
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
*/
export function getTimingFunction(name = 'easeInBack') {
if (typeof name === 'function') {
return name;
}
if (easingJs[name]) {
return easingJs[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 フレーム番号の調整に使用した関数
*/
export function createRenderSchedule(x, y, width, height, options = {}) {
const opts = Object.create(options);
if (opts.pixelSize === undefined) {
opts.pixelSize = 1;
}
const dataWidth = Math.ceil(width / opts.pixelSize);
const dataHeight = Math.ceil(height / opts.pixelSize);
const data = [];
// ピクセルごとの表示を開始するフレーム番号を定義する
let c = 0;// maxFrame
for (let i = 0; i < dataHeight; i++) {
if (data[i] === undefined) {
data[i] = [];
}
for (let j = 0; j < dataWidth; j++) {
const originalX = opts.pixelSize * j;
const originalY = opts.pixelSize * i;
// x, yからの距離をピクセル基準で求める
const distanceX = Math.abs(originalX - x);
const distanceY = Math.abs(originalY - y);
const distance = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2));
const showFrame = Math.floor(distance / opts.pixelSize);
if (c < showFrame) {
c = showFrame;
}
data[i].push(showFrame);
}
}
// アニメーションの緩急を変更する
// http://d.hatena.ne.jp/nakamura001/20111117/1321539246
const timingFunction = getTimingFunction(opts.timingFunction);
if (timingFunction) {
const d = 1;
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
const b = data[i][j];// showFrame
const 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,
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]
*/
export function getPixelColor(colorName = 'rgba(0,0,0,.3)') {
const 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);
}