q5 2.17.0 → 2.18.3

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/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
@@ -223,6 +223,8 @@ function Q5(scope, parent, renderer) {
223
223
  $.postProcess ??= () => {};
224
224
 
225
225
  let userFns = [
226
+ 'setup',
227
+ 'postProcess',
226
228
  'mouseMoved',
227
229
  'mousePressed',
228
230
  'mouseReleased',
@@ -311,7 +313,7 @@ function createCanvas(w, h, opt) {
311
313
  }
312
314
  }
313
315
 
314
- Q5.version = Q5.VERSION = '2.17';
316
+ Q5.version = Q5.VERSION = '2.18';
315
317
 
316
318
  if (typeof document == 'object') {
317
319
  document.addEventListener('DOMContentLoaded', () => {
@@ -2082,8 +2084,8 @@ Q5.modules.ai = ($) => {
2082
2084
  let parts = errFile.split(':');
2083
2085
  let lineNum = parseInt(parts.at(-2));
2084
2086
  if (askAI) lineNum++;
2085
- parts[3] = parts[3].split(')')[0];
2086
- let fileUrl = parts.slice(0, 2).join(':');
2087
+ parts[parts.length - 1] = parts.at(-1).split(')')[0];
2088
+ let fileUrl = parts.slice(0, -2).join(':');
2087
2089
  let fileBase = fileUrl.split('/').at(-1);
2088
2090
 
2089
2091
  try {
@@ -2107,7 +2109,7 @@ Q5.modules.ai = ($) => {
2107
2109
  askAI && e.message.length > 10 ? e.message.slice(10) : 'Whats+wrong+with+this+line%3F+short+answer';
2108
2110
 
2109
2111
  let url =
2110
- 'https://chatgpt.com/?q=q5.js+' +
2112
+ 'https://chatgpt.com/?q=using+q5.js+not+p5.js+' +
2111
2113
  question +
2112
2114
  (askAI ? '' : '%0A%0A' + encodeURIComponent(e.name + ': ' + e.message)) +
2113
2115
  '%0A%0ALine%3A+' +
@@ -2625,7 +2627,7 @@ Q5.modules.dom = ($) => {
2625
2627
  };
2626
2628
 
2627
2629
  el.show = () => {
2628
- el.style.display = 'block';
2630
+ el.style.display = '';
2629
2631
  return el;
2630
2632
  };
2631
2633
 
@@ -2662,6 +2664,7 @@ Q5.modules.dom = ($) => {
2662
2664
  let lbl = $.createEl('label', label);
2663
2665
  lbl.addEventListener('click', () => {
2664
2666
  el.checked = !el.checked;
2667
+ el.dispatchEvent(new Event('input', { bubbles: true }));
2665
2668
  el.dispatchEvent(new Event('change', { bubbles: true }));
2666
2669
  });
2667
2670
  el.insertAdjacentElement('afterend', lbl);
@@ -2715,12 +2718,13 @@ Q5.modules.dom = ($) => {
2715
2718
  btn.type = 'radio';
2716
2719
  btn.name = el.name;
2717
2720
  btn.value = value || label;
2718
- btn.addEventListener('change', () => (el.selected = btn));
2721
+ btn.addEventListener('input', () => (el.selected = btn));
2719
2722
 
2720
2723
  let lbl = $.createEl('label', label);
2721
2724
  lbl.addEventListener('click', () => {
2722
2725
  btn.checked = true;
2723
2726
  el.selected = btn;
2727
+ btn.dispatchEvent(new Event('input', { bubbles: true }));
2724
2728
  btn.dispatchEvent(new Event('change', { bubbles: true }));
2725
2729
  });
2726
2730
 
@@ -2744,14 +2748,19 @@ Q5.modules.dom = ($) => {
2744
2748
  }
2745
2749
  Object.defineProperty(el, 'selected', {
2746
2750
  get: () => {
2747
- if (el.multiple) return Array.from(el.selectedOptions);
2748
- return el.selectedOptions[0];
2751
+ if (el.multiple) {
2752
+ return Array.from(el.selectedOptions).map((opt) => opt.textContent);
2753
+ }
2754
+ return el.selectedOptions[0]?.textContent;
2749
2755
  },
2750
2756
  set: (v) => {
2751
2757
  if (el.multiple) {
2752
- el.options.forEach((o) => (o.selected = v.includes(o)));
2758
+ Array.from(el.options).forEach((opt) => {
2759
+ opt.selected = v.includes(opt.textContent);
2760
+ });
2753
2761
  } else {
2754
- v.selected = true;
2762
+ const option = Array.from(el.options).find((opt) => opt.textContent === v);
2763
+ if (option) option.selected = true;
2755
2764
  }
2756
2765
  }
2757
2766
  });
@@ -3160,6 +3169,14 @@ Q5.modules.math = ($, q) => {
3160
3169
  }
3161
3170
  };
3162
3171
 
3172
+ if ($._renderer == 'c2d' && !$._webgpuFallback) {
3173
+ $.randomX = (v = 0) => $.random(-v, $.canvas.w + v);
3174
+ $.randomY = (v = 0) => $.random(-v, $.canvas.h + v);
3175
+ } else {
3176
+ $.randomX = (v = 0) => $.random(-$.canvas.hw - v, $.canvas.hw + v);
3177
+ $.randomY = (v = 0) => $.random(-$.canvas.hh - v, $.canvas.hh + v);
3178
+ }
3179
+
3163
3180
  $.randomGenerator = (method) => {
3164
3181
  if (method == $.LCG) rng1 = lcg();
3165
3182
  else if (method == $.SHR3) rng1 = shr3();
@@ -3448,6 +3465,348 @@ Q5.PerlinNoise = class extends Q5.Noise {
3448
3465
  return (total / maxAmp + 1) / 2;
3449
3466
  }
3450
3467
  };
3468
+ Q5.modules.record = ($, q) => {
3469
+ let rec, btn0, btn1, timer, formatSelect, qualitySelect;
3470
+
3471
+ $.recording = false;
3472
+
3473
+ function initRecorder(opt = {}) {
3474
+ document.head.insertAdjacentHTML(
3475
+ 'beforeend',
3476
+ `<style>
3477
+ .rec {
3478
+ display: flex;
3479
+ z-index: 1000;
3480
+ gap: 6px;
3481
+ background: #1a1b1d;
3482
+ padding: 6px 8px;
3483
+ border-radius: 21px;
3484
+ box-shadow: #0000001a 0px 4px 12px;
3485
+ border: 2px solid transparent;
3486
+ opacity: 0.6;
3487
+ transition: all 0.3s;
3488
+ width: 134px;
3489
+ overflow: hidden;
3490
+ }
3491
+
3492
+ .rec:hover {
3493
+ width: unset;
3494
+ opacity: 0.96;
3495
+ }
3496
+
3497
+ .rec.recording { border-color: #cc3e44; }
3498
+
3499
+ .rec button,
3500
+ .rec select { cursor: pointer; }
3501
+
3502
+ .rec button,
3503
+ .rec select,
3504
+ .rec .record-timer {
3505
+ font-family: sans-serif;
3506
+ font-size: 14px;
3507
+ padding: 2px 10px;
3508
+ border-radius: 18px;
3509
+ outline: none;
3510
+ background-color: #232529;
3511
+ color: #d4dae6;
3512
+ box-shadow: #0000001a 0px 4px 12px;
3513
+ border: 1px solid #46494e;
3514
+ vertical-align: middle;
3515
+ line-height: 18px;
3516
+ transition: all 0.3s;
3517
+ }
3518
+
3519
+ .rec .record-button {
3520
+ color: #cc3e44;
3521
+ font-size: 18px;
3522
+ }
3523
+
3524
+ .rec select:hover,
3525
+ .rec button:hover { background-color: #292b30; }
3526
+
3527
+ .rec button:disabled {
3528
+ opacity: 0.5;
3529
+ color: #969ba5;
3530
+ cursor: not-allowed;
3531
+ }
3532
+ </style>`
3533
+ );
3534
+
3535
+ rec = $.createEl('div');
3536
+ rec.className = 'rec';
3537
+ rec.innerHTML = `
3538
+ <button class="record-button"></button>
3539
+ <span class="record-timer"></span>
3540
+ <button></button>
3541
+ `;
3542
+
3543
+ [btn0, timer, btn1] = rec.children;
3544
+
3545
+ rec.x = rec.y = 8;
3546
+
3547
+ rec.resetTimer = () => (rec.time = { hours: 0, minutes: 0, seconds: 0, frames: 0 });
3548
+ rec.resetTimer();
3549
+
3550
+ rec.formats = opt.formats || {
3551
+ 'H.264': 'video/mp4; codecs="avc1.42E01E"',
3552
+ VP9: 'video/mp4; codecs=vp9'
3553
+ };
3554
+
3555
+ // remove unsupported formats
3556
+ for (let format in rec.formats) {
3557
+ if (!MediaRecorder.isTypeSupported(rec.formats[format])) {
3558
+ delete rec.formats[format];
3559
+ }
3560
+ }
3561
+
3562
+ formatSelect = $.createSelect('format');
3563
+ for (const name in rec.formats) {
3564
+ formatSelect.option(name, rec.formats[name]);
3565
+ }
3566
+ formatSelect.title = 'Video Format';
3567
+ rec.append(formatSelect);
3568
+
3569
+ let qMult = {
3570
+ min: 0.1,
3571
+ low: 0.25,
3572
+ mid: 0.5,
3573
+ high: 0.75,
3574
+ ultra: 0.9,
3575
+ max: 1
3576
+ };
3577
+
3578
+ qualitySelect = $.createSelect('quality');
3579
+ for (let name in qMult) {
3580
+ qualitySelect.option(name, qMult[name]);
3581
+ }
3582
+ qualitySelect.title = 'Video Quality';
3583
+ rec.append(qualitySelect);
3584
+
3585
+ rec.encoderSettings = {};
3586
+
3587
+ function changeFormat() {
3588
+ rec.encoderSettings.mimeType = formatSelect.value;
3589
+ }
3590
+
3591
+ function changeQuality() {
3592
+ rec.encoderSettings.videoBitsPerSecond = maxVideoBitRate * qualitySelect.value;
3593
+ }
3594
+
3595
+ formatSelect.addEventListener('change', changeFormat);
3596
+ qualitySelect.addEventListener('change', changeQuality);
3597
+
3598
+ Object.defineProperty(rec, 'quality', {
3599
+ get: () => qualitySelect.selected,
3600
+ set: (v) => {
3601
+ v = v.toLowerCase();
3602
+ if (qMult[v]) {
3603
+ qualitySelect.selected = v;
3604
+ changeQuality();
3605
+ }
3606
+ }
3607
+ });
3608
+
3609
+ Object.defineProperty(rec, 'format', {
3610
+ get: () => formatSelect.selected,
3611
+ set: (v) => {
3612
+ v = v.toUpperCase();
3613
+ if (rec.formats[v]) {
3614
+ formatSelect.selected = v;
3615
+ changeFormat();
3616
+ }
3617
+ }
3618
+ });
3619
+
3620
+ let h = $.canvas.height;
3621
+
3622
+ if (h >= 1440 && rec.formats.VP9) rec.format = 'VP9';
3623
+ else rec.format = 'H.264';
3624
+
3625
+ let maxVideoBitRate =
3626
+ (h >= 4320 ? 128 : h >= 2160 ? 75 : h >= 1440 ? 36 : h >= 1080 ? 28 : h >= 720 ? 22 : 16) * 1000000;
3627
+
3628
+ rec.quality = 'high';
3629
+
3630
+ btn0.addEventListener('click', () => {
3631
+ if (!$.recording) start();
3632
+ else if (!rec.paused) $.pauseRecording();
3633
+ else resumeRecording();
3634
+ });
3635
+
3636
+ btn1.addEventListener('click', () => {
3637
+ if (rec.paused) $.saveRecording();
3638
+ else $.deleteRecording();
3639
+ });
3640
+
3641
+ resetUI();
3642
+
3643
+ $.registerMethod('post', updateTimer);
3644
+ }
3645
+
3646
+ function start() {
3647
+ if ($.recording) return;
3648
+
3649
+ if (!rec.stream) {
3650
+ rec.frameRate ??= $.getTargetFrameRate();
3651
+ let canvasStream = $.canvas.captureStream(rec.frameRate);
3652
+ // let audioStream = Q5.aud.createMediaStreamDestination().stream;
3653
+ // rec.stream = new MediaStream([canvasStream.getTracks()[0], ...audioStream.getTracks()]);
3654
+ rec.stream = canvasStream;
3655
+ }
3656
+
3657
+ try {
3658
+ rec.mediaRecorder = new MediaRecorder(rec.stream, rec.encoderSettings);
3659
+ } catch (e) {
3660
+ console.error('Failed to initialize MediaRecorder: ', e);
3661
+ return;
3662
+ }
3663
+
3664
+ rec.chunks = [];
3665
+ rec.mediaRecorder.addEventListener('dataavailable', (e) => {
3666
+ if (e.data.size > 0) rec.chunks.push(e.data);
3667
+ });
3668
+
3669
+ rec.mediaRecorder.start();
3670
+ q.recording = true;
3671
+ rec.paused = false;
3672
+ rec.classList.add('recording');
3673
+
3674
+ rec.resetTimer();
3675
+ resetUI(true);
3676
+ }
3677
+
3678
+ function resumeRecording() {
3679
+ if (!$.recording || !rec.paused) return;
3680
+
3681
+ rec.mediaRecorder.resume();
3682
+ rec.paused = false;
3683
+ resetUI(true);
3684
+ }
3685
+
3686
+ function stop() {
3687
+ if (!$.recording) return;
3688
+
3689
+ rec.resetTimer();
3690
+ rec.mediaRecorder.stop();
3691
+ q.recording = false;
3692
+ rec.paused = false;
3693
+ rec.classList.remove('recording');
3694
+ }
3695
+
3696
+ function resetUI(r) {
3697
+ btn0.textContent = r ? '⏸' : '⏺';
3698
+ btn0.title = (r ? 'Pause' : 'Start') + ' Recording';
3699
+ btn1.textContent = r ? '🗑️' : '💾';
3700
+ btn1.title = (r ? 'Delete' : 'Save') + ' Recording';
3701
+ btn1.disabled = !r;
3702
+ }
3703
+
3704
+ function updateTimer() {
3705
+ if ($.recording && !rec.paused) {
3706
+ rec.time.frames++;
3707
+ let fr = $.getTargetFrameRate();
3708
+
3709
+ if (rec.time.frames >= fr) {
3710
+ rec.time.seconds += Math.floor(rec.time.frames / fr);
3711
+ rec.time.frames %= fr;
3712
+
3713
+ if (rec.time.seconds >= 60) {
3714
+ rec.time.minutes += Math.floor(rec.time.seconds / 60);
3715
+ rec.time.seconds %= 60;
3716
+
3717
+ if (rec.time.minutes >= 60) {
3718
+ rec.time.hours += Math.floor(rec.time.minutes / 60);
3719
+ rec.time.minutes %= 60;
3720
+ }
3721
+ }
3722
+ }
3723
+ }
3724
+ timer.textContent = formatTime();
3725
+ }
3726
+
3727
+ function formatTime() {
3728
+ let { hours, minutes, seconds, frames } = rec.time;
3729
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(
3730
+ 2,
3731
+ '0'
3732
+ )}:${String(frames).padStart(2, '0')}`;
3733
+ }
3734
+
3735
+ $.createRecorder = (opt) => {
3736
+ if (!rec) initRecorder(opt);
3737
+ return rec;
3738
+ };
3739
+
3740
+ $.record = (opt) => {
3741
+ if (!rec) {
3742
+ initRecorder(opt);
3743
+ rec.hide();
3744
+ }
3745
+ if (!$.recording) start();
3746
+ else if (rec.paused) resumeRecording();
3747
+ };
3748
+
3749
+ $.pauseRecording = () => {
3750
+ if (!$.recording || rec.paused) return;
3751
+
3752
+ rec.mediaRecorder.pause();
3753
+ rec.paused = true;
3754
+
3755
+ resetUI();
3756
+ btn0.title = 'Resume Recording';
3757
+ btn1.disabled = false;
3758
+ };
3759
+
3760
+ $.deleteRecording = () => {
3761
+ stop();
3762
+ resetUI();
3763
+ q.recording = false;
3764
+ };
3765
+
3766
+ $.saveRecording = async (fileName) => {
3767
+ if (!$.recording) return;
3768
+
3769
+ await new Promise((resolve) => {
3770
+ rec.mediaRecorder.onstop = resolve;
3771
+ stop();
3772
+ });
3773
+
3774
+ let type = rec.encoderSettings.mimeType,
3775
+ extension = type.slice(6, type.indexOf(';')),
3776
+ dataUrl = URL.createObjectURL(new Blob(rec.chunks, { type })),
3777
+ iframe = document.createElement('iframe'),
3778
+ a = document.createElement('a');
3779
+
3780
+ // Create an invisible iframe to detect load completion
3781
+ iframe.style.display = 'none';
3782
+ iframe.name = 'download_' + Date.now();
3783
+ document.body.append(iframe);
3784
+
3785
+ a.target = iframe.name;
3786
+ a.href = dataUrl;
3787
+ fileName ??=
3788
+ document.title +
3789
+ ' ' +
3790
+ new Date()
3791
+ .toLocaleString(undefined, { hour12: false })
3792
+ .replace(',', ' at')
3793
+ .replaceAll('/', '-')
3794
+ .replaceAll(':', '_');
3795
+ a.download = `${fileName}.${extension}`;
3796
+
3797
+ await new Promise((resolve) => {
3798
+ iframe.onload = () => {
3799
+ document.body.removeChild(iframe);
3800
+ resolve();
3801
+ };
3802
+ a.click();
3803
+ });
3804
+
3805
+ setTimeout(() => URL.revokeObjectURL(dataUrl), 1000);
3806
+ resetUI();
3807
+ q.recording = false;
3808
+ };
3809
+ };
3451
3810
  Q5.modules.sound = ($, q) => {
3452
3811
  $.Sound = Q5.Sound;
3453
3812
  let sounds = [];