asciify-engine 1.0.44 → 1.0.45
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/README.md +19 -35
- package/dist/index.cjs +61 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -33
- package/dist/index.d.ts +50 -33
- package/dist/index.js +61 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,58 +77,41 @@ setInterval(() => {
|
|
|
77
77
|
}, 1000 / fps);
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
### Video
|
|
80
|
+
### Video
|
|
81
81
|
|
|
82
|
-
`
|
|
82
|
+
`asciifyVideo` streams video as live ASCII art in real time. Instant start, constant memory, unlimited duration.
|
|
83
83
|
|
|
84
|
-
> ⚠️ Never set the `<video>` element to `display: none
|
|
84
|
+
> ⚠️ Never set the backing `<video>` element to `display: none` — browsers skip GPU frame decoding. When given a URL string, `asciifyVideo` handles this automatically.
|
|
85
85
|
|
|
86
86
|
```ts
|
|
87
|
-
import {
|
|
87
|
+
import { asciifyVideo } from 'asciify-engine';
|
|
88
88
|
|
|
89
89
|
const canvas = document.getElementById('ascii') as HTMLCanvasElement;
|
|
90
90
|
|
|
91
|
-
//
|
|
92
|
-
const stop = await
|
|
91
|
+
// Minimal
|
|
92
|
+
const stop = await asciifyVideo('/clip.mp4', canvas);
|
|
93
93
|
|
|
94
|
-
//
|
|
95
|
-
const stop = await
|
|
96
|
-
|
|
97
|
-
fontSize: 6,
|
|
98
|
-
artStyle: 'matrix',
|
|
94
|
+
// Fit canvas to a container and re-size automatically on resize:
|
|
95
|
+
const stop = await asciifyVideo('/clip.mp4', canvas, {
|
|
96
|
+
fitTo: '#hero', // or an HTMLElement
|
|
99
97
|
});
|
|
100
98
|
|
|
101
|
-
//
|
|
102
|
-
const stop = await
|
|
99
|
+
// Lifecycle hooks — ready state, timers, etc.:
|
|
100
|
+
const stop = await asciifyVideo('/clip.mp4', canvas, {
|
|
101
|
+
fitTo: '#hero',
|
|
103
102
|
fontSize: 6,
|
|
104
|
-
onReady: (
|
|
105
|
-
|
|
106
|
-
// Resize the canvas to match the video here, update your UI, etc.
|
|
107
|
-
canvas.width = video.videoWidth;
|
|
108
|
-
canvas.height = video.videoHeight;
|
|
109
|
-
setReady(true);
|
|
110
|
-
},
|
|
111
|
-
onFrame: () => {
|
|
112
|
-
// Called after every rendered frame.
|
|
113
|
-
setElapsed(Math.floor(performance.now() / 1000));
|
|
114
|
-
},
|
|
103
|
+
onReady: () => setLoading(false),
|
|
104
|
+
onFrame: () => setElapsed(t => t + 1),
|
|
115
105
|
});
|
|
116
106
|
|
|
107
|
+
// Pre-extract all frames before playback (frame-perfect loops, short clips):
|
|
108
|
+
const stop = await asciifyVideo('/clip.mp4', canvas, { preExtract: true });
|
|
109
|
+
|
|
117
110
|
// Clean up:
|
|
118
111
|
stop();
|
|
119
112
|
```
|
|
120
113
|
|
|
121
|
-
### Video — pre-extracted frames
|
|
122
114
|
|
|
123
|
-
`asciifyVideo` seeks through the clip frame by frame and returns a frame sequence. Good for short clips where you want frame-perfect control, but requires up-front processing time.
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
import { asciifyVideo } from 'asciify-engine';
|
|
127
|
-
|
|
128
|
-
const canvas = document.getElementById('ascii') as HTMLCanvasElement;
|
|
129
|
-
const stop = await asciifyVideo('/clip.mp4', canvas, { fontSize: 8 });
|
|
130
|
-
stop();
|
|
131
|
-
```
|
|
132
115
|
|
|
133
116
|
---
|
|
134
117
|
|
|
@@ -274,7 +257,6 @@ const animatedHtml = generateAnimatedEmbedCode(frames, options, fps);
|
|
|
274
257
|
| Function | Signature | Returns |
|
|
275
258
|
|---|---|---|
|
|
276
259
|
| `asciify` | `(source, canvas, options?)` | `Promise<void>` |
|
|
277
|
-
| `asciifyLiveVideo` | `(source, canvas, options?)` | `Promise<() => void>` |
|
|
278
260
|
| `asciifyVideo` | `(source, canvas, options?)` | `Promise<() => void>` |
|
|
279
261
|
| `asciifyGif` | `(source, canvas, options?)` | `Promise<() => void>` |
|
|
280
262
|
| `asciifyWebcam` | `(canvas, options?)` | `Promise<() => void>` |
|
|
@@ -286,6 +268,8 @@ const animatedHtml = generateAnimatedEmbedCode(frames, options, fps);
|
|
|
286
268
|
| `generateEmbedCode` | `(frame, options)` | `string` |
|
|
287
269
|
| `generateAnimatedEmbedCode` | `(frames, options, fps)` | `string` |
|
|
288
270
|
|
|
271
|
+
`asciifyVideo` options: `fitTo` (HTMLElement/selector — fits canvas to container + ResizeObserver), `preExtract` (pre-decode all frames, default false), `onReady(video)`, `onFrame()`
|
|
272
|
+
|
|
289
273
|
---
|
|
290
274
|
|
|
291
275
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -985,6 +985,17 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
|
|
|
985
985
|
}
|
|
986
986
|
|
|
987
987
|
// src/core/simple-api.ts
|
|
988
|
+
function sizeCanvasToContainer(canvas, container, aspect) {
|
|
989
|
+
const { width, height } = container.getBoundingClientRect();
|
|
990
|
+
if (!width || !height) return;
|
|
991
|
+
let w = width, h = w / aspect;
|
|
992
|
+
if (h > height) {
|
|
993
|
+
h = height;
|
|
994
|
+
w = h * aspect;
|
|
995
|
+
}
|
|
996
|
+
canvas.width = Math.round(w);
|
|
997
|
+
canvas.height = Math.round(h);
|
|
998
|
+
}
|
|
988
999
|
async function asciify(source, canvas, { fontSize = 10, artStyle = "classic", options = {} } = {}) {
|
|
989
1000
|
let el;
|
|
990
1001
|
if (typeof source === "string") {
|
|
@@ -1038,46 +1049,47 @@ async function asciifyGif(source, canvas, { fontSize = 10, artStyle = "classic",
|
|
|
1038
1049
|
cancelAnimationFrame(animId);
|
|
1039
1050
|
};
|
|
1040
1051
|
}
|
|
1041
|
-
async function asciifyVideo(source, canvas, { fontSize = 10, artStyle = "classic", options = {} } = {}) {
|
|
1042
|
-
let video;
|
|
1043
|
-
if (typeof source === "string") {
|
|
1044
|
-
video = document.createElement("video");
|
|
1045
|
-
video.crossOrigin = "anonymous";
|
|
1046
|
-
video.src = source;
|
|
1047
|
-
if (video.readyState < 2) {
|
|
1048
|
-
await new Promise((resolve, reject) => {
|
|
1049
|
-
video.onloadeddata = () => resolve();
|
|
1050
|
-
video.onerror = () => reject(new Error(`Failed to load video: ${source}`));
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
} else {
|
|
1054
|
-
video = source;
|
|
1055
|
-
}
|
|
1052
|
+
async function asciifyVideo(source, canvas, { fontSize = 10, artStyle = "classic", options = {}, fitTo, preExtract = false, onReady, onFrame } = {}) {
|
|
1056
1053
|
const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[artStyle], ...options, fontSize };
|
|
1057
1054
|
const ctx = canvas.getContext("2d");
|
|
1058
|
-
if (!ctx) throw new Error("
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1055
|
+
if (!ctx) throw new Error("asciifyVideo: could not get 2d context from canvas.");
|
|
1056
|
+
const container = typeof fitTo === "string" ? document.querySelector(fitTo) : fitTo instanceof HTMLElement ? fitTo : null;
|
|
1057
|
+
if (preExtract) {
|
|
1058
|
+
let video2;
|
|
1059
|
+
if (typeof source === "string") {
|
|
1060
|
+
video2 = document.createElement("video");
|
|
1061
|
+
video2.crossOrigin = "anonymous";
|
|
1062
|
+
video2.src = source;
|
|
1063
|
+
if (video2.readyState < 2) {
|
|
1064
|
+
await new Promise((resolve, reject) => {
|
|
1065
|
+
video2.onloadeddata = () => resolve();
|
|
1066
|
+
video2.onerror = () => reject(new Error(`asciifyVideo: failed to load "${source}"`));
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
} else {
|
|
1070
|
+
video2 = source;
|
|
1071
|
+
}
|
|
1072
|
+
if (container) sizeCanvasToContainer(canvas, container, video2.videoWidth / video2.videoHeight);
|
|
1073
|
+
onReady?.(video2);
|
|
1074
|
+
const { frames, fps } = await videoToAsciiFrames(video2, merged, canvas.width, canvas.height);
|
|
1075
|
+
let cancelled2 = false, animId2, i = 0, last = performance.now();
|
|
1076
|
+
const interval = 1e3 / fps;
|
|
1077
|
+
const tick2 = (now) => {
|
|
1078
|
+
if (cancelled2) return;
|
|
1079
|
+
if (now - last >= interval) {
|
|
1080
|
+
renderFrameToCanvas(ctx, frames[i], merged, canvas.width, canvas.height);
|
|
1081
|
+
i = (i + 1) % frames.length;
|
|
1082
|
+
last = now;
|
|
1083
|
+
onFrame?.();
|
|
1084
|
+
}
|
|
1085
|
+
animId2 = requestAnimationFrame(tick2);
|
|
1086
|
+
};
|
|
1087
|
+
animId2 = requestAnimationFrame(tick2);
|
|
1088
|
+
return () => {
|
|
1089
|
+
cancelled2 = true;
|
|
1090
|
+
cancelAnimationFrame(animId2);
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1081
1093
|
let video;
|
|
1082
1094
|
let ownedVideo = false;
|
|
1083
1095
|
if (typeof source === "string") {
|
|
@@ -1101,7 +1113,7 @@ async function asciifyLiveVideo(source, canvas, { fontSize = 10, artStyle = "cla
|
|
|
1101
1113
|
ownedVideo = true;
|
|
1102
1114
|
await new Promise((resolve, reject) => {
|
|
1103
1115
|
video.onloadedmetadata = () => resolve();
|
|
1104
|
-
video.onerror = () => reject(new Error(`
|
|
1116
|
+
video.onerror = () => reject(new Error(`asciifyVideo: failed to load "${source}"`));
|
|
1105
1117
|
});
|
|
1106
1118
|
await video.play().catch(() => {
|
|
1107
1119
|
});
|
|
@@ -1110,14 +1122,14 @@ async function asciifyLiveVideo(source, canvas, { fontSize = 10, artStyle = "cla
|
|
|
1110
1122
|
if (video.paused) await video.play().catch(() => {
|
|
1111
1123
|
});
|
|
1112
1124
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1125
|
+
let ro = null;
|
|
1126
|
+
if (container) {
|
|
1127
|
+
const aspect = video.videoWidth / video.videoHeight;
|
|
1128
|
+
sizeCanvasToContainer(canvas, container, aspect);
|
|
1129
|
+
ro = new ResizeObserver(() => sizeCanvasToContainer(canvas, container, aspect));
|
|
1130
|
+
ro.observe(container);
|
|
1116
1131
|
}
|
|
1117
1132
|
onReady?.(video);
|
|
1118
|
-
const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[artStyle], ...options, fontSize };
|
|
1119
|
-
const ctx = canvas.getContext("2d");
|
|
1120
|
-
if (!ctx) throw new Error("asciifyLiveVideo: could not get 2d context from canvas.");
|
|
1121
1133
|
let cancelled = false;
|
|
1122
1134
|
let animId;
|
|
1123
1135
|
const tick = () => {
|
|
@@ -1134,6 +1146,7 @@ async function asciifyLiveVideo(source, canvas, { fontSize = 10, artStyle = "cla
|
|
|
1134
1146
|
return () => {
|
|
1135
1147
|
cancelled = true;
|
|
1136
1148
|
cancelAnimationFrame(animId);
|
|
1149
|
+
ro?.disconnect();
|
|
1137
1150
|
if (ownedVideo) {
|
|
1138
1151
|
video.pause();
|
|
1139
1152
|
video.src = "";
|
|
@@ -1141,6 +1154,9 @@ async function asciifyLiveVideo(source, canvas, { fontSize = 10, artStyle = "cla
|
|
|
1141
1154
|
}
|
|
1142
1155
|
};
|
|
1143
1156
|
}
|
|
1157
|
+
function asciifyLiveVideo(source, canvas, opts) {
|
|
1158
|
+
return asciifyVideo(source, canvas, opts);
|
|
1159
|
+
}
|
|
1144
1160
|
|
|
1145
1161
|
// src/backgrounds/rain.ts
|
|
1146
1162
|
function renderRainBackground(ctx, width, height, time, options = {}) {
|