asciify-engine 1.0.32 → 1.0.34

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  <p align="left">
4
4
  <a href="https://www.npmjs.com/package/asciify-engine"><img src="https://img.shields.io/npm/v/asciify-engine?color=d4ff00&labelColor=0a0a0a&style=flat-square" alt="npm version" /></a>
5
5
  <a href="https://www.npmjs.com/package/asciify-engine"><img src="https://img.shields.io/npm/dm/asciify-engine?color=d4ff00&labelColor=0a0a0a&style=flat-square" alt="downloads" /></a>
6
- <a href="https://github.com/ayangabryl/asciify/blob/main/packages/asciify-engine/LICENSE"><img src="https://img.shields.io/badge/license-MIT-d4ff00?labelColor=0a0a0a&style=flat-square" alt="MIT license" /></a>
6
+ <a href="https://github.com/ayangabryl/asciify-engine/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-d4ff00?labelColor=0a0a0a&style=flat-square" alt="MIT license" /></a>
7
7
  <a href="https://www.buymeacoffee.com/asciify"><img src="https://img.shields.io/badge/buy_me_a_coffee-support-d4ff00?labelColor=0a0a0a&style=flat-square" alt="Buy Me A Coffee" /></a>
8
8
  </p>
9
9
 
package/dist/index.cjs CHANGED
@@ -2285,144 +2285,143 @@ function renderTextBackground(ctx, width, height, text, options = {}, hoverPos)
2285
2285
  }
2286
2286
 
2287
2287
  // src/core/record.ts
2288
- function createRecorder(canvas, options = {}) {
2289
- const {
2290
- fps = 15,
2291
- maxFrames = 120,
2292
- format = "gif",
2293
- quality = 10,
2294
- scale = 1
2295
- } = options;
2296
- const interval = 1e3 / fps;
2297
- let recording = false;
2298
- let timerId = -1;
2299
- const blobs = [];
2300
- const captureFrame = () => {
2301
- if (!recording || blobs.length >= maxFrames) return;
2288
+ function captureSnapshot(canvas, { format = "png", quality = 0.92, scale = 1 } = {}) {
2289
+ return new Promise((resolve, reject) => {
2302
2290
  let src = canvas;
2303
2291
  if (scale !== 1) {
2304
2292
  const off = document.createElement("canvas");
2305
2293
  off.width = Math.round(canvas.width * scale);
2306
2294
  off.height = Math.round(canvas.height * scale);
2307
2295
  const offCtx = off.getContext("2d");
2296
+ if (!offCtx) {
2297
+ reject(new Error("captureSnapshot: could not get 2d context"));
2298
+ return;
2299
+ }
2308
2300
  offCtx.drawImage(canvas, 0, 0, off.width, off.height);
2309
2301
  src = off;
2310
2302
  }
2311
- 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
2312
2349
  };
2313
- const encodeGif = async (frames) => {
2314
- return new Promise((resolve, reject) => {
2315
- if (typeof GIF === "undefined") {
2316
- reject(new Error('[asciify recorder] gif.js not found. Add <script src="/gif.worker.js"> to your page.'));
2317
- return;
2318
- }
2319
- const gif = new GIF({
2320
- workers: 2,
2321
- quality,
2322
- workerScript: "/gif.worker.js"
2323
- });
2324
- let loaded = 0;
2325
- const total = frames.length;
2326
- frames.forEach((dataUrl) => {
2327
- const img = new Image();
2328
- img.onload = () => {
2329
- gif.addFrame(img, { delay: interval, copy: true });
2330
- loaded++;
2331
- if (loaded === total) gif.render();
2332
- };
2333
- img.src = dataUrl;
2334
- });
2335
- gif.on("finished", (blob) => {
2336
- const reader = new FileReader();
2337
- reader.onload = () => resolve(reader.result);
2338
- reader.readAsDataURL(blob);
2339
- });
2340
- gif.on("error", reject);
2341
- });
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
+ };
2342
2364
  };
2343
- const encodeWebP = async (frames, _fps) => {
2344
- if (typeof MediaRecorder === "undefined") {
2345
- throw new Error("[asciify recorder] MediaRecorder not available in this browser.");
2346
- }
2347
- const off = document.createElement("canvas");
2348
- if (frames.length === 0) return "";
2349
- const probe = new Image();
2350
- await new Promise((res) => {
2351
- probe.onload = () => res();
2352
- probe.src = frames[0];
2353
- });
2354
- off.width = probe.naturalWidth;
2355
- off.height = probe.naturalHeight;
2356
- const offCtx = off.getContext("2d");
2357
- const stream = off.captureStream(_fps);
2358
- const recorder = new MediaRecorder(stream, { mimeType: "video/webm;codecs=vp9" });
2359
- const chunks = [];
2360
- recorder.ondataavailable = (e) => chunks.push(e.data);
2361
- return new Promise((resolve, reject) => {
2362
- recorder.onstop = () => {
2363
- const blob = new Blob(chunks, { type: "video/webm" });
2364
- const reader = new FileReader();
2365
- reader.onload = () => resolve(reader.result);
2366
- reader.readAsDataURL(blob);
2367
- };
2368
- recorder.onerror = reject;
2369
- recorder.start();
2370
- let idx = 0;
2371
- const drawNext = () => {
2372
- if (idx >= frames.length) {
2373
- recorder.stop();
2374
- return;
2375
- }
2376
- const img = new Image();
2377
- img.onload = () => {
2378
- offCtx.drawImage(img, 0, 0);
2379
- idx++;
2380
- setTimeout(drawNext, interval);
2381
- };
2382
- img.src = frames[idx];
2383
- };
2384
- drawNext();
2385
- });
2365
+ const onMouseLeave = () => {
2366
+ hoverPos = null;
2386
2367
  };
2387
- return {
2388
- get isRecording() {
2389
- return recording;
2390
- },
2391
- get frameCount() {
2392
- return blobs.length;
2393
- },
2394
- start() {
2395
- if (recording) return;
2396
- blobs.length = 0;
2397
- recording = true;
2398
- timerId = window.setInterval(captureFrame, interval);
2399
- },
2400
- async stop() {
2401
- if (!recording) return "";
2402
- recording = false;
2403
- clearInterval(timerId);
2404
- const frames = blobs.slice();
2405
- if (format === "png-sequence") {
2406
- 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);
2389
+ }
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;
2407
2402
  }
2408
- if (format === "webp") {
2409
- return encodeWebP(frames, fps);
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);
2410
2412
  }
2411
- return encodeGif(frames);
2412
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;
2413
2424
  };
2414
- }
2415
- async function recordAndDownload(canvas, durationMs, options = {}) {
2416
- const { filename = "asciify-recording", ...recOpts } = options;
2417
- const recorder = createRecorder(canvas, recOpts);
2418
- recorder.start();
2419
- await new Promise((res) => setTimeout(res, durationMs));
2420
- const dataUrl = await recorder.stop();
2421
- const ext = options.format === "webp" ? "webm" : options.format === "png-sequence" ? "json" : "gif";
2422
- const a = document.createElement("a");
2423
- a.href = dataUrl;
2424
- a.download = `${filename}.${ext}`;
2425
- a.click();
2426
2425
  }
2427
2426
 
2428
2427
  exports.ART_STYLE_PRESETS = ART_STYLE_PRESETS;
@@ -2436,12 +2435,12 @@ exports.asciiTextAnsi = asciiTextAnsi;
2436
2435
  exports.asciify = asciify;
2437
2436
  exports.asciifyGif = asciifyGif;
2438
2437
  exports.asciifyVideo = asciifyVideo;
2438
+ exports.asciifyWebcam = asciifyWebcam;
2439
2439
  exports.buildTextFrame = buildTextFrame;
2440
- exports.createRecorder = createRecorder;
2440
+ exports.captureSnapshot = captureSnapshot;
2441
2441
  exports.gifToAsciiFrames = gifToAsciiFrames;
2442
2442
  exports.imageToAsciiFrame = imageToAsciiFrame;
2443
2443
  exports.mountWaveBackground = mountWaveBackground;
2444
- exports.recordAndDownload = recordAndDownload;
2445
2444
  exports.renderAuroraBackground = renderAuroraBackground;
2446
2445
  exports.renderCircuitBackground = renderCircuitBackground;
2447
2446
  exports.renderDnaBackground = renderDnaBackground;
@@ -2458,6 +2457,7 @@ exports.renderTerrainBackground = renderTerrainBackground;
2458
2457
  exports.renderTextBackground = renderTextBackground;
2459
2458
  exports.renderVoidBackground = renderVoidBackground;
2460
2459
  exports.renderWaveBackground = renderWaveBackground;
2460
+ exports.snapshotAndDownload = snapshotAndDownload;
2461
2461
  exports.videoToAsciiFrames = videoToAsciiFrames;
2462
2462
  //# sourceMappingURL=index.cjs.map
2463
2463
  //# sourceMappingURL=index.cjs.map