ascii-shape-renderer 1.0.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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/ascii-renderer.esm.js +447 -0
- package/dist/ascii-renderer.min.js +4 -0
- package/dist/browser.d.ts +48 -0
- package/dist/browser.js +499 -0
- package/dist/contrast.d.ts +14 -0
- package/dist/contrast.js +50 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +16 -0
- package/dist/lookup.d.ts +24 -0
- package/dist/lookup.js +86 -0
- package/dist/renderer.d.ts +40 -0
- package/dist/renderer.js +170 -0
- package/dist/shape-vectors.d.ts +15 -0
- package/dist/shape-vectors.js +125 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.js +2 -0
- package/dist/video-renderer.d.ts +30 -0
- package/dist/video-renderer.js +198 -0
- package/dist/video-types.d.ts +10 -0
- package/dist/video-types.js +2 -0
- package/dist/worker.d.ts +48 -0
- package/dist/worker.js +173 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Will Laws
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ASCII Shape Renderer
|
|
2
|
+
|
|
3
|
+
> ## 📖 This library implements the techniques from [Alex Harri's brilliant blog post](https://alexharri.com/blog/ascii-rendering)
|
|
4
|
+
>
|
|
5
|
+
> **"ASCII characters are not pixels"** — If you want to understand *why* this works, read his post. It's excellent and covers everything in depth with interactive demos.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
**[Live Demo](https://jeeshofone.github.io/ascii-shape-renderer/demo.html)** | **[Video Demo](https://jeeshofone.github.io/ascii-shape-renderer/video-demo.html)** | **[All Examples](https://jeeshofone.github.io/ascii-shape-renderer/example.html)**
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
From [Alex Harri's post](https://alexharri.com/blog/ascii-rendering):
|
|
14
|
+
|
|
15
|
+
> "Most ASCII renderers map each pixel's brightness to a character... but this ignores the *shape* of each character."
|
|
16
|
+
|
|
17
|
+
Instead, this library:
|
|
18
|
+
|
|
19
|
+
1. **Captures character shapes** using 6 sampling circles in a staggered grid
|
|
20
|
+
2. **Matches shapes** via k-d tree lookup in 6D space
|
|
21
|
+
3. **Enhances contrast** to sharpen edges and prevent "staircasing"
|
|
22
|
+
|
|
23
|
+
The result: characters follow contours instead of creating blocky pixels.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- Sharp edges via shape matching (not brightness mapping)
|
|
28
|
+
- Color modes: none, foreground, background, or both
|
|
29
|
+
- Video & webcam support with Web Workers
|
|
30
|
+
- Single script tag — no build step required
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
### Browser
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<script src="https://unpkg.com/ascii-shape-renderer"></script>
|
|
38
|
+
<pre id="output"></pre>
|
|
39
|
+
<script>
|
|
40
|
+
AsciiRenderer.image('photo.jpg', '#output', { colorMode: 'fg' });
|
|
41
|
+
</script>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Video
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<script>
|
|
48
|
+
AsciiRenderer.video('video.mp4', '#output', { colorMode: 'both' });
|
|
49
|
+
</script>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Webcam
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<script>
|
|
56
|
+
AsciiRenderer.webcam('#output', { colorMode: 'fg' });
|
|
57
|
+
</script>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### npm
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install ascii-shape-renderer
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { AsciiRenderer } from 'ascii-shape-renderer';
|
|
68
|
+
|
|
69
|
+
const renderer = new AsciiRenderer({ colorMode: 'fg' });
|
|
70
|
+
const ascii = renderer.render(imageData);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Node.js
|
|
74
|
+
|
|
75
|
+
Works in Node.js with the `canvas` package:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install ascii-shape-renderer canvas
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
const { createCanvas, loadImage } = require('canvas');
|
|
83
|
+
const { AsciiRenderer } = require('ascii-shape-renderer');
|
|
84
|
+
|
|
85
|
+
const renderer = new AsciiRenderer({ colorMode: 'fg' });
|
|
86
|
+
|
|
87
|
+
const img = await loadImage('photo.jpg');
|
|
88
|
+
const canvas = createCanvas(img.width, img.height);
|
|
89
|
+
const ctx = canvas.getContext('2d');
|
|
90
|
+
ctx.drawImage(img, 0, 0);
|
|
91
|
+
|
|
92
|
+
const ascii = renderer.render(ctx.getImageData(0, 0, img.width, img.height));
|
|
93
|
+
console.log(ascii);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Options
|
|
97
|
+
|
|
98
|
+
| Option | Default | Description |
|
|
99
|
+
|--------|---------|-------------|
|
|
100
|
+
| `colorMode` | `'none'` | `'none'`, `'fg'`, `'bg'`, or `'both'` |
|
|
101
|
+
| `globalContrastExponent` | `2` | Sharpens all boundaries |
|
|
102
|
+
| `directionalContrastExponent` | `3` | Prevents staircasing at edges |
|
|
103
|
+
| `cellWidth` / `cellHeight` | `10` / `16` | Cell size in source pixels |
|
|
104
|
+
|
|
105
|
+
## Credits
|
|
106
|
+
|
|
107
|
+
This is an implementation of [Alex Harri's ASCII rendering technique](https://alexharri.com/blog/ascii-rendering). All the clever ideas — shape vectors, contrast enhancement, k-d tree lookups — come from his post.
|
|
108
|
+
|
|
109
|
+
Sample video: [Big Buck Bunny](https://peach.blender.org/) © Blender Foundation | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/)
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// src/browser.ts
|
|
2
|
+
var BASE_CW = 8;
|
|
3
|
+
var BASE_CH = 14;
|
|
4
|
+
var CIRCLE_R = 0.18;
|
|
5
|
+
var CHARS = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
|
|
6
|
+
var INT_POS = [{ x: 0.25, y: 0.2 }, { x: 0.75, y: 0.13 }, { x: 0.25, y: 0.5 }, { x: 0.75, y: 0.5 }, { x: 0.25, y: 0.8 }, { x: 0.75, y: 0.87 }];
|
|
7
|
+
var EXT_POS = [{ x: 0.25, y: -0.15 }, { x: 0.75, y: -0.15 }, { x: -0.15, y: 0.2 }, { x: 1.15, y: 0.13 }, { x: -0.15, y: 0.5 }, { x: 1.15, y: 0.5 }, { x: -0.15, y: 0.8 }, { x: 1.15, y: 0.87 }, { x: 0.25, y: 1.15 }, { x: 0.75, y: 1.15 }];
|
|
8
|
+
var AFFECT = [[0, 1, 2, 4], [0, 1, 3, 5], [2, 4, 6], [3, 5, 7], [4, 6, 8, 9], [5, 7, 8, 9]];
|
|
9
|
+
var shapeVecs = [];
|
|
10
|
+
var kdTree = null;
|
|
11
|
+
var initialized = false;
|
|
12
|
+
var workerPool = [];
|
|
13
|
+
var workerIndex = 0;
|
|
14
|
+
var pendingCallbacks = /* @__PURE__ */ new Map();
|
|
15
|
+
var messageId = 0;
|
|
16
|
+
function getWorker() {
|
|
17
|
+
if (typeof Worker === "undefined") return null;
|
|
18
|
+
if (workerPool.length === 0) {
|
|
19
|
+
const workerCode = `${WORKER_CODE}`;
|
|
20
|
+
try {
|
|
21
|
+
const blob = new Blob([workerCode], { type: "application/javascript" });
|
|
22
|
+
const url = URL.createObjectURL(blob);
|
|
23
|
+
const numWorkers = Math.min(navigator.hardwareConcurrency || 4, 4);
|
|
24
|
+
for (let i = 0; i < numWorkers; i++) {
|
|
25
|
+
const w = new Worker(url);
|
|
26
|
+
w.onmessage = (e) => {
|
|
27
|
+
const cb = pendingCallbacks.get(e.data.id);
|
|
28
|
+
if (cb) {
|
|
29
|
+
pendingCallbacks.delete(e.data.id);
|
|
30
|
+
cb(e.data);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
workerPool.push(w);
|
|
34
|
+
}
|
|
35
|
+
console.log(`[AsciiRenderer] Using ${numWorkers} Web Workers for rendering`);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log("[AsciiRenderer] Web Workers unavailable, using main thread");
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const worker = workerPool[workerIndex];
|
|
42
|
+
workerIndex = (workerIndex + 1) % workerPool.length;
|
|
43
|
+
return worker;
|
|
44
|
+
}
|
|
45
|
+
function renderInWorker(data, width, height, options) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const worker = getWorker();
|
|
48
|
+
if (!worker) {
|
|
49
|
+
init();
|
|
50
|
+
resolve(renderFrame({ data, width, height }, options));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const id = messageId++;
|
|
54
|
+
pendingCallbacks.set(id, resolve);
|
|
55
|
+
worker.postMessage({ type: "render", id, data, width, height, options }, [data.buffer]);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
var WORKER_CODE = `
|
|
59
|
+
const BASE_CW=8,BASE_CH=14,CIRCLE_R=.18,CHARS=' !"#$%&\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^\`_abcdefghijklmnopqrstuvwxyz{|}~',INT_POS=[{x:.25,y:.2},{x:.75,y:.13},{x:.25,y:.5},{x:.75,y:.5},{x:.25,y:.8},{x:.75,y:.87}],EXT_POS=[{x:.25,y:-.15},{x:.75,y:-.15},{x:-.15,y:.2},{x:1.15,y:.13},{x:-.15,y:.5},{x:1.15,y:.5},{x:-.15,y:.8},{x:1.15,y:.87},{x:.25,y:1.15},{x:.75,y:1.15}],AFFECT=[[0,1,2,4],[0,1,3,5],[2,4,6],[3,5,7],[4,6,8,9],[5,7,8,9]];let shapeVecs=[],kdTree=null;const cache=new Map;function init(){if(kdTree)return;const c=new OffscreenCanvas(BASE_CW,BASE_CH),ctx=c.getContext("2d",{willReadFrequently:true});for(const ch of CHARS){ctx.fillStyle="#000",ctx.fillRect(0,0,BASE_CW,BASE_CH),ctx.fillStyle="#fff",ctx.font=BASE_CH+"px monospace",ctx.textBaseline="top",ctx.textAlign="center",ctx.fillText(ch,BASE_CW/2,0);const d=ctx.getImageData(0,0,BASE_CW,BASE_CH),v=INT_POS.map(p=>sampleC(d,p.x*BASE_CW,p.y*BASE_CH,CIRCLE_R*BASE_CH));shapeVecs.push({ch,v})}const max=[0,0,0,0,0,0];shapeVecs.forEach(({v})=>v.forEach((x,i)=>max[i]=Math.max(max[i],x))),shapeVecs=shapeVecs.map(({ch,v})=>({ch,v:v.map((x,i)=>max[i]?x/max[i]:0)})),kdTree=buildKd(shapeVecs)}function sampleC(d,cx,cy,r){let t=0,c=0;for(let dy=-r;dy<=r;dy++)for(let dx=-r;dx<=r;dx++)dx*dx+dy*dy<=r*r&&(x=~~(cx+dx),y=~~(cy+dy),x>=0&&x<d.width&&y>=0&&y<d.height&&(t+=d.data[(y*d.width+x)*4]/255,c++));return c?t/c:0}function buildKd(items,depth=0){if(!items.length)return null;const a=depth%6;items.sort((x,y)=>x.v[a]-y.v[a]);const m=items.length>>1;return{item:items[m],left:buildKd(items.slice(0,m),depth+1),right:buildKd(items.slice(m+1),depth+1),axis:a}}function findNear(n,t,best={dist:1/0,ch:" "}){if(!n)return best;const d=n.item.v.reduce((s,x,i)=>s+(x-t[i])**2,0);d<best.dist&&(best={dist:d,ch:n.item.ch});const diff=t[n.axis]-n.item.v[n.axis];return best=findNear(diff<0?n.left:n.right,t,best),diff*diff<best.dist&&(best=findNear(diff<0?n.right:n.left,t,best)),best}function contrast(int,ext,gE,dE){let v=int.map((val,i)=>{if(dE<=1)return val;let m=val;for(const e of AFFECT[i])m=Math.max(m,ext[e]);return m?Math.pow(val/m,dE)*m:val});if(gE>1){const m=Math.max(...v);m&&(v=v.map(x=>Math.pow(x/m,gE)*m))}return v}function sampleImg(data,w,h,cx,cy,r){let t=0,c=0;for(let dy=-r;dy<=r;dy+=2)for(let dx=-r;dx<=r;dx+=2)dx*dx+dy*dy<=r*r&&(x=~~(cx+dx),y=~~(cy+dy),x>=0&&x<w&&y>=0&&y<h&&(i=(y*w+x)*4,t+=.2126*data[i]/255+.7152*data[i+1]/255+.0722*data[i+2]/255,c++));return c?t/c:0}function sampleClr(data,w,h,cx,cy,cw,ch){let r=0,g=0,b=0,c=0;for(let dy=ch*.25;dy<ch*.75;dy+=2)for(let dx=cw*.25;dx<cw*.75;dx+=2){const x=~~(cx+dx),y=~~(cy+dy);x>=0&&x<w&&y>=0&&y<h&&(i=(y*w+x)*4,r+=data[i],g+=data[i+1],b+=data[i+2],c++)}return c?"#"+[r,g,b].map(v=>(~~(v/c)).toString(16).padStart(2,"0")).join(""):"#000"}function darken(h,a){return"#"+[1,3,5].map(i=>(~~(parseInt(h.slice(i,i+2),16)*(1-a))).toString(16).padStart(2,"0")).join("")}function renderFrame(data,w,h,opts){const gE=opts.globalContrast??2,dE=opts.directionalContrast??3,colorMode=opts.colorMode??"none",cols=Math.round(w/BASE_CW),rows=Math.round(h/BASE_CH),cW=w/cols,cH=h/rows,r=CIRCLE_R*cH;cache.clear();let html="";for(let row=0;row<rows;row++){for(let col=0;col<cols;col++){const cx=col*cW,cy=row*cH,int=INT_POS.map(p=>sampleImg(data,w,h,cx+p.x*cW,cy+p.y*cH,r)),ext=EXT_POS.map(p=>sampleImg(data,w,h,cx+p.x*cW,cy+p.y*cH,r)),enh=contrast(int,ext,gE,dE),key=enh.map(v=>~~(31*v)).join(",");cache.has(key)||cache.set(key,findNear(kdTree,enh).ch);let ch=cache.get(key);"<"===ch?ch="<":">"===ch?ch=">":"&"===ch&&(ch="&"),"none"===colorMode?html+=ch:(clr=sampleClr(data,w,h,cx,cy,cW,cH),st=[],"fg"!==colorMode&&"both"!==colorMode||st.push("color:"+clr),"bg"!==colorMode&&"both"!==colorMode||st.push("background:"+darken(clr,.5)),html+='<span style="'+st.join(";")+'">'+ch+"</span>")}html+="\\n"}return{html,cols,rows}}self.onmessage=e=>{if("render"===e.data.type){init();const{id,data,width,height,options}=e.data,result=renderFrame(data,width,height,options);self.postMessage({id,...result})}};
|
|
60
|
+
`;
|
|
61
|
+
function init() {
|
|
62
|
+
if (initialized) return;
|
|
63
|
+
const c = document.createElement("canvas");
|
|
64
|
+
c.width = BASE_CW;
|
|
65
|
+
c.height = BASE_CH;
|
|
66
|
+
const ctx = c.getContext("2d", { willReadFrequently: true });
|
|
67
|
+
for (const ch of CHARS) {
|
|
68
|
+
ctx.fillStyle = "#000";
|
|
69
|
+
ctx.fillRect(0, 0, BASE_CW, BASE_CH);
|
|
70
|
+
ctx.fillStyle = "#fff";
|
|
71
|
+
ctx.font = `${BASE_CH}px monospace`;
|
|
72
|
+
ctx.textBaseline = "top";
|
|
73
|
+
ctx.textAlign = "center";
|
|
74
|
+
ctx.fillText(ch, BASE_CW / 2, 0);
|
|
75
|
+
const d = ctx.getImageData(0, 0, BASE_CW, BASE_CH);
|
|
76
|
+
const v = INT_POS.map((p) => sampleCircle(d, p.x * BASE_CW, p.y * BASE_CH, CIRCLE_R * BASE_CH));
|
|
77
|
+
shapeVecs.push({ ch, v });
|
|
78
|
+
}
|
|
79
|
+
const max = [0, 0, 0, 0, 0, 0];
|
|
80
|
+
shapeVecs.forEach(({ v }) => v.forEach((x, i) => max[i] = Math.max(max[i], x)));
|
|
81
|
+
shapeVecs = shapeVecs.map(({ ch, v }) => ({ ch, v: v.map((x, i) => max[i] ? x / max[i] : 0) }));
|
|
82
|
+
kdTree = buildKd(shapeVecs);
|
|
83
|
+
initialized = true;
|
|
84
|
+
}
|
|
85
|
+
function sampleCircle(d, cx, cy, r) {
|
|
86
|
+
let t = 0, c = 0;
|
|
87
|
+
for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++)
|
|
88
|
+
if (dx * dx + dy * dy <= r * r) {
|
|
89
|
+
const x = ~~(cx + dx), y = ~~(cy + dy);
|
|
90
|
+
if (x >= 0 && x < d.width && y >= 0 && y < d.height) {
|
|
91
|
+
t += d.data[(y * d.width + x) * 4] / 255;
|
|
92
|
+
c++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return c ? t / c : 0;
|
|
96
|
+
}
|
|
97
|
+
function buildKd(items, depth = 0) {
|
|
98
|
+
if (!items.length) return null;
|
|
99
|
+
const a = depth % 6;
|
|
100
|
+
items.sort((x, y) => x.v[a] - y.v[a]);
|
|
101
|
+
const m = items.length >> 1;
|
|
102
|
+
return { item: items[m], left: buildKd(items.slice(0, m), depth + 1), right: buildKd(items.slice(m + 1), depth + 1), axis: a };
|
|
103
|
+
}
|
|
104
|
+
function findNear(n, t, best = { dist: Infinity, ch: " " }) {
|
|
105
|
+
if (!n) return best;
|
|
106
|
+
const d = n.item.v.reduce((s, x, i) => s + (x - t[i]) ** 2, 0);
|
|
107
|
+
if (d < best.dist) best = { dist: d, ch: n.item.ch };
|
|
108
|
+
const diff = t[n.axis] - n.item.v[n.axis];
|
|
109
|
+
best = findNear(diff < 0 ? n.left : n.right, t, best);
|
|
110
|
+
if (diff * diff < best.dist) best = findNear(diff < 0 ? n.right : n.left, t, best);
|
|
111
|
+
return best;
|
|
112
|
+
}
|
|
113
|
+
function contrast(int, ext, gE, dE) {
|
|
114
|
+
let v = int.map((val, i) => {
|
|
115
|
+
if (dE <= 1) return val;
|
|
116
|
+
let m = val;
|
|
117
|
+
for (const e of AFFECT[i]) m = Math.max(m, ext[e]);
|
|
118
|
+
return m ? Math.pow(val / m, dE) * m : val;
|
|
119
|
+
});
|
|
120
|
+
if (gE > 1) {
|
|
121
|
+
const m = Math.max(...v);
|
|
122
|
+
if (m) v = v.map((x) => Math.pow(x / m, gE) * m);
|
|
123
|
+
}
|
|
124
|
+
return v;
|
|
125
|
+
}
|
|
126
|
+
function sampleImg(d, cx, cy, r) {
|
|
127
|
+
let t = 0, c = 0;
|
|
128
|
+
for (let dy = -r; dy <= r; dy += 2) for (let dx = -r; dx <= r; dx += 2)
|
|
129
|
+
if (dx * dx + dy * dy <= r * r) {
|
|
130
|
+
const x = ~~(cx + dx), y = ~~(cy + dy);
|
|
131
|
+
if (x >= 0 && x < d.width && y >= 0 && y < d.height) {
|
|
132
|
+
const i = (y * d.width + x) * 4;
|
|
133
|
+
t += 0.2126 * d.data[i] / 255 + 0.7152 * d.data[i + 1] / 255 + 0.0722 * d.data[i + 2] / 255;
|
|
134
|
+
c++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return c ? t / c : 0;
|
|
138
|
+
}
|
|
139
|
+
function sampleClr(d, cx, cy, cw, ch) {
|
|
140
|
+
let r = 0, g = 0, b = 0, c = 0;
|
|
141
|
+
for (let dy = ch * 0.25; dy < ch * 0.75; dy += 2) for (let dx = cw * 0.25; dx < cw * 0.75; dx += 2) {
|
|
142
|
+
const x = ~~(cx + dx), y = ~~(cy + dy);
|
|
143
|
+
if (x >= 0 && x < d.width && y >= 0 && y < d.height) {
|
|
144
|
+
const i = (y * d.width + x) * 4;
|
|
145
|
+
r += d.data[i];
|
|
146
|
+
g += d.data[i + 1];
|
|
147
|
+
b += d.data[i + 2];
|
|
148
|
+
c++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return c ? "#" + [r, g, b].map((v) => (~~(v / c)).toString(16).padStart(2, "0")).join("") : "#000";
|
|
152
|
+
}
|
|
153
|
+
function darken(h, a) {
|
|
154
|
+
return "#" + [1, 3, 5].map((i) => (~~(parseInt(h.slice(i, i + 2), 16) * (1 - a))).toString(16).padStart(2, "0")).join("");
|
|
155
|
+
}
|
|
156
|
+
var cache = /* @__PURE__ */ new Map();
|
|
157
|
+
function renderFrame(imgData, opts) {
|
|
158
|
+
const gE = opts.globalContrast ?? 2;
|
|
159
|
+
const dE = opts.directionalContrast ?? 3;
|
|
160
|
+
const colorMode = opts.colorMode ?? "none";
|
|
161
|
+
const cols = Math.round(imgData.width / BASE_CW);
|
|
162
|
+
const rows = Math.round(imgData.height / BASE_CH);
|
|
163
|
+
const cW = imgData.width / cols, cH = imgData.height / rows;
|
|
164
|
+
const r = CIRCLE_R * cH;
|
|
165
|
+
cache.clear();
|
|
166
|
+
let html = "";
|
|
167
|
+
for (let row = 0; row < rows; row++) {
|
|
168
|
+
for (let col = 0; col < cols; col++) {
|
|
169
|
+
const cx = col * cW, cy = row * cH;
|
|
170
|
+
const int = INT_POS.map((p) => sampleImg(imgData, cx + p.x * cW, cy + p.y * cH, r));
|
|
171
|
+
const ext = EXT_POS.map((p) => sampleImg(imgData, cx + p.x * cW, cy + p.y * cH, r));
|
|
172
|
+
const enh = contrast(int, ext, gE, dE);
|
|
173
|
+
const key = enh.map((v) => ~~(v * 31)).join(",");
|
|
174
|
+
if (!cache.has(key)) cache.set(key, findNear(kdTree, enh).ch);
|
|
175
|
+
let ch = cache.get(key);
|
|
176
|
+
if (ch === "<") ch = "<";
|
|
177
|
+
else if (ch === ">") ch = ">";
|
|
178
|
+
else if (ch === "&") ch = "&";
|
|
179
|
+
if (colorMode === "none") {
|
|
180
|
+
html += ch;
|
|
181
|
+
} else {
|
|
182
|
+
const clr = sampleClr(imgData, cx, cy, cW, cH);
|
|
183
|
+
const st = [];
|
|
184
|
+
if (colorMode === "fg" || colorMode === "both") st.push(`color:${clr}`);
|
|
185
|
+
if (colorMode === "bg" || colorMode === "both") st.push(`background:${darken(clr, 0.5)}`);
|
|
186
|
+
html += `<span style="${st.join(";")}">${ch}</span>`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
html += "\n";
|
|
190
|
+
}
|
|
191
|
+
return html;
|
|
192
|
+
}
|
|
193
|
+
function getEl(el) {
|
|
194
|
+
return typeof el === "string" ? document.querySelector(el) : el;
|
|
195
|
+
}
|
|
196
|
+
function createOutput(container, colorMode) {
|
|
197
|
+
const pre = document.createElement("pre");
|
|
198
|
+
pre.style.cssText = "margin:0;line-height:1;font-family:monospace;white-space:pre;";
|
|
199
|
+
if (colorMode === "none") pre.style.color = "#0f0";
|
|
200
|
+
pre.style.background = "#000";
|
|
201
|
+
container.innerHTML = "";
|
|
202
|
+
container.appendChild(pre);
|
|
203
|
+
return pre;
|
|
204
|
+
}
|
|
205
|
+
function image(source, container, options = {}) {
|
|
206
|
+
init();
|
|
207
|
+
const containerEl = getEl(container);
|
|
208
|
+
const output = createOutput(containerEl, options.colorMode ?? "none");
|
|
209
|
+
const canvas = document.createElement("canvas");
|
|
210
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
211
|
+
let img;
|
|
212
|
+
let currentOpts = { ...options };
|
|
213
|
+
function render() {
|
|
214
|
+
const maxW = currentOpts.maxWidth ?? 800;
|
|
215
|
+
const scale = Math.min(1, maxW / img.width);
|
|
216
|
+
canvas.width = ~~(img.width * scale);
|
|
217
|
+
canvas.height = ~~(img.height * scale);
|
|
218
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
219
|
+
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
220
|
+
const html = renderFrame(imgData, currentOpts);
|
|
221
|
+
const cols = Math.round(canvas.width / BASE_CW);
|
|
222
|
+
const containerWidth = containerEl.clientWidth - 16;
|
|
223
|
+
const fontSize = containerWidth / (cols * 0.6);
|
|
224
|
+
output.style.fontSize = fontSize + "px";
|
|
225
|
+
output.innerHTML = html;
|
|
226
|
+
if (currentOpts.colorMode === "none") output.style.color = "#0f0";
|
|
227
|
+
else output.style.color = "";
|
|
228
|
+
}
|
|
229
|
+
function load(src) {
|
|
230
|
+
if (typeof src === "string") {
|
|
231
|
+
img = new Image();
|
|
232
|
+
img.crossOrigin = "anonymous";
|
|
233
|
+
img.onload = render;
|
|
234
|
+
img.src = src;
|
|
235
|
+
} else {
|
|
236
|
+
img = src;
|
|
237
|
+
render();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
load(source);
|
|
241
|
+
return {
|
|
242
|
+
update(opts) {
|
|
243
|
+
if (opts) currentOpts = { ...currentOpts, ...opts };
|
|
244
|
+
if (img) render();
|
|
245
|
+
},
|
|
246
|
+
destroy() {
|
|
247
|
+
containerEl.innerHTML = "";
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function video(source, container, options = {}) {
|
|
252
|
+
init();
|
|
253
|
+
const containerEl = getEl(container);
|
|
254
|
+
const output = createOutput(containerEl, options.colorMode ?? "none");
|
|
255
|
+
const canvas = document.createElement("canvas");
|
|
256
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
257
|
+
const vid = document.createElement("video");
|
|
258
|
+
vid.playsInline = true;
|
|
259
|
+
vid.muted = true;
|
|
260
|
+
vid.loop = true;
|
|
261
|
+
let currentOpts = { ...options };
|
|
262
|
+
let playing = false;
|
|
263
|
+
let animId = null;
|
|
264
|
+
let lastRender = 0;
|
|
265
|
+
const minFrameTime = 1e3 / (options.fps ?? 30);
|
|
266
|
+
let canvasReady = false;
|
|
267
|
+
function setupCanvas() {
|
|
268
|
+
if (canvasReady || !vid.videoWidth) return;
|
|
269
|
+
const maxW = currentOpts.maxWidth ?? 640;
|
|
270
|
+
const scale = Math.min(1, maxW / vid.videoWidth);
|
|
271
|
+
canvas.width = ~~(vid.videoWidth * scale);
|
|
272
|
+
canvas.height = ~~(vid.videoHeight * scale);
|
|
273
|
+
canvasReady = true;
|
|
274
|
+
}
|
|
275
|
+
let rendering = false;
|
|
276
|
+
let cachedFontSize = 0;
|
|
277
|
+
async function render() {
|
|
278
|
+
if (!vid.videoWidth || rendering) return;
|
|
279
|
+
setupCanvas();
|
|
280
|
+
rendering = true;
|
|
281
|
+
ctx.drawImage(vid, 0, 0, canvas.width, canvas.height);
|
|
282
|
+
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
283
|
+
const result = await renderInWorker(
|
|
284
|
+
new Uint8ClampedArray(imgData.data),
|
|
285
|
+
canvas.width,
|
|
286
|
+
canvas.height,
|
|
287
|
+
currentOpts
|
|
288
|
+
);
|
|
289
|
+
if (!cachedFontSize) {
|
|
290
|
+
const containerWidth = containerEl.clientWidth - 16;
|
|
291
|
+
cachedFontSize = containerWidth / (result.cols * 0.6);
|
|
292
|
+
output.style.fontSize = cachedFontSize + "px";
|
|
293
|
+
}
|
|
294
|
+
output.innerHTML = result.html;
|
|
295
|
+
if (currentOpts.colorMode === "none") output.style.color = "#0f0";
|
|
296
|
+
else output.style.color = "";
|
|
297
|
+
rendering = false;
|
|
298
|
+
}
|
|
299
|
+
function loop() {
|
|
300
|
+
if (!playing) return;
|
|
301
|
+
const now = performance.now();
|
|
302
|
+
if (now - lastRender >= minFrameTime) {
|
|
303
|
+
lastRender = now;
|
|
304
|
+
render();
|
|
305
|
+
}
|
|
306
|
+
animId = requestAnimationFrame(loop);
|
|
307
|
+
}
|
|
308
|
+
if (source instanceof File) {
|
|
309
|
+
vid.src = URL.createObjectURL(source);
|
|
310
|
+
} else if (typeof source === "string") {
|
|
311
|
+
vid.src = source;
|
|
312
|
+
} else {
|
|
313
|
+
vid.src = source.src;
|
|
314
|
+
}
|
|
315
|
+
vid.onloadedmetadata = () => {
|
|
316
|
+
render();
|
|
317
|
+
if (options.autoplay) {
|
|
318
|
+
playing = true;
|
|
319
|
+
vid.play();
|
|
320
|
+
loop();
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
play() {
|
|
325
|
+
if (playing || !vid.readyState) return;
|
|
326
|
+
playing = true;
|
|
327
|
+
vid.play();
|
|
328
|
+
loop();
|
|
329
|
+
},
|
|
330
|
+
pause() {
|
|
331
|
+
playing = false;
|
|
332
|
+
vid.pause();
|
|
333
|
+
if (animId) cancelAnimationFrame(animId);
|
|
334
|
+
},
|
|
335
|
+
seek(time) {
|
|
336
|
+
vid.currentTime = time;
|
|
337
|
+
if (!playing) render();
|
|
338
|
+
},
|
|
339
|
+
update(opts) {
|
|
340
|
+
if (opts) currentOpts = { ...currentOpts, ...opts };
|
|
341
|
+
if (!playing) render();
|
|
342
|
+
},
|
|
343
|
+
destroy() {
|
|
344
|
+
playing = false;
|
|
345
|
+
if (animId) cancelAnimationFrame(animId);
|
|
346
|
+
if (vid.src.startsWith("blob:")) URL.revokeObjectURL(vid.src);
|
|
347
|
+
containerEl.innerHTML = "";
|
|
348
|
+
},
|
|
349
|
+
get isPlaying() {
|
|
350
|
+
return playing;
|
|
351
|
+
},
|
|
352
|
+
get currentTime() {
|
|
353
|
+
return vid.currentTime;
|
|
354
|
+
},
|
|
355
|
+
get duration() {
|
|
356
|
+
return vid.duration;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
var browser_default = { image, video, webcam };
|
|
361
|
+
function webcam(container, options = {}) {
|
|
362
|
+
init();
|
|
363
|
+
const containerEl = getEl(container);
|
|
364
|
+
const output = createOutput(containerEl, options.colorMode ?? "none");
|
|
365
|
+
const canvas = document.createElement("canvas");
|
|
366
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
367
|
+
let currentOpts = { ...options };
|
|
368
|
+
let animId = null;
|
|
369
|
+
let stream = null;
|
|
370
|
+
let lastRender = 0;
|
|
371
|
+
const minFrameTime = 1e3 / (options.fps ?? 30);
|
|
372
|
+
const vid = document.createElement("video");
|
|
373
|
+
vid.playsInline = true;
|
|
374
|
+
vid.muted = true;
|
|
375
|
+
let canvasReady = false;
|
|
376
|
+
function setupCanvas() {
|
|
377
|
+
if (canvasReady || !vid.videoWidth) return;
|
|
378
|
+
const maxW = currentOpts.maxWidth ?? 640;
|
|
379
|
+
const scale = Math.min(1, maxW / vid.videoWidth);
|
|
380
|
+
canvas.width = ~~(vid.videoWidth * scale);
|
|
381
|
+
canvas.height = ~~(vid.videoHeight * scale);
|
|
382
|
+
canvasReady = true;
|
|
383
|
+
}
|
|
384
|
+
let rendering = false;
|
|
385
|
+
let cachedFontSize = 0;
|
|
386
|
+
async function render() {
|
|
387
|
+
if (!vid.videoWidth || rendering) return;
|
|
388
|
+
setupCanvas();
|
|
389
|
+
rendering = true;
|
|
390
|
+
ctx.drawImage(vid, 0, 0, canvas.width, canvas.height);
|
|
391
|
+
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
392
|
+
const result = await renderInWorker(
|
|
393
|
+
new Uint8ClampedArray(imgData.data),
|
|
394
|
+
canvas.width,
|
|
395
|
+
canvas.height,
|
|
396
|
+
currentOpts
|
|
397
|
+
);
|
|
398
|
+
if (!cachedFontSize) {
|
|
399
|
+
const containerWidth = containerEl.clientWidth - 16;
|
|
400
|
+
cachedFontSize = containerWidth / (result.cols * 0.6);
|
|
401
|
+
output.style.fontSize = cachedFontSize + "px";
|
|
402
|
+
}
|
|
403
|
+
output.innerHTML = result.html;
|
|
404
|
+
if (currentOpts.colorMode === "none") output.style.color = "#0f0";
|
|
405
|
+
else output.style.color = "";
|
|
406
|
+
rendering = false;
|
|
407
|
+
}
|
|
408
|
+
function loop() {
|
|
409
|
+
const now = performance.now();
|
|
410
|
+
if (now - lastRender >= minFrameTime) {
|
|
411
|
+
lastRender = now;
|
|
412
|
+
render();
|
|
413
|
+
}
|
|
414
|
+
animId = requestAnimationFrame(loop);
|
|
415
|
+
}
|
|
416
|
+
const videoConstraints = {
|
|
417
|
+
video: {
|
|
418
|
+
width: { ideal: 1280 },
|
|
419
|
+
height: { ideal: 720 }
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
return navigator.mediaDevices.getUserMedia(videoConstraints).then((s) => {
|
|
423
|
+
stream = s;
|
|
424
|
+
vid.srcObject = stream;
|
|
425
|
+
vid.play();
|
|
426
|
+
vid.onplaying = () => {
|
|
427
|
+
setupCanvas();
|
|
428
|
+
loop();
|
|
429
|
+
};
|
|
430
|
+
return {
|
|
431
|
+
stop() {
|
|
432
|
+
if (animId) cancelAnimationFrame(animId);
|
|
433
|
+
stream?.getTracks().forEach((t) => t.stop());
|
|
434
|
+
containerEl.innerHTML = "";
|
|
435
|
+
},
|
|
436
|
+
update(opts) {
|
|
437
|
+
if (opts) currentOpts = { ...currentOpts, ...opts };
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
export {
|
|
443
|
+
browser_default as default,
|
|
444
|
+
image,
|
|
445
|
+
video,
|
|
446
|
+
webcam
|
|
447
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"use strict";var AsciiRenderer=(()=>{var W=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var D=Object.prototype.hasOwnProperty;var z=(e,i)=>{for(var r in i)W(e,r,{get:i[r],enumerable:!0})},$=(e,i,r,a)=>{if(i&&typeof i=="object"||typeof i=="function")for(let n of q(i))!D.call(e,n)&&n!==r&&W(e,n,{get:()=>i[n],enumerable:!(a=N(i,n))||a.enumerable});return e};var K=e=>$(W({},"__esModule",{value:!0}),e);var ie={};z(ie,{default:()=>oe,image:()=>P,video:()=>j,webcam:()=>U});var X=" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",L=[{x:.25,y:.2},{x:.75,y:.13},{x:.25,y:.5},{x:.75,y:.5},{x:.25,y:.8},{x:.75,y:.87}],G=[{x:.25,y:-.15},{x:.75,y:-.15},{x:-.15,y:.2},{x:1.15,y:.13},{x:-.15,y:.5},{x:1.15,y:.5},{x:-.15,y:.8},{x:1.15,y:.87},{x:.25,y:1.15},{x:.75,y:1.15}],J=[[0,1,2,4],[0,1,3,5],[2,4,6],[3,5,7],[4,6,8,9],[5,7,8,9]],C=[],O=null,k=!1,v=[],A=0,H=new Map,Q=0;function Y(){if(typeof Worker>"u")return null;if(v.length===0){let i=`${Z}`;try{let r=new Blob([i],{type:"application/javascript"}),a=URL.createObjectURL(r),n=Math.min(navigator.hardwareConcurrency||4,4);for(let o=0;o<n;o++){let c=new Worker(a);c.onmessage=t=>{let s=H.get(t.data.id);s&&(H.delete(t.data.id),s(t.data))},v.push(c)}console.log(`[AsciiRenderer] Using ${n} Web Workers for rendering`)}catch{return console.log("[AsciiRenderer] Web Workers unavailable, using main thread"),null}}let e=v[A];return A=(A+1)%v.length,e}function F(e,i,r,a){return new Promise(n=>{let o=Y();if(!o){S(),n(V({data:e,width:i,height:r},a));return}let c=Q++;H.set(c,n),o.postMessage({type:"render",id:c,data:e,width:i,height:r,options:a},[e.buffer])})}var Z=`
|
|
2
|
+
const BASE_CW=8,BASE_CH=14,CIRCLE_R=.18,CHARS=' !"#$%&\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^\`_abcdefghijklmnopqrstuvwxyz{|}~',INT_POS=[{x:.25,y:.2},{x:.75,y:.13},{x:.25,y:.5},{x:.75,y:.5},{x:.25,y:.8},{x:.75,y:.87}],EXT_POS=[{x:.25,y:-.15},{x:.75,y:-.15},{x:-.15,y:.2},{x:1.15,y:.13},{x:-.15,y:.5},{x:1.15,y:.5},{x:-.15,y:.8},{x:1.15,y:.87},{x:.25,y:1.15},{x:.75,y:1.15}],AFFECT=[[0,1,2,4],[0,1,3,5],[2,4,6],[3,5,7],[4,6,8,9],[5,7,8,9]];let shapeVecs=[],kdTree=null;const cache=new Map;function init(){if(kdTree)return;const c=new OffscreenCanvas(BASE_CW,BASE_CH),ctx=c.getContext("2d",{willReadFrequently:true});for(const ch of CHARS){ctx.fillStyle="#000",ctx.fillRect(0,0,BASE_CW,BASE_CH),ctx.fillStyle="#fff",ctx.font=BASE_CH+"px monospace",ctx.textBaseline="top",ctx.textAlign="center",ctx.fillText(ch,BASE_CW/2,0);const d=ctx.getImageData(0,0,BASE_CW,BASE_CH),v=INT_POS.map(p=>sampleC(d,p.x*BASE_CW,p.y*BASE_CH,CIRCLE_R*BASE_CH));shapeVecs.push({ch,v})}const max=[0,0,0,0,0,0];shapeVecs.forEach(({v})=>v.forEach((x,i)=>max[i]=Math.max(max[i],x))),shapeVecs=shapeVecs.map(({ch,v})=>({ch,v:v.map((x,i)=>max[i]?x/max[i]:0)})),kdTree=buildKd(shapeVecs)}function sampleC(d,cx,cy,r){let t=0,c=0;for(let dy=-r;dy<=r;dy++)for(let dx=-r;dx<=r;dx++)dx*dx+dy*dy<=r*r&&(x=~~(cx+dx),y=~~(cy+dy),x>=0&&x<d.width&&y>=0&&y<d.height&&(t+=d.data[(y*d.width+x)*4]/255,c++));return c?t/c:0}function buildKd(items,depth=0){if(!items.length)return null;const a=depth%6;items.sort((x,y)=>x.v[a]-y.v[a]);const m=items.length>>1;return{item:items[m],left:buildKd(items.slice(0,m),depth+1),right:buildKd(items.slice(m+1),depth+1),axis:a}}function findNear(n,t,best={dist:1/0,ch:" "}){if(!n)return best;const d=n.item.v.reduce((s,x,i)=>s+(x-t[i])**2,0);d<best.dist&&(best={dist:d,ch:n.item.ch});const diff=t[n.axis]-n.item.v[n.axis];return best=findNear(diff<0?n.left:n.right,t,best),diff*diff<best.dist&&(best=findNear(diff<0?n.right:n.left,t,best)),best}function contrast(int,ext,gE,dE){let v=int.map((val,i)=>{if(dE<=1)return val;let m=val;for(const e of AFFECT[i])m=Math.max(m,ext[e]);return m?Math.pow(val/m,dE)*m:val});if(gE>1){const m=Math.max(...v);m&&(v=v.map(x=>Math.pow(x/m,gE)*m))}return v}function sampleImg(data,w,h,cx,cy,r){let t=0,c=0;for(let dy=-r;dy<=r;dy+=2)for(let dx=-r;dx<=r;dx+=2)dx*dx+dy*dy<=r*r&&(x=~~(cx+dx),y=~~(cy+dy),x>=0&&x<w&&y>=0&&y<h&&(i=(y*w+x)*4,t+=.2126*data[i]/255+.7152*data[i+1]/255+.0722*data[i+2]/255,c++));return c?t/c:0}function sampleClr(data,w,h,cx,cy,cw,ch){let r=0,g=0,b=0,c=0;for(let dy=ch*.25;dy<ch*.75;dy+=2)for(let dx=cw*.25;dx<cw*.75;dx+=2){const x=~~(cx+dx),y=~~(cy+dy);x>=0&&x<w&&y>=0&&y<h&&(i=(y*w+x)*4,r+=data[i],g+=data[i+1],b+=data[i+2],c++)}return c?"#"+[r,g,b].map(v=>(~~(v/c)).toString(16).padStart(2,"0")).join(""):"#000"}function darken(h,a){return"#"+[1,3,5].map(i=>(~~(parseInt(h.slice(i,i+2),16)*(1-a))).toString(16).padStart(2,"0")).join("")}function renderFrame(data,w,h,opts){const gE=opts.globalContrast??2,dE=opts.directionalContrast??3,colorMode=opts.colorMode??"none",cols=Math.round(w/BASE_CW),rows=Math.round(h/BASE_CH),cW=w/cols,cH=h/rows,r=CIRCLE_R*cH;cache.clear();let html="";for(let row=0;row<rows;row++){for(let col=0;col<cols;col++){const cx=col*cW,cy=row*cH,int=INT_POS.map(p=>sampleImg(data,w,h,cx+p.x*cW,cy+p.y*cH,r)),ext=EXT_POS.map(p=>sampleImg(data,w,h,cx+p.x*cW,cy+p.y*cH,r)),enh=contrast(int,ext,gE,dE),key=enh.map(v=>~~(31*v)).join(",");cache.has(key)||cache.set(key,findNear(kdTree,enh).ch);let ch=cache.get(key);"<"===ch?ch="<":">"===ch?ch=">":"&"===ch&&(ch="&"),"none"===colorMode?html+=ch:(clr=sampleClr(data,w,h,cx,cy,cW,cH),st=[],"fg"!==colorMode&&"both"!==colorMode||st.push("color:"+clr),"bg"!==colorMode&&"both"!==colorMode||st.push("background:"+darken(clr,.5)),html+='<span style="'+st.join(";")+'">'+ch+"</span>")}html+="\\n"}return{html,cols,rows}}self.onmessage=e=>{if("render"===e.data.type){init();const{id,data,width,height,options}=e.data,result=renderFrame(data,width,height,options);self.postMessage({id,...result})}};
|
|
3
|
+
`;function S(){if(k)return;let e=document.createElement("canvas");e.width=8,e.height=14;let i=e.getContext("2d",{willReadFrequently:!0});for(let a of X){i.fillStyle="#000",i.fillRect(0,0,8,14),i.fillStyle="#fff",i.font="14px monospace",i.textBaseline="top",i.textAlign="center",i.fillText(a,8/2,0);let n=i.getImageData(0,0,8,14),o=L.map(c=>ee(n,c.x*8,c.y*14,.18*14));C.push({ch:a,v:o})}let r=[0,0,0,0,0,0];C.forEach(({v:a})=>a.forEach((n,o)=>r[o]=Math.max(r[o],n))),C=C.map(({ch:a,v:n})=>({ch:a,v:n.map((o,c)=>r[c]?o/r[c]:0)})),O=T(C),k=!0}function ee(e,i,r,a){let n=0,o=0;for(let c=-a;c<=a;c++)for(let t=-a;t<=a;t++)if(t*t+c*c<=a*a){let s=~~(i+t),l=~~(r+c);s>=0&&s<e.width&&l>=0&&l<e.height&&(n+=e.data[(l*e.width+s)*4]/255,o++)}return o?n/o:0}function T(e,i=0){if(!e.length)return null;let r=i%6;e.sort((n,o)=>n.v[r]-o.v[r]);let a=e.length>>1;return{item:e[a],left:T(e.slice(0,a),i+1),right:T(e.slice(a+1),i+1),axis:r}}function _(e,i,r={dist:1/0,ch:" "}){if(!e)return r;let a=e.item.v.reduce((o,c,t)=>o+(c-i[t])**2,0);a<r.dist&&(r={dist:a,ch:e.item.ch});let n=i[e.axis]-e.item.v[e.axis];return r=_(n<0?e.left:e.right,i,r),n*n<r.dist&&(r=_(n<0?e.right:e.left,i,r)),r}function te(e,i,r,a){let n=e.map((o,c)=>{if(a<=1)return o;let t=o;for(let s of J[c])t=Math.max(t,i[s]);return t?Math.pow(o/t,a)*t:o});if(r>1){let o=Math.max(...n);o&&(n=n.map(c=>Math.pow(c/o,r)*o))}return n}function B(e,i,r,a){let n=0,o=0;for(let c=-a;c<=a;c+=2)for(let t=-a;t<=a;t+=2)if(t*t+c*c<=a*a){let s=~~(i+t),l=~~(r+c);if(s>=0&&s<e.width&&l>=0&&l<e.height){let h=(l*e.width+s)*4;n+=.2126*e.data[h]/255+.7152*e.data[h+1]/255+.0722*e.data[h+2]/255,o++}}return o?n/o:0}function ne(e,i,r,a,n){let o=0,c=0,t=0,s=0;for(let l=n*.25;l<n*.75;l+=2)for(let h=a*.25;h<a*.75;h+=2){let d=~~(i+h),f=~~(r+l);if(d>=0&&d<e.width&&f>=0&&f<e.height){let y=(f*e.width+d)*4;o+=e.data[y],c+=e.data[y+1],t+=e.data[y+2],s++}}return s?"#"+[o,c,t].map(l=>(~~(l/s)).toString(16).padStart(2,"0")).join(""):"#000"}function re(e,i){return"#"+[1,3,5].map(r=>(~~(parseInt(e.slice(r,r+2),16)*(1-i))).toString(16).padStart(2,"0")).join("")}var M=new Map;function V(e,i){let r=i.globalContrast??2,a=i.directionalContrast??3,n=i.colorMode??"none",o=Math.round(e.width/8),c=Math.round(e.height/14),t=e.width/o,s=e.height/c,l=.18*s;M.clear();let h="";for(let d=0;d<c;d++){for(let f=0;f<o;f++){let y=f*t,p=d*s,g=L.map(u=>B(e,y+u.x*t,p+u.y*s,l)),E=G.map(u=>B(e,y+u.x*t,p+u.y*s,l)),x=te(g,E,r,a),w=x.map(u=>~~(u*31)).join(",");M.has(w)||M.set(w,_(O,x).ch);let m=M.get(w);if(m==="<"?m="<":m===">"?m=">":m==="&"&&(m="&"),n==="none")h+=m;else{let u=ne(e,y,p,t,s),b=[];(n==="fg"||n==="both")&&b.push(`color:${u}`),(n==="bg"||n==="both")&&b.push(`background:${re(u,.5)}`),h+=`<span style="${b.join(";")}">${m}</span>`}}h+=`
|
|
4
|
+
`}return h}function R(e){return typeof e=="string"?document.querySelector(e):e}function I(e,i){let r=document.createElement("pre");return r.style.cssText="margin:0;line-height:1;font-family:monospace;white-space:pre;",i==="none"&&(r.style.color="#0f0"),r.style.background="#000",e.innerHTML="",e.appendChild(r),r}function P(e,i,r={}){S();let a=R(i),n=I(a,r.colorMode??"none"),o=document.createElement("canvas"),c=o.getContext("2d",{willReadFrequently:!0}),t,s={...r};function l(){let d=s.maxWidth??800,f=Math.min(1,d/t.width);o.width=~~(t.width*f),o.height=~~(t.height*f),c.drawImage(t,0,0,o.width,o.height);let y=c.getImageData(0,0,o.width,o.height),p=V(y,s),g=Math.round(o.width/8),x=(a.clientWidth-16)/(g*.6);n.style.fontSize=x+"px",n.innerHTML=p,s.colorMode==="none"?n.style.color="#0f0":n.style.color=""}function h(d){typeof d=="string"?(t=new Image,t.crossOrigin="anonymous",t.onload=l,t.src=d):(t=d,l())}return h(e),{update(d){d&&(s={...s,...d}),t&&l()},destroy(){a.innerHTML=""}}}function j(e,i,r={}){S();let a=R(i),n=I(a,r.colorMode??"none"),o=document.createElement("canvas"),c=o.getContext("2d",{willReadFrequently:!0}),t=document.createElement("video");t.playsInline=!0,t.muted=!0,t.loop=!0;let s={...r},l=!1,h=null,d=0,f=1e3/(r.fps??30),y=!1;function p(){if(y||!t.videoWidth)return;let m=s.maxWidth??640,u=Math.min(1,m/t.videoWidth);o.width=~~(t.videoWidth*u),o.height=~~(t.videoHeight*u),y=!0}let g=!1,E=0;async function x(){if(!t.videoWidth||g)return;p(),g=!0,c.drawImage(t,0,0,o.width,o.height);let m=c.getImageData(0,0,o.width,o.height),u=await F(new Uint8ClampedArray(m.data),o.width,o.height,s);E||(E=(a.clientWidth-16)/(u.cols*.6),n.style.fontSize=E+"px"),n.innerHTML=u.html,s.colorMode==="none"?n.style.color="#0f0":n.style.color="",g=!1}function w(){if(!l)return;let m=performance.now();m-d>=f&&(d=m,x()),h=requestAnimationFrame(w)}return e instanceof File?t.src=URL.createObjectURL(e):typeof e=="string"?t.src=e:t.src=e.src,t.onloadedmetadata=()=>{x(),r.autoplay&&(l=!0,t.play(),w())},{play(){l||!t.readyState||(l=!0,t.play(),w())},pause(){l=!1,t.pause(),h&&cancelAnimationFrame(h)},seek(m){t.currentTime=m,l||x()},update(m){m&&(s={...s,...m}),l||x()},destroy(){l=!1,h&&cancelAnimationFrame(h),t.src.startsWith("blob:")&&URL.revokeObjectURL(t.src),a.innerHTML=""},get isPlaying(){return l},get currentTime(){return t.currentTime},get duration(){return t.duration}}}var oe={image:P,video:j,webcam:U};function U(e,i={}){S();let r=R(e),a=I(r,i.colorMode??"none"),n=document.createElement("canvas"),o=n.getContext("2d",{willReadFrequently:!0}),c={...i},t=null,s=null,l=0,h=1e3/(i.fps??30),d=document.createElement("video");d.playsInline=!0,d.muted=!0;let f=!1;function y(){if(f||!d.videoWidth)return;let m=c.maxWidth??640,u=Math.min(1,m/d.videoWidth);n.width=~~(d.videoWidth*u),n.height=~~(d.videoHeight*u),f=!0}let p=!1,g=0;async function E(){if(!d.videoWidth||p)return;y(),p=!0,o.drawImage(d,0,0,n.width,n.height);let m=o.getImageData(0,0,n.width,n.height),u=await F(new Uint8ClampedArray(m.data),n.width,n.height,c);g||(g=(r.clientWidth-16)/(u.cols*.6),a.style.fontSize=g+"px"),a.innerHTML=u.html,c.colorMode==="none"?a.style.color="#0f0":a.style.color="",p=!1}function x(){let m=performance.now();m-l>=h&&(l=m,E()),t=requestAnimationFrame(x)}let w={video:{width:{ideal:1280},height:{ideal:720}}};return navigator.mediaDevices.getUserMedia(w).then(m=>(s=m,d.srcObject=s,d.play(),d.onplaying=()=>{y(),x()},{stop(){t&&cancelAnimationFrame(t),s?.getTracks().forEach(u=>u.stop()),r.innerHTML=""},update(u){u&&(c={...c,...u})}}))}return K(ie);})();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII Shape Renderer - Browser Bundle
|
|
3
|
+
* Simple drop-in library for rendering images/video as ASCII art
|
|
4
|
+
*/
|
|
5
|
+
export interface RenderOptions {
|
|
6
|
+
globalContrast?: number;
|
|
7
|
+
directionalContrast?: number;
|
|
8
|
+
colorMode?: 'none' | 'fg' | 'bg' | 'both';
|
|
9
|
+
maxWidth?: number;
|
|
10
|
+
characters?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface VideoOptions extends RenderOptions {
|
|
13
|
+
fps?: number;
|
|
14
|
+
autoplay?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Render an image as ASCII art
|
|
18
|
+
*/
|
|
19
|
+
export declare function image(source: string | HTMLImageElement | HTMLCanvasElement, container: string | HTMLElement, options?: RenderOptions): {
|
|
20
|
+
update: (opts?: RenderOptions) => void;
|
|
21
|
+
destroy: () => void;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Render a video as ASCII art
|
|
25
|
+
*/
|
|
26
|
+
export declare function video(source: string | HTMLVideoElement | File, container: string | HTMLElement, options?: VideoOptions): {
|
|
27
|
+
play: () => void;
|
|
28
|
+
pause: () => void;
|
|
29
|
+
seek: (time: number) => void;
|
|
30
|
+
update: (opts?: VideoOptions) => void;
|
|
31
|
+
destroy: () => void;
|
|
32
|
+
readonly isPlaying: boolean;
|
|
33
|
+
readonly currentTime: number;
|
|
34
|
+
readonly duration: number;
|
|
35
|
+
};
|
|
36
|
+
declare const _default: {
|
|
37
|
+
image: typeof image;
|
|
38
|
+
video: typeof video;
|
|
39
|
+
webcam: typeof webcam;
|
|
40
|
+
};
|
|
41
|
+
export default _default;
|
|
42
|
+
/**
|
|
43
|
+
* Render webcam as ASCII art
|
|
44
|
+
*/
|
|
45
|
+
export declare function webcam(container: string | HTMLElement, options?: VideoOptions): Promise<{
|
|
46
|
+
stop: () => void;
|
|
47
|
+
update: (opts?: VideoOptions) => void;
|
|
48
|
+
}>;
|