asciify-engine 1.0.32 → 1.0.35
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 +380 -1
- package/dist/index.cjs +136 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -53
- package/dist/index.d.ts +174 -53
- package/dist/index.js +134 -133
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -968,7 +968,7 @@ function renderFrameToCanvas(ctx, frame, options, canvasWidth, canvasHeight, tim
|
|
|
968
968
|
}
|
|
969
969
|
|
|
970
970
|
// src/core/simple-api.ts
|
|
971
|
-
async function asciify(source, canvas, { fontSize = 10,
|
|
971
|
+
async function asciify(source, canvas, { fontSize = 10, artStyle = "classic", options = {} } = {}) {
|
|
972
972
|
let el;
|
|
973
973
|
if (typeof source === "string") {
|
|
974
974
|
const img = new Image();
|
|
@@ -988,16 +988,16 @@ async function asciify(source, canvas, { fontSize = 10, style = "classic", optio
|
|
|
988
988
|
} else {
|
|
989
989
|
el = source;
|
|
990
990
|
}
|
|
991
|
-
const preset = ART_STYLE_PRESETS[
|
|
991
|
+
const preset = ART_STYLE_PRESETS[artStyle];
|
|
992
992
|
const merged = { ...DEFAULT_OPTIONS, ...preset, ...options, fontSize };
|
|
993
993
|
const ctx = canvas.getContext("2d");
|
|
994
994
|
if (!ctx) throw new Error("Could not get 2d context from canvas");
|
|
995
995
|
const { frame } = imageToAsciiFrame(el, merged, canvas.width, canvas.height);
|
|
996
996
|
renderFrameToCanvas(ctx, frame, merged, canvas.width, canvas.height);
|
|
997
997
|
}
|
|
998
|
-
async function asciifyGif(source, canvas, { fontSize = 10,
|
|
998
|
+
async function asciifyGif(source, canvas, { fontSize = 10, artStyle = "classic", options = {} } = {}) {
|
|
999
999
|
const buffer = typeof source === "string" ? await fetch(source).then((r) => r.arrayBuffer()) : source;
|
|
1000
|
-
const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[
|
|
1000
|
+
const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[artStyle], ...options, fontSize };
|
|
1001
1001
|
const ctx = canvas.getContext("2d");
|
|
1002
1002
|
if (!ctx) throw new Error("Could not get 2d context from canvas");
|
|
1003
1003
|
const { frames, fps } = await gifToAsciiFrames(buffer, merged, canvas.width, canvas.height);
|
|
@@ -1021,20 +1021,22 @@ async function asciifyGif(source, canvas, { fontSize = 10, style = "classic", op
|
|
|
1021
1021
|
cancelAnimationFrame(animId);
|
|
1022
1022
|
};
|
|
1023
1023
|
}
|
|
1024
|
-
async function asciifyVideo(source, canvas, { fontSize = 10,
|
|
1024
|
+
async function asciifyVideo(source, canvas, { fontSize = 10, artStyle = "classic", options = {} } = {}) {
|
|
1025
1025
|
let video;
|
|
1026
1026
|
if (typeof source === "string") {
|
|
1027
1027
|
video = document.createElement("video");
|
|
1028
1028
|
video.crossOrigin = "anonymous";
|
|
1029
1029
|
video.src = source;
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1030
|
+
if (video.readyState < 2) {
|
|
1031
|
+
await new Promise((resolve, reject) => {
|
|
1032
|
+
video.onloadeddata = () => resolve();
|
|
1033
|
+
video.onerror = () => reject(new Error(`Failed to load video: ${source}`));
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1034
1036
|
} else {
|
|
1035
1037
|
video = source;
|
|
1036
1038
|
}
|
|
1037
|
-
const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[
|
|
1039
|
+
const merged = { ...DEFAULT_OPTIONS, ...ART_STYLE_PRESETS[artStyle], ...options, fontSize };
|
|
1038
1040
|
const ctx = canvas.getContext("2d");
|
|
1039
1041
|
if (!ctx) throw new Error("Could not get 2d context from canvas");
|
|
1040
1042
|
const { frames, fps } = await videoToAsciiFrames(video, merged, canvas.width, canvas.height);
|
|
@@ -2283,146 +2285,145 @@ function renderTextBackground(ctx, width, height, text, options = {}, hoverPos)
|
|
|
2283
2285
|
}
|
|
2284
2286
|
|
|
2285
2287
|
// src/core/record.ts
|
|
2286
|
-
function
|
|
2287
|
-
|
|
2288
|
-
fps = 15,
|
|
2289
|
-
maxFrames = 120,
|
|
2290
|
-
format = "gif",
|
|
2291
|
-
quality = 10,
|
|
2292
|
-
scale = 1
|
|
2293
|
-
} = options;
|
|
2294
|
-
const interval = 1e3 / fps;
|
|
2295
|
-
let recording = false;
|
|
2296
|
-
let timerId = -1;
|
|
2297
|
-
const blobs = [];
|
|
2298
|
-
const captureFrame = () => {
|
|
2299
|
-
if (!recording || blobs.length >= maxFrames) return;
|
|
2288
|
+
function captureSnapshot(canvas, { format = "png", quality = 0.92, scale = 1 } = {}) {
|
|
2289
|
+
return new Promise((resolve, reject) => {
|
|
2300
2290
|
let src = canvas;
|
|
2301
2291
|
if (scale !== 1) {
|
|
2302
2292
|
const off = document.createElement("canvas");
|
|
2303
2293
|
off.width = Math.round(canvas.width * scale);
|
|
2304
2294
|
off.height = Math.round(canvas.height * scale);
|
|
2305
2295
|
const offCtx = off.getContext("2d");
|
|
2296
|
+
if (!offCtx) {
|
|
2297
|
+
reject(new Error("captureSnapshot: could not get 2d context"));
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2306
2300
|
offCtx.drawImage(canvas, 0, 0, off.width, off.height);
|
|
2307
2301
|
src = off;
|
|
2308
2302
|
}
|
|
2309
|
-
|
|
2303
|
+
src.toBlob(
|
|
2304
|
+
(blob) => blob ? resolve(blob) : reject(new Error("captureSnapshot: toBlob returned null")),
|
|
2305
|
+
`image/${format}`,
|
|
2306
|
+
quality
|
|
2307
|
+
);
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
async function snapshotAndDownload(canvas, options = {}) {
|
|
2311
|
+
const { filename = "asciify-snapshot", format = "png", ...snapOpts } = options;
|
|
2312
|
+
const blob = await captureSnapshot(canvas, { format, ...snapOpts });
|
|
2313
|
+
const ext = format === "jpeg" ? "jpg" : format;
|
|
2314
|
+
const a = document.createElement("a");
|
|
2315
|
+
a.href = URL.createObjectURL(blob);
|
|
2316
|
+
a.download = `${filename}.${ext}`;
|
|
2317
|
+
a.click();
|
|
2318
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 1e4);
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// src/core/webcam.ts
|
|
2322
|
+
async function asciifyWebcam(canvas, {
|
|
2323
|
+
fontSize = 10,
|
|
2324
|
+
style = "classic",
|
|
2325
|
+
options = {},
|
|
2326
|
+
liveOptions,
|
|
2327
|
+
mirror = true,
|
|
2328
|
+
constraints = { facingMode: "user" },
|
|
2329
|
+
dpr: dprOverride
|
|
2330
|
+
} = {}) {
|
|
2331
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
2332
|
+
throw new Error("asciifyWebcam: getUserMedia is not supported in this browser.");
|
|
2333
|
+
}
|
|
2334
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: constraints });
|
|
2335
|
+
const video = document.createElement("video");
|
|
2336
|
+
video.srcObject = stream;
|
|
2337
|
+
video.muted = true;
|
|
2338
|
+
video.playsInline = true;
|
|
2339
|
+
await new Promise((resolve, reject) => {
|
|
2340
|
+
video.onloadedmetadata = () => resolve();
|
|
2341
|
+
video.onerror = () => reject(new Error("asciifyWebcam: video stream failed to load."));
|
|
2342
|
+
video.play().catch(reject);
|
|
2343
|
+
});
|
|
2344
|
+
const merged = {
|
|
2345
|
+
...DEFAULT_OPTIONS,
|
|
2346
|
+
...ART_STYLE_PRESETS[style],
|
|
2347
|
+
...options,
|
|
2348
|
+
fontSize
|
|
2310
2349
|
};
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
const img = new Image();
|
|
2326
|
-
img.onload = () => {
|
|
2327
|
-
gif.addFrame(img, { delay: interval, copy: true });
|
|
2328
|
-
loaded++;
|
|
2329
|
-
if (loaded === total) gif.render();
|
|
2330
|
-
};
|
|
2331
|
-
img.src = dataUrl;
|
|
2332
|
-
});
|
|
2333
|
-
gif.on("finished", (blob) => {
|
|
2334
|
-
const reader = new FileReader();
|
|
2335
|
-
reader.onload = () => resolve(reader.result);
|
|
2336
|
-
reader.readAsDataURL(blob);
|
|
2337
|
-
});
|
|
2338
|
-
gif.on("error", reject);
|
|
2339
|
-
});
|
|
2350
|
+
const ctx = canvas.getContext("2d");
|
|
2351
|
+
if (!ctx) throw new Error("asciifyWebcam: could not get 2d context from canvas.");
|
|
2352
|
+
const deviceRatio = dprOverride ?? (typeof window !== "undefined" ? window.devicePixelRatio : 1) ?? 1;
|
|
2353
|
+
if (deviceRatio !== 1) {
|
|
2354
|
+
ctx.scale(deviceRatio, deviceRatio);
|
|
2355
|
+
}
|
|
2356
|
+
let hoverPos = null;
|
|
2357
|
+
const smoothHover = { x: 0.5, y: 0.5, intensity: 0 };
|
|
2358
|
+
const onMouseMove = (e) => {
|
|
2359
|
+
const rect = canvas.getBoundingClientRect();
|
|
2360
|
+
hoverPos = {
|
|
2361
|
+
x: (e.clientX - rect.left) / rect.width,
|
|
2362
|
+
y: (e.clientY - rect.top) / rect.height
|
|
2363
|
+
};
|
|
2340
2364
|
};
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2343
|
-
throw new Error("[asciify recorder] MediaRecorder not available in this browser.");
|
|
2344
|
-
}
|
|
2345
|
-
const off = document.createElement("canvas");
|
|
2346
|
-
if (frames.length === 0) return "";
|
|
2347
|
-
const probe = new Image();
|
|
2348
|
-
await new Promise((res) => {
|
|
2349
|
-
probe.onload = () => res();
|
|
2350
|
-
probe.src = frames[0];
|
|
2351
|
-
});
|
|
2352
|
-
off.width = probe.naturalWidth;
|
|
2353
|
-
off.height = probe.naturalHeight;
|
|
2354
|
-
const offCtx = off.getContext("2d");
|
|
2355
|
-
const stream = off.captureStream(_fps);
|
|
2356
|
-
const recorder = new MediaRecorder(stream, { mimeType: "video/webm;codecs=vp9" });
|
|
2357
|
-
const chunks = [];
|
|
2358
|
-
recorder.ondataavailable = (e) => chunks.push(e.data);
|
|
2359
|
-
return new Promise((resolve, reject) => {
|
|
2360
|
-
recorder.onstop = () => {
|
|
2361
|
-
const blob = new Blob(chunks, { type: "video/webm" });
|
|
2362
|
-
const reader = new FileReader();
|
|
2363
|
-
reader.onload = () => resolve(reader.result);
|
|
2364
|
-
reader.readAsDataURL(blob);
|
|
2365
|
-
};
|
|
2366
|
-
recorder.onerror = reject;
|
|
2367
|
-
recorder.start();
|
|
2368
|
-
let idx = 0;
|
|
2369
|
-
const drawNext = () => {
|
|
2370
|
-
if (idx >= frames.length) {
|
|
2371
|
-
recorder.stop();
|
|
2372
|
-
return;
|
|
2373
|
-
}
|
|
2374
|
-
const img = new Image();
|
|
2375
|
-
img.onload = () => {
|
|
2376
|
-
offCtx.drawImage(img, 0, 0);
|
|
2377
|
-
idx++;
|
|
2378
|
-
setTimeout(drawNext, interval);
|
|
2379
|
-
};
|
|
2380
|
-
img.src = frames[idx];
|
|
2381
|
-
};
|
|
2382
|
-
drawNext();
|
|
2383
|
-
});
|
|
2365
|
+
const onMouseLeave = () => {
|
|
2366
|
+
hoverPos = null;
|
|
2384
2367
|
};
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2368
|
+
if (merged.hoverStrength > 0) {
|
|
2369
|
+
canvas.addEventListener("mousemove", onMouseMove);
|
|
2370
|
+
canvas.addEventListener("mouseleave", onMouseLeave);
|
|
2371
|
+
}
|
|
2372
|
+
let cancelled = false;
|
|
2373
|
+
let animId;
|
|
2374
|
+
const startTime = performance.now();
|
|
2375
|
+
const tick = (timestamp) => {
|
|
2376
|
+
if (cancelled) return;
|
|
2377
|
+
if (video.readyState >= video.HAVE_CURRENT_DATA) {
|
|
2378
|
+
const displayW = canvas.width / deviceRatio;
|
|
2379
|
+
const displayH = canvas.height / deviceRatio;
|
|
2380
|
+
const elapsed = (timestamp - startTime) / 1e3;
|
|
2381
|
+
const frameOptions = liveOptions ? { ...merged, ...liveOptions() } : merged;
|
|
2382
|
+
const wantsHover = frameOptions.hoverStrength > 0;
|
|
2383
|
+
if (wantsHover) {
|
|
2384
|
+
canvas.addEventListener("mousemove", onMouseMove);
|
|
2385
|
+
canvas.addEventListener("mouseleave", onMouseLeave);
|
|
2386
|
+
} else {
|
|
2387
|
+
canvas.removeEventListener("mousemove", onMouseMove);
|
|
2388
|
+
canvas.removeEventListener("mouseleave", onMouseLeave);
|
|
2405
2389
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2390
|
+
const { frame } = imageToAsciiFrame(video, frameOptions, displayW, displayH);
|
|
2391
|
+
if (hoverPos) {
|
|
2392
|
+
const dx = hoverPos.x - smoothHover.x;
|
|
2393
|
+
const dy = hoverPos.y - smoothHover.y;
|
|
2394
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
2395
|
+
const speed = Math.min(0.25, 0.06 + dist * 0.8);
|
|
2396
|
+
smoothHover.x += dx * speed;
|
|
2397
|
+
smoothHover.y += dy * speed;
|
|
2398
|
+
smoothHover.intensity += (1 - smoothHover.intensity) * 0.12;
|
|
2399
|
+
} else {
|
|
2400
|
+
smoothHover.intensity *= 0.965;
|
|
2401
|
+
if (smoothHover.intensity < 3e-3) smoothHover.intensity = 0;
|
|
2402
|
+
}
|
|
2403
|
+
const hoverArg = smoothHover.intensity > 3e-3 ? { x: smoothHover.x, y: smoothHover.y, intensity: smoothHover.intensity } : null;
|
|
2404
|
+
if (mirror) {
|
|
2405
|
+
ctx.save();
|
|
2406
|
+
ctx.scale(-1, 1);
|
|
2407
|
+
ctx.translate(-displayW, 0);
|
|
2408
|
+
renderFrameToCanvas(ctx, frame, frameOptions, displayW, displayH, elapsed, hoverArg);
|
|
2409
|
+
ctx.restore();
|
|
2410
|
+
} else {
|
|
2411
|
+
renderFrameToCanvas(ctx, frame, frameOptions, displayW, displayH, elapsed, hoverArg);
|
|
2408
2412
|
}
|
|
2409
|
-
return encodeGif(frames);
|
|
2410
2413
|
}
|
|
2414
|
+
animId = requestAnimationFrame(tick);
|
|
2415
|
+
};
|
|
2416
|
+
animId = requestAnimationFrame(tick);
|
|
2417
|
+
return () => {
|
|
2418
|
+
cancelled = true;
|
|
2419
|
+
cancelAnimationFrame(animId);
|
|
2420
|
+
canvas.removeEventListener("mousemove", onMouseMove);
|
|
2421
|
+
canvas.removeEventListener("mouseleave", onMouseLeave);
|
|
2422
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
2423
|
+
video.srcObject = null;
|
|
2411
2424
|
};
|
|
2412
|
-
}
|
|
2413
|
-
async function recordAndDownload(canvas, durationMs, options = {}) {
|
|
2414
|
-
const { filename = "asciify-recording", ...recOpts } = options;
|
|
2415
|
-
const recorder = createRecorder(canvas, recOpts);
|
|
2416
|
-
recorder.start();
|
|
2417
|
-
await new Promise((res) => setTimeout(res, durationMs));
|
|
2418
|
-
const dataUrl = await recorder.stop();
|
|
2419
|
-
const ext = options.format === "webp" ? "webm" : options.format === "png-sequence" ? "json" : "gif";
|
|
2420
|
-
const a = document.createElement("a");
|
|
2421
|
-
a.href = dataUrl;
|
|
2422
|
-
a.download = `${filename}.${ext}`;
|
|
2423
|
-
a.click();
|
|
2424
2425
|
}
|
|
2425
2426
|
|
|
2426
|
-
export { ART_STYLE_PRESETS, CHARSETS, DEFAULT_OPTIONS, HOVER_PRESETS, PALETTE_THEMES, asciiBackground, asciiText, asciiTextAnsi, asciify, asciifyGif, asciifyVideo, buildTextFrame,
|
|
2427
|
+
export { ART_STYLE_PRESETS, CHARSETS, DEFAULT_OPTIONS, HOVER_PRESETS, PALETTE_THEMES, asciiBackground, asciiText, asciiTextAnsi, asciify, asciifyGif, asciifyVideo, asciifyWebcam, buildTextFrame, captureSnapshot, gifToAsciiFrames, imageToAsciiFrame, mountWaveBackground, renderAuroraBackground, renderCircuitBackground, renderDnaBackground, renderFireBackground, renderFrameToCanvas, renderGridBackground, renderMorphBackground, renderNoiseBackground, renderPulseBackground, renderRainBackground, renderSilkBackground, renderStarsBackground, renderTerrainBackground, renderTextBackground, renderVoidBackground, renderWaveBackground, snapshotAndDownload, videoToAsciiFrames };
|
|
2427
2428
|
//# sourceMappingURL=index.js.map
|
|
2428
2429
|
//# sourceMappingURL=index.js.map
|