UNPKG

@9am/img-halftone

Version:

A web component turns <img> into halftone.

295 lines (294 loc) 11.6 kB
/** * name: @9am/img-halftone@1.0.3 * desc: A web component turns <img> into halftone. * author: 9am <tech.9am@gmail.com> [https://9am.github.io/] * homepage: https://github.com/9am/halftone#readme * license: MIT */ var L = Object.defineProperty; var A = (c, t, e) => t in c ? L(c, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : c[t] = e; var l = (c, t, e) => (A(c, typeof t != "symbol" ? t + "" : t, e), e); const R = `:host{display:inline-block;position:relative;font-size:0;overflow:hidden;width:100%}:host *{box-sizing:border-box}:root{width:100%}.painter{width:100%}.grid-painter{--r: 50%;position:relative;width:100%;aspect-ratio:var(--ratio);isolation:isolate}.grid-painter .layer{mix-blend-mode:multiply;position:absolute;top:0;left:0;width:100%;height:100%;display:grid;grid-template-columns:repeat(var(--column),1fr);grid-template-rows:repeat(var(--row),1fr);transform:rotate3d(0,0,1,calc(var(--rad) * 1rad)) scale(var(--scale-x),var(--scale-y))}.grid-painter .layer .cell{background:var(--color);border-radius:100%;transform:scale(calc(var(--size) * 1.5));clip-path:polygon(calc(var(--r) + var(--r) * cos(0)) calc(var(--r) + var(--r) * sin(0)),calc(var(--r) + var(--r) * cos(var(--sl))) calc(var(--r) + var(--r) * sin(var(--sl))),calc(var(--r) + var(--r) * cos(var(--sl) * 2)) calc(var(--r) + var(--r) * sin(var(--sl) * 2)),calc(var(--r) + var(--r) * cos(var(--sl) * 3)) calc(var(--r) + var(--r) * sin(var(--sl) * 3)),calc(var(--r) + var(--r) * cos(var(--sl) * 4)) calc(var(--r) + var(--r) * sin(var(--sl) * 4)),calc(var(--r) + var(--r) * cos(var(--sl) * 5)) calc(var(--r) + var(--r) * sin(var(--sl) * 5)))}.grid-painter.triangle .cell{border-radius:0;--sl: 1turn / 3}.grid-painter.rectangle .cell{border-radius:0;--sl: 1turn / 4}.grid-painter.hexagon .cell{border-radius:0;--sl: 1turn / 6}.grid-painter.char .cell{color:var(--color);background:unset;border-radius:0;font:900 calc(100cqh / var(--row)) monospace;position:relative}.grid-painter.char .cell:before{position:absolute;content:var(--char)}#img{position:absolute;top:0;left:0;z-index:1;width:100%;visibility:hidden} `; var g = /* @__PURE__ */ ((c) => (c.CIRCLE = "circle", c.TRIANGLE = "triangle", c.RECTANGLE = "rectangle", c.HEXAGON = "hexagon", c.CHAR = "char", c))(g || {}); const p = window.devicePixelRatio || 1, _ = (c, t, e, s, a) => { let r = 0; for (; r < c; ) { const i = r === 0 ? t.moveTo : t.lineTo, o = r * Math.PI * 2 / c; i.call(t, e + a * Math.cos(o), s + a * Math.sin(o)), r++; } }, w = class w { constructor({ shape: t }) { l(this, "dom"); l(this, "ctx"); l(this, "shape"); this.dom = document.createElement("canvas"), this.dom.classList.add("painter", "canvas-painter"), this.ctx = this.dom.getContext("2d", { antialias: !1 }), this.shape = w.shapeMap.get(t); } draw(t, e) { const [s, a] = e; this.dom.width = Math.floor(s * p), this.dom.height = Math.floor(a * p), t.forEach((r) => { this.ctx.globalCompositeOperation = "multiply", this.ctx.imageSmoothingEnabled = !1, this.ctx.scale(p, p), this.ctx.rotate(-r.angle), this.ctx.translate(-a * Math.sin(r.angle), 0), this.ctx.fillStyle = r.color; const [i, o] = r.size, [n, h] = r.cellSize, m = new Path2D(); r.cells.forEach((y, d) => { const [M, z] = [d % i, Math.floor(d / i)], [E, C] = [M * n + n * 0.5, z * h + h * 0.5]; this.shape(m, E, C, n, h, y); }), this.ctx.fill(m), this.ctx.resetTransform(); }); } }; l(w, "shapeMap", /* @__PURE__ */ new Map([ [ g.CIRCLE, (t, e, s, a, r, i) => { const o = i * a * 0.7; t.moveTo(e, s), t.arc(e, s, o, 0, Math.PI * 2); } ], [ g.TRIANGLE, (t, e, s, a, r, i) => { _(3, t, e, s, i * a * 0.7); } ], [ g.RECTANGLE, (t, e, s, a, r, i) => { _(4, t, e, s, i * a * 0.7); } ], [ g.HEXAGON, (t, e, s, a, r, i) => { _(6, t, e, s, i * a * 0.7); } ] ])); let f = w; const v = class v { constructor({ shape: t }) { l(this, "dom"); this.dom = document.createElement("div"), this.dom.classList.add("painter", "grid-painter", t); } createLayer(t, e, s) { const [a, r] = t.size, [i, o] = t.viewBox, n = document.createElement("section"); n.classList.add("layer", t.name), n.style.setProperty("--rad", `${-t.angle}`), n.style.setProperty("--color", `${t.color}`), n.style.setProperty("--column", `${a}`), n.style.setProperty("--row", `${r}`), n.style.setProperty("--scale-x", `${i / e}`), n.style.setProperty("--scale-y", `${o / s}`); const h = t.cells.reduce((m, y) => { const d = document.createElement("div"); return d.classList.add("cell"), d.style.setProperty("--size", `${y}`), d.style.setProperty("--char", `'${v.randomChar()}'`), m.append(d), m; }, document.createDocumentFragment()); return n.append(h), n; } draw(t, e) { const [s, a] = e; this.dom.innerHTML = "", this.dom.style.setProperty("--ratio", `${s} / ${a}`); const r = t.reduce((i, o) => { const n = this.createLayer(o, s, a); return i.append(n), i; }, document.createDocumentFragment()); this.dom.append(r); } }; l(v, "randomChar", () => { const t = Math.floor(Math.random() * 99 + 30); return String.fromCharCode(t); }); let b = v; class P { constructor(t) { this._waitForWorker = t || (() => new Promise((e) => { this.addWorker = e; })), this.run = this.run.bind(this); } async run(t, e) { return this._waitForWorker().then((s) => new Promise((a, r) => { s.onmessage = (i) => { a(i.data), e(s); }, s.onerror = (i) => { r(i), e(s); }, s.postMessage(t); })); } } class T { constructor({ worker: t = () => ({}), size: e = 1 }) { this._worker = t, this._size = e, this._running = 0, this._workers = [], this._taskQueue = [], this._getWorker = this._getWorker.bind(this), this._freeWorker = this._freeWorker.bind(this); } addTask(t) { const e = this._workers.length || this._running < this._size, s = new P(e ? this._getWorker : null); return e || this._taskQueue.push(s), s.run(t, this._freeWorker); } async _getWorker() { return new Promise((t, e) => { this._workers.length && (this._running++, t(this._workers.pop())), this._running < this._size && (this._running++, t(this._worker())), e(`max worker: ${this._size}`); }); } _freeWorker(t) { if (this._taskQueue.length) { this._taskQueue.shift().addWorker(t); return; } this._running--, this._workers.push(t); } } const W = async () => { const c = await import("./worker-cd185440.js"); return new Worker( URL.createObjectURL(new Blob([c.default], { type: "application/script" })) ); }, I = 4, $ = new T({ worker: W, size: window.navigator.hardwareConcurrency && window.navigator.hardwareConcurrency > 1 ? Math.max(1, I) : 1 }); class u { constructor(t) { l(this, "_canvas"); l(this, "_ctx"); l(this, "_cells"); l(this, "_size"); l(this, "_angle"); l(this, "_options"); l(this, "viewBox"); l(this, "color", "black"); this.color = t.color, this._canvas = document.createElement("canvas"), this._ctx = this._canvas.getContext("2d", { alpha: !1, willReadFrequently: !0, antialias: !1 }), this._ctx.imageSmoothingEnabled = !1, this.update(t); } static deg2rad(t = 0) { return t * Math.PI / 180; } getOrigin() { const { source: t, deg: e } = this._options, [s, a] = [t.width, t.height]; this._angle = u.deg2rad(e); const r = Math.cos(this.angle), i = Math.sin(this.angle), [o, n] = [Math.ceil(s * r + a * i), Math.ceil(s * i + a * r)]; this._canvas.width = o, this._canvas.height = n, this.viewBox = [o, n], this._ctx.fillStyle = "white", this._ctx.fillRect(0, 0, o, n), this._ctx.translate(a * i, 0), this._ctx.rotate(this.angle), this._ctx.drawImage(t, 0, 0, s, a), this._ctx.resetTransform(); const { data: h } = this._ctx.getImageData(0, 0, o, n); return { origin: h, vw: o, vh: n }; } async update(t) { if (this._options = { ...this._options, ...t }, !this._options.source) return; const { name: e, cellSize: s } = this._options, a = this.getOrigin(), { cells: r, column: i, row: o } = await $.addTask({ ...a, name: e, cellSize: s }); this._size = [i, o], this._cells = r; } get angle() { return this._angle; } get size() { return this._size; } get cellSize() { return this._options.cellSize; } get name() { return this._options.name; } get cells() { return this._cells; } destory() { } } const S = Math.pow(2, 21), k = document.createElement("template"); k.innerHTML = `<style>${R}</style><img id="img" alt="img-halftone" />`; class x extends HTMLElement { constructor() { super(); l(this, "img"); l(this, "painter"); l(this, "channels"); this.attachShadow({ mode: "open" }), this.shadowRoot.append(k.content.cloneNode(!0)), this.painter = this.varient === "grid" ? new b({ shape: this.shape }) : new f({ shape: this.shape }), this.channels = [ new u({ name: "key", color: "#333", deg: 45 }), new u({ name: "cyan", color: "cyan", deg: 15 }), new u({ name: "magenta", color: "magenta", deg: 75 }), new u({ name: "yellow", color: "yellow", deg: 0 }) ], this.img = this.shadowRoot.querySelector("#img"); } static loadImage(e = "") { return new Promise((s, a) => { let r = new Image(); r.crossOrigin = "anonymous", r.id = "img", r.setAttribute("part", "img"), r.onload = () => { s(r); }, r.onerror = (i) => a(i), r.src = e; }); } static get observedAttributes() { return ["src", "alt"]; } async attributeChangedCallback(e, s, a) { var r; if (s !== a) switch (e) { case "src": { if (!this.src) break; try { this.shadowRoot.host.classList.add("loading"); const i = await x.loadImage(this.src); i.setAttribute("alt", this.alt), this.img.parentNode.replaceChild(i, this.img), this.img = i; const o = this.img.cloneNode(), n = o.width * o.height, h = Math.sqrt(S / n); o.width = Math.ceil(o.width * h), o.height = Math.ceil(o.height * h), await this.update({ source: o }); } finally { this.shadowRoot.host.classList.remove("loading"); } break; } case "alt": { (r = this.img) == null || r.setAttribute("alt", this.alt); break; } } } async update({ source: e }) { const s = this.cellsize, a = [s, s]; await Promise.all( this.channels.map((r) => r.update({ source: e, cellSize: a })) ), this.painter.draw(this.channels, [e.width, e.height]); } connectedCallback() { this.shadowRoot.appendChild(this.painter.dom), this.src || (this.src = ""); } disconnectedCallback() { this.img = null; } get src() { return this.getAttribute("src") ?? ""; } set src(e) { this.setAttribute("src", e); } get alt() { return this.getAttribute("alt") ?? "img-halftone"; } set alt(e) { this.setAttribute("alt", e); } get varient() { return this.getAttribute("varient") ?? "canvas"; } get cellsize() { return +this.getAttribute("cellsize") || 4; } get shape() { const e = this.getAttribute("shape"); return Object.values(g).includes(e) ? e : g.CIRCLE; } } window.customElements.get("img-halftone") || window.customElements.define("img-halftone", x); export { x as default };