stippled-image
Version:
Custom element to show the stippled version of image
255 lines (254 loc) • 7.29 kB
JavaScript
import { workerScript } from './worker';
function fire(element, name, detail) {
element.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail }));
}
export class StippledImage extends HTMLElement {
constructor() {
super();
this._src = '';
this._n = 0;
this._r = 3;
this._c = '#000000';
this._sampling = 50;
this.refreshing = false;
this.messageHandler = (event) => {
if (this.worker) {
const { points, iteration } = event.data;
this.stippleData = points;
this.draw();
if (iteration === 50) {
const worker = this.worker;
setTimeout(() => {
if (worker === this.worker) {
this.terminateWorker();
}
});
}
fire(this, iteration === 50 ? 'load' : 'render');
}
};
this.root = this.attachShadow({ mode: 'open' });
this.root.innerHTML = `
<style>
:host {
display: inline-block;
overflow: hidden;
}
canvas {
display: block;
}
</style>
<canvas></canvas>
`;
}
static get observedAttributes() {
return [
'width',
'height',
'src',
'points',
'color',
'radius',
'sampling'
];
}
attributeChangedCallback(name, _, newValue) {
switch (name) {
case 'width':
this.width = +newValue;
break;
case 'height':
this.height = +newValue;
break;
case 'points':
this.points = +newValue;
break;
case 'src':
this.src = newValue;
break;
case 'radius':
this.radius = +newValue;
break;
case 'color':
this.color = newValue;
break;
case 'sampling':
this.sampling = +newValue;
break;
}
}
connectedCallback() {
this.refresh();
}
disconnectedCallback() {
this.terminateWorker();
if (this._workerUrl) {
URL.revokeObjectURL(this._workerUrl);
this._workerUrl = undefined;
}
}
get workerUrl() {
if (!this._workerUrl) {
const blob = new Blob([workerScript], {
type: 'text/javascript'
});
this._workerUrl = URL.createObjectURL(blob);
}
return this._workerUrl;
}
get canvas() {
if (!this.canvasElement) {
this.canvasElement = this.root.querySelector('canvas');
}
return this.canvasElement;
}
get width() {
return this._w || (this._i && this._i.width) || 0;
}
get height() {
return this._h || (this._i && this._i.height) || 0;
}
set width(value) {
if (this._w !== value) {
this._w = value;
this.refresh();
}
}
set height(value) {
if (this._h !== value) {
this._h = value;
this.refresh();
}
}
get points() {
return Math.min(20000, this._n || (this.width * this.height) / this.sampling);
}
set points(value) {
value = Math.min(20000, value);
if (this._n !== value) {
this._n = value;
this.refresh();
}
}
get src() {
return this._src;
}
set src(value) {
if (this._src !== value) {
this._src = value;
this.refresh();
}
}
get color() {
return this._c;
}
set color(value) {
if (this._c !== value) {
this._c = value;
this.draw();
}
}
get radius() {
return this._r;
}
set radius(value) {
if (this._r !== value) {
this._r = value;
this.draw();
}
}
get sampling() {
return this._sampling;
}
set sampling(value) {
if (this._sampling !== value) {
this._sampling = value;
this.refresh();
}
}
refresh() {
if (!this.refreshing) {
this.refreshing = true;
this.doRefresh()
.then(() => this.refreshing = false)
.catch(() => this.refreshing = false);
}
}
async doRefresh() {
// terminate worker
this.terminateWorker();
if (!this._src) {
return;
}
// fetch image and get data
const image = this._i = await this.loadImage(this._src);
const { width, height } = this;
const ctx = this.createContext([width, height]);
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
// resize canvas
this.canvas.width = width;
this.canvas.height = height;
// Create worker
this.worker = new Worker(this.workerUrl);
this.worker.addEventListener('message', this.messageHandler);
this.worker.postMessage({
buffer: imageData.data.buffer,
width,
height,
pointCount: this.points
}, [imageData.data.buffer]);
}
async loadImage(url) {
if (this._i && this._i.src === url) {
return this._i;
}
return new Promise((resolve, reject) => {
const image = new Image();
image.crossOrigin = 'anonymous';
image.onload = () => resolve(image);
image.onerror = () => reject();
image.onabort = () => reject();
image.src = url;
});
}
createContext(size) {
const canvas = ('OffscreenCanvas' in window) ? new OffscreenCanvas(size[0], size[1]) : document.createElement('canvas');
canvas.width = size[0];
canvas.height = size[1];
return canvas.getContext('2d');
}
terminateWorker() {
if (this.worker) {
this.worker.removeEventListener('message', this.messageHandler);
try {
this.worker.terminate();
}
catch (err) {
console.log(err);
}
this.worker = undefined;
}
}
draw() {
const canvas = this.canvas;
if (canvas) {
const ctx = this.canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!this.stippleData) {
return;
}
ctx.fillStyle = this.color;
ctx.beginPath();
for (let i = 0; i < (this.stippleData.length - 1); i += 2) {
const x = this.stippleData[i];
const y = this.stippleData[i + 1];
ctx.moveTo(x, y);
ctx.arc(x, y, this.radius, 0, Math.PI * 2);
}
ctx.fill();
ctx.closePath();
}
}
}
customElements.define('stippled-image', StippledImage);