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/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, style = "classic", options = {} } = {}) {
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[style];
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, style = "classic", options = {} } = {}) {
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[style], ...options, fontSize };
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, style = "classic", options = {} } = {}) {
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
- await new Promise((resolve, reject) => {
1031
- video.onloadeddata = () => resolve();
1032
- video.onerror = () => reject(new Error(`Failed to load video: ${source}`));
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[style], ...options, fontSize };
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 createRecorder(canvas, options = {}) {
2287
- const {
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
- blobs.push(src.toDataURL("image/png"));
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 encodeGif = async (frames) => {
2312
- return new Promise((resolve, reject) => {
2313
- if (typeof GIF === "undefined") {
2314
- reject(new Error('[asciify recorder] gif.js not found. Add <script src="/gif.worker.js"> to your page.'));
2315
- return;
2316
- }
2317
- const gif = new GIF({
2318
- workers: 2,
2319
- quality,
2320
- workerScript: "/gif.worker.js"
2321
- });
2322
- let loaded = 0;
2323
- const total = frames.length;
2324
- frames.forEach((dataUrl) => {
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 encodeWebP = async (frames, _fps) => {
2342
- if (typeof MediaRecorder === "undefined") {
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
- return {
2386
- get isRecording() {
2387
- return recording;
2388
- },
2389
- get frameCount() {
2390
- return blobs.length;
2391
- },
2392
- start() {
2393
- if (recording) return;
2394
- blobs.length = 0;
2395
- recording = true;
2396
- timerId = window.setInterval(captureFrame, interval);
2397
- },
2398
- async stop() {
2399
- if (!recording) return "";
2400
- recording = false;
2401
- clearInterval(timerId);
2402
- const frames = blobs.slice();
2403
- if (format === "png-sequence") {
2404
- return JSON.stringify(frames);
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
- if (format === "webp") {
2407
- return encodeWebP(frames, fps);
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, createRecorder, gifToAsciiFrames, imageToAsciiFrame, mountWaveBackground, recordAndDownload, renderAuroraBackground, renderCircuitBackground, renderDnaBackground, renderFireBackground, renderFrameToCanvas, renderGridBackground, renderMorphBackground, renderNoiseBackground, renderPulseBackground, renderRainBackground, renderSilkBackground, renderStarsBackground, renderTerrainBackground, renderTextBackground, renderVoidBackground, renderWaveBackground, videoToAsciiFrames };
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