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.
- package/deno.json +1 -1
- package/package.json +2 -2
- package/q5.d.ts +160 -33
- package/q5.js +351 -8
- package/q5.min.js +2 -2
package/q5.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* q5.js
|
|
3
|
-
* @version 2.
|
|
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.
|
|
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 = '
|
|
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('
|
|
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)
|
|
2748
|
-
|
|
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((
|
|
2756
|
+
Array.from(el.options).forEach((opt) => {
|
|
2757
|
+
opt.selected = v.includes(opt.textContent);
|
|
2758
|
+
});
|
|
2753
2759
|
} else {
|
|
2754
|
-
|
|
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 = [];
|