q5 2.16.4 → 2.18.1

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.
Files changed (5) hide show
  1. package/deno.json +1 -1
  2. package/package.json +2 -2
  3. package/q5.d.ts +567 -182
  4. package/q5.js +607 -4
  5. package/q5.min.js +2 -2
package/q5.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 2.16
3
+ * @version 2.18
4
4
  * @author quinton-ashley, Tezumie, and LingDong-
5
5
  * @license LGPL-3.0
6
6
  * @class Q5
@@ -311,7 +311,7 @@ function createCanvas(w, h, opt) {
311
311
  }
312
312
  }
313
313
 
314
- Q5.version = Q5.VERSION = '2.16';
314
+ Q5.version = Q5.VERSION = '2.18';
315
315
 
316
316
  if (typeof document == 'object') {
317
317
  document.addEventListener('DOMContentLoaded', () => {
@@ -464,8 +464,10 @@ Q5.modules.canvas = ($, q) => {
464
464
  };
465
465
 
466
466
  $._setCanvasSize = (w, h) => {
467
+ if (!w) h ??= window.innerHeight;
468
+ else h ??= w;
467
469
  w ??= window.innerWidth;
468
- h ??= window.innerHeight;
470
+
469
471
  $.defaultWidth = c.w = w = Math.ceil(w);
470
472
  $.defaultHeight = c.h = h = Math.ceil(h);
471
473
  c.hw = w / 2;
@@ -1761,7 +1763,7 @@ Q5.renderers.c2d.soft_filters = ($) => {
1761
1763
 
1762
1764
  $._softFilter = (typ, x) => {
1763
1765
  if (!$._filters) initSoftFilters();
1764
- let imgData = $.ctx._getImageData(0, 0, $.canvas.width, $.canvas.height);
1766
+ let imgData = $._getImageData(0, 0, $.canvas.width, $.canvas.height);
1765
1767
  $._filters[typ](imgData.data, x);
1766
1768
  $.ctx.putImageData(imgData, 0, 0);
1767
1769
  };
@@ -2549,6 +2551,271 @@ main {
2549
2551
  else document.body.exitFullscreen();
2550
2552
  };
2551
2553
  };
2554
+ Q5.modules.dom = ($) => {
2555
+ $.elementMode = (mode) => ($._elementMode = mode);
2556
+
2557
+ $.createElement = (tag, content) => {
2558
+ let el = document.createElement(tag);
2559
+
2560
+ if ($._elementMode == 'center') {
2561
+ el.style.transform = 'translate(-50%, -50%)';
2562
+ }
2563
+
2564
+ if (content) el.innerHTML = content;
2565
+
2566
+ Object.defineProperty(el, 'x', {
2567
+ get: () => el._x,
2568
+ set: (v) => {
2569
+ let pos = el.style.position;
2570
+ if (!pos || pos == 'relative') {
2571
+ el.style.position = 'absolute';
2572
+ }
2573
+ let x = $.canvas.offsetLeft + v;
2574
+ el.style.left = x + 'px';
2575
+ el._x = x;
2576
+ }
2577
+ });
2578
+
2579
+ Object.defineProperty(el, 'y', {
2580
+ get: () => el._y,
2581
+ set: (v) => {
2582
+ let pos = el.style.position;
2583
+ if (!pos || pos == 'relative') {
2584
+ el.style.position = 'absolute';
2585
+ }
2586
+ let y = $.canvas.offsetTop + v;
2587
+ el.style.top = y + 'px';
2588
+ el._y = y;
2589
+ }
2590
+ });
2591
+
2592
+ Object.defineProperty(el, 'width', {
2593
+ get: () => el.style.width,
2594
+ set: (v) => (el.style.width = v + 'px')
2595
+ });
2596
+
2597
+ Object.defineProperty(el, 'height', {
2598
+ get: () => el.style.height,
2599
+ set: (v) => (el.style.height = v + 'px')
2600
+ });
2601
+
2602
+ el.position = (x, y, scheme) => {
2603
+ if (scheme) el.style.position = scheme;
2604
+ el.x = x;
2605
+ el.y = y;
2606
+ return el;
2607
+ };
2608
+
2609
+ // overwrite size
2610
+ Object.defineProperty(el, 'size', {
2611
+ writable: true
2612
+ });
2613
+
2614
+ el.size = (w, h) => {
2615
+ el.width = w;
2616
+ el.height = h;
2617
+ return el;
2618
+ };
2619
+
2620
+ el.center = () => {
2621
+ el.style.position = 'absolute';
2622
+ el.x = $.canvas.hw;
2623
+ el.y = $.canvas.hh;
2624
+ return el;
2625
+ };
2626
+
2627
+ el.show = () => {
2628
+ el.style.display = '';
2629
+ return el;
2630
+ };
2631
+
2632
+ el.hide = () => {
2633
+ el.style.display = 'none';
2634
+ return el;
2635
+ };
2636
+
2637
+ el.parent = (parent) => {
2638
+ parent.append(el);
2639
+ return el;
2640
+ };
2641
+
2642
+ $._elements.push(el);
2643
+ $.canvas.parentElement.append(el);
2644
+
2645
+ return el;
2646
+ };
2647
+ $.createEl = $.createElement;
2648
+
2649
+ $.createA = (href, content, newTab) => {
2650
+ let el = $.createEl('a', content);
2651
+ el.href = href;
2652
+ el.target = newTab ? '_blank' : '_self';
2653
+ return el;
2654
+ };
2655
+
2656
+ $.createButton = (content) => $.createEl('button', content);
2657
+
2658
+ $.createCheckbox = (label = '', checked = false) => {
2659
+ let el = $.createEl('input');
2660
+ el.type = 'checkbox';
2661
+ el.checked = checked;
2662
+ let lbl = $.createEl('label', label);
2663
+ lbl.addEventListener('click', () => {
2664
+ el.checked = !el.checked;
2665
+ el.dispatchEvent(new Event('input', { bubbles: true }));
2666
+ el.dispatchEvent(new Event('change', { bubbles: true }));
2667
+ });
2668
+ el.insertAdjacentElement('afterend', lbl);
2669
+ el.label = lbl;
2670
+ return el;
2671
+ };
2672
+
2673
+ $.createColorPicker = (value = '#ffffff') => {
2674
+ let el = $.createEl('input');
2675
+ el.type = 'color';
2676
+ el.value = value.toString();
2677
+ return el;
2678
+ };
2679
+
2680
+ $.createDiv = (content) => $.createEl('div', content);
2681
+
2682
+ $.createImg = (src) => {
2683
+ let el = $.createEl('img');
2684
+ el.crossOrigin = 'anonymous';
2685
+ el.src = src;
2686
+ return el;
2687
+ };
2688
+
2689
+ $.createInput = (value = '', type = 'text') => {
2690
+ let el = $.createEl('input');
2691
+ el.value = value;
2692
+ el.type = type;
2693
+ el.style.boxSizing = 'border-box';
2694
+ return el;
2695
+ };
2696
+
2697
+ $.createP = (content) => $.createEl('p', content);
2698
+
2699
+ let radioCount = 0;
2700
+ $.createRadio = (name) => {
2701
+ let el = $.createEl('div');
2702
+ el.name = name || 'radio' + radioCount++;
2703
+ el.buttons = [];
2704
+ Object.defineProperty(el, 'value', {
2705
+ get: () => el.selected?.value,
2706
+ set: (v) => {
2707
+ let btn = el.buttons.find((b) => b.value == v);
2708
+ if (btn) {
2709
+ btn.checked = true;
2710
+ el.selected = btn;
2711
+ }
2712
+ }
2713
+ });
2714
+ el.option = (label, value) => {
2715
+ let btn = $.createEl('input');
2716
+ btn.type = 'radio';
2717
+ btn.name = el.name;
2718
+ btn.value = value || label;
2719
+ btn.addEventListener('input', () => (el.selected = btn));
2720
+
2721
+ let lbl = $.createEl('label', label);
2722
+ lbl.addEventListener('click', () => {
2723
+ btn.checked = true;
2724
+ el.selected = btn;
2725
+ btn.dispatchEvent(new Event('input', { bubbles: true }));
2726
+ btn.dispatchEvent(new Event('change', { bubbles: true }));
2727
+ });
2728
+
2729
+ btn.label = lbl;
2730
+ el.append(btn);
2731
+ el.append(lbl);
2732
+ el.buttons.push(btn);
2733
+ return el;
2734
+ };
2735
+
2736
+ return el;
2737
+ };
2738
+
2739
+ $.createSelect = (placeholder) => {
2740
+ let el = $.createEl('select');
2741
+ if (placeholder) {
2742
+ let opt = $.createEl('option', placeholder);
2743
+ opt.disabled = true;
2744
+ opt.selected = true;
2745
+ el.append(opt);
2746
+ }
2747
+ Object.defineProperty(el, 'selected', {
2748
+ get: () => {
2749
+ if (el.multiple) {
2750
+ return Array.from(el.selectedOptions).map((opt) => opt.textContent);
2751
+ }
2752
+ return el.selectedOptions[0]?.textContent;
2753
+ },
2754
+ set: (v) => {
2755
+ if (el.multiple) {
2756
+ Array.from(el.options).forEach((opt) => {
2757
+ opt.selected = v.includes(opt.textContent);
2758
+ });
2759
+ } else {
2760
+ const option = Array.from(el.options).find((opt) => opt.textContent === v);
2761
+ if (option) option.selected = true;
2762
+ }
2763
+ }
2764
+ });
2765
+ Object.defineProperty(el, 'value', {
2766
+ get: () => {
2767
+ if (el.multiple) {
2768
+ return Array.from(el.selectedOptions).map((o) => o.value);
2769
+ }
2770
+ return el.selectedOptions[0]?.value;
2771
+ },
2772
+ set: (v) => {
2773
+ if (el.multiple) {
2774
+ el.options.forEach((o) => (o.selected = v.includes(o.value)));
2775
+ } else {
2776
+ let opt;
2777
+ for (let i = 0; i < el.options.length; i++) {
2778
+ if (el.options[i].value == v) {
2779
+ opt = el.options[i];
2780
+ break;
2781
+ }
2782
+ }
2783
+ if (opt) opt.selected = true;
2784
+ }
2785
+ }
2786
+ });
2787
+ el.option = (label, value) => {
2788
+ let opt = $.createEl('option', label);
2789
+ opt.value = value || label;
2790
+ el.append(opt);
2791
+ return el;
2792
+ };
2793
+ return el;
2794
+ };
2795
+
2796
+ $.createSlider = (min, max, value, step) => {
2797
+ let el = $.createEl('input');
2798
+ el.type = 'range';
2799
+ el.min = min;
2800
+ el.max = max;
2801
+ el.value = value;
2802
+ el.step = step;
2803
+ el.val = () => parseFloat(el.value);
2804
+ return el;
2805
+ };
2806
+
2807
+ $.createSpan = (content) => $.createEl('span', content);
2808
+
2809
+ $.createVideo = (src) => {
2810
+ let el = $.createEl('video');
2811
+ el.crossOrigin = 'anonymous';
2812
+ el.src = src;
2813
+ return el;
2814
+ };
2815
+
2816
+ $.findElement = (selector) => document.querySelector(selector);
2817
+ $.findElements = (selector) => document.querySelectorAll(selector);
2818
+ };
2552
2819
  Q5.modules.input = ($, q) => {
2553
2820
  if ($._scope == 'graphics') return;
2554
2821
 
@@ -3188,6 +3455,342 @@ Q5.PerlinNoise = class extends Q5.Noise {
3188
3455
  return (total / maxAmp + 1) / 2;
3189
3456
  }
3190
3457
  };
3458
+ Q5.modules.record = ($) => {
3459
+ let rec, btn0, btn1, timer, formatSelect, qualitySelect;
3460
+
3461
+ $.recording = false;
3462
+
3463
+ function initRecorder(opt = {}) {
3464
+ document.head.insertAdjacentHTML(
3465
+ 'beforeend',
3466
+ `<style>
3467
+ .rec {
3468
+ display: flex;
3469
+ z-index: 1000;
3470
+ gap: 6px;
3471
+ background: #1a1b1d;
3472
+ padding: 6px 8px;
3473
+ border-radius: 21px;
3474
+ box-shadow: #0000001a 0px 4px 12px;
3475
+ border: 2px solid transparent;
3476
+ opacity: 0.6;
3477
+ transition: all 0.3s;
3478
+ width: 134px;
3479
+ overflow: hidden;
3480
+ }
3481
+
3482
+ .rec:hover {
3483
+ width: unset;
3484
+ opacity: 0.96;
3485
+ }
3486
+
3487
+ .rec.recording { border-color: #cc3e44; }
3488
+
3489
+ .rec button,
3490
+ .rec select { cursor: pointer; }
3491
+
3492
+ .rec button,
3493
+ .rec select,
3494
+ .rec .record-timer {
3495
+ font-family: sans-serif;
3496
+ font-size: 14px;
3497
+ padding: 2px 10px;
3498
+ border-radius: 18px;
3499
+ outline: none;
3500
+ background-color: #232529;
3501
+ color: #d4dae6;
3502
+ box-shadow: #0000001a 0px 4px 12px;
3503
+ border: 1px solid #46494e;
3504
+ vertical-align: middle;
3505
+ line-height: 18px;
3506
+ transition: all 0.3s;
3507
+ }
3508
+
3509
+ .rec .record-button {
3510
+ color: #cc3e44;
3511
+ font-size: 18px;
3512
+ }
3513
+
3514
+ .rec select:hover,
3515
+ .rec button:hover { background-color: #292b30; }
3516
+
3517
+ .rec button:disabled {
3518
+ opacity: 0.5;
3519
+ color: #969ba5;
3520
+ cursor: not-allowed;
3521
+ }
3522
+ </style>`
3523
+ );
3524
+
3525
+ rec = $.createEl('div');
3526
+ rec.className = 'rec';
3527
+ rec.innerHTML = `
3528
+ <button class="record-button"></button>
3529
+ <span class="record-timer"></span>
3530
+ <button></button>
3531
+ `;
3532
+
3533
+ [btn0, timer, btn1] = rec.children;
3534
+
3535
+ rec.x = rec.y = 8;
3536
+
3537
+ rec.resetTimer = () => (rec.time = { hours: 0, minutes: 0, seconds: 0, frames: 0 });
3538
+ rec.resetTimer();
3539
+
3540
+ rec.formats = opt.formats || {
3541
+ 'H.264': 'video/mp4; codecs="avc1.42E01E"',
3542
+ VP9: 'video/mp4; codecs=vp9'
3543
+ };
3544
+
3545
+ // remove unsupported formats
3546
+ for (let format in rec.formats) {
3547
+ if (!MediaRecorder.isTypeSupported(rec.formats[format])) {
3548
+ delete rec.formats[format];
3549
+ }
3550
+ }
3551
+
3552
+ formatSelect = $.createSelect();
3553
+ for (const name in rec.formats) {
3554
+ formatSelect.option(name, rec.formats[name]);
3555
+ }
3556
+ rec.append(formatSelect);
3557
+
3558
+ // prettier-ignore
3559
+ rec.qualityPresets = {
3560
+ SD: 10000000, // 10 Mbps
3561
+ HD: 16000000, // 16 Mbps
3562
+ FHD: 22000000, // 22 Mbps
3563
+ QHD: 28000000, // 28 Mbps
3564
+ '4K': 48000000, // 48 Mbps
3565
+ '8K': 75000000 // 75 Mbps
3566
+ };
3567
+
3568
+ qualitySelect = $.createSelect();
3569
+ for (let name in rec.qualityPresets) {
3570
+ qualitySelect.option(name);
3571
+ }
3572
+ rec.append(qualitySelect);
3573
+
3574
+ rec.encoderSettings = {};
3575
+
3576
+ function changeFormat() {
3577
+ rec.encoderSettings.mimeType = formatSelect.value;
3578
+ }
3579
+
3580
+ function changeQuality() {
3581
+ rec.encoderSettings.videoBitsPerSecond = rec.qualityPresets[qualitySelect.value];
3582
+ }
3583
+
3584
+ formatSelect.addEventListener('change', changeFormat);
3585
+ qualitySelect.addEventListener('change', changeQuality);
3586
+
3587
+ Object.defineProperty(rec, 'quality', {
3588
+ get: () => qualitySelect.selected,
3589
+ set: (v) => {
3590
+ v = v.toUpperCase();
3591
+ if (rec.qualityPresets[v]) {
3592
+ qualitySelect.selected = v;
3593
+ changeQuality();
3594
+ }
3595
+ }
3596
+ });
3597
+
3598
+ Object.defineProperty(rec, 'format', {
3599
+ get: () => formatSelect.selected,
3600
+ set: (v) => {
3601
+ v = v.toUpperCase();
3602
+ if (rec.formats[v]) {
3603
+ formatSelect.selected = v;
3604
+ changeFormat();
3605
+ }
3606
+ }
3607
+ });
3608
+
3609
+ let h = $.canvas.height;
3610
+ rec.quality = h >= 4320 ? '8K' : h >= 2160 ? '4K' : h >= 1440 ? 'QHD' : h >= 1080 ? 'FHD' : h >= 720 ? 'HD' : 'SD';
3611
+
3612
+ if (h >= 1440 && rec.formats.VP9) rec.format = 'VP9';
3613
+ else rec.format = 'H.264';
3614
+
3615
+ btn0.addEventListener('click', () => {
3616
+ if (!$.recording) start();
3617
+ else if (!rec.paused) $.pauseRecording();
3618
+ else resumeRecording();
3619
+ });
3620
+
3621
+ btn1.addEventListener('click', () => {
3622
+ if (rec.paused) $.saveRecording();
3623
+ else $.deleteRecording();
3624
+ });
3625
+
3626
+ resetUI();
3627
+
3628
+ $.registerMethod('post', updateTimer);
3629
+ }
3630
+
3631
+ function start() {
3632
+ if ($.recording) return;
3633
+
3634
+ if (!rec.stream) {
3635
+ rec.frameRate ??= $.getTargetFrameRate();
3636
+ let canvasStream = $.canvas.captureStream(rec.frameRate);
3637
+ // let audioStream = Q5.aud.createMediaStreamDestination().stream;
3638
+ // rec.stream = new MediaStream([canvasStream.getTracks()[0], ...audioStream.getTracks()]);
3639
+ rec.stream = canvasStream;
3640
+ }
3641
+
3642
+ try {
3643
+ rec.mediaRecorder = new MediaRecorder(rec.stream, rec.encoderSettings);
3644
+ } catch (e) {
3645
+ console.error('Failed to initialize MediaRecorder: ', e);
3646
+ return;
3647
+ }
3648
+
3649
+ rec.chunks = [];
3650
+ rec.mediaRecorder.addEventListener('dataavailable', (e) => {
3651
+ if (e.data.size > 0) rec.chunks.push(e.data);
3652
+ });
3653
+
3654
+ rec.mediaRecorder.start();
3655
+ $.recording = true;
3656
+ rec.paused = false;
3657
+ rec.classList.add('recording');
3658
+
3659
+ rec.resetTimer();
3660
+ resetUI(true);
3661
+ }
3662
+
3663
+ function resumeRecording() {
3664
+ if (!$.recording || !rec.paused) return;
3665
+
3666
+ rec.mediaRecorder.resume();
3667
+ rec.paused = false;
3668
+ resetUI(true);
3669
+ }
3670
+
3671
+ function stop() {
3672
+ if (!$.recording) return;
3673
+
3674
+ rec.resetTimer();
3675
+ rec.mediaRecorder.stop();
3676
+ $.recording = false;
3677
+ rec.paused = false;
3678
+ rec.classList.remove('recording');
3679
+ }
3680
+
3681
+ function resetUI(r) {
3682
+ btn0.textContent = r ? '⏸' : '⏺';
3683
+ btn0.title = (r ? 'Pause' : 'Start') + ' Recording';
3684
+ btn1.textContent = r ? '🗑️' : '💾';
3685
+ btn1.title = (r ? 'Delete' : 'Save') + ' Recording';
3686
+ btn1.disabled = !r;
3687
+ }
3688
+
3689
+ function updateTimer() {
3690
+ if ($.recording && !rec.paused) {
3691
+ rec.time.frames++;
3692
+ let fr = $.getTargetFrameRate();
3693
+
3694
+ if (rec.time.frames >= fr) {
3695
+ rec.time.seconds += Math.floor(rec.time.frames / fr);
3696
+ rec.time.frames %= fr;
3697
+
3698
+ if (rec.time.seconds >= 60) {
3699
+ rec.time.minutes += Math.floor(rec.time.seconds / 60);
3700
+ rec.time.seconds %= 60;
3701
+
3702
+ if (rec.time.minutes >= 60) {
3703
+ rec.time.hours += Math.floor(rec.time.minutes / 60);
3704
+ rec.time.minutes %= 60;
3705
+ }
3706
+ }
3707
+ }
3708
+ }
3709
+ timer.textContent = formatTime();
3710
+ }
3711
+
3712
+ function formatTime() {
3713
+ let { hours, minutes, seconds, frames } = rec.time;
3714
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(
3715
+ 2,
3716
+ '0'
3717
+ )}:${String(frames).padStart(2, '0')}`;
3718
+ }
3719
+
3720
+ $.createRecorder = (opt) => {
3721
+ if (!rec) initRecorder(opt);
3722
+ return rec;
3723
+ };
3724
+
3725
+ $.record = (opt) => {
3726
+ if (!rec) {
3727
+ initRecorder(opt);
3728
+ rec.hide();
3729
+ }
3730
+ if (!$.recording) start();
3731
+ else if (rec.paused) resumeRecording();
3732
+ };
3733
+
3734
+ $.pauseRecording = () => {
3735
+ if (!$.recording || rec.paused) return;
3736
+
3737
+ rec.mediaRecorder.pause();
3738
+ rec.paused = true;
3739
+
3740
+ resetUI();
3741
+ btn0.title = 'Resume Recording';
3742
+ btn1.disabled = false;
3743
+ };
3744
+
3745
+ $.deleteRecording = () => {
3746
+ stop();
3747
+ resetUI();
3748
+ $.recording = false;
3749
+ };
3750
+
3751
+ $.saveRecording = async (fileName) => {
3752
+ if (!$.recording) return;
3753
+
3754
+ await new Promise((resolve) => {
3755
+ rec.mediaRecorder.onstop = resolve;
3756
+ stop();
3757
+ });
3758
+
3759
+ let type = rec.encoderSettings.mimeType,
3760
+ extension = type.slice(6, type.indexOf(';')),
3761
+ dataUrl = URL.createObjectURL(new Blob(rec.chunks, { type })),
3762
+ iframe = document.createElement('iframe'),
3763
+ a = document.createElement('a');
3764
+
3765
+ // Create an invisible iframe to detect load completion
3766
+ iframe.style.display = 'none';
3767
+ iframe.name = 'download_' + Date.now();
3768
+ document.body.append(iframe);
3769
+
3770
+ a.target = iframe.name;
3771
+ a.href = dataUrl;
3772
+ fileName ??=
3773
+ 'recording ' +
3774
+ new Date()
3775
+ .toLocaleString(undefined, { hour12: false })
3776
+ .replace(',', ' at')
3777
+ .replaceAll('/', '-')
3778
+ .replaceAll(':', '_');
3779
+ a.download = `${fileName}.${extension}`;
3780
+
3781
+ await new Promise((resolve) => {
3782
+ iframe.onload = () => {
3783
+ document.body.removeChild(iframe);
3784
+ resolve();
3785
+ };
3786
+ a.click();
3787
+ });
3788
+
3789
+ setTimeout(() => URL.revokeObjectURL(dataUrl), 1000);
3790
+ resetUI();
3791
+ $.recording = false;
3792
+ };
3793
+ };
3191
3794
  Q5.modules.sound = ($, q) => {
3192
3795
  $.Sound = Q5.Sound;
3193
3796
  let sounds = [];