dopaminejs 1.0.2 → 1.2.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.
@@ -5,153 +5,227 @@
5
5
 
6
6
  export class ParticleSystem {
7
7
  constructor(config = {}) {
8
+ this.container = config.container ?
9
+ (typeof config.container === 'string' ? document.querySelector(config.container) : config.container)
10
+ : document.body;
11
+
8
12
  this.canvasId = config.canvasId || 'dopamine-particle-canvas';
9
13
  this.canvas = document.getElementById(this.canvasId);
10
14
 
11
15
  if (!this.canvas) {
12
- // Create overlay canvas if it doesn't exist
13
16
  this.canvas = document.createElement('canvas');
14
17
  this.canvas.id = this.canvasId;
15
- this.canvas.style.position = 'fixed';
16
- this.canvas.style.top = '0';
17
- this.canvas.style.left = '0';
18
- this.canvas.style.width = '100%';
19
- this.canvas.style.height = '100%';
20
18
  this.canvas.style.pointerEvents = 'none';
21
19
  this.canvas.style.zIndex = config.zIndex || '9999';
22
- document.body.appendChild(this.canvas);
20
+
21
+ if (this.container === document.body) {
22
+ this.canvas.style.position = 'fixed';
23
+ this.canvas.style.top = '0';
24
+ this.canvas.style.left = '0';
25
+ } else {
26
+ this.canvas.style.position = 'absolute';
27
+ this.canvas.style.top = '0';
28
+ this.canvas.style.left = '0';
29
+ // Ensure container is positioned
30
+ const style = window.getComputedStyle(this.container);
31
+ if (style.position === 'static') {
32
+ this.container.style.position = 'relative';
33
+ }
34
+ }
35
+
36
+ this.container.appendChild(this.canvas);
23
37
  }
24
38
 
25
39
  this.ctx = this.canvas.getContext('2d');
26
40
  this.particles = [];
41
+ this.pool = []; // Object pool
42
+ this.sprites = new Map(); // key -> Image
43
+ this.customEffects = new Map(); // key -> callback
27
44
  this.isAnimating = false;
28
45
 
29
46
  this._resize();
30
- window.addEventListener('resize', () => this._resize());
47
+ // Use ResizeObserver for container resizing if supported, fallback to window resize
48
+ if (window.ResizeObserver && this.container !== document.body) {
49
+ new ResizeObserver(() => this._resize()).observe(this.container);
50
+ } else {
51
+ window.addEventListener('resize', () => this._resize());
52
+ }
31
53
  }
32
54
 
33
55
  _resize() {
34
- this.canvas.width = window.innerWidth;
35
- this.canvas.height = window.innerHeight;
56
+ if (this.container === document.body) {
57
+ this.canvas.width = window.innerWidth;
58
+ this.canvas.height = window.innerHeight;
59
+ } else {
60
+ this.canvas.width = this.container.clientWidth;
61
+ this.canvas.height = this.container.clientHeight;
62
+ }
36
63
  }
37
64
 
38
65
  /**
39
- * Confetti explosion (for achievements, level ups)
66
+ * Register a custom sprite image
67
+ * @param {string} key
68
+ * @param {string} url
40
69
  */
41
- confetti(x, y, count = 50) {
42
- const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f7dc6f', '#c06c84', '#6c5ce7'];
43
-
44
- for (let i = 0; i < count; i++) {
45
- this.particles.push({
46
- x, y,
47
- vx: (Math.random() - 0.5) * 10,
48
- vy: (Math.random() - 0.5) * 10 - 5,
49
- rotation: Math.random() * Math.PI * 2,
50
- rotationSpeed: (Math.random() - 0.5) * 0.2,
51
- color: colors[Math.floor(Math.random() * colors.length)],
52
- size: Math.random() * 8 + 4,
53
- life: 1.0,
54
- decay: 0.015 + Math.random() * 0.01,
55
- gravity: 0.3,
56
- type: 'confetti'
57
- });
58
- }
59
-
60
- this._startAnimation();
70
+ registerSprite(key, url) {
71
+ const img = new Image();
72
+ img.src = url;
73
+ this.sprites.set(key, img);
61
74
  }
62
75
 
63
76
  /**
64
- * Sparkle effect (for score milestones, combos)
77
+ * Register a custom effect
78
+ * @param {string} name
79
+ * @param {Function} callback - (x, y, ...args) => void
65
80
  */
66
- sparkle(x, y, count = 20, color = '#ffd700') {
67
- for (let i = 0; i < count; i++) {
68
- const angle = (Math.PI * 2 / count) * i;
69
- const speed = 2 + Math.random() * 3;
70
-
71
- this.particles.push({
72
- x, y,
73
- vx: Math.cos(angle) * speed,
74
- vy: Math.sin(angle) * speed,
75
- color,
76
- size: Math.random() * 4 + 2,
77
- life: 1.0,
78
- decay: 0.02,
79
- gravity: -0.1, // Slight upward drift
80
- type: 'spark'
81
- });
82
- }
83
-
84
- this._startAnimation();
81
+ registerEffect(name, callback) {
82
+ this.customEffects.set(name, callback);
85
83
  }
86
84
 
87
85
  /**
88
- * Coin shower (for collecting coins)
86
+ * Play a registered effect
89
87
  */
90
- coinShower(x, y, count = 10) {
91
- for (let i = 0; i < count; i++) {
92
- this.particles.push({
93
- x: x + (Math.random() - 0.5) * 100,
94
- y: y - Math.random() * 50,
95
- vx: (Math.random() - 0.5) * 3,
96
- vy: Math.random() * -2 - 3,
97
- color: '#ffd700',
98
- size: 12,
99
- life: 1.0,
100
- decay: 0.012,
101
- gravity: 0.4,
102
- type: 'coin'
103
- });
88
+ play(name, x, y, ...args) {
89
+ const effect = this.customEffects.get(name);
90
+ if (effect) {
91
+ effect(x, y, ...args);
92
+ } else {
93
+ console.warn(`[DopamineJS] Effect '${name}' not found.`);
104
94
  }
105
-
106
- this._startAnimation();
107
95
  }
108
96
 
109
97
  /**
110
- * Fire particles (for streaks, combos)
98
+ * Emit particles with custom config
99
+ * @param {Object} config
111
100
  */
112
- fire(x, y, count = 15) {
113
- const colors = ['#ff6b00', '#ff8800', '#ffaa00', '#ff4400'];
101
+ emit(config) {
102
+ const {
103
+ x, y,
104
+ count = 10,
105
+ speed = 5,
106
+ gravity = 0.1,
107
+ decay = 0.02,
108
+ life = 1.0,
109
+ color = '#ffd700',
110
+ sprite = null,
111
+ spread = Math.PI * 2,
112
+ angle = 0,
113
+ size = 5
114
+ } = config;
114
115
 
115
116
  for (let i = 0; i < count; i++) {
116
- this.particles.push({
117
- x: x + (Math.random() - 0.5) * 20,
118
- y,
119
- vx: (Math.random() - 0.5) * 2,
120
- vy: -Math.random() * 3 - 2,
121
- color: colors[Math.floor(Math.random() * colors.length)],
122
- size: Math.random() * 6 + 3,
123
- life: 1.0,
124
- decay: 0.025,
125
- gravity: -0.2, // Upward
126
- type: 'fire'
127
- });
117
+ const p = this._getParticle();
118
+ const pAngle = angle + (Math.random() - 0.5) * spread;
119
+ const pSpeed = Math.random() * speed;
120
+
121
+ p.x = x;
122
+ p.y = y;
123
+ p.vx = Math.cos(pAngle) * pSpeed;
124
+ p.vy = Math.sin(pAngle) * pSpeed;
125
+ p.life = life;
126
+ p.decay = decay * (0.8 + Math.random() * 0.4); // Randomize decay slightly
127
+ p.gravity = gravity;
128
+ p.color = Array.isArray(color) ? color[Math.floor(Math.random() * color.length)] : color;
129
+ p.size = size * (0.8 + Math.random() * 0.4);
130
+ p.sprite = sprite;
131
+ p.rotation = Math.random() * Math.PI * 2;
132
+ p.rotationSpeed = (Math.random() - 0.5) * 0.2;
133
+
134
+ this.particles.push(p);
128
135
  }
129
136
 
130
137
  this._startAnimation();
131
138
  }
132
139
 
133
- /**
134
- * Star burst (for "perfect" moments)
135
- */
140
+ _getParticle() {
141
+ if (this.pool.length > 0) {
142
+ return this.pool.pop();
143
+ }
144
+ return {}; // New particle object
145
+ }
146
+
147
+ _recycleParticle(p) {
148
+ this.pool.push(p);
149
+ }
150
+
151
+ // --- Built-in Effects ---
152
+
153
+ confetti(x, y, count = 50) {
154
+ const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f7dc6f', '#c06c84', '#6c5ce7'];
155
+ this.emit({
156
+ x, y, count,
157
+ color: colors,
158
+ speed: 10,
159
+ gravity: 0.3,
160
+ decay: 0.015,
161
+ size: 8,
162
+ spread: Math.PI * 2
163
+ });
164
+ }
165
+
166
+ coinShower(x, y, count = 10) {
167
+ this.emit({
168
+ x, y, count,
169
+ color: '#ffd700',
170
+ speed: 4,
171
+ gravity: 0.4,
172
+ decay: 0.012,
173
+ size: 12,
174
+ angle: -Math.PI / 2, // Upwards initially
175
+ spread: Math.PI / 2
176
+ });
177
+ }
178
+
179
+ sparkle(x, y, count = 20, color = '#ffd700') {
180
+ this.emit({
181
+ x, y, count,
182
+ color,
183
+ speed: 3,
184
+ gravity: -0.05, // Float up
185
+ decay: 0.03,
186
+ size: 3
187
+ });
188
+ }
189
+
190
+ fire(x, y, count = 15) {
191
+ const colors = ['#ff6b00', '#ff8800', '#ffaa00', '#ff4400'];
192
+ this.emit({
193
+ x, y, count,
194
+ color: colors,
195
+ speed: 3,
196
+ gravity: -0.2,
197
+ decay: 0.04,
198
+ size: 5,
199
+ angle: -Math.PI / 2,
200
+ spread: Math.PI / 4
201
+ });
202
+ }
203
+
136
204
  starBurst(x, y, count = 8) {
205
+ // Star burst is unique because of fixed angles, so we keep manual loop or use emit carefully
206
+ // For simplicity, let's use emit but we lose the perfect star shape distribution
207
+ // To keep it perfect, we'll manually push particles but use the pool
137
208
  for (let i = 0; i < count; i++) {
209
+ const p = this._getParticle();
138
210
  const angle = (Math.PI * 2 / count) * i;
139
211
  const speed = 4;
140
212
 
141
- this.particles.push({
142
- x, y,
143
- vx: Math.cos(angle) * speed,
144
- vy: Math.sin(angle) * speed,
145
- rotation: angle,
146
- color: '#ffffff',
147
- size: 15,
148
- life: 1.0,
149
- decay: 0.03,
150
- gravity: 0,
151
- type: 'star'
152
- });
213
+ p.x = x;
214
+ p.y = y;
215
+ p.vx = Math.cos(angle) * speed;
216
+ p.vy = Math.sin(angle) * speed;
217
+ p.life = 1.0;
218
+ p.decay = 0.03;
219
+ p.gravity = 0;
220
+ p.color = '#ffffff';
221
+ p.size = 15;
222
+ p.sprite = null;
223
+ p.type = 'star'; // Special type for drawing logic
224
+ p.rotation = angle;
225
+ p.rotationSpeed = 0;
226
+
227
+ this.particles.push(p);
153
228
  }
154
-
155
229
  this._startAnimation();
156
230
  }
157
231
 
@@ -165,11 +239,10 @@ export class ParticleSystem {
165
239
  _animate() {
166
240
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
167
241
 
168
- // Update and draw particles
169
242
  for (let i = this.particles.length - 1; i >= 0; i--) {
170
243
  const p = this.particles[i];
171
244
 
172
- // Update physics
245
+ // Physics
173
246
  p.vy += p.gravity;
174
247
  p.x += p.vx;
175
248
  p.y += p.vy;
@@ -179,43 +252,41 @@ export class ParticleSystem {
179
252
  p.rotation += p.rotationSpeed;
180
253
  }
181
254
 
182
- // Remove dead particles
255
+ // Death
183
256
  if (p.life <= 0) {
257
+ this._recycleParticle(p);
184
258
  this.particles.splice(i, 1);
185
259
  continue;
186
260
  }
187
261
 
188
- // Draw particle
262
+ // Draw
189
263
  this.ctx.save();
190
264
  this.ctx.globalAlpha = p.life;
191
- this.ctx.fillStyle = p.color;
192
-
193
- if (p.type === 'confetti') {
194
- this.ctx.translate(p.x, p.y);
195
- this.ctx.rotate(p.rotation);
196
- this.ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size / 2);
197
- } else if (p.type === 'coin') {
198
- this.ctx.beginPath();
199
- this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
200
- this.ctx.fill();
201
- // Add shine
202
- this.ctx.fillStyle = '#ffee88';
203
- this.ctx.beginPath();
204
- this.ctx.arc(p.x - p.size / 3, p.y - p.size / 3, p.size / 3, 0, Math.PI * 2);
205
- this.ctx.fill();
265
+ this.ctx.translate(p.x, p.y);
266
+ this.ctx.rotate(p.rotation);
267
+
268
+ if (p.sprite && this.sprites.has(p.sprite)) {
269
+ const img = this.sprites.get(p.sprite);
270
+ if (img.complete) {
271
+ this.ctx.drawImage(img, -p.size / 2, -p.size / 2, p.size, p.size);
272
+ }
206
273
  } else if (p.type === 'star') {
207
- this._drawStar(p.x, p.y, 5, p.size, p.size / 2, p.rotation);
274
+ this.ctx.fillStyle = p.color;
275
+ this._drawStarPath(0, 0, 5, p.size, p.size / 2);
276
+ this.ctx.fill();
208
277
  } else {
209
- // Default: circle
278
+ this.ctx.fillStyle = p.color;
279
+ // Default shape is square for performance/confetti, or circle
280
+ // Let's usefillRect for speed unless it's a coin/sparkle which usually implies circle
281
+ // For generic emit, let's assume circle if no sprite
210
282
  this.ctx.beginPath();
211
- this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
283
+ this.ctx.arc(0, 0, p.size, 0, Math.PI * 2);
212
284
  this.ctx.fill();
213
285
  }
214
286
 
215
287
  this.ctx.restore();
216
288
  }
217
289
 
218
- // Continue animation if particles remain
219
290
  if (this.particles.length > 0) {
220
291
  requestAnimationFrame(() => this._animate());
221
292
  } else {
@@ -223,16 +294,18 @@ export class ParticleSystem {
223
294
  }
224
295
  }
225
296
 
226
- _drawStar(cx, cy, spikes, outerRadius, innerRadius, rotation) {
227
- let rot = Math.PI / 2 * 3 + rotation;
297
+ _drawStarPath(cx, cy, spikes, outerRadius, innerRadius) {
298
+ let rot = Math.PI / 2 * 3;
299
+ let x = cx;
300
+ let y = cy;
228
301
  const step = Math.PI / spikes;
229
302
 
230
303
  this.ctx.beginPath();
231
304
  this.ctx.moveTo(cx, cy - outerRadius);
232
305
 
233
306
  for (let i = 0; i < spikes; i++) {
234
- let x = cx + Math.cos(rot) * outerRadius;
235
- let y = cy + Math.sin(rot) * outerRadius;
307
+ x = cx + Math.cos(rot) * outerRadius;
308
+ y = cy + Math.sin(rot) * outerRadius;
236
309
  this.ctx.lineTo(x, y);
237
310
  rot += step;
238
311
 
@@ -241,16 +314,13 @@ export class ParticleSystem {
241
314
  this.ctx.lineTo(x, y);
242
315
  rot += step;
243
316
  }
244
-
245
317
  this.ctx.lineTo(cx, cy - outerRadius);
246
318
  this.ctx.closePath();
247
- this.ctx.fill();
248
319
  }
249
320
 
250
- /**
251
- * Clear all particles
252
- */
253
321
  clear() {
322
+ // Recycle all
323
+ this.particles.forEach(p => this._recycleParticle(p));
254
324
  this.particles = [];
255
325
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
256
326
  }