@9am/img-halftone
Version:
A web component turns <img> into halftone.
295 lines (294 loc) • 11.6 kB
JavaScript
/**
* 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
};