UNPKG

stippled-image

Version:

Custom element to show the stippled version of image

255 lines (254 loc) 7.29 kB
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);