@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,473 @@
1
+ /**
2
+ * Sound - Stateless sound generation primitives
3
+ * Similar to Motion class for animation, provides quick sound effects
4
+ * @module sound/sound
5
+ */
6
+ import { Synth } from "./synth.js";
7
+
8
+ export class Sound {
9
+ /**
10
+ * Generate a beep/blip sound effect
11
+ * @param {number} [frequency=440] - Frequency in Hz
12
+ * @param {number} [duration=0.1] - Duration in seconds
13
+ * @param {Object} [options] - Sound options
14
+ */
15
+ static beep(frequency = 440, duration = 0.1, options = {}) {
16
+ if (!Synth.isInitialized) return;
17
+
18
+ const { volume = 0.3, type = "sine" } = options;
19
+ Synth.osc.tone(frequency, duration, {
20
+ type,
21
+ volume,
22
+ attack: 0.001,
23
+ decay: duration * 0.8,
24
+ sustain: 0,
25
+ release: duration * 0.2,
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Generate a click/tick sound
31
+ * @param {number} [volume=0.3] - Volume (0-1)
32
+ */
33
+ static click(volume = 0.3) {
34
+ if (!Synth.isInitialized) return;
35
+
36
+ Synth.osc.tone(1000, 0.01, {
37
+ type: "square",
38
+ volume,
39
+ attack: 0.001,
40
+ decay: 0.009,
41
+ sustain: 0,
42
+ release: 0.001,
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Generate a sweep sound (rising or falling)
48
+ * @param {number} startFreq - Starting frequency
49
+ * @param {number} endFreq - Ending frequency
50
+ * @param {number} duration - Duration in seconds
51
+ * @param {Object} [options] - Options
52
+ */
53
+ static sweep(startFreq, endFreq, duration, options = {}) {
54
+ if (!Synth.isInitialized) return;
55
+
56
+ const { volume = 0.3, type = "sine" } = options;
57
+ Synth.osc.sweep(startFreq, endFreq, duration, { type, volume });
58
+ }
59
+
60
+ /**
61
+ * Play a note from scale based on value
62
+ * Perfect for mapping visual data to sound
63
+ * @param {number} value - Value (0-1)
64
+ * @param {Object} [options] - Options
65
+ */
66
+ static fromValue(value, options = {}) {
67
+ if (!Synth.isInitialized) return;
68
+
69
+ const {
70
+ root = "C4",
71
+ scale = "pentatonic",
72
+ octaves = 2,
73
+ duration = 0.2,
74
+ volume = 0.3,
75
+ type = "sine",
76
+ } = options;
77
+
78
+ const freq = Synth.music.mapToScale(value, root, scale, octaves);
79
+ Synth.osc.tone(freq, duration, { volume, type });
80
+ }
81
+
82
+ /**
83
+ * Collision/impact sound
84
+ * @param {number} [intensity=0.5] - Impact intensity (0-1)
85
+ */
86
+ static impact(intensity = 0.5) {
87
+ if (!Synth.isInitialized) return;
88
+
89
+ const freq = 80 + intensity * 200;
90
+ const duration = 0.05 + intensity * 0.15;
91
+
92
+ // Low thud
93
+ Synth.osc.tone(freq, duration, {
94
+ type: "sine",
95
+ volume: 0.4 * intensity,
96
+ attack: 0.001,
97
+ decay: duration,
98
+ sustain: 0,
99
+ release: 0.02,
100
+ });
101
+
102
+ // Add noise burst
103
+ const noise = Synth.noise.white(Synth.ctx, 0.08);
104
+ const noiseGain = Synth.ctx.createGain();
105
+ noiseGain.gain.setValueAtTime(0.25 * intensity, Synth.now);
106
+ noiseGain.gain.exponentialRampToValueAtTime(0.001, Synth.now + 0.08);
107
+
108
+ noise.connect(noiseGain);
109
+ noiseGain.connect(Synth.master);
110
+ noise.start();
111
+ noise.stop(Synth.now + 0.1);
112
+ }
113
+
114
+ /**
115
+ * Explosion sound effect
116
+ * @param {number} [intensity=0.7] - Explosion intensity (0-1)
117
+ */
118
+ static explosion(intensity = 0.7) {
119
+ if (!Synth.isInitialized) return;
120
+
121
+ const duration = 0.3 + intensity * 0.4;
122
+
123
+ // Low rumble
124
+ Synth.osc.tone(50 + intensity * 30, duration, {
125
+ type: "sine",
126
+ volume: 0.4 * intensity,
127
+ attack: 0.001,
128
+ decay: duration * 0.3,
129
+ sustain: 0.3,
130
+ release: duration * 0.7,
131
+ });
132
+
133
+ // Noise burst
134
+ const noise = Synth.noise.brown(Synth.ctx, duration);
135
+ const noiseGain = Synth.ctx.createGain();
136
+ const filter = Synth.fx.filter("lowpass", 800 + intensity * 400, 1);
137
+
138
+ noiseGain.gain.setValueAtTime(0.5 * intensity, Synth.now);
139
+ noiseGain.gain.exponentialRampToValueAtTime(0.001, Synth.now + duration);
140
+
141
+ noise.connect(filter);
142
+ filter.connect(noiseGain);
143
+ noiseGain.connect(Synth.master);
144
+ noise.start();
145
+ noise.stop(Synth.now + duration + 0.1);
146
+ }
147
+
148
+ /**
149
+ * Laser/shoot sound effect
150
+ * @param {Object} [options] - Options
151
+ */
152
+ static laser(options = {}) {
153
+ if (!Synth.isInitialized) return;
154
+
155
+ const {
156
+ startFreq = 1200,
157
+ endFreq = 200,
158
+ duration = 0.15,
159
+ volume = 0.25,
160
+ type = "sawtooth",
161
+ } = options;
162
+
163
+ Synth.osc.sweep(startFreq, endFreq, duration, { type, volume });
164
+ }
165
+
166
+ /**
167
+ * Power-up sound effect
168
+ * @param {Object} [options] - Options
169
+ */
170
+ static powerUp(options = {}) {
171
+ if (!Synth.isInitialized) return;
172
+
173
+ const {
174
+ startFreq = 300,
175
+ endFreq = 1200,
176
+ duration = 0.3,
177
+ volume = 0.3,
178
+ } = options;
179
+
180
+ // Rising sweep with harmonics
181
+ Synth.osc.sweep(startFreq, endFreq, duration, { type: "square", volume });
182
+ Synth.osc.sweep(startFreq * 1.5, endFreq * 1.5, duration, {
183
+ type: "sine",
184
+ volume: volume * 0.5,
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Hurt/damage sound effect
190
+ * @param {number} [intensity=0.5] - Damage intensity (0-1)
191
+ */
192
+ static hurt(intensity = 0.5) {
193
+ if (!Synth.isInitialized) return;
194
+
195
+ // Low thump
196
+ Synth.osc.tone(80 + intensity * 40, 0.1, {
197
+ type: "sine",
198
+ volume: 0.4 * intensity,
199
+ attack: 0.001,
200
+ decay: 0.08,
201
+ sustain: 0,
202
+ release: 0.02,
203
+ });
204
+
205
+ // Distorted mid
206
+ Synth.osc.tone(200 + intensity * 100, 0.08, {
207
+ type: "sawtooth",
208
+ volume: 0.2 * intensity,
209
+ attack: 0.001,
210
+ decay: 0.06,
211
+ sustain: 0,
212
+ release: 0.02,
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Coin/pickup sound effect
218
+ * @param {Object} [options] - Options
219
+ */
220
+ static coin(options = {}) {
221
+ if (!Synth.isInitialized) return;
222
+
223
+ const { baseFreq = 987.77, volume = 0.25 } = options; // B5
224
+
225
+ // Two quick notes
226
+ Synth.osc.tone(baseFreq, 0.08, {
227
+ type: "square",
228
+ volume,
229
+ attack: 0.001,
230
+ decay: 0.05,
231
+ sustain: 0.3,
232
+ release: 0.02,
233
+ });
234
+
235
+ // Second note slightly delayed and higher
236
+ setTimeout(() => {
237
+ if (Synth.isInitialized) {
238
+ Synth.osc.tone(baseFreq * 1.5, 0.12, {
239
+ type: "square",
240
+ volume,
241
+ attack: 0.001,
242
+ decay: 0.08,
243
+ sustain: 0.2,
244
+ release: 0.04,
245
+ });
246
+ }
247
+ }, 80);
248
+ }
249
+
250
+ /**
251
+ * Jump sound effect
252
+ * @param {Object} [options] - Options
253
+ */
254
+ static jump(options = {}) {
255
+ if (!Synth.isInitialized) return;
256
+
257
+ const { startFreq = 150, endFreq = 400, duration = 0.15, volume = 0.25 } = options;
258
+
259
+ Synth.osc.sweep(startFreq, endFreq, duration, {
260
+ type: "square",
261
+ volume,
262
+ });
263
+ }
264
+
265
+ /**
266
+ * Menu select/confirm sound
267
+ * @param {Object} [options] - Options
268
+ */
269
+ static select(options = {}) {
270
+ if (!Synth.isInitialized) return;
271
+
272
+ const { frequency = 660, volume = 0.2 } = options;
273
+
274
+ Synth.osc.tone(frequency, 0.08, {
275
+ type: "sine",
276
+ volume,
277
+ attack: 0.001,
278
+ decay: 0.05,
279
+ sustain: 0.3,
280
+ release: 0.03,
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Error/denied sound
286
+ * @param {Object} [options] - Options
287
+ */
288
+ static error(options = {}) {
289
+ if (!Synth.isInitialized) return;
290
+
291
+ const { volume = 0.25 } = options;
292
+
293
+ // Two descending notes
294
+ Synth.osc.tone(400, 0.1, {
295
+ type: "square",
296
+ volume,
297
+ attack: 0.001,
298
+ decay: 0.08,
299
+ sustain: 0,
300
+ release: 0.02,
301
+ });
302
+
303
+ setTimeout(() => {
304
+ if (Synth.isInitialized) {
305
+ Synth.osc.tone(300, 0.15, {
306
+ type: "square",
307
+ volume,
308
+ attack: 0.001,
309
+ decay: 0.12,
310
+ sustain: 0,
311
+ release: 0.03,
312
+ });
313
+ }
314
+ }, 100);
315
+ }
316
+
317
+ /**
318
+ * Ambient drone generator
319
+ * @param {string} [root='C2'] - Root note
320
+ * @param {Object} [options] - Options
321
+ * @returns {Object|null} Controller to stop/modify the drone
322
+ */
323
+ static drone(root = "C2", options = {}) {
324
+ if (!Synth.isInitialized) return null;
325
+
326
+ const { volume = 0.2, richness = 0.5 } = options;
327
+ const freq = Synth.music.noteToFreq(root);
328
+
329
+ // Create layered oscillators
330
+ const oscs = [];
331
+ const gains = [];
332
+ const frequencies = [freq, freq * 1.5, freq * 2, freq * 3];
333
+ const volumes = [1, 0.5 * richness, 0.3 * richness, 0.2 * richness];
334
+
335
+ frequencies.forEach((f, i) => {
336
+ const osc = Synth.ctx.createOscillator();
337
+ const gain = Synth.ctx.createGain();
338
+
339
+ osc.type = "sine";
340
+ osc.frequency.value = f;
341
+ gain.gain.value = volumes[i] * volume;
342
+
343
+ osc.connect(gain);
344
+ gain.connect(Synth.master);
345
+ osc.start();
346
+
347
+ oscs.push(osc);
348
+ gains.push(gain);
349
+ });
350
+
351
+ return {
352
+ stop: (fadeTime = 0.5) => {
353
+ const now = Synth.now;
354
+ gains.forEach((g) => {
355
+ g.gain.linearRampToValueAtTime(0, now + fadeTime);
356
+ });
357
+ setTimeout(
358
+ () => {
359
+ oscs.forEach((o) => {
360
+ try {
361
+ o.stop();
362
+ } catch (e) {
363
+ /* already stopped */
364
+ }
365
+ });
366
+ },
367
+ fadeTime * 1000 + 100
368
+ );
369
+ },
370
+ setVolume: (v) => {
371
+ gains.forEach((g, i) => {
372
+ g.gain.linearRampToValueAtTime(volumes[i] * v, Synth.now + 0.1);
373
+ });
374
+ },
375
+ };
376
+ }
377
+
378
+ /**
379
+ * Play a musical note
380
+ * @param {string} note - Note name (e.g., 'C4', 'A#3')
381
+ * @param {number} [duration=0.5] - Duration in seconds
382
+ * @param {Object} [options] - Options
383
+ */
384
+ static note(note, duration = 0.5, options = {}) {
385
+ if (!Synth.isInitialized) return;
386
+
387
+ const { volume = 0.3, type = "sine", envelope = {} } = options;
388
+ const freq = Synth.music.noteToFreq(note);
389
+
390
+ Synth.osc.tone(freq, duration, {
391
+ type,
392
+ volume,
393
+ ...Synth.env.presets.pluck,
394
+ ...envelope,
395
+ });
396
+ }
397
+
398
+ /**
399
+ * Play a chord
400
+ * @param {string} root - Root note (e.g., 'C4')
401
+ * @param {string} [chordType='major'] - Chord type
402
+ * @param {number} [duration=0.5] - Duration in seconds
403
+ * @param {Object} [options] - Options
404
+ */
405
+ static chord(root, chordType = "major", duration = 0.5, options = {}) {
406
+ if (!Synth.isInitialized) return;
407
+
408
+ const { volume = 0.2, type = "sine", strum = 0 } = options;
409
+ const frequencies = Synth.music.chord(root, chordType);
410
+
411
+ frequencies.forEach((freq, i) => {
412
+ const delay = strum * i;
413
+ Synth.osc.tone(freq, duration, {
414
+ type,
415
+ volume: volume / frequencies.length,
416
+ attack: 0.01,
417
+ decay: 0.1,
418
+ sustain: 0.6,
419
+ release: 0.2,
420
+ startTime: Synth.now + delay,
421
+ });
422
+ });
423
+ }
424
+
425
+ /**
426
+ * Play notes in a sequence
427
+ * @param {string[]} notes - Array of note names
428
+ * @param {number} [noteDuration=0.2] - Duration per note
429
+ * @param {number} [gap=0] - Gap between notes
430
+ * @param {Object} [options] - Options
431
+ */
432
+ static sequence(notes, noteDuration = 0.2, gap = 0, options = {}) {
433
+ if (!Synth.isInitialized) return;
434
+
435
+ const { volume = 0.3, type = "sine" } = options;
436
+ const totalNoteTime = noteDuration + gap;
437
+
438
+ notes.forEach((note, i) => {
439
+ const startTime = Synth.now + i * totalNoteTime;
440
+ const freq = Synth.music.noteToFreq(note);
441
+
442
+ Synth.osc.tone(freq, noteDuration, {
443
+ type,
444
+ volume,
445
+ attack: 0.01,
446
+ decay: 0.05,
447
+ sustain: 0.5,
448
+ release: 0.1,
449
+ startTime,
450
+ });
451
+ });
452
+ }
453
+
454
+ /**
455
+ * Win/victory fanfare
456
+ */
457
+ static win() {
458
+ if (!Synth.isInitialized) return;
459
+
460
+ const notes = ["C5", "E5", "G5", "C6"];
461
+ this.sequence(notes, 0.15, 0.05, { volume: 0.25, type: "square" });
462
+ }
463
+
464
+ /**
465
+ * Lose/game over sound
466
+ */
467
+ static lose() {
468
+ if (!Synth.isInitialized) return;
469
+
470
+ const notes = ["E4", "D4", "C4"];
471
+ this.sequence(notes, 0.25, 0, { volume: 0.25, type: "sawtooth" });
472
+ }
473
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * SynthAnalyzer - FFT analysis for audio visualization
3
+ * @module sound/synth.analyzer
4
+ */
5
+ export class SynthAnalyzer {
6
+ static #_ctx = null;
7
+ static #_analyzer = null;
8
+ static #_dataArray = null;
9
+ static #_frequencyData = null;
10
+
11
+ /**
12
+ * Initialize the analyzer
13
+ * @param {AudioContext} ctx - Audio context
14
+ * @param {AudioNode} source - Source node to analyze
15
+ */
16
+ static init(ctx, source) {
17
+ this.#_ctx = ctx;
18
+ this.#_analyzer = ctx.createAnalyser();
19
+ this.#_analyzer.fftSize = 2048;
20
+
21
+ source.connect(this.#_analyzer);
22
+ this.#_analyzer.connect(ctx.destination);
23
+
24
+ this.#_dataArray = new Uint8Array(this.#_analyzer.frequencyBinCount);
25
+ this.#_frequencyData = new Uint8Array(this.#_analyzer.frequencyBinCount);
26
+ }
27
+
28
+ /**
29
+ * Check if analyzer is initialized
30
+ * @returns {boolean}
31
+ */
32
+ static get isInitialized() {
33
+ return this.#_analyzer !== null;
34
+ }
35
+
36
+ /**
37
+ * Get the analyzer node
38
+ * @returns {AnalyserNode|null}
39
+ */
40
+ static get node() {
41
+ return this.#_analyzer;
42
+ }
43
+
44
+ /**
45
+ * Set FFT size (must be power of 2)
46
+ * @param {number} size - FFT size (32-32768)
47
+ */
48
+ static setFFTSize(size) {
49
+ if (this.#_analyzer) {
50
+ this.#_analyzer.fftSize = size;
51
+ this.#_dataArray = new Uint8Array(this.#_analyzer.frequencyBinCount);
52
+ this.#_frequencyData = new Uint8Array(this.#_analyzer.frequencyBinCount);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get waveform data (time domain)
58
+ * @returns {Uint8Array} Waveform data (0-255)
59
+ */
60
+ static getWaveform() {
61
+ if (!this.#_analyzer) return new Uint8Array(0);
62
+ this.#_analyzer.getByteTimeDomainData(this.#_dataArray);
63
+ return this.#_dataArray;
64
+ }
65
+
66
+ /**
67
+ * Get frequency data (spectrum)
68
+ * @returns {Uint8Array} Frequency data (0-255)
69
+ */
70
+ static getFrequency() {
71
+ if (!this.#_analyzer) return new Uint8Array(0);
72
+ this.#_analyzer.getByteFrequencyData(this.#_frequencyData);
73
+ return this.#_frequencyData;
74
+ }
75
+
76
+ /**
77
+ * Get normalized frequency bands
78
+ * @param {number} [bands=8] - Number of bands
79
+ * @returns {number[]} Normalized band values (0-1)
80
+ */
81
+ static getBands(bands = 8) {
82
+ const freq = this.getFrequency();
83
+ if (freq.length === 0) return new Array(bands).fill(0);
84
+
85
+ const bandSize = Math.floor(freq.length / bands);
86
+ const result = [];
87
+
88
+ for (let i = 0; i < bands; i++) {
89
+ let sum = 0;
90
+ for (let j = 0; j < bandSize; j++) {
91
+ sum += freq[i * bandSize + j];
92
+ }
93
+ result.push(sum / (bandSize * 255));
94
+ }
95
+
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * Get current amplitude (volume level)
101
+ * @returns {number} Amplitude (0-1)
102
+ */
103
+ static getAmplitude() {
104
+ const waveform = this.getWaveform();
105
+ if (waveform.length === 0) return 0;
106
+
107
+ let sum = 0;
108
+ for (let i = 0; i < waveform.length; i++) {
109
+ const value = (waveform[i] - 128) / 128;
110
+ sum += value * value;
111
+ }
112
+ return Math.sqrt(sum / waveform.length);
113
+ }
114
+
115
+ /**
116
+ * Get peak frequency
117
+ * @returns {number} Peak frequency in Hz
118
+ */
119
+ static getPeakFrequency() {
120
+ if (!this.#_analyzer || !this.#_ctx) return 0;
121
+
122
+ const freq = this.getFrequency();
123
+ let maxIndex = 0;
124
+ let maxValue = 0;
125
+
126
+ for (let i = 0; i < freq.length; i++) {
127
+ if (freq[i] > maxValue) {
128
+ maxValue = freq[i];
129
+ maxIndex = i;
130
+ }
131
+ }
132
+
133
+ // Convert bin index to frequency
134
+ const nyquist = this.#_ctx.sampleRate / 2;
135
+ return (maxIndex * nyquist) / this.#_analyzer.frequencyBinCount;
136
+ }
137
+
138
+ /**
139
+ * Disconnect and cleanup
140
+ */
141
+ static dispose() {
142
+ if (this.#_analyzer) {
143
+ this.#_analyzer.disconnect();
144
+ this.#_analyzer = null;
145
+ }
146
+ this.#_dataArray = null;
147
+ this.#_frequencyData = null;
148
+ }
149
+ }