Skip to content

Instantly share code, notes, and snippets.

@nolanlawson
Last active August 18, 2019 11:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nolanlawson/3a7b078f6934b61bba1d327f7aed3cba to your computer and use it in GitHub Desktop.
Save nolanlawson/3a7b078f6934b61bba1d327f7aed3cba to your computer and use it in GitHub Desktop.
OffscreenCanvas demo
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Blurhash = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var digitCharacters = [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"#",
"$",
"%",
"*",
"+",
",",
"-",
".",
":",
";",
"=",
"?",
"@",
"[",
"]",
"^",
"_",
"{",
"|",
"}",
"~"
];
exports.decode83 = function (str) {
var value = 0;
for (var i = 0; i < str.length; i++) {
var c = str[i];
var digit = digitCharacters.indexOf(c);
value = value * 83 + digit;
}
return value;
};
exports.encode83 = function (n, length) {
var result = "";
for (var i = 1; i <= length; i++) {
var digit = (Math.floor(n) / Math.pow(83, length - i)) % 83;
result += digitCharacters[Math.floor(digit)];
}
return result;
};
},{}],2:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var base83_1 = require("./base83");
var utils_1 = require("./utils");
var error_1 = require("./error");
/**
* Returns an error message if invalid or undefined if valid
* @param blurhash
*/
var validateBlurhash = function (blurhash) {
if (!blurhash || blurhash.length < 6) {
throw new error_1.ValidationError("The blurhash string must be at least 6 characters");
}
var sizeFlag = base83_1.decode83(blurhash[0]);
var numY = Math.floor(sizeFlag / 9) + 1;
var numX = (sizeFlag % 9) + 1;
if (blurhash.length !== 4 + 2 * numX * numY) {
throw new error_1.ValidationError("blurhash length mismatch: length is " + blurhash.length + " but it should be " + (4 + 2 * numX * numY));
}
};
exports.isBlurhashValid = function (blurhash) {
try {
validateBlurhash(blurhash);
}
catch (error) {
return { result: false, errorReason: error.message };
}
return { result: true };
};
var decodeDC = function (value) {
var intR = value >> 16;
var intG = (value >> 8) & 255;
var intB = value & 255;
return [utils_1.sRGBToLinear(intR), utils_1.sRGBToLinear(intG), utils_1.sRGBToLinear(intB)];
};
var decodeAC = function (value, maximumValue) {
var quantR = Math.floor(value / (19 * 19));
var quantG = Math.floor(value / 19) % 19;
var quantB = value % 19;
var rgb = [
utils_1.signPow((quantR - 9) / 9, 2.0) * maximumValue,
utils_1.signPow((quantG - 9) / 9, 2.0) * maximumValue,
utils_1.signPow((quantB - 9) / 9, 2.0) * maximumValue
];
return rgb;
};
var decode = function (blurhash, width, height, punch) {
validateBlurhash(blurhash);
punch = punch | 1;
var sizeFlag = base83_1.decode83(blurhash[0]);
var numY = Math.floor(sizeFlag / 9) + 1;
var numX = (sizeFlag % 9) + 1;
var quantisedMaximumValue = base83_1.decode83(blurhash[1]);
var maximumValue = (quantisedMaximumValue + 1) / 166;
var colors = new Array(numX * numY);
for (var i = 0; i < colors.length; i++) {
if (i === 0) {
var value = base83_1.decode83(blurhash.substring(2, 6));
colors[i] = decodeDC(value);
}
else {
var value = base83_1.decode83(blurhash.substring(4 + i * 2, 6 + i * 2));
colors[i] = decodeAC(value, maximumValue * punch);
}
}
var bytesPerRow = width * 4;
var pixels = new Uint8ClampedArray(bytesPerRow * height);
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
var r = 0;
var g = 0;
var b = 0;
for (var j = 0; j < numY; j++) {
for (var i = 0; i < numX; i++) {
var basis = Math.cos((Math.PI * x * i) / width) *
Math.cos((Math.PI * y * j) / height);
var color = colors[i + j * numX];
r += color[0] * basis;
g += color[1] * basis;
b += color[2] * basis;
}
}
var intR = utils_1.linearTosRGB(r);
var intG = utils_1.linearTosRGB(g);
var intB = utils_1.linearTosRGB(b);
pixels[4 * x + 0 + y * bytesPerRow] = intR;
pixels[4 * x + 1 + y * bytesPerRow] = intG;
pixels[4 * x + 2 + y * bytesPerRow] = intB;
pixels[4 * x + 3 + y * bytesPerRow] = 255; // alpha
}
}
return pixels;
};
exports.default = decode;
},{"./base83":1,"./error":4,"./utils":5}],3:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var base83_1 = require("./base83");
var utils_1 = require("./utils");
var error_1 = require("./error");
var bytesPerPixel = 4;
var multiplyBasisFunction = function (pixels, width, height, basisFunction) {
var r = 0;
var g = 0;
var b = 0;
var bytesPerRow = width * bytesPerPixel;
for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++) {
var basis = basisFunction(x, y);
r +=
basis * utils_1.sRGBToLinear(pixels[bytesPerPixel * x + 0 + y * bytesPerRow]);
g +=
basis * utils_1.sRGBToLinear(pixels[bytesPerPixel * x + 1 + y * bytesPerRow]);
b +=
basis * utils_1.sRGBToLinear(pixels[bytesPerPixel * x + 2 + y * bytesPerRow]);
}
}
var scale = 1 / (width * height);
return [r * scale, g * scale, b * scale];
};
var encodeDC = function (value) {
var roundedR = utils_1.linearTosRGB(value[0]);
var roundedG = utils_1.linearTosRGB(value[1]);
var roundedB = utils_1.linearTosRGB(value[2]);
return (roundedR << 16) + (roundedG << 8) + roundedB;
};
var encodeAC = function (value, maximumValue) {
var quantR = Math.floor(Math.max(0, Math.min(18, Math.floor(utils_1.signPow(value[0] / maximumValue, 0.5) * 9 + 9.5))));
var quantG = Math.floor(Math.max(0, Math.min(18, Math.floor(utils_1.signPow(value[1] / maximumValue, 0.5) * 9 + 9.5))));
var quantB = Math.floor(Math.max(0, Math.min(18, Math.floor(utils_1.signPow(value[2] / maximumValue, 0.5) * 9 + 9.5))));
return quantR * 19 * 19 + quantG * 19 + quantB;
};
var encode = function (pixels, width, height, componentX, componentY) {
if (componentX < 1 || componentX > 9 || componentY < 1 || componentY > 9) {
throw new error_1.ValidationError("BlurHash must have between 1 and 9 components");
}
if (width * height * 4 !== pixels.length) {
throw new error_1.ValidationError("Width and height must match the pixels array");
}
var factors = [];
var _loop_1 = function (y) {
var _loop_2 = function (x) {
var normalisation = x == 0 && y == 0 ? 1 : 2;
var factor = multiplyBasisFunction(pixels, width, height, function (i, j) {
return normalisation *
Math.cos((Math.PI * x * i) / width) *
Math.cos((Math.PI * y * j) / height);
});
factors.push(factor);
};
for (var x = 0; x < componentX; x++) {
_loop_2(x);
}
};
for (var y = 0; y < componentY; y++) {
_loop_1(y);
}
var dc = factors[0];
var ac = factors.slice(1);
var hash = "";
var sizeFlag = componentX - 1 + (componentY - 1) * 9;
hash += base83_1.encode83(sizeFlag, 1);
var maximumValue;
if (ac.length > 0) {
var actualMaximumValue = Math.max.apply(Math, ac.map(function (val) { return Math.max.apply(Math, val); }));
var quantisedMaximumValue = Math.floor(Math.max(0, Math.min(82, Math.floor(actualMaximumValue * 166 - 0.5))));
maximumValue = (quantisedMaximumValue + 1) / 166;
hash += base83_1.encode83(quantisedMaximumValue, 1);
}
else {
maximumValue = 1;
hash += base83_1.encode83(0, 1);
}
hash += base83_1.encode83(encodeDC(dc), 4);
ac.forEach(function (factor) {
hash += base83_1.encode83(encodeAC(factor, maximumValue), 2);
});
return hash;
};
exports.default = encode;
},{"./base83":1,"./error":4,"./utils":5}],4:[function(require,module,exports){
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
var ValidationError = /** @class */ (function (_super) {
__extends(ValidationError, _super);
function ValidationError(message) {
var _this = _super.call(this, message) || this;
_this.name = "ValidationError";
_this.message = message;
return _this;
}
return ValidationError;
}(Error));
exports.ValidationError = ValidationError;
},{}],5:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sRGBToLinear = function (value) {
var v = value / 255;
if (v <= 0.04045) {
return v / 12.92;
}
else {
return Math.pow((v + 0.055) / 1.055, 2.4);
}
};
exports.linearTosRGB = function (value) {
var v = Math.max(0, Math.min(1, value));
if (v <= 0.0031308) {
return Math.round(v * 12.92 * 255 + 0.5);
}
else {
return Math.round((1.055 * Math.pow(v, 1 / 2.4) - 0.055) * 255 + 0.5);
}
};
exports.sign = function (n) { return (n < 0 ? -1 : 1); };
exports.signPow = function (val, exp) {
return exports.sign(val) * Math.pow(Math.abs(val), exp);
};
},{}],"blurhash":[function(require,module,exports){
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
var decode_1 = require("./decode");
exports.decode = decode_1.default;
exports.isBlurhashValid = decode_1.isBlurhashValid;
var encode_1 = require("./encode");
exports.encode = encode_1.default;
__export(require("./error"));
},{"./decode":2,"./encode":3,"./error":4}]},{},[])("blurhash")
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Offscreen canvas demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Offscreen canvas demo</h1>
<button id="canvas-btn">Decode using worker+&lt;canvas&gt;</button>
<button id="offscreen-canvas-btn">Decode using OffscreenCanvas</button>
<pre id="display"></pre>
<script src="script.js"></script>
</body>
</html>
(() => {
const RESOLUTION = 32
const $ = document.querySelector.bind(document)
const $display = $('#display')
const $canvasBtn = $('#canvas-btn')
const $offscreenBtn = $('#offscreen-canvas-btn')
const canvas = document.createElement('canvas')
canvas.height = RESOLUTION
canvas.width = RESOLUTION
const canvasContext2D = canvas.getContext('2d')
const worker = new Worker('worker.js')
const encoded = 'UJDJS1-.rUox0JxspIocwZxro}WB%~oyVqa}'
function postMessageToWorker (msg) {
return new Promise((resolve, reject) => {
function cleanup () {
worker.removeEventListener('message', onMessage)
worker.removeEventListener('error', onError)
}
function onMessage (e) {
cleanup()
resolve(e.data)
}
function onError (e) {
cleanup()
reject(e)
}
worker.addEventListener('message', onMessage)
worker.addEventListener('error', onError)
worker.postMessage(msg)
})
}
async function decodeUsingCanvas () {
const { imageData, error } = await postMessageToWorker({ encoded, type: 'canvas' })
if (error) {
throw new Error(error)
}
canvasContext2D.putImageData(imageData, 0, 0)
const blob = await new Promise(resolve => canvas.toBlob(resolve))
return URL.createObjectURL(blob)
}
async function decodeUsingOffscreenCanvas () {
const { decoded, error } = await postMessageToWorker({ encoded, type: 'offscreen' })
if (error) {
throw new Error(error)
}
return decoded
}
$canvasBtn.addEventListener('click', async () => {
performance.mark('canvas-start')
let error
try {
const url = await decodeUsingCanvas()
console.log('url', url)
} catch (e) {
error = true
} finally {
performance.measure('canvas', 'canvas-start')
$display.innerHTML += `&lt;canvas&gt ${error ? 'errored after' : 'took'} ` +
`${performance.getEntriesByName('canvas').slice(-1)[0].duration}ms\n`
}
})
$offscreenBtn.addEventListener('click', async () => {
performance.mark('offscreen-start')
let error
try {
const url = await decodeUsingOffscreenCanvas()
console.log('url', url)
} catch (e) {
error = true
} finally {
performance.measure('offscreen', 'offscreen-start')
$display.innerHTML += `OffscreenCanvas ${error ? 'errored after' : 'took'} ` +
`${performance.getEntriesByName('offscreen').slice(-1)[0].duration}ms\n`
}
})
})()
importScripts('blurhash.js')
const RESOLUTION = 32
const OFFSCREEN_CANVAS = typeof OffscreenCanvas === 'function'
? new OffscreenCanvas(RESOLUTION, RESOLUTION) : null
const OFFSCREEN_CANVAS_CONTEXT_2D = OFFSCREEN_CANVAS
? OFFSCREEN_CANVAS.getContext('2d') : null
self.onmessage = async (e) => {
performance.mark('onmessage')
try {
const { encoded, type } = e.data
const pixels = Blurhash.decode(encoded, RESOLUTION, RESOLUTION)
const imageData = new ImageData(pixels, RESOLUTION, RESOLUTION)
if (type === 'offscreen') {
OFFSCREEN_CANVAS_CONTEXT_2D.putImageData(imageData, 0, 0)
performance.mark('convertToBlob')
const blob = await OFFSCREEN_CANVAS.convertToBlob()
performance.measure('convertToBlob', 'convertToBlob')
const decoded = URL.createObjectURL(blob)
postMessage({ decoded, imageData: null })
} else {
postMessage({ imageData, decoded: null })
}
} catch (error) {
postMessage({ error: error.message, imageData: null, decoded: null })
}
performance.measure('onmessage', 'onmessage')
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment