@labcat2020/p5.audioreactive 0.1.2
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 +68 -0
- package/package.json +49 -0
- package/patches/p5.sound@0.2.0.patch +11 -0
- package/src/p5.Polar.min.js +3 -0
- package/src/p5.audioReact.js +221 -0
- package/src/p5.colorGenerator.js +205 -0
- package/src/p5.pattern.js +944 -0
- package/src/p5.polygon.js +71 -0
- package/src/p5.randomColor.README.md +95 -0
- package/src/p5.randomColor.js +302 -0
- package/src/p5.setGlobalP5.js +3 -0
- package/src/p5.soundBoot.js +2 -0
- package/src/sacredGeometry.js +198 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @labcat2020/p5.audioreactive
|
|
2
|
+
|
|
3
|
+
p5.js audio-reactive toolkit — MIDI/FFT helpers, sound boot, and shared sketch utilities for Animation Lab series.
|
|
4
|
+
|
|
5
|
+
> Renamed from `@labcat2020/audio-react` → `@labcat2020/p5.audioreactive` (Sep 2026). Published to npm.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add @labcat2020/p5.audioreactive p5
|
|
10
|
+
# optional peers already handled by host:
|
|
11
|
+
# p5@^2.2.3
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exports
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
".": "./src/p5.audioReact.js",
|
|
18
|
+
"./p5.audioReact.js": "./src/p5.audioReact.js",
|
|
19
|
+
"./p5.colorGenerator.js": "./src/p5.colorGenerator.js",
|
|
20
|
+
"./p5.Polar.min.js": "./src/p5.Polar.min.js",
|
|
21
|
+
"./p5.polygon.js": "./src/p5.polygon.js",
|
|
22
|
+
"./p5.pattern.js": "./src/p5.pattern.js",
|
|
23
|
+
"./p5.randomColor.js": "./src/p5.randomColor.js",
|
|
24
|
+
"./sacredGeometry.js": "./src/sacredGeometry.js"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
- `p5.audioReact.js` — `getSongPlaybackTime()`, `scheduleCueSet()`, `loadSong()`/`loadMidi` via `@tonejs/midi` + `p5.sound` boot (`p5.soundBoot.js`, `p5.setGlobalP5.js`).
|
|
28
|
+
- `p5.colorGenerator.js` / `p5.randomColor.js` — palette helpers (randomColor port).
|
|
29
|
+
- `p5.Polar.min.js` (Liz Peng) — `polarTriangle`, `polarEllipse`, etc.
|
|
30
|
+
- `p5.polygon.js` — **deprecated re-export** → canonical is `@labcat2020/p5.polygon`.
|
|
31
|
+
- `p5.pattern.js` (Taichi Sayama) — pattern fills.
|
|
32
|
+
- `sacredGeometry.js` — now canonical is `@labcat2020/p5.sacredgeometry`; kept here for compat.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
```js
|
|
36
|
+
import p5 from 'p5';
|
|
37
|
+
import '@labcat2020/p5.audioreactive/p5.audioReact.js';
|
|
38
|
+
import ColorGenerator from '@labcat2020/p5.audioreactive/p5.colorGenerator.js';
|
|
39
|
+
|
|
40
|
+
// in sketch setup:
|
|
41
|
+
await p.loadSong(audioUrl, midiUrl, (data) => {
|
|
42
|
+
p.midiPpq = data.header.ppq;
|
|
43
|
+
p.scheduleCueSet(data.tracks[0].notes, 'onCue');
|
|
44
|
+
});
|
|
45
|
+
p.fft = new p5.FFT();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Geometry (prefer new libs for new code):
|
|
49
|
+
```js
|
|
50
|
+
import '@labcat2020/p5.audioreactive/p5.Polar.min.js';
|
|
51
|
+
import '@labcat2020/p5.polygon'; // or @labcat2020/p5.audioreactive/p5.polygon.js (compat)
|
|
52
|
+
import { PTN } from '@labcat2020/p5.audioreactive/p5.pattern.js';
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Migration
|
|
56
|
+
|
|
57
|
+
- `audio-react` → `p5.audioreactive` — update imports `@labcat2020/audio-react` → `@labcat2020/p5.audioreactive`.
|
|
58
|
+
- For polygons only (no audio), use `@labcat2020/p5.polygon` (70 lines, no Tone/MIDI).
|
|
59
|
+
- For sacred geometry, use `@labcat2020/p5.sacredgeometry`.
|
|
60
|
+
|
|
61
|
+
## Peers & deps
|
|
62
|
+
|
|
63
|
+
- `peerDependencies: p5@^2.2.3`
|
|
64
|
+
- `dependencies: @tonejs/midi@^2.0.28, p5.sound@^0.2.0`
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT — LABCAT
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@labcat2020/p5.audioreactive",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "p5.js audio-reactive helpers, sound boot, and shared sketch utilities",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/p5.audioReact.js",
|
|
8
|
+
"./p5.audioReact.js": "./src/p5.audioReact.js",
|
|
9
|
+
"./p5.colorGenerator.js": "./src/p5.colorGenerator.js",
|
|
10
|
+
"./p5.Polar.min.js": "./src/p5.Polar.min.js",
|
|
11
|
+
"./p5.polygon.js": "./src/p5.polygon.js",
|
|
12
|
+
"./p5.pattern.js": "./src/p5.pattern.js",
|
|
13
|
+
"./p5.randomColor.js": "./src/p5.randomColor.js",
|
|
14
|
+
"./sacredGeometry.js": "./src/sacredGeometry.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"patches"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"./src/p5.audioReact.js",
|
|
22
|
+
"./src/p5.soundBoot.js",
|
|
23
|
+
"./src/p5.setGlobalP5.js"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@tonejs/midi": "^2.0.28",
|
|
30
|
+
"p5.sound": "^0.2.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"p5": "^2.2.3"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"author": "LABCAT (labcat.nz)",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/LABCAT/animations.labcat.nz.git",
|
|
40
|
+
"directory": "packages/p5.audioreactive"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"p5",
|
|
44
|
+
"audio",
|
|
45
|
+
"midi",
|
|
46
|
+
"generative",
|
|
47
|
+
"labcat"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
diff --git a/package.json b/package.json
|
|
2
|
+
index 0000000..0000000 100644
|
|
3
|
+
--- a/package.json
|
|
4
|
+
+++ b/package.json
|
|
5
|
+
@@ -39,5 +39,5 @@
|
|
6
|
+
"webpack": "^5.91.0",
|
|
7
|
+
"webpack-cli": "^5.1.4"
|
|
8
|
+
},
|
|
9
|
+
- "sideEffects": false
|
|
10
|
+
+ "sideEffects": ["dist/p5.sound.js", "dist/p5.sound.min.js"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// p5.Polar.min.js, version 2.3 Sep 5th 2023, created by Liz Peng
|
|
2
|
+
import p5 from 'p5';
|
|
3
|
+
p5.prototype.setCenter=function(t,s){void 0===this.center&&(this.center={x:t,y:s}),this.translate(this.center.x=t,this.center.y=s)},p5.prototype.shiftRotate=function(t,s){const i=this.radians(t);this.translate(this.sin(i)*s,this.cos(i)*-s),this.rotate(this.radians(t))},p5.prototype.polarDrawCallback=function(t,s,i,o){const h=360/t;for(let p=1;p<=t;p++)this.push(),this.shiftRotate(p*h,i),o(p,h,s,i),this.pop()},p5.prototype.polarTriangle=function(t,s,i){this.push(),this.shiftRotate(t,i),this.triangle(this.sin(0),this.cos(0)*-s,this.sin(1*this.TWO_PI/3)*s,this.cos(1*this.TWO_PI/3)*-s,this.sin(2*this.TWO_PI/3)*s,this.cos(2*this.TWO_PI/3)*-s),this.pop()},p5.prototype.polarTriangles=function(t,s,i,o){const h=360/t;for(let p=1;p<=t;p++)if(o){const t=o(p,h,s,i);this.polarTriangle(t[0]*t[1],t[2],t[3])}else this.polarTriangle(p*h,s,i)},p5.prototype.polarEllipse=function(t,s,i,o){this.push(),this.shiftRotate(t,o),this.ellipse(0,0,2*s,2*i),this.pop()},p5.prototype.polarEllipses=function(t,s,i,o,h){const p=360/t;for(let e=1;e<=t;e++)if(h){const t=h(e,p,s,i,o);this.polarEllipse(t[0]*t[1],t[2],t[3],t[4])}else this.polarEllipse(e*p,s,i,o)},p5.prototype.polarLine=function(t,s,i){this.push(),this.shiftRotate(t,i),this.line(0,s,0,-s),this.pop()},p5.prototype.polarLines=function(t,s,i,o){const h=360/t;for(let p=1;p<=t;p++)if(o){const t=o(p,h,s,i);this.polarLine(t[0]*t[1],t[2],t[3])}else this.polarLine(p*h,s,i)},p5.prototype.polarSquare=function(t,s,i){this.push(),this.shiftRotate(t,i),this.square(-s,-s,2*s),this.pop()},p5.prototype.polarSquares=function(t,s,i,o){const h=360/t;for(let p=1;p<=t;p++)if(o){const t=o(p,h,s,i);this.polarSquare(t[0]*t[1],t[2],t[3])}else this.polarSquare(p*h,s,i)},p5.prototype.polarPentagon=function(t,s,i){this.push();const o=this.radians(t);this.translate(this.sin(o)*i,this.cos(o)*-i),this.rotate(this.radians(t)+60),this.beginShape();for(let t=1;t<=5;t++)this.vertex(this.cos(this.TWO_PI*t/5)*s,this.sin(this.TWO_PI*t/5)*s);this.endShape(this.CLOSE),this.pop()},p5.prototype.polarPentagons=function(t,s,i,o){const h=360/t;for(let p=1;p<=t;p++)if(o){const t=o(p,h,s,i);this.polarPentagon(t[0]*t[1],t[2],t[3])}else this.polarPentagon(p*h,s,i)},p5.prototype.polarHexagon=function(t,s,i){this.push(),this.shiftRotate(t,i),this.beginShape();for(let t=0;t<6;t++)this.vertex(this.cos(this.TWO_PI*t/6)*s,this.sin(this.TWO_PI*t/6)*s);this.endShape(this.CLOSE),this.pop()},p5.prototype.polarHexagons=function(t,s,i,o){const h=360/t;for(let p=1;p<=t;p++)if(o){const t=o(p,h,s,i);this.polarHexagon(t[0]*t[1],t[2],t[3])}else this.polarHexagon(p*h,s,i)},p5.prototype.polarHeptagon=function(t,s,i){this.push();const o=this.radians(t);this.translate(this.sin(o)*i,this.cos(o)*-i),this.rotate(this.radians(t)+11),this.beginShape();for(let t=1;t<=7;t++)this.vertex(this.cos(this.TWO_PI*t/7)*s,this.sin(this.TWO_PI*t/7)*s);this.endShape(this.CLOSE),this.pop()},p5.prototype.polarHeptagons=function(t,s,i,o){const h=360/t;for(let p=1;p<=t;p++)if(o){const t=o(p,h,s,i);this.polarHeptagon(t[0]*t[1],t[2],t[3])}else this.polarHeptagon(p*h,s,i)},p5.prototype.polarOctagon=function(t,s,i){this.push(),this.shiftRotate(t,i),this.beginShape();for(let t=1;t<=8;t++)this.vertex(this.cos(this.TWO_PI*t/8)*s,this.sin(this.TWO_PI*t/8)*s);this.endShape(this.CLOSE),this.pop()},p5.prototype.polarOctagons=function(t,s,i,o){const h=360/t;for(let p=1;p<=t;p++)if(o){const t=o(p,h,s,i);this.polarOctagon(t[0]*t[1],t[2],t[3])}else this.polarOctagon(p*h,s,i)},p5.prototype.polarPolygon=function(t,s,i,o){this.push(),this.shiftRotate(s,o),this.beginShape();for(let s=1;s<=t;s++)this.vertex(this.cos(this.TWO_PI*s/t)*i,this.sin(this.TWO_PI*s/t)*i);this.endShape(this.CLOSE),this.pop()},p5.prototype.polarPolygons=function(t,s,i,o,h){const p=360/t;for(let e=1;e<=t;e++)if(h){const t=h(e,p,s,i,o);this.polarPolygon(t[2],t[0]*t[1],t[3],t[4])}else this.polarPolygon(s,e*p,i,o)};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import './p5.soundBoot.js';
|
|
2
|
+
import p5 from 'p5';
|
|
3
|
+
import { Midi } from '@tonejs/midi';
|
|
4
|
+
|
|
5
|
+
p5.prototype.getSongPlaybackTime = function () {
|
|
6
|
+
if (!this.song) return NaN;
|
|
7
|
+
if (
|
|
8
|
+
this.captureInProgress &&
|
|
9
|
+
typeof this.audioSampleRate === 'number' &&
|
|
10
|
+
this.audioSampleRate > 0 &&
|
|
11
|
+
typeof this.song._lastPos === 'number'
|
|
12
|
+
) {
|
|
13
|
+
const t = this.song._lastPos / this.audioSampleRate;
|
|
14
|
+
if (Number.isFinite(t)) return t;
|
|
15
|
+
}
|
|
16
|
+
if (this.song.isPlaying()) {
|
|
17
|
+
if (this._playbackWallStartPerf == null) return 0;
|
|
18
|
+
const rate = this.song.speed ?? 1;
|
|
19
|
+
return (
|
|
20
|
+
this._playbackSongSecondsAtWallStart +
|
|
21
|
+
((performance.now() - this._playbackWallStartPerf) / 1000) * rate
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return Number.isFinite(this._playbackFrozenSec) ? this._playbackFrozenSec : 0;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
p5.prototype._stopMidiCuePoll = function () {
|
|
28
|
+
if (this._midiCuePollId != null) {
|
|
29
|
+
clearInterval(this._midiCuePollId);
|
|
30
|
+
this._midiCuePollId = null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
p5.prototype._reindexMidiCues = function (fromSeconds) {
|
|
35
|
+
const sorted = [...(this._midiTransportCues || [])].sort((a, b) => a.time - b.time);
|
|
36
|
+
const t0 = Math.max(0, fromSeconds);
|
|
37
|
+
let i = 0;
|
|
38
|
+
while (i < sorted.length && sorted[i].time < t0 - 1e-4) {
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
this._midiCueSorted = sorted;
|
|
42
|
+
this._midiCueNext = i;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
p5.prototype._midiCuePollTick = function () {
|
|
46
|
+
if (!this.song?.isPlaying()) return;
|
|
47
|
+
const t = this.getSongPlaybackTime();
|
|
48
|
+
if (!Number.isFinite(t)) return;
|
|
49
|
+
const cues = this._midiCueSorted;
|
|
50
|
+
if (!cues?.length) return;
|
|
51
|
+
const slack = 0.03;
|
|
52
|
+
while (this._midiCueNext < cues.length && cues[this._midiCueNext].time <= t + slack) {
|
|
53
|
+
const { fn, note } = cues[this._midiCueNext];
|
|
54
|
+
this._midiCueNext++;
|
|
55
|
+
fn.call(this, note);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
p5.prototype._startSongPlayback = function (fromSec = 0) {
|
|
60
|
+
this._playbackWallStartPerf = performance.now();
|
|
61
|
+
this._playbackSongSecondsAtWallStart = fromSec;
|
|
62
|
+
this._reindexMidiCues(fromSec);
|
|
63
|
+
this._stopMidiCuePoll();
|
|
64
|
+
this.userStartAudio();
|
|
65
|
+
this.song.play();
|
|
66
|
+
this._midiCuePollId = setInterval(() => this._midiCuePollTick(), 20);
|
|
67
|
+
this._midiCuePollTick();
|
|
68
|
+
this.showingStatic = false;
|
|
69
|
+
this.songHasFinished = false;
|
|
70
|
+
if (this.canvas) {
|
|
71
|
+
this.canvas.classList.add('p5Canvas--cursor-pause');
|
|
72
|
+
this.canvas.classList.remove('p5Canvas--cursor-play');
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
p5.prototype._restartSongPlayback = function () {
|
|
77
|
+
this.resetAnimation?.();
|
|
78
|
+
this.song.stop();
|
|
79
|
+
this.song.paused = false;
|
|
80
|
+
this.song.playing = false;
|
|
81
|
+
this._playbackFrozenSec = 0;
|
|
82
|
+
this._startSongPlayback(0);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
p5.prototype.loadSong = async function (audioUrl, midiUrl, callback) {
|
|
86
|
+
try {
|
|
87
|
+
const sound = await this.loadSound(audioUrl);
|
|
88
|
+
this.song = sound;
|
|
89
|
+
this.songHasFinished = false;
|
|
90
|
+
this.loopAudio = this.loopAudio ?? false;
|
|
91
|
+
this._midiTransportCues = [];
|
|
92
|
+
this._midiCueSorted = [];
|
|
93
|
+
this._midiCueNext = 0;
|
|
94
|
+
this._stopMidiCuePoll();
|
|
95
|
+
this.song._cues = [];
|
|
96
|
+
|
|
97
|
+
this.audioSampleRate = sound.sampleRate?.() ?? 44100;
|
|
98
|
+
this.totalAnimationFrames = Math.floor((sound.duration() || 0) * 60);
|
|
99
|
+
|
|
100
|
+
sound.onended(() => {
|
|
101
|
+
if (this.loopAudio) {
|
|
102
|
+
if (this.captureEnabled && this.captureInProgress) {
|
|
103
|
+
this.captureInProgress = false;
|
|
104
|
+
this.downloadFrames?.();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this._restartSongPlayback();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.songHasFinished = true;
|
|
112
|
+
this.song.playing = false;
|
|
113
|
+
this.song.paused = false;
|
|
114
|
+
this._stopMidiCuePoll();
|
|
115
|
+
this._playbackWallStartPerf = null;
|
|
116
|
+
const dur = this.song.duration?.() ?? 0;
|
|
117
|
+
this._playbackFrozenSec = Number.isFinite(dur) ? dur : 0;
|
|
118
|
+
if (this.canvas) {
|
|
119
|
+
this.canvas.classList.add('p5Canvas--cursor-play');
|
|
120
|
+
this.canvas.classList.remove('p5Canvas--cursor-pause');
|
|
121
|
+
}
|
|
122
|
+
if (this.captureEnabled && this.captureInProgress) {
|
|
123
|
+
this.captureInProgress = false;
|
|
124
|
+
this.downloadFrames?.();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const midiData = await this.loadMidi(midiUrl);
|
|
129
|
+
callback?.(midiData);
|
|
130
|
+
this.hideLoader();
|
|
131
|
+
return midiData;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Failed to load song or MIDI:', error);
|
|
134
|
+
this.hideLoader();
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
p5.prototype.scheduleCueSet = function (noteSet, callbackName, polyMode = false) {
|
|
140
|
+
const fn = this[callbackName];
|
|
141
|
+
if (typeof fn !== 'function') {
|
|
142
|
+
console.error(`scheduleCueSet: missing handler "${callbackName}"`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!this.song?._cues) {
|
|
146
|
+
this.song._cues = [];
|
|
147
|
+
}
|
|
148
|
+
if (!this._midiTransportCues) {
|
|
149
|
+
this._midiTransportCues = [];
|
|
150
|
+
}
|
|
151
|
+
let lastTicks = -1;
|
|
152
|
+
let currentCue = 1;
|
|
153
|
+
for (let i = 0; i < noteSet.length; i++) {
|
|
154
|
+
const note = noteSet[i];
|
|
155
|
+
const { ticks, time } = note;
|
|
156
|
+
if (ticks !== lastTicks || polyMode) {
|
|
157
|
+
note.currentCue = currentCue;
|
|
158
|
+
const cueTime = time <= 0 ? 1e-6 : time;
|
|
159
|
+
this.song._cues.push({ time: cueTime, callback: fn, val: note, scope: this });
|
|
160
|
+
this._midiTransportCues.push({ time: cueTime, fn, note });
|
|
161
|
+
lastTicks = ticks;
|
|
162
|
+
currentCue++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
p5.prototype.loadMidi = async function (midiUrl) {
|
|
168
|
+
const result = await Midi.fromUrl(midiUrl);
|
|
169
|
+
console.log('MIDI loaded:', result);
|
|
170
|
+
return result;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
p5.prototype.hideLoader = function () {
|
|
174
|
+
const loader = document.getElementById('loader');
|
|
175
|
+
const playIcon = document.getElementById('play-icon');
|
|
176
|
+
if (loader) loader.classList.add('loading--complete');
|
|
177
|
+
if (playIcon) playIcon.classList.add('fade-in');
|
|
178
|
+
this.audioLoaded = true;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
p5.prototype.togglePlayback = function () {
|
|
182
|
+
if (this.audioLoaded && this.song) {
|
|
183
|
+
if (this.captureEnabled) {
|
|
184
|
+
this.startCapture();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (this.song.isPlaying()) {
|
|
188
|
+
if (this._playbackWallStartPerf != null) {
|
|
189
|
+
const rate = this.song.speed ?? 1;
|
|
190
|
+
this._playbackFrozenSec =
|
|
191
|
+
this._playbackSongSecondsAtWallStart +
|
|
192
|
+
((performance.now() - this._playbackWallStartPerf) / 1000) * rate;
|
|
193
|
+
}
|
|
194
|
+
this._playbackWallStartPerf = null;
|
|
195
|
+
this._stopMidiCuePoll();
|
|
196
|
+
this.song.pause();
|
|
197
|
+
this.canvas.classList.add('p5Canvas--cursor-play');
|
|
198
|
+
this.canvas.classList.remove('p5Canvas--cursor-pause');
|
|
199
|
+
} else {
|
|
200
|
+
let fromSec = Math.max(0, this.getSongPlaybackTime() || 0);
|
|
201
|
+
const atEnd = this.songHasFinished;
|
|
202
|
+
if (atEnd) {
|
|
203
|
+
this._restartSongPlayback();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const playIcon = document.getElementById('play-icon');
|
|
208
|
+
if (playIcon) playIcon.classList.remove('fade-in');
|
|
209
|
+
|
|
210
|
+
this._startSongPlayback(fromSec);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
p5.prototype.saveSketchImage = function () {
|
|
216
|
+
if (this.keyIsDown(this.CONTROL) && this.key === 's') {
|
|
217
|
+
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
|
218
|
+
this.save(`sketch_${timestamp}.png`);
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export default class ColorGenerator {
|
|
2
|
+
/**
|
|
3
|
+
* Constructor for ColorGenerator.
|
|
4
|
+
* Initializes the generator with a base color. If no color is provided, it generates a random one.
|
|
5
|
+
* @param {string} colorString - The base color in any valid p5.js color format (e.g., hex or RGB).
|
|
6
|
+
* @param {number} [opacity=1] - The opacity of the color, defaults to 1.
|
|
7
|
+
*/
|
|
8
|
+
constructor(p5, colorString, opacity = 1) {
|
|
9
|
+
this.p5 = p5;
|
|
10
|
+
this.opacity = opacity;
|
|
11
|
+
this.p5.push();
|
|
12
|
+
this.p5.colorMode(this.p5.HSB, 360, 100, 100, 1);
|
|
13
|
+
|
|
14
|
+
if (colorString === undefined) {
|
|
15
|
+
let h = this.p5.random(0, 360);
|
|
16
|
+
let s = this.p5.random(0, 100);
|
|
17
|
+
let b = this.p5.random(0, 100);
|
|
18
|
+
colorString = this.p5.color(h, s, b, this.opacity);
|
|
19
|
+
} else if (colorString === 'bright') {
|
|
20
|
+
colorString = this.getBrightColor();
|
|
21
|
+
} else {
|
|
22
|
+
let c = this.p5.color(colorString);
|
|
23
|
+
colorString = this.p5.color(this.p5.hue(c), this.p5.saturation(c), this.p5.brightness(c), this.opacity);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Store the color and its HSB components
|
|
27
|
+
this.color = this.p5.color(colorString);
|
|
28
|
+
this.h = this.p5.hue(this.color);
|
|
29
|
+
this.s = this.p5.saturation(this.color);
|
|
30
|
+
this.b = this.p5.brightness(this.color);
|
|
31
|
+
this.p5.pop();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generates a bright color with high saturation and brightness values.
|
|
36
|
+
* @return {Object} A bright p5.Color object.
|
|
37
|
+
*/
|
|
38
|
+
getBrightColor() {
|
|
39
|
+
return this.p5.color(
|
|
40
|
+
this.p5.random(0, 360), // Full hue range for variety
|
|
41
|
+
this.p5.random(80, 100), // High saturation (80-100)
|
|
42
|
+
this.p5.random(80, 100), // High brightness (80-100)
|
|
43
|
+
this.opacity
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generates shades of the base color by decreasing brightness.
|
|
49
|
+
* @param {number} nr - The number of shades to generate.
|
|
50
|
+
* @param {number} limit - The minimum brightness value to limit the shades (default: 0).
|
|
51
|
+
* @return {array} An array of colors in HSB format.
|
|
52
|
+
*/
|
|
53
|
+
getShades(nr, limit = 0) {
|
|
54
|
+
this.p5.push(); // Use this.p5 directly
|
|
55
|
+
this.p5.colorMode(this.p5.HSB, 360, 100, 100, 1); // Use this.p5 directly
|
|
56
|
+
let result = [];
|
|
57
|
+
|
|
58
|
+
limit = this.p5.constrain(limit, 0, this.b); // Use this.p5 directly
|
|
59
|
+
|
|
60
|
+
let equalDifference = (this.b - limit) / nr;
|
|
61
|
+
for (let i = 0; i < nr; i++) {
|
|
62
|
+
result.push(this.p5.color(this.h, this.s, this.b - (i * equalDifference), this.opacity));
|
|
63
|
+
}
|
|
64
|
+
this.p5.pop(); // Use this.p5 directly
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generates tints of the base color by increasing brightness.
|
|
70
|
+
* @param {number} nr - The number of tints to generate.
|
|
71
|
+
* @param {number} limit - The maximum brightness value to limit the tints (default: 100).
|
|
72
|
+
* @return {array} An array of colors in HSB format.
|
|
73
|
+
*/
|
|
74
|
+
getTints(nr, limit = 100) {
|
|
75
|
+
this.p5.push();
|
|
76
|
+
this.p5.colorMode(this.p5.HSB, 360, 100, 100, 1);
|
|
77
|
+
let result = [];
|
|
78
|
+
|
|
79
|
+
limit = this.p5.constrain(limit, this.b, 100);
|
|
80
|
+
|
|
81
|
+
let equalDifferenceBrightness = (limit - this.b) / nr;
|
|
82
|
+
let equalDifferenceSaturation = this.s / nr;
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < nr; i++) {
|
|
85
|
+
result.push(this.p5.color(this.h, this.s - (i * equalDifferenceSaturation), this.b + (i * equalDifferenceBrightness), this.opacity));
|
|
86
|
+
}
|
|
87
|
+
this.p5.pop();
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generates a monochromatic palette by adjusting saturation and brightness.
|
|
93
|
+
* @param {number} nr - The number of monochromatic colors to generate.
|
|
94
|
+
* @param {boolean} increaseSaturation - Whether to increase saturation for each color (default: false).
|
|
95
|
+
* @param {boolean} increaseBrightness - Whether to increase brightness for each color (default: false).
|
|
96
|
+
* @return {array} An array of colors in HSB format.
|
|
97
|
+
*/
|
|
98
|
+
getMonochromatic(nr, increaseSaturation = false, increaseBrightness = false) {
|
|
99
|
+
this.p5.push();
|
|
100
|
+
this.p5.colorMode(this.p5.HSB, 360, 100, 100, 1);
|
|
101
|
+
let result = [];
|
|
102
|
+
|
|
103
|
+
// Calculate saturation and brightness steps
|
|
104
|
+
let satDifference = this.s / nr;
|
|
105
|
+
let brightDifference = (100 - this.b) / nr;
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < nr; i++) {
|
|
108
|
+
let currentSaturation, currentBrightness;
|
|
109
|
+
|
|
110
|
+
// Adjust saturation based on the direction
|
|
111
|
+
currentSaturation = increaseSaturation
|
|
112
|
+
? this.p5.constrain(this.s + (i * satDifference), 0, 100)
|
|
113
|
+
: this.p5.constrain(this.s - (i * satDifference), 0, 100);
|
|
114
|
+
|
|
115
|
+
// Adjust brightness based on the direction
|
|
116
|
+
currentBrightness = increaseBrightness
|
|
117
|
+
? this.p5.constrain(this.b + (i * brightDifference), 0, 100)
|
|
118
|
+
: this.p5.constrain(this.b - (i * brightDifference), 0, 100);
|
|
119
|
+
|
|
120
|
+
result.push(this.p5.color(this.h, currentSaturation, currentBrightness, this.opacity));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.p5.pop();
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generates the complementary color of the base color (180 degrees on the color wheel).
|
|
129
|
+
* @return {array} An array with the base color and its complementary color.
|
|
130
|
+
*/
|
|
131
|
+
getComplementary() {
|
|
132
|
+
this.p5.push();
|
|
133
|
+
this.p5.colorMode(this.p5.HSB, 360, 100, 100, 1);
|
|
134
|
+
let complementaryColor = this.p5.color((this.h + 180) % 360, this.s, this.b, this.opacity);
|
|
135
|
+
this.p5.pop();
|
|
136
|
+
return [this.color, complementaryColor];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generates a triadic color scheme (three colors evenly spaced on the color wheel, 120 degrees apart).
|
|
141
|
+
* @return {array} An array of three colors in HSB format.
|
|
142
|
+
*/
|
|
143
|
+
getTriadic() {
|
|
144
|
+
this.p5.push();
|
|
145
|
+
this.p5.colorMode(this.p5.HSB, 360, 100, 100, 1);
|
|
146
|
+
let triadicColors = [
|
|
147
|
+
this.color,
|
|
148
|
+
this.p5.color((this.h + 120) % 360, this.s, this.b, this.opacity),
|
|
149
|
+
this.p5.color((this.h + 240) % 360, this.s, this.b, this.opacity)
|
|
150
|
+
];
|
|
151
|
+
this.p5.pop();
|
|
152
|
+
return triadicColors;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generates a split complementary color scheme (two colors adjacent to the complementary color).
|
|
157
|
+
* @param {number} degree - The degree difference from the complementary color (default: 30).
|
|
158
|
+
* @return {array} An array of three colors in HSB format.
|
|
159
|
+
*/
|
|
160
|
+
getSplitComplementary(degree = 30) {
|
|
161
|
+
this.p5.push();
|
|
162
|
+
this.p5.colorMode(this.p5.HSB, 360, 100, 100, 1);
|
|
163
|
+
let splitComplementaryColors = [
|
|
164
|
+
this.color,
|
|
165
|
+
this.p5.color((this.h + 180 - degree + 360) % 360, this.s, this.b, this.opacity),
|
|
166
|
+
this.p5.color((this.h + 180 + degree) % 360, this.s, this.b, this.opacity)
|
|
167
|
+
];
|
|
168
|
+
this.p5.pop();
|
|
169
|
+
return splitComplementaryColors;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Generates a tetradic color scheme (four colors, two complementary pairs).
|
|
174
|
+
* @return {array} An array of four colors in HSB format.
|
|
175
|
+
*/
|
|
176
|
+
getTetradic() {
|
|
177
|
+
this.p5.push();
|
|
178
|
+
this.p5.colorMode(this.p5.HSB, 360, 100, 100, 1);
|
|
179
|
+
let tetradicColors = [
|
|
180
|
+
this.color,
|
|
181
|
+
this.p5.color((this.h + 90) % 360, this.s, this.b, this.opacity),
|
|
182
|
+
this.p5.color((this.h + 180) % 360, this.s, this.b, this.opacity),
|
|
183
|
+
this.p5.color((this.h + 270) % 360, this.s, this.b, this.opacity)
|
|
184
|
+
];
|
|
185
|
+
this.p5.pop();
|
|
186
|
+
return tetradicColors;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Generates an array of colors with varying opacities based on the base color.
|
|
191
|
+
* @param {number} numColors - The number of colors to generate.
|
|
192
|
+
* @return {array} An array of colors with different opacities.
|
|
193
|
+
*/
|
|
194
|
+
getOpacityVariations(numColors) {
|
|
195
|
+
this.p5.push();
|
|
196
|
+
this.p5.colorMode(this.p5.HSB, 360, 100, 100, 1);
|
|
197
|
+
let result = [];
|
|
198
|
+
for (let i = 1; i <= numColors; i++) {
|
|
199
|
+
let currentOpacity = i / numColors;
|
|
200
|
+
result.push(this.p5.color(this.h, this.s, this.b, currentOpacity));
|
|
201
|
+
}
|
|
202
|
+
this.p5.pop();
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
}
|