@guinetik/gcanvas 1.0.0

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 (349) hide show
  1. package/.github/workflows/release.yaml +70 -0
  2. package/.jshintrc +4 -0
  3. package/.vscode/settings.json +22 -0
  4. package/CLAUDE.md +310 -0
  5. package/blackhole.jpg +0 -0
  6. package/demo.png +0 -0
  7. package/demos/CNAME +1 -0
  8. package/demos/animations.html +31 -0
  9. package/demos/basic.html +38 -0
  10. package/demos/baskara.html +31 -0
  11. package/demos/bezier.html +35 -0
  12. package/demos/beziersignature.html +29 -0
  13. package/demos/blackhole.html +28 -0
  14. package/demos/blob.html +35 -0
  15. package/demos/demos.css +289 -0
  16. package/demos/easing.html +28 -0
  17. package/demos/events.html +195 -0
  18. package/demos/fluent.html +647 -0
  19. package/demos/fractals.html +36 -0
  20. package/demos/genart.html +26 -0
  21. package/demos/gendream.html +26 -0
  22. package/demos/group.html +36 -0
  23. package/demos/home.html +587 -0
  24. package/demos/index.html +364 -0
  25. package/demos/isometric.html +34 -0
  26. package/demos/js/animations.js +452 -0
  27. package/demos/js/basic.js +204 -0
  28. package/demos/js/baskara.js +751 -0
  29. package/demos/js/bezier.js +692 -0
  30. package/demos/js/beziersignature.js +241 -0
  31. package/demos/js/blackhole/accretiondisk.obj.js +379 -0
  32. package/demos/js/blackhole/blackhole.obj.js +318 -0
  33. package/demos/js/blackhole/index.js +409 -0
  34. package/demos/js/blackhole/particle.js +56 -0
  35. package/demos/js/blackhole/starfield.obj.js +218 -0
  36. package/demos/js/blob.js +2263 -0
  37. package/demos/js/easing.js +477 -0
  38. package/demos/js/fluent.js +183 -0
  39. package/demos/js/fractals.js +931 -0
  40. package/demos/js/fractalworker.js +93 -0
  41. package/demos/js/genart.js +268 -0
  42. package/demos/js/gendream.js +209 -0
  43. package/demos/js/group.js +140 -0
  44. package/demos/js/info-toggle.js +25 -0
  45. package/demos/js/isometric.js +863 -0
  46. package/demos/js/kerr.js +1556 -0
  47. package/demos/js/lavalamp.js +590 -0
  48. package/demos/js/layout.js +354 -0
  49. package/demos/js/mondrian.js +285 -0
  50. package/demos/js/opacity.js +275 -0
  51. package/demos/js/painter.js +484 -0
  52. package/demos/js/particles-showcase.js +514 -0
  53. package/demos/js/particles.js +299 -0
  54. package/demos/js/patterns.js +397 -0
  55. package/demos/js/penrose/artifact.js +69 -0
  56. package/demos/js/penrose/blackhole.js +121 -0
  57. package/demos/js/penrose/constants.js +73 -0
  58. package/demos/js/penrose/game.js +943 -0
  59. package/demos/js/penrose/lore.js +278 -0
  60. package/demos/js/penrose/penrosescene.js +892 -0
  61. package/demos/js/penrose/ship.js +216 -0
  62. package/demos/js/penrose/sounds.js +211 -0
  63. package/demos/js/penrose/voidparticle.js +55 -0
  64. package/demos/js/penrose/voidscene.js +258 -0
  65. package/demos/js/penrose/voidship.js +144 -0
  66. package/demos/js/penrose/wormhole.js +46 -0
  67. package/demos/js/pipeline.js +555 -0
  68. package/demos/js/scene.js +304 -0
  69. package/demos/js/scenes.js +320 -0
  70. package/demos/js/schrodinger.js +410 -0
  71. package/demos/js/schwarzschild.js +1023 -0
  72. package/demos/js/shapes.js +628 -0
  73. package/demos/js/space/alien.js +171 -0
  74. package/demos/js/space/boom.js +98 -0
  75. package/demos/js/space/boss.js +353 -0
  76. package/demos/js/space/buff.js +73 -0
  77. package/demos/js/space/bullet.js +102 -0
  78. package/demos/js/space/constants.js +85 -0
  79. package/demos/js/space/game.js +1884 -0
  80. package/demos/js/space/hud.js +112 -0
  81. package/demos/js/space/laserbeam.js +179 -0
  82. package/demos/js/space/lightning.js +277 -0
  83. package/demos/js/space/minion.js +192 -0
  84. package/demos/js/space/missile.js +212 -0
  85. package/demos/js/space/player.js +430 -0
  86. package/demos/js/space/powerup.js +90 -0
  87. package/demos/js/space/starfield.js +58 -0
  88. package/demos/js/space/starpower.js +90 -0
  89. package/demos/js/spacetime.js +559 -0
  90. package/demos/js/svgtween.js +204 -0
  91. package/demos/js/tde/accretiondisk.js +418 -0
  92. package/demos/js/tde/blackhole.js +219 -0
  93. package/demos/js/tde/blackholescene.js +209 -0
  94. package/demos/js/tde/config.js +59 -0
  95. package/demos/js/tde/index.js +695 -0
  96. package/demos/js/tde/jets.js +290 -0
  97. package/demos/js/tde/lensedstarfield.js +147 -0
  98. package/demos/js/tde/tdestar.js +317 -0
  99. package/demos/js/tde/tidalstream.js +356 -0
  100. package/demos/js/tde_old/blackhole.obj.js +354 -0
  101. package/demos/js/tde_old/debris.obj.js +791 -0
  102. package/demos/js/tde_old/flare.obj.js +239 -0
  103. package/demos/js/tde_old/index.js +448 -0
  104. package/demos/js/tde_old/star.obj.js +812 -0
  105. package/demos/js/tiles.js +312 -0
  106. package/demos/js/tweendemo.js +79 -0
  107. package/demos/js/visibility.js +102 -0
  108. package/demos/kerr.html +28 -0
  109. package/demos/lavalamp.html +27 -0
  110. package/demos/layouts.html +37 -0
  111. package/demos/logo.svg +4 -0
  112. package/demos/loop.html +84 -0
  113. package/demos/mondrian.html +32 -0
  114. package/demos/og_image.png +0 -0
  115. package/demos/opacity.html +36 -0
  116. package/demos/painter.html +39 -0
  117. package/demos/particles-showcase.html +28 -0
  118. package/demos/particles.html +24 -0
  119. package/demos/patterns.html +33 -0
  120. package/demos/penrose-game.html +31 -0
  121. package/demos/pipeline.html +737 -0
  122. package/demos/scene.html +33 -0
  123. package/demos/scenes.html +96 -0
  124. package/demos/schrodinger.html +27 -0
  125. package/demos/schwarzschild.html +27 -0
  126. package/demos/shapes.html +16 -0
  127. package/demos/space.html +85 -0
  128. package/demos/spacetime.html +27 -0
  129. package/demos/svgtween.html +29 -0
  130. package/demos/tde.html +28 -0
  131. package/demos/tiles.html +28 -0
  132. package/demos/transforms.html +400 -0
  133. package/demos/tween.html +45 -0
  134. package/demos/visibility.html +33 -0
  135. package/disk_example.png +0 -0
  136. package/docs/README.md +222 -0
  137. package/docs/concepts/architecture-overview.md +204 -0
  138. package/docs/concepts/lifecycle.md +255 -0
  139. package/docs/concepts/rendering-pipeline.md +279 -0
  140. package/docs/concepts/tde-zorder.md +106 -0
  141. package/docs/concepts/two-layer-architecture.md +229 -0
  142. package/docs/getting-started/first-game.md +354 -0
  143. package/docs/getting-started/hello-world.md +269 -0
  144. package/docs/getting-started/installation.md +157 -0
  145. package/docs/modules/collision/README.md +453 -0
  146. package/docs/modules/fluent/README.md +1075 -0
  147. package/docs/modules/game/README.md +303 -0
  148. package/docs/modules/isometric-camera.md +210 -0
  149. package/docs/modules/isometric.md +275 -0
  150. package/docs/modules/painter/README.md +328 -0
  151. package/docs/modules/particle/README.md +559 -0
  152. package/docs/modules/shapes/README.md +221 -0
  153. package/docs/modules/shapes/base/euclidian.md +123 -0
  154. package/docs/modules/shapes/base/geometry2d.md +204 -0
  155. package/docs/modules/shapes/base/renderable.md +215 -0
  156. package/docs/modules/shapes/base/shape.md +262 -0
  157. package/docs/modules/shapes/base/transformable.md +243 -0
  158. package/docs/modules/shapes/hierarchy.md +218 -0
  159. package/docs/modules/state/README.md +577 -0
  160. package/docs/modules/util/README.md +99 -0
  161. package/docs/modules/util/camera3d.md +412 -0
  162. package/docs/modules/util/scene3d.md +395 -0
  163. package/index.html +17 -0
  164. package/jsdoc.json +50 -0
  165. package/package.json +55 -0
  166. package/readme.md +599 -0
  167. package/scripts/build-demo.js +69 -0
  168. package/scripts/bundle4llm.js +276 -0
  169. package/scripts/clearconsole.js +48 -0
  170. package/src/collision/collision-system.js +332 -0
  171. package/src/collision/collision.js +303 -0
  172. package/src/collision/index.js +10 -0
  173. package/src/fluent/fluent-game.js +430 -0
  174. package/src/fluent/fluent-go.js +1060 -0
  175. package/src/fluent/fluent-layer.js +152 -0
  176. package/src/fluent/fluent-scene.js +291 -0
  177. package/src/fluent/index.js +98 -0
  178. package/src/fluent/sketch.js +380 -0
  179. package/src/game/game.js +467 -0
  180. package/src/game/index.js +49 -0
  181. package/src/game/objects/go.js +220 -0
  182. package/src/game/objects/imagego.js +30 -0
  183. package/src/game/objects/index.js +54 -0
  184. package/src/game/objects/isometric-scene.js +260 -0
  185. package/src/game/objects/layoutscene.js +549 -0
  186. package/src/game/objects/scene.js +175 -0
  187. package/src/game/objects/scene3d.js +118 -0
  188. package/src/game/objects/text.js +221 -0
  189. package/src/game/objects/wrapper.js +232 -0
  190. package/src/game/pipeline.js +243 -0
  191. package/src/game/ui/button.js +396 -0
  192. package/src/game/ui/cursor.js +93 -0
  193. package/src/game/ui/fps.js +91 -0
  194. package/src/game/ui/index.js +5 -0
  195. package/src/game/ui/togglebutton.js +93 -0
  196. package/src/game/ui/tooltip.js +249 -0
  197. package/src/index.js +25 -0
  198. package/src/io/events.js +20 -0
  199. package/src/io/index.js +86 -0
  200. package/src/io/input.js +70 -0
  201. package/src/io/keys.js +152 -0
  202. package/src/io/mouse.js +61 -0
  203. package/src/io/touch.js +39 -0
  204. package/src/logger/debugtab.js +138 -0
  205. package/src/logger/index.js +3 -0
  206. package/src/logger/loggable.js +47 -0
  207. package/src/logger/logger.js +113 -0
  208. package/src/math/complex.js +37 -0
  209. package/src/math/constants.js +1 -0
  210. package/src/math/fractal.js +1271 -0
  211. package/src/math/gr.js +201 -0
  212. package/src/math/heat.js +202 -0
  213. package/src/math/index.js +12 -0
  214. package/src/math/noise.js +433 -0
  215. package/src/math/orbital.js +191 -0
  216. package/src/math/patterns.js +1339 -0
  217. package/src/math/penrose.js +259 -0
  218. package/src/math/quantum.js +115 -0
  219. package/src/math/random.js +195 -0
  220. package/src/math/tensor.js +1009 -0
  221. package/src/mixins/anchor.js +131 -0
  222. package/src/mixins/draggable.js +72 -0
  223. package/src/mixins/index.js +2 -0
  224. package/src/motion/bezier.js +132 -0
  225. package/src/motion/bounce.js +58 -0
  226. package/src/motion/easing.js +349 -0
  227. package/src/motion/float.js +130 -0
  228. package/src/motion/follow.js +125 -0
  229. package/src/motion/hop.js +52 -0
  230. package/src/motion/index.js +82 -0
  231. package/src/motion/motion.js +1124 -0
  232. package/src/motion/orbit.js +49 -0
  233. package/src/motion/oscillate.js +39 -0
  234. package/src/motion/parabolic.js +141 -0
  235. package/src/motion/patrol.js +147 -0
  236. package/src/motion/pendulum.js +48 -0
  237. package/src/motion/pulse.js +88 -0
  238. package/src/motion/shake.js +83 -0
  239. package/src/motion/spiral.js +144 -0
  240. package/src/motion/spring.js +150 -0
  241. package/src/motion/swing.js +47 -0
  242. package/src/motion/tween.js +92 -0
  243. package/src/motion/tweenetik.js +139 -0
  244. package/src/motion/waypoint.js +210 -0
  245. package/src/painter/index.js +8 -0
  246. package/src/painter/painter.colors.js +331 -0
  247. package/src/painter/painter.effects.js +230 -0
  248. package/src/painter/painter.img.js +229 -0
  249. package/src/painter/painter.js +295 -0
  250. package/src/painter/painter.lines.js +189 -0
  251. package/src/painter/painter.opacity.js +41 -0
  252. package/src/painter/painter.shapes.js +277 -0
  253. package/src/painter/painter.text.js +273 -0
  254. package/src/particle/emitter.js +124 -0
  255. package/src/particle/index.js +11 -0
  256. package/src/particle/particle-system.js +322 -0
  257. package/src/particle/particle.js +71 -0
  258. package/src/particle/updaters.js +170 -0
  259. package/src/shapes/arc.js +43 -0
  260. package/src/shapes/arrow.js +33 -0
  261. package/src/shapes/bezier.js +42 -0
  262. package/src/shapes/circle.js +62 -0
  263. package/src/shapes/clouds.js +56 -0
  264. package/src/shapes/cone.js +219 -0
  265. package/src/shapes/cross.js +70 -0
  266. package/src/shapes/cube.js +244 -0
  267. package/src/shapes/cylinder.js +254 -0
  268. package/src/shapes/diamond.js +48 -0
  269. package/src/shapes/euclidian.js +111 -0
  270. package/src/shapes/figure.js +115 -0
  271. package/src/shapes/geometry.js +220 -0
  272. package/src/shapes/group.js +375 -0
  273. package/src/shapes/heart.js +42 -0
  274. package/src/shapes/hexagon.js +26 -0
  275. package/src/shapes/image.js +192 -0
  276. package/src/shapes/index.js +111 -0
  277. package/src/shapes/line.js +29 -0
  278. package/src/shapes/pattern.js +90 -0
  279. package/src/shapes/pin.js +44 -0
  280. package/src/shapes/poly.js +31 -0
  281. package/src/shapes/prism.js +226 -0
  282. package/src/shapes/rect.js +35 -0
  283. package/src/shapes/renderable.js +333 -0
  284. package/src/shapes/ring.js +26 -0
  285. package/src/shapes/roundrect.js +95 -0
  286. package/src/shapes/shape.js +117 -0
  287. package/src/shapes/slice.js +26 -0
  288. package/src/shapes/sphere.js +314 -0
  289. package/src/shapes/sphere3d.js +537 -0
  290. package/src/shapes/square.js +15 -0
  291. package/src/shapes/star.js +99 -0
  292. package/src/shapes/svg.js +408 -0
  293. package/src/shapes/text.js +553 -0
  294. package/src/shapes/traceable.js +83 -0
  295. package/src/shapes/transform.js +357 -0
  296. package/src/shapes/transformable.js +172 -0
  297. package/src/shapes/triangle.js +26 -0
  298. package/src/sound/index.js +17 -0
  299. package/src/sound/sound.js +473 -0
  300. package/src/sound/synth.analyzer.js +149 -0
  301. package/src/sound/synth.effects.js +207 -0
  302. package/src/sound/synth.envelope.js +59 -0
  303. package/src/sound/synth.js +229 -0
  304. package/src/sound/synth.musical.js +160 -0
  305. package/src/sound/synth.noise.js +85 -0
  306. package/src/sound/synth.oscillators.js +293 -0
  307. package/src/state/index.js +10 -0
  308. package/src/state/state-machine.js +371 -0
  309. package/src/util/camera3d.js +438 -0
  310. package/src/util/index.js +6 -0
  311. package/src/util/isometric-camera.js +235 -0
  312. package/src/util/layout.js +317 -0
  313. package/src/util/position.js +147 -0
  314. package/src/util/tasks.js +47 -0
  315. package/src/util/zindex.js +287 -0
  316. package/src/webgl/index.js +9 -0
  317. package/src/webgl/shaders/sphere-shaders.js +994 -0
  318. package/src/webgl/webgl-renderer.js +388 -0
  319. package/tde.png +0 -0
  320. package/test/math/orbital.test.js +61 -0
  321. package/test/math/tensor.test.js +114 -0
  322. package/test/particle/emitter.test.js +204 -0
  323. package/test/particle/particle-system.test.js +310 -0
  324. package/test/particle/particle.test.js +116 -0
  325. package/test/particle/updaters.test.js +386 -0
  326. package/test/setup.js +120 -0
  327. package/test/shapes/euclidian.test.js +44 -0
  328. package/test/shapes/geometry.test.js +86 -0
  329. package/test/shapes/group.test.js +86 -0
  330. package/test/shapes/rectangle.test.js +64 -0
  331. package/test/shapes/transform.test.js +379 -0
  332. package/test/util/camera3d.test.js +428 -0
  333. package/test/util/scene3d.test.js +352 -0
  334. package/types/collision.d.ts +249 -0
  335. package/types/common.d.ts +155 -0
  336. package/types/game.d.ts +497 -0
  337. package/types/index.d.ts +309 -0
  338. package/types/io.d.ts +188 -0
  339. package/types/logger.d.ts +127 -0
  340. package/types/math.d.ts +268 -0
  341. package/types/mixins.d.ts +92 -0
  342. package/types/motion.d.ts +678 -0
  343. package/types/painter.d.ts +378 -0
  344. package/types/shapes.d.ts +864 -0
  345. package/types/sound.d.ts +672 -0
  346. package/types/state.d.ts +251 -0
  347. package/types/util.d.ts +253 -0
  348. package/vite.config.js +50 -0
  349. package/vitest.config.js +13 -0
@@ -0,0 +1,207 @@
1
+ /**
2
+ * SynthEffects - Audio effects processing
3
+ * @module sound/synth.effects
4
+ */
5
+ export class SynthEffects {
6
+ static #_ctx = null;
7
+ static #_output = null;
8
+
9
+ /**
10
+ * Initialize the effects module
11
+ * @param {AudioContext} ctx - Audio context
12
+ * @param {AudioNode} output - Output node
13
+ */
14
+ static init(ctx, output) {
15
+ this.#_ctx = ctx;
16
+ this.#_output = output;
17
+ }
18
+
19
+ /**
20
+ * Get the audio context
21
+ * @returns {AudioContext}
22
+ */
23
+ static get ctx() {
24
+ return this.#_ctx;
25
+ }
26
+
27
+ /**
28
+ * Create a filter node
29
+ * @param {string} [type='lowpass'] - Filter type
30
+ * @param {number} [frequency=1000] - Cutoff frequency
31
+ * @param {number} [q=1] - Q factor (resonance)
32
+ * @returns {BiquadFilterNode}
33
+ */
34
+ static filter(type = "lowpass", frequency = 1000, q = 1) {
35
+ const filter = this.#_ctx.createBiquadFilter();
36
+ filter.type = type;
37
+ filter.frequency.value = frequency;
38
+ filter.Q.value = q;
39
+ return filter;
40
+ }
41
+
42
+ /**
43
+ * Create a delay effect
44
+ * @param {number} [time=0.3] - Delay time in seconds
45
+ * @param {number} [feedback=0.4] - Feedback amount (0-1)
46
+ * @param {number} [mix=0.5] - Wet/dry mix (0-1)
47
+ * @returns {Object} Delay effect controller
48
+ */
49
+ static delay(time = 0.3, feedback = 0.4, mix = 0.5) {
50
+ const delay = this.#_ctx.createDelay(5);
51
+ const feedbackGain = this.#_ctx.createGain();
52
+ const wetGain = this.#_ctx.createGain();
53
+ const dryGain = this.#_ctx.createGain();
54
+ const input = this.#_ctx.createGain();
55
+ const output = this.#_ctx.createGain();
56
+
57
+ delay.delayTime.value = time;
58
+ feedbackGain.gain.value = feedback;
59
+ wetGain.gain.value = mix;
60
+ dryGain.gain.value = 1 - mix;
61
+
62
+ // Signal flow
63
+ input.connect(delay);
64
+ input.connect(dryGain);
65
+ delay.connect(feedbackGain);
66
+ feedbackGain.connect(delay);
67
+ delay.connect(wetGain);
68
+ wetGain.connect(output);
69
+ dryGain.connect(output);
70
+
71
+ return {
72
+ input,
73
+ output,
74
+ setTime: (t) =>
75
+ delay.delayTime.setValueAtTime(t, this.#_ctx.currentTime),
76
+ setFeedback: (f) =>
77
+ feedbackGain.gain.setValueAtTime(f, this.#_ctx.currentTime),
78
+ setMix: (m) => {
79
+ wetGain.gain.setValueAtTime(m, this.#_ctx.currentTime);
80
+ dryGain.gain.setValueAtTime(1 - m, this.#_ctx.currentTime);
81
+ },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Create a simple reverb using convolution
87
+ * @param {number} [duration=2] - Reverb duration
88
+ * @param {number} [decay=2] - Decay rate
89
+ * @returns {ConvolverNode}
90
+ */
91
+ static reverb(duration = 2, decay = 2) {
92
+ const convolver = this.#_ctx.createConvolver();
93
+ const sampleRate = this.#_ctx.sampleRate;
94
+ const length = sampleRate * duration;
95
+ const impulse = this.#_ctx.createBuffer(2, length, sampleRate);
96
+
97
+ for (let channel = 0; channel < 2; channel++) {
98
+ const channelData = impulse.getChannelData(channel);
99
+ for (let i = 0; i < length; i++) {
100
+ channelData[i] =
101
+ (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay);
102
+ }
103
+ }
104
+
105
+ convolver.buffer = impulse;
106
+ return convolver;
107
+ }
108
+
109
+ /**
110
+ * Create a distortion effect
111
+ * @param {number} [amount=50] - Distortion amount (0-100)
112
+ * @returns {WaveShaperNode}
113
+ */
114
+ static distortion(amount = 50) {
115
+ const shaper = this.#_ctx.createWaveShaper();
116
+ const k = amount;
117
+ const samples = 44100;
118
+ const curve = new Float32Array(samples);
119
+
120
+ for (let i = 0; i < samples; i++) {
121
+ const x = (i * 2) / samples - 1;
122
+ curve[i] =
123
+ ((3 + k) * x * 20 * (Math.PI / 180)) / (Math.PI + k * Math.abs(x));
124
+ }
125
+
126
+ shaper.curve = curve;
127
+ shaper.oversample = "4x";
128
+ return shaper;
129
+ }
130
+
131
+ /**
132
+ * Create a tremolo effect
133
+ * @param {number} [rate=5] - Tremolo rate in Hz
134
+ * @param {number} [depth=0.5] - Tremolo depth (0-1)
135
+ * @returns {Object} Tremolo effect controller
136
+ */
137
+ static tremolo(rate = 5, depth = 0.5) {
138
+ const lfo = this.#_ctx.createOscillator();
139
+ const lfoGain = this.#_ctx.createGain();
140
+ const outputGain = this.#_ctx.createGain();
141
+
142
+ lfo.frequency.value = rate;
143
+ lfoGain.gain.value = depth * 0.5;
144
+ outputGain.gain.value = 1 - depth * 0.5;
145
+
146
+ lfo.connect(lfoGain);
147
+ lfoGain.connect(outputGain.gain);
148
+ lfo.start();
149
+
150
+ return {
151
+ input: outputGain,
152
+ output: outputGain,
153
+ lfo,
154
+ setRate: (r) =>
155
+ lfo.frequency.setValueAtTime(r, this.#_ctx.currentTime),
156
+ setDepth: (d) =>
157
+ lfoGain.gain.setValueAtTime(d * 0.5, this.#_ctx.currentTime),
158
+ stop: () => lfo.stop(),
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Create a compressor
164
+ * @param {Object} options - Compressor options
165
+ * @returns {DynamicsCompressorNode}
166
+ */
167
+ static compressor(options = {}) {
168
+ const {
169
+ threshold = -24,
170
+ knee = 30,
171
+ ratio = 12,
172
+ attack = 0.003,
173
+ release = 0.25,
174
+ } = options;
175
+
176
+ const compressor = this.#_ctx.createDynamicsCompressor();
177
+ compressor.threshold.value = threshold;
178
+ compressor.knee.value = knee;
179
+ compressor.ratio.value = ratio;
180
+ compressor.attack.value = attack;
181
+ compressor.release.value = release;
182
+
183
+ return compressor;
184
+ }
185
+
186
+ /**
187
+ * Create a stereo panner
188
+ * @param {number} [pan=0] - Pan value (-1 left, 0 center, 1 right)
189
+ * @returns {StereoPannerNode}
190
+ */
191
+ static panner(pan = 0) {
192
+ const panner = this.#_ctx.createStereoPanner();
193
+ panner.pan.value = pan;
194
+ return panner;
195
+ }
196
+
197
+ /**
198
+ * Create a gain node
199
+ * @param {number} [volume=1] - Volume level
200
+ * @returns {GainNode}
201
+ */
202
+ static gain(volume = 1) {
203
+ const gain = this.#_ctx.createGain();
204
+ gain.gain.value = volume;
205
+ return gain;
206
+ }
207
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * SynthEnvelope - ADSR envelope utilities
3
+ * @module sound/synth.envelope
4
+ */
5
+ export class SynthEnvelope {
6
+ /**
7
+ * Apply ADSR envelope to an AudioParam
8
+ * @param {AudioParam} param - The parameter to modulate
9
+ * @param {Object} options - Envelope options
10
+ * @param {number} [options.attack=0.01] - Attack time in seconds
11
+ * @param {number} [options.decay=0.1] - Decay time in seconds
12
+ * @param {number} [options.sustain=0.7] - Sustain level (0-1)
13
+ * @param {number} [options.release=0.2] - Release time in seconds
14
+ * @param {number} [options.startTime=0] - Start time
15
+ * @param {number} [options.duration=1] - Total duration
16
+ * @param {number} [options.peakVolume=1] - Peak volume level
17
+ */
18
+ static applyADSR(param, options = {}) {
19
+ const {
20
+ attack = 0.01,
21
+ decay = 0.1,
22
+ sustain = 0.7,
23
+ release = 0.2,
24
+ startTime = 0,
25
+ duration = 1,
26
+ peakVolume = 1,
27
+ } = options;
28
+
29
+ const sustainLevel = peakVolume * sustain;
30
+ const sustainDuration = Math.max(0, duration - attack - decay);
31
+
32
+ param.setValueAtTime(0, startTime);
33
+ param.linearRampToValueAtTime(peakVolume, startTime + attack);
34
+ param.linearRampToValueAtTime(sustainLevel, startTime + attack + decay);
35
+ param.setValueAtTime(
36
+ sustainLevel,
37
+ startTime + attack + decay + sustainDuration
38
+ );
39
+ param.linearRampToValueAtTime(0, startTime + duration + release);
40
+ }
41
+
42
+ /**
43
+ * Create envelope presets for common sounds
44
+ * @returns {Object} Preset envelope configurations
45
+ */
46
+ static get presets() {
47
+ return {
48
+ pluck: { attack: 0.001, decay: 0.2, sustain: 0.0, release: 0.1 },
49
+ pad: { attack: 0.5, decay: 0.3, sustain: 0.8, release: 1.0 },
50
+ organ: { attack: 0.01, decay: 0.0, sustain: 1.0, release: 0.05 },
51
+ perc: { attack: 0.001, decay: 0.1, sustain: 0.0, release: 0.05 },
52
+ string: { attack: 0.1, decay: 0.2, sustain: 0.7, release: 0.3 },
53
+ brass: { attack: 0.05, decay: 0.1, sustain: 0.8, release: 0.2 },
54
+ blip: { attack: 0.001, decay: 0.05, sustain: 0.0, release: 0.02 },
55
+ laser: { attack: 0.001, decay: 0.15, sustain: 0.0, release: 0.05 },
56
+ explosion: { attack: 0.001, decay: 0.3, sustain: 0.2, release: 0.5 },
57
+ };
58
+ }
59
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Synth - Static utility class for procedural audio generation
3
+ * Provides a clean API for Web Audio operations with GCanvas integration
4
+ *
5
+ * Similar to Painter for canvas, Synth abstracts Web Audio complexity
6
+ * @module sound/synth
7
+ */
8
+ import { SynthOscillators } from "./synth.oscillators.js";
9
+ import { SynthEffects } from "./synth.effects.js";
10
+ import { SynthEnvelope } from "./synth.envelope.js";
11
+ import { SynthNoise } from "./synth.noise.js";
12
+ import { SynthMusical } from "./synth.musical.js";
13
+ import { SynthAnalyzer } from "./synth.analyzer.js";
14
+
15
+ export class Synth {
16
+ static #_ctx = null;
17
+ static #_masterGain = null;
18
+ static #_initialized = false;
19
+
20
+ /**
21
+ * Initialize the audio system
22
+ * @param {Object} options - Configuration options
23
+ * @param {number} [options.masterVolume=0.5] - Master volume (0-1)
24
+ * @param {number} [options.sampleRate=44100] - Sample rate
25
+ * @param {boolean} [options.enableAnalyzer=false] - Enable audio analyzer
26
+ */
27
+ static init(options = {}) {
28
+ if (this.#_initialized) {
29
+ console.warn("[Synth] Already initialized");
30
+ return;
31
+ }
32
+
33
+ const { masterVolume = 0.5, sampleRate = 44100, enableAnalyzer = false } = options;
34
+
35
+ try {
36
+ this.#_ctx = new (window.AudioContext || window.webkitAudioContext)({
37
+ sampleRate,
38
+ });
39
+
40
+ this.#_masterGain = this.#_ctx.createGain();
41
+ this.#_masterGain.gain.value = masterVolume;
42
+ this.#_masterGain.connect(this.#_ctx.destination);
43
+
44
+ // Initialize sub-modules
45
+ SynthOscillators.init(this.#_ctx, this.#_masterGain);
46
+ SynthEffects.init(this.#_ctx, this.#_masterGain);
47
+
48
+ if (enableAnalyzer) {
49
+ SynthAnalyzer.init(this.#_ctx, this.#_masterGain);
50
+ }
51
+
52
+ this.#_initialized = true;
53
+ console.log("[Synth] Audio system initialized");
54
+ } catch (e) {
55
+ console.error("[Synth] Failed to initialize audio:", e);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Check if audio is initialized
61
+ * @returns {boolean}
62
+ */
63
+ static get isInitialized() {
64
+ return this.#_initialized;
65
+ }
66
+
67
+ /**
68
+ * Get the AudioContext
69
+ * @returns {AudioContext|null}
70
+ */
71
+ static get ctx() {
72
+ return this.#_ctx;
73
+ }
74
+
75
+ /**
76
+ * Get the master gain node
77
+ * @returns {GainNode|null}
78
+ */
79
+ static get master() {
80
+ return this.#_masterGain;
81
+ }
82
+
83
+ /**
84
+ * Get oscillator utilities
85
+ * @returns {typeof SynthOscillators}
86
+ */
87
+ static get osc() {
88
+ return SynthOscillators;
89
+ }
90
+
91
+ /**
92
+ * Get effects utilities
93
+ * @returns {typeof SynthEffects}
94
+ */
95
+ static get fx() {
96
+ return SynthEffects;
97
+ }
98
+
99
+ /**
100
+ * Get envelope utilities
101
+ * @returns {typeof SynthEnvelope}
102
+ */
103
+ static get env() {
104
+ return SynthEnvelope;
105
+ }
106
+
107
+ /**
108
+ * Get noise utilities
109
+ * @returns {typeof SynthNoise}
110
+ */
111
+ static get noise() {
112
+ return SynthNoise;
113
+ }
114
+
115
+ /**
116
+ * Get musical utilities
117
+ * @returns {typeof SynthMusical}
118
+ */
119
+ static get music() {
120
+ return SynthMusical;
121
+ }
122
+
123
+ /**
124
+ * Get analyzer utilities
125
+ * @returns {typeof SynthAnalyzer}
126
+ */
127
+ static get analyzer() {
128
+ return SynthAnalyzer;
129
+ }
130
+
131
+ /**
132
+ * Resume audio context (required after user interaction)
133
+ * @returns {Promise<void>}
134
+ */
135
+ static async resume() {
136
+ if (this.#_ctx && this.#_ctx.state === "suspended") {
137
+ await this.#_ctx.resume();
138
+ console.log("[Synth] Audio context resumed");
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Suspend audio context
144
+ * @returns {Promise<void>}
145
+ */
146
+ static async suspend() {
147
+ if (this.#_ctx && this.#_ctx.state === "running") {
148
+ await this.#_ctx.suspend();
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Get current audio time (for scheduling)
154
+ * @returns {number}
155
+ */
156
+ static get now() {
157
+ return this.#_ctx ? this.#_ctx.currentTime : 0;
158
+ }
159
+
160
+ /**
161
+ * Get audio context state
162
+ * @returns {string} 'suspended' | 'running' | 'closed'
163
+ */
164
+ static get state() {
165
+ return this.#_ctx ? this.#_ctx.state : "closed";
166
+ }
167
+
168
+ /**
169
+ * Set master volume
170
+ * @param {number} value - Volume (0-1)
171
+ */
172
+ static set volume(value) {
173
+ if (this.#_masterGain) {
174
+ this.#_masterGain.gain.setValueAtTime(
175
+ Math.max(0, Math.min(1, value)),
176
+ this.#_ctx.currentTime
177
+ );
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Get master volume
183
+ * @returns {number}
184
+ */
185
+ static get volume() {
186
+ return this.#_masterGain ? this.#_masterGain.gain.value : 0;
187
+ }
188
+
189
+ /**
190
+ * Create a custom audio node chain
191
+ * @param {...AudioNode} nodes - Nodes to connect in sequence
192
+ * @returns {Object} First and last node references
193
+ */
194
+ static chain(...nodes) {
195
+ for (let i = 0; i < nodes.length - 1; i++) {
196
+ nodes[i].connect(nodes[i + 1]);
197
+ }
198
+ return {
199
+ first: nodes[0],
200
+ last: nodes[nodes.length - 1],
201
+ connectTo: (target) => nodes[nodes.length - 1].connect(target),
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Schedule a function to run at a specific audio time
207
+ * @param {Function} fn - Function to execute
208
+ * @param {number} time - Audio time to execute at
209
+ * @returns {number} setTimeout ID for cancellation
210
+ */
211
+ static schedule(fn, time) {
212
+ const delay = Math.max(0, (time - this.now) * 1000);
213
+ return setTimeout(fn, delay);
214
+ }
215
+
216
+ /**
217
+ * Close the audio context and cleanup
218
+ */
219
+ static async close() {
220
+ if (this.#_ctx) {
221
+ SynthAnalyzer.dispose();
222
+ await this.#_ctx.close();
223
+ this.#_ctx = null;
224
+ this.#_masterGain = null;
225
+ this.#_initialized = false;
226
+ console.log("[Synth] Audio system closed");
227
+ }
228
+ }
229
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * SynthMusical - Musical scales, notes, and chord generation
3
+ * @module sound/synth.musical
4
+ */
5
+ export class SynthMusical {
6
+ static NOTE_FREQUENCIES = {
7
+ C: 16.35,
8
+ "C#": 17.32,
9
+ Db: 17.32,
10
+ D: 18.35,
11
+ "D#": 19.45,
12
+ Eb: 19.45,
13
+ E: 20.6,
14
+ F: 21.83,
15
+ "F#": 23.12,
16
+ Gb: 23.12,
17
+ G: 24.5,
18
+ "G#": 25.96,
19
+ Ab: 25.96,
20
+ A: 27.5,
21
+ "A#": 29.14,
22
+ Bb: 29.14,
23
+ B: 30.87,
24
+ };
25
+
26
+ static SCALES = {
27
+ major: [0, 2, 4, 5, 7, 9, 11],
28
+ minor: [0, 2, 3, 5, 7, 8, 10],
29
+ pentatonic: [0, 2, 4, 7, 9],
30
+ pentatonicMinor: [0, 3, 5, 7, 10],
31
+ blues: [0, 3, 5, 6, 7, 10],
32
+ dorian: [0, 2, 3, 5, 7, 9, 10],
33
+ mixolydian: [0, 2, 4, 5, 7, 9, 10],
34
+ chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
35
+ wholeTone: [0, 2, 4, 6, 8, 10],
36
+ diminished: [0, 2, 3, 5, 6, 8, 9, 11],
37
+ };
38
+
39
+ static CHORDS = {
40
+ major: [0, 4, 7],
41
+ minor: [0, 3, 7],
42
+ diminished: [0, 3, 6],
43
+ augmented: [0, 4, 8],
44
+ sus2: [0, 2, 7],
45
+ sus4: [0, 5, 7],
46
+ major7: [0, 4, 7, 11],
47
+ minor7: [0, 3, 7, 10],
48
+ dom7: [0, 4, 7, 10],
49
+ dim7: [0, 3, 6, 9],
50
+ add9: [0, 4, 7, 14],
51
+ power: [0, 7],
52
+ };
53
+
54
+ /**
55
+ * Convert note name to frequency
56
+ * @param {string} note - Note name (e.g., 'A4', 'C#3')
57
+ * @returns {number} Frequency in Hz
58
+ */
59
+ static noteToFreq(note) {
60
+ const match = note.match(/^([A-G][#b]?)(\d+)$/);
61
+ if (!match) throw new Error(`Invalid note: ${note}`);
62
+
63
+ const [, noteName, octave] = match;
64
+ const baseFreq = this.NOTE_FREQUENCIES[noteName];
65
+ if (baseFreq === undefined) throw new Error(`Unknown note: ${noteName}`);
66
+ return baseFreq * Math.pow(2, parseInt(octave));
67
+ }
68
+
69
+ /**
70
+ * Get frequencies for a scale
71
+ * @param {string} root - Root note (e.g., 'C4')
72
+ * @param {string} [scaleName='major'] - Scale name
73
+ * @param {number} [octaves=1] - Number of octaves
74
+ * @returns {number[]} Array of frequencies
75
+ */
76
+ static scale(root, scaleName = "major", octaves = 1) {
77
+ const rootFreq = this.noteToFreq(root);
78
+ const intervals = this.SCALES[scaleName];
79
+ if (!intervals) throw new Error(`Unknown scale: ${scaleName}`);
80
+
81
+ const frequencies = [];
82
+
83
+ for (let oct = 0; oct < octaves; oct++) {
84
+ for (const interval of intervals) {
85
+ frequencies.push(rootFreq * Math.pow(2, (interval + oct * 12) / 12));
86
+ }
87
+ }
88
+
89
+ return frequencies;
90
+ }
91
+
92
+ /**
93
+ * Get frequencies for a chord
94
+ * @param {string} root - Root note
95
+ * @param {string} [chordType='major'] - Chord type
96
+ * @returns {number[]} Array of frequencies
97
+ */
98
+ static chord(root, chordType = "major") {
99
+ const rootFreq = this.noteToFreq(root);
100
+ const intervals = this.CHORDS[chordType];
101
+ if (!intervals) throw new Error(`Unknown chord type: ${chordType}`);
102
+ return intervals.map((i) => rootFreq * Math.pow(2, i / 12));
103
+ }
104
+
105
+ /**
106
+ * Map a value (0-1) to a frequency in a scale
107
+ * @param {number} value - Value between 0 and 1
108
+ * @param {string} [root='C4'] - Root note
109
+ * @param {string} [scaleName='pentatonic'] - Scale name
110
+ * @param {number} [octaves=2] - Number of octaves
111
+ * @returns {number} Frequency in Hz
112
+ */
113
+ static mapToScale(value, root = "C4", scaleName = "pentatonic", octaves = 2) {
114
+ const frequencies = this.scale(root, scaleName, octaves);
115
+ const clampedValue = Math.max(0, Math.min(1, value));
116
+ const index =
117
+ Math.floor(clampedValue * frequencies.length) % frequencies.length;
118
+ return frequencies[index];
119
+ }
120
+
121
+ /**
122
+ * Convert MIDI note number to frequency
123
+ * @param {number} midi - MIDI note number (0-127)
124
+ * @returns {number} Frequency in Hz
125
+ */
126
+ static midiToFreq(midi) {
127
+ return 440 * Math.pow(2, (midi - 69) / 12);
128
+ }
129
+
130
+ /**
131
+ * Convert frequency to MIDI note number
132
+ * @param {number} freq - Frequency in Hz
133
+ * @returns {number} MIDI note number
134
+ */
135
+ static freqToMidi(freq) {
136
+ return Math.round(12 * Math.log2(freq / 440) + 69);
137
+ }
138
+
139
+ /**
140
+ * Get a random note from a scale
141
+ * @param {string} [root='C4'] - Root note
142
+ * @param {string} [scaleName='pentatonic'] - Scale name
143
+ * @param {number} [octaves=2] - Number of octaves
144
+ * @returns {number} Frequency in Hz
145
+ */
146
+ static randomNote(root = "C4", scaleName = "pentatonic", octaves = 2) {
147
+ const frequencies = this.scale(root, scaleName, octaves);
148
+ return frequencies[Math.floor(Math.random() * frequencies.length)];
149
+ }
150
+
151
+ /**
152
+ * Get frequency with cents offset
153
+ * @param {number} freq - Base frequency
154
+ * @param {number} cents - Cents offset (-100 to 100 typical)
155
+ * @returns {number} Adjusted frequency
156
+ */
157
+ static detune(freq, cents) {
158
+ return freq * Math.pow(2, cents / 1200);
159
+ }
160
+ }