@ascii-web/client 0.0.0-stage → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ascii-img.d.ts +11 -0
- package/dist/ascii-img.js +146 -0
- package/package.json +38 -3
- package/README.md +0 -3
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class AsciiImg extends HTMLElement {
|
|
2
|
+
#private;
|
|
3
|
+
static observedAttributes: string[];
|
|
4
|
+
constructor();
|
|
5
|
+
connectedCallback(): void;
|
|
6
|
+
disconnectedCallback(): void;
|
|
7
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
8
|
+
private loadImage;
|
|
9
|
+
private measureCharWidth;
|
|
10
|
+
private renderAscii;
|
|
11
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import init, { ascii_art } from '@ascii-web/wasm/web';
|
|
2
|
+
let wasmReady = null;
|
|
3
|
+
function ensureInit() {
|
|
4
|
+
if (!wasmReady)
|
|
5
|
+
wasmReady = init();
|
|
6
|
+
return wasmReady;
|
|
7
|
+
}
|
|
8
|
+
const DEFAULT_MIN_COLS = 80;
|
|
9
|
+
const DEFAULT_MAX_COLS = 300;
|
|
10
|
+
const STYLES = `
|
|
11
|
+
:host {
|
|
12
|
+
display: block;
|
|
13
|
+
width: 100%;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
contain: inline-size;
|
|
16
|
+
}
|
|
17
|
+
pre {
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
white-space: pre;
|
|
22
|
+
max-width: 100%;
|
|
23
|
+
font-family: var(--ascii-font-family, monospace);
|
|
24
|
+
font-size: var(--ascii-font-size, 8px);
|
|
25
|
+
line-height: var(--ascii-line-height, 1);
|
|
26
|
+
color: var(--ascii-color, white);
|
|
27
|
+
background-color: var(--ascii-bg, black);
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
export class AsciiImg extends HTMLElement {
|
|
31
|
+
static observedAttributes = ['src', 'alt', 'min-cols', 'max-cols'];
|
|
32
|
+
#shadow;
|
|
33
|
+
#pre;
|
|
34
|
+
#resizeObserver;
|
|
35
|
+
#imageData = null;
|
|
36
|
+
#imageNaturalWidth = 0;
|
|
37
|
+
#imageNaturalHeight = 0;
|
|
38
|
+
#minCols = DEFAULT_MIN_COLS;
|
|
39
|
+
#maxCols = DEFAULT_MAX_COLS;
|
|
40
|
+
#lastRenderedCols = 0;
|
|
41
|
+
constructor() {
|
|
42
|
+
super();
|
|
43
|
+
this.#shadow = this.attachShadow({ mode: 'open' });
|
|
44
|
+
const style = document.createElement('style');
|
|
45
|
+
style.textContent = STYLES;
|
|
46
|
+
this.#shadow.appendChild(style);
|
|
47
|
+
this.#pre = document.createElement('pre');
|
|
48
|
+
this.#pre.setAttribute('aria-hidden', 'true');
|
|
49
|
+
this.#shadow.appendChild(this.#pre);
|
|
50
|
+
this.setAttribute('role', 'img');
|
|
51
|
+
this.#resizeObserver = new ResizeObserver(() => this.renderAscii());
|
|
52
|
+
}
|
|
53
|
+
connectedCallback() {
|
|
54
|
+
this.#resizeObserver.observe(this);
|
|
55
|
+
this.loadImage();
|
|
56
|
+
}
|
|
57
|
+
disconnectedCallback() {
|
|
58
|
+
this.#resizeObserver.disconnect();
|
|
59
|
+
}
|
|
60
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
61
|
+
if (oldValue === newValue)
|
|
62
|
+
return;
|
|
63
|
+
switch (name) {
|
|
64
|
+
case 'src':
|
|
65
|
+
this.#lastRenderedCols = 0;
|
|
66
|
+
this.loadImage();
|
|
67
|
+
break;
|
|
68
|
+
case 'alt':
|
|
69
|
+
this.setAttribute('aria-label', newValue ?? '');
|
|
70
|
+
break;
|
|
71
|
+
case 'min-cols': {
|
|
72
|
+
const parsed = parseInt(newValue ?? '', 10);
|
|
73
|
+
this.#minCols = isNaN(parsed) ? DEFAULT_MIN_COLS : parsed;
|
|
74
|
+
this.renderAscii();
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case 'max-cols': {
|
|
78
|
+
const parsed = parseInt(newValue ?? '', 10);
|
|
79
|
+
this.#maxCols = isNaN(parsed) ? DEFAULT_MAX_COLS : parsed;
|
|
80
|
+
this.renderAscii();
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
loadImage() {
|
|
86
|
+
const src = this.getAttribute('src');
|
|
87
|
+
if (!src)
|
|
88
|
+
return;
|
|
89
|
+
const img = new Image();
|
|
90
|
+
img.crossOrigin = 'anonymous';
|
|
91
|
+
img.src = src;
|
|
92
|
+
img.onload = () => {
|
|
93
|
+
const canvas = document.createElement('canvas');
|
|
94
|
+
this.#imageNaturalWidth = img.naturalWidth;
|
|
95
|
+
this.#imageNaturalHeight = img.naturalHeight;
|
|
96
|
+
canvas.width = this.#imageNaturalWidth;
|
|
97
|
+
canvas.height = this.#imageNaturalHeight;
|
|
98
|
+
const ctx = canvas.getContext('2d');
|
|
99
|
+
if (!ctx) {
|
|
100
|
+
console.error('Could not get canvas 2d context');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
ctx.drawImage(img, 0, 0);
|
|
104
|
+
this.#imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
105
|
+
this.renderAscii();
|
|
106
|
+
};
|
|
107
|
+
img.onerror = () => {
|
|
108
|
+
console.error(`Failed to load image: ${src}`);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
measureCharWidth() {
|
|
112
|
+
const span = document.createElement('span');
|
|
113
|
+
span.style.font = getComputedStyle(this.#pre).font;
|
|
114
|
+
span.style.visibility = 'hidden';
|
|
115
|
+
span.style.position = 'absolute';
|
|
116
|
+
span.textContent = 'M';
|
|
117
|
+
document.body.appendChild(span);
|
|
118
|
+
const width = span.getBoundingClientRect().width;
|
|
119
|
+
document.body.removeChild(span);
|
|
120
|
+
return width;
|
|
121
|
+
}
|
|
122
|
+
async renderAscii() {
|
|
123
|
+
if (!this.#imageData || !this.#imageNaturalHeight || !this.#imageNaturalWidth)
|
|
124
|
+
return;
|
|
125
|
+
await ensureInit();
|
|
126
|
+
this.#pre.style.fontSize = '';
|
|
127
|
+
const baseCharWidth = this.measureCharWidth();
|
|
128
|
+
if (baseCharWidth === 0)
|
|
129
|
+
return;
|
|
130
|
+
const containerWidth = this.clientWidth;
|
|
131
|
+
const rawCols = Math.floor(containerWidth / baseCharWidth);
|
|
132
|
+
const cols = Math.min(this.#maxCols, Math.max(this.#minCols, rawCols));
|
|
133
|
+
if (rawCols < this.#minCols) {
|
|
134
|
+
const baseFontSize = parseFloat(getComputedStyle(this.#pre).fontSize);
|
|
135
|
+
this.#pre.style.fontSize = `${baseFontSize * (containerWidth / (cols * baseCharWidth))}px`;
|
|
136
|
+
}
|
|
137
|
+
if (cols === this.#lastRenderedCols)
|
|
138
|
+
return;
|
|
139
|
+
this.#lastRenderedCols = cols;
|
|
140
|
+
const aspectRatio = this.#imageNaturalWidth / this.#imageNaturalHeight;
|
|
141
|
+
const rows = Math.round((cols / aspectRatio) * 0.5);
|
|
142
|
+
const art = ascii_art(new Uint8Array(this.#imageData.data.buffer), this.#imageNaturalWidth, this.#imageNaturalHeight, cols, rows);
|
|
143
|
+
this.#pre.textContent = art;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
customElements.define('ascii-img', AsciiImg);
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ascii-web/client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "A web component that renders images as ASCII art using WASM",
|
|
5
|
+
"author": "Raffaele Cataldo",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/ascii-img.js",
|
|
8
|
+
"types": "./dist/ascii-img.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/ascii-img.js",
|
|
12
|
+
"types": "./dist/ascii-img.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@ascii-web/wasm": "0.9.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "^5.6.0"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": true,
|
|
25
|
+
"keywords": [
|
|
26
|
+
"ascii",
|
|
27
|
+
"art",
|
|
28
|
+
"image",
|
|
29
|
+
"wasm",
|
|
30
|
+
"web-component"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT OR Apache-2.0",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/farmeroy/ascii-web.git",
|
|
36
|
+
"directory": "packages/client"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc"
|
|
40
|
+
}
|
|
6
41
|
}
|
package/README.md
DELETED