glitch-canvas
Version:
JavaScript library for applying a glitch effect to a canvas element
652 lines (525 loc) • 15.5 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('stream')) :
typeof define === 'function' && define.amd ? define(['stream'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.glitch = factory(global.stream));
}(this, (function (stream) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var stream__default = /*#__PURE__*/_interopDefaultLegacy(stream);
function clamp (value, min, max) {
return value < min ? min : value > max ? max : value;
}
function clone (obj) {
var result = false;
if (typeof obj !== 'undefined') {
try {
result = JSON.parse(JSON.stringify(obj));
} catch (e) {}
}
return result;
}
var defaultParams = {
amount: 35,
iterations: 20,
quality: 30,
seed: 25,
};
function sanitizeInput (params) {
params = clone(params);
if (typeof params !== 'object') {
params = {};
}
Object.keys(defaultParams)
.filter(function (key) { return key !== 'iterations'; })
.forEach(function (key) {
if (typeof params[key] !== 'number' || isNaN(params[key])) {
params[key] = defaultParams[key];
} else {
params[key] = clamp(params[key], 0, 100);
}
params[key] = Math.round(params[key]);
});
if (
typeof params.iterations !== 'number' ||
isNaN(params.iterations) ||
params.iterations <= 0
) {
params.iterations = defaultParams.iterations;
}
params.iterations = Math.round(params.iterations);
return params;
}
var ref = require('canvas');
var createCanvas = ref.createCanvas;
var Image$3 = ref.Image;
var Canvas = function Canvas(width, height) {
if ( width === void 0 ) width = 300;
if ( height === void 0 ) height = 150;
this.canvasEl = createCanvas(width, height);
this.canvasEl.width = width;
this.canvasEl.height = height;
this.ctx = this.canvasEl.getContext('2d');
};
var prototypeAccessors = { width: { configurable: true },height: { configurable: true } };
Canvas.prototype.getContext = function getContext () {
return this.ctx;
};
Canvas.prototype.toDataURL = function toDataURL (type, encoderOptions, cb) {
if (typeof cb === 'function') {
cb(this.canvasEl.toDataURL(type, encoderOptions));
} else {
return this.canvasEl.toDataURL(type, encoderOptions);
}
};
Canvas.prototype.toBuffer = function toBuffer (params) {
return this.canvasEl.toBuffer(params);
};
Canvas.prototype.pngStream = function pngStream (params) {
return this.canvasEl.createPNGStream(params);
};
Canvas.prototype.jpgStream = function jpgStream (params) {
return this.canvasEl.createJPEGStream(params);
};
Canvas.prototype.jpegStream = function jpegStream (params) {
return this.canvasEl.createJPEGStream(params);
};
prototypeAccessors.width.get = function () {
return this.canvasEl.width;
};
prototypeAccessors.width.set = function (newWidth) {
this.canvasEl.width = newWidth;
};
prototypeAccessors.height.get = function () {
return this.canvasEl.height;
};
prototypeAccessors.height.set = function (newHeight) {
this.canvasEl.height = newHeight;
};
Object.defineProperties( Canvas.prototype, prototypeAccessors );
Canvas.Image = Image$3;
// https://github.com/Automattic/node-canvas#imagesrcbuffer
var Image$2 = Canvas.Image;
function fromBufferToImageData (buffer) {
if (buffer instanceof Buffer) {
var image = new Image$2();
image.src = buffer;
var canvas = new Canvas(image.width, image.height);
var ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, image.width, image.height);
return ctx.getImageData(0, 0, canvas.width, canvas.height);
} else {
throw new Error("Can't work with the buffer object provided.");
}
}
var Readable = stream__default['default'].Readable;
var Image$1 = Canvas.Image;
function fromStreamToImageData (stream, resolve, reject) {
if (stream instanceof Readable) {
var bufferContent = [];
stream.on('data', function (chunk) {
bufferContent.push(chunk);
});
stream.on('end', function () {
try {
var buffer = Buffer.concat(bufferContent);
var image = new Image$1();
image.src = buffer;
var canvas = new Canvas(image.width, image.height);
var ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
resolve(ctx.getImageData(0, 0, canvas.width, canvas.height));
} catch (err) {
reject(err);
}
});
} else {
reject(new Error("Can't work with the buffer object provided."));
}
}
var Image = Canvas.Image;
function loadBase64Image (base64URL) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.onload = function () {
resolve(image);
};
image.onerror = reject;
try {
image.src = base64URL;
} catch (err) {
reject(err);
}
});
}
function getImageSize (image) {
return {
width: image.width || image.naturalWidth,
height: image.height || image.naturalHeight,
};
}
function canvasFromImage (image) {
var size = getImageSize(image);
var canvas = new Canvas(size.width, size.height);
var ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, size.width, size.height);
return {
canvas: canvas,
ctx: ctx,
};
}
function base64URLToBuffer (base64URL, options, resolve, reject) {
loadBase64Image(base64URL).then(function (image) {
var buffer = canvasFromImage(image).canvas.toBuffer();
resolve(buffer);
}, reject);
}
function base64URLToImageData (base64URL, options, resolve, reject) {
loadBase64Image(base64URL).then(function (image) {
var size = getImageSize(image);
var imageData = canvasFromImage(image).ctx.getImageData(
0,
0,
size.width,
size.height
);
if (!imageData.width) {
imageData.width = size.width;
}
if (!imageData.height) {
imageData.height = size.height;
}
resolve(imageData);
}, reject);
}
// https://github.com/Automattic/node-canvas#canvaspngstream
function base64URLToPNGStream (base64URL, options, resolve, reject) {
loadBase64Image(base64URL).then(
function (image) {
var stream = canvasFromImage(image).canvas.pngStream();
resolve(stream);
},
function (err) {
reject(err);
}
);
}
// https://github.com/Automattic/node-canvas#canvasjpegstream-and-canvassyncjpegstream
function base64URLToJPGStream (base64URL, options, resolve, reject) {
options = options || {};
var streamParams = {
bufsize: options.bufsize || 4096,
quality: options.quality || 75,
progressive: options.progressive || false,
};
loadBase64Image(base64URL).then(function (image) {
var c = canvasFromImage(image);
var stream = c.canvas.jpegStream(streamParams);
resolve(stream);
}, reject);
}
function isImageData (imageData) {
return (
imageData &&
typeof imageData.width === 'number' &&
typeof imageData.height === 'number' &&
imageData.data &&
typeof imageData.data.length === 'number' &&
typeof imageData.data === 'object'
);
}
function imageDataToBase64 (imageData, quality) {
return new Promise(function (resolve, reject) {
if (isImageData(imageData)) {
var canvas = new Canvas(imageData.width, imageData.height);
var ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0);
var base64URL = canvas.toDataURL('image/jpeg', quality / 100);
switch (base64URL.length % 4) {
case 3:
base64URL += '=';
break;
case 2:
base64URL += '==';
break;
case 1:
base64URL += '===';
break;
}
resolve(base64URL);
} else {
reject(new Error('object is not valid imageData'));
}
});
}
var base64Chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var base64Map$1 = base64Chars.split('');
var reversedBase64Map$1 = {};
base64Map$1.forEach(function (val, key) {
reversedBase64Map$1[val] = key;
});
var maps = {
base64Map: base64Map$1,
reversedBase64Map: reversedBase64Map$1,
};
var reversedBase64Map = maps.reversedBase64Map;
// https://github.com/mutaphysis/smackmyglitchupjs/blob/master/glitch.html
// base64 is 2^6, byte is 2^8, every 4 base64 values create three bytes
function base64ToByteArray (base64URL) {
var result = [];
var prev;
var srcURL = base64URL.replace('data:image/jpeg;base64,', '');
for (var i = 0, len = srcURL.length; i < len; i++) {
srcURL[i];
var currentChar = reversedBase64Map[srcURL[i]];
var digitNum = i % 4;
switch (digitNum) {
// case 0: first digit - do nothing, not enough info to work with
case 1: // second digit
result.push((prev << 2) | (currentChar >> 4));
break;
case 2: // third digit
result.push(((prev & 0x0f) << 4) | (currentChar >> 2));
break;
case 3: // fourth digit
result.push(((prev & 3) << 6) | currentChar);
break;
}
prev = currentChar;
}
return result;
}
// http://stackoverflow.com/a/10424014/229189
function jpgHeaderLength (byteArr) {
var result = 417;
// https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure
// looks for the first occurence of 0xFF, 0xDA in the byteArray
// which is the start of scan
for (var i = 0, len = byteArr.length; i < len; i++) {
if (byteArr[i] === 0xff && byteArr[i + 1] === 0xda) {
result = i + 2;
break;
}
}
return result;
}
function glitchByteArray (byteArray, seed, amount, iterationCount) {
var headerLength = jpgHeaderLength(byteArray);
var maxIndex = byteArray.length - headerLength - 4;
var amountPercent = amount / 100;
var seedPercent = seed / 100;
for (
var iterationIndex = 0;
iterationIndex < iterationCount;
iterationIndex++
) {
var minPixelIndex =
((maxIndex / iterationCount) * iterationIndex) | 0;
var maxPixelIndex =
((maxIndex / iterationCount) * (iterationIndex + 1)) | 0;
var delta = maxPixelIndex - minPixelIndex;
var pixelIndex = (minPixelIndex + delta * seedPercent) | 0;
if (pixelIndex > maxIndex) {
pixelIndex = maxIndex;
}
var indexInByteArray = ~~(headerLength + pixelIndex);
byteArray[indexInByteArray] = ~~(amountPercent * 256);
}
return byteArray;
}
var base64Map = maps.base64Map;
function byteArrayToBase64 (byteArray) {
var result = ['data:image/jpeg;base64,'];
var byteNum;
var previousByte;
for (var i = 0, len = byteArray.length; i < len; i++) {
var currentByte = byteArray[i];
byteNum = i % 3;
switch (byteNum) {
case 0: // first byte
result.push(base64Map[currentByte >> 2]);
break;
case 1: // second byte
result.push(
base64Map[((previousByte & 3) << 4) | (currentByte >> 4)]
);
break;
case 2: // third byte
result.push(
base64Map[((previousByte & 0x0f) << 2) | (currentByte >> 6)]
);
result.push(base64Map[currentByte & 0x3f]);
break;
}
previousByte = currentByte;
}
if (byteNum === 0) {
result.push(base64Map[(previousByte & 3) << 4]);
result.push('==');
} else {
if (byteNum === 1) {
result.push(base64Map[(previousByte & 0x0f) << 2]);
result.push('=');
}
}
return result.join('');
}
function glitchImageData (imageData, base64URL, params) {
if (isImageData(imageData)) {
var byteArray = base64ToByteArray(base64URL);
var glitchedByteArray = glitchByteArray(
byteArray,
params.seed,
params.amount,
params.iterations
);
var glitchedBase64URL = byteArrayToBase64(glitchedByteArray);
return glitchedBase64URL;
} else {
throw new Error('glitchImageData: imageData seems to be corrupt.');
}
}
// import objectAssign from 'object-assign'
var objectAssign = Object.assign;
// constructing an object that allows for a chained interface.
// for example stuff like:
//
// glitch( params )
// .fromBuffer( buffer )
// .toImageData()
//
// etc...
//
function index (params) {
params = sanitizeInput(params);
var inputFn;
var outputFn;
var api = { getParams: getParams, getInput: getInput, getOutput: getOutput };
var inputMethods = { fromBuffer: fromBuffer, fromImageData: fromImageData, fromStream: fromStream };
var outputMethods = {
toBuffer: toBuffer,
toDataURL: toDataURL,
toImageData: toImageData,
toPNGStream: toPNGStream,
toJPGStream: toJPGStream,
toJPEGStream: toJPEGStream,
};
function getParams() {
return params;
}
function getInput() {
var result = objectAssign({}, api);
if (!inputFn) {
objectAssign(result, inputMethods);
}
return result;
}
function getOutput() {
var result = objectAssign({}, api);
if (!outputFn) {
objectAssign(result, outputMethods);
}
return result;
}
function noTransform(x) {
return x;
}
function fromBuffer(inputOptions) {
return setInput(fromBufferToImageData, inputOptions);
}
function fromStream(inputOptions) {
return setInput(fromStreamToImageData, inputOptions, true);
}
function fromImageData(inputOptions) {
return setInput(noTransform, inputOptions);
}
function toBuffer(outputOptions) {
return setOutput(base64URLToBuffer, outputOptions, true);
}
function toDataURL(outputOptions) {
return setOutput(noTransform, outputOptions);
}
function toImageData(outputOptions) {
return setOutput(base64URLToImageData, outputOptions, true);
}
function toPNGStream(outputOptions) {
return setOutput(base64URLToPNGStream, outputOptions, true);
}
function toJPGStream(outputOptions) {
return setOutput(base64URLToJPGStream, outputOptions, true);
}
function toJPEGStream(outputOptions) {
return setOutput(base64URLToJPGStream, outputOptions, true);
}
function setInput(fn, inputOptions, canResolve) {
inputFn = function () {
return new Promise(function (resolve, reject) {
if (canResolve) {
fn(inputOptions, resolve, reject);
} else {
if (fn === noTransform) {
resolve(inputOptions);
} else {
try {
resolve(fn(inputOptions, resolve, reject));
} catch (err) {
reject(err);
}
}
}
});
};
if (isReady()) {
return getResult();
} else {
return getOutput();
}
}
function setOutput(fn, outputOptions, canResolve) {
outputFn = function (base64URL) {
return new Promise(function (resolve, reject) {
if (canResolve) {
fn(base64URL, outputOptions, resolve, reject);
} else {
if (fn === noTransform) {
resolve(base64URL);
} else {
fn(base64URL, outputOptions).then(resolve, reject);
}
}
});
};
if (isReady()) {
return getResult();
} else {
return getInput();
}
}
function isReady() {
return inputFn && outputFn;
}
function getResult() {
return new Promise(function (resolve, reject) {
inputFn()
.then(function (imageData) {
return glitch(imageData, params);
}, reject)
.then(function (base64URL) {
outputFn(base64URL).then(resolve, reject);
}, reject);
});
}
function glitch(imageData, params) {
return new Promise(function (resolve, reject) {
imageDataToBase64(imageData, params.quality).then(function (base64URL) {
try {
resolve(glitchImageData(imageData, base64URL, params));
} catch (err) {
reject(err);
}
}, reject);
});
}
return getInput();
}
return index;
})));