q5 2.17.0 → 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 +160 -33
  4. package/q5.js +351 -8
  5. package/q5.min.js +2 -2
package/q5.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * q5.js
3
- * @version 2.17
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.17';
314
+ Q5.version = Q5.VERSION = '2.18';
315
315
 
316
316
  if (typeof document == 'object') {
317
317
  document.addEventListener('DOMContentLoaded', () => {
@@ -2625,7 +2625,7 @@ Q5.modules.dom = ($) => {
2625
2625
  };
2626
2626
 
2627
2627
  el.show = () => {
2628
- el.style.display = 'block';
2628
+ el.style.display = '';
2629
2629
  return el;
2630
2630
  };
2631
2631
 
@@ -2662,6 +2662,7 @@ Q5.modules.dom = ($) => {
2662
2662
  let lbl = $.createEl('label', label);
2663
2663
  lbl.addEventListener('click', () => {
2664
2664
  el.checked = !el.checked;
2665
+ el.dispatchEvent(new Event('input', { bubbles: true }));
2665
2666
  el.dispatchEvent(new Event('change', { bubbles: true }));
2666
2667
  });
2667
2668
  el.insertAdjacentElement('afterend', lbl);
@@ -2715,12 +2716,13 @@ Q5.modules.dom = ($) => {
2715
2716
  btn.type = 'radio';
2716
2717
  btn.name = el.name;
2717
2718
  btn.value = value || label;
2718
- btn.addEventListener('change', () => (el.selected = btn));
2719
+ btn.addEventListener('input', () => (el.selected = btn));
2719
2720
 
2720
2721
  let lbl = $.createEl('label', label);
2721
2722
  lbl.addEventListener('click', () => {
2722
2723
  btn.checked = true;
2723
2724
  el.selected = btn;
2725
+ btn.dispatchEvent(new Event('input', { bubbles: true }));
2724
2726
  btn.dispatchEvent(new Event('change', { bubbles: true }));
2725
2727
  });
2726
2728
 
@@ -2744,14 +2746,19 @@ Q5.modules.dom = ($) => {
2744
2746
  }
2745
2747
  Object.defineProperty(el, 'selected', {
2746
2748
  get: () => {
2747
- if (el.multiple) return Array.from(el.selectedOptions);
2748
- return el.selectedOptions[0];
2749
+ if (el.multiple) {
2750
+ return Array.from(el.selectedOptions).map((opt) => opt.textContent);
2751
+ }
2752
+ return el.selectedOptions[0]?.textContent;
2749
2753
  },
2750
2754
  set: (v) => {
2751
2755
  if (el.multiple) {
2752
- el.options.forEach((o) => (o.selected = v.includes(o)));
2756
+ Array.from(el.options).forEach((opt) => {
2757
+ opt.selected = v.includes(opt.textContent);
2758
+ });
2753
2759
  } else {
2754
- v.selected = true;
2760
+ const option = Array.from(el.options).find((opt) => opt.textContent === v);
2761
+ if (option) option.selected = true;
2755
2762
  }
2756
2763
  }
2757
2764
  });
@@ -3448,6 +3455,342 @@ Q5.PerlinNoise = class extends Q5.Noise {
3448
3455
  return (total / maxAmp + 1) / 2;
3449
3456
  }
3450
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
+ };
3451
3794
  Q5.modules.sound = ($, q) => {
3452
3795
  $.Sound = Q5.Sound;
3453
3796
  let sounds = [];