@modernman00/shared-js-lib 1.2.90 → 1.6.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.
@@ -0,0 +1,568 @@
1
+ /**
2
+ * CanvasPhysicsEngine & Social Mini-Games Suite
3
+ *
4
+ * Engineered by the Advanced Technology Group (ATG) under Olutobi (Deloitte),
5
+ * Victor (CTO), Kaelen Vance (Game Systems Architect), and Maya Lindqvist (Canvas Specialist).
6
+ *
7
+ * High-performance, zero-npm, hardware-accelerated 2D Verlet physics and interactive
8
+ * social mini-games for PartyPlatform, FamilyPlatform, and iDecideApp.
9
+ *
10
+ * Performance budget: < 45KB, 60fps, touch targets >= 44px (Fitts's Law).
11
+ */
12
+
13
+ export class CanvasPhysicsEngine {
14
+ constructor(canvas, options = {}) {
15
+ this.canvas = typeof canvas === 'string' ? document.getElementById(canvas) : canvas;
16
+ if (!this.canvas) {
17
+ throw new Error('[CanvasPhysicsEngine] Canvas element not found');
18
+ }
19
+ this.ctx = this.canvas.getContext('2d');
20
+ this.width = options.width || this.canvas.width || 600;
21
+ this.height = options.height || this.canvas.height || 400;
22
+ this.canvas.width = this.width;
23
+ this.canvas.height = this.height;
24
+
25
+ this.gravity = options.gravity !== undefined ? options.gravity : { x: 0, y: 0.25 };
26
+ this.friction = options.friction || 0.99;
27
+ this.restitution = options.restitution || 0.75; // Bounciness
28
+
29
+ this.bodies = [];
30
+ this.staticColliders = [];
31
+ this.particles = [];
32
+ this.draggedBody = null;
33
+ this.dragOffset = { x: 0, y: 0 };
34
+ this.isRunning = false;
35
+ this.animFrameId = null;
36
+
37
+ this._initInteractions();
38
+ }
39
+
40
+ _initInteractions() {
41
+ const getPos = (e) => {
42
+ const rect = this.canvas.getBoundingClientRect();
43
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX;
44
+ const clientY = e.touches ? e.touches[0].clientY : e.clientY;
45
+ const scaleX = this.width / rect.width;
46
+ const scaleY = this.height / rect.height;
47
+ return {
48
+ x: (clientX - rect.left) * scaleX,
49
+ y: (clientY - rect.top) * scaleY
50
+ };
51
+ };
52
+
53
+ const onStart = (e) => {
54
+ const pos = getPos(e);
55
+ for (let i = this.bodies.length - 1; i >= 0; i--) {
56
+ const b = this.bodies[i];
57
+ if (!b.isDraggable) continue;
58
+ const hitRadius = Math.max(b.radius || 22, 22);
59
+ const dx = pos.x - b.x;
60
+ const dy = pos.y - b.y;
61
+ if (dx * dx + dy * dy <= hitRadius * hitRadius) {
62
+ this.draggedBody = b;
63
+ b.isBeingDragged = true;
64
+ this.dragOffset = { x: dx, y: dy };
65
+ if (e.cancelable) e.preventDefault();
66
+ break;
67
+ }
68
+ }
69
+ };
70
+
71
+ const onMove = (e) => {
72
+ if (!this.draggedBody) return;
73
+ const pos = getPos(e);
74
+ this.draggedBody.x = pos.x - this.dragOffset.x;
75
+ this.draggedBody.y = pos.y - this.dragOffset.y;
76
+ this.draggedBody.oldX = this.draggedBody.x;
77
+ this.draggedBody.oldY = this.draggedBody.y;
78
+ if (e.cancelable) e.preventDefault();
79
+ };
80
+
81
+ const onEnd = () => {
82
+ if (this.draggedBody) {
83
+ this.draggedBody.isBeingDragged = false;
84
+ if (this.draggedBody.onRelease) {
85
+ this.draggedBody.onRelease(this.draggedBody);
86
+ }
87
+ this.draggedBody = null;
88
+ }
89
+ };
90
+
91
+ this.canvas.addEventListener('mousedown', onStart);
92
+ window.addEventListener('mousemove', onMove);
93
+ window.addEventListener('mouseup', onEnd);
94
+
95
+ this.canvas.addEventListener('touchstart', onStart, { passive: false });
96
+ window.addEventListener('touchmove', onMove, { passive: false });
97
+ window.addEventListener('touchend', onEnd);
98
+ }
99
+
100
+ addCircle(options = {}) {
101
+ if (this.bodies.length >= 200) {
102
+ this.bodies.shift();
103
+ }
104
+ const body = {
105
+ id: options.id || 'circle_' + Math.random().toString(36).substr(2, 6),
106
+ type: 'circle',
107
+ x: options.x || 100,
108
+ y: options.y || 100,
109
+ oldX: options.x || 100,
110
+ oldY: options.y || 100,
111
+ radius: options.radius || 20,
112
+ mass: options.mass || 1,
113
+ color: options.color || '#3b82f6',
114
+ label: options.label || '',
115
+ emoji: options.emoji || '',
116
+ isDraggable: options.isDraggable !== false,
117
+ isBeingDragged: false,
118
+ restitution: options.restitution !== undefined ? options.restitution : this.restitution,
119
+ onCollision: options.onCollision || null,
120
+ onRelease: options.onRelease || null,
121
+ };
122
+ if (options.vx || options.vy) {
123
+ body.oldX -= options.vx || 0;
124
+ body.oldY -= options.vy || 0;
125
+ }
126
+ this.bodies.push(body);
127
+ return body;
128
+ }
129
+
130
+ addStaticPeg(x, y, radius = 8, color = '#64748b') {
131
+ const peg = { x, y, radius, color, isStatic: true };
132
+ this.staticColliders.push(peg);
133
+ return peg;
134
+ }
135
+
136
+ addParticleBurst(x, y, count = 25, colors = ['#f59e0b', '#ec4899', '#3b82f6', '#10b981']) {
137
+ const safeCount = Math.min(Math.max(1, count), 60);
138
+ for (let i = 0; i < safeCount; i++) {
139
+ if (this.particles.length >= 500) {
140
+ this.particles.shift();
141
+ }
142
+ const angle = Math.random() * Math.PI * 2;
143
+ const speed = Math.random() * 6 + 2;
144
+ this.particles.push({
145
+ x,
146
+ y,
147
+ vx: Math.cos(angle) * speed,
148
+ vy: Math.sin(angle) * speed - 2,
149
+ radius: Math.random() * 3 + 2,
150
+ color: colors[Math.floor(Math.random() * colors.length)],
151
+ alpha: 1.0,
152
+ decay: Math.random() * 0.02 + 0.015,
153
+ });
154
+ }
155
+ }
156
+
157
+ step() {
158
+ for (let i = 0; i < this.bodies.length; i++) {
159
+ const b = this.bodies[i];
160
+ if (b.isBeingDragged) continue;
161
+
162
+ const vx = (b.x - b.oldX) * this.friction;
163
+ const vy = (b.y - b.oldY) * this.friction;
164
+
165
+ b.oldX = b.x;
166
+ b.oldY = b.y;
167
+
168
+ b.x += vx + this.gravity.x;
169
+ b.y += vy + this.gravity.y;
170
+
171
+ if (b.x - b.radius < 0) {
172
+ b.x = b.radius;
173
+ b.oldX = b.x + vx * b.restitution;
174
+ } else if (b.x + b.radius > this.width) {
175
+ b.x = this.width - b.radius;
176
+ b.oldX = b.x + vx * b.restitution;
177
+ }
178
+
179
+ if (b.y - b.radius < 0) {
180
+ b.y = b.radius;
181
+ b.oldY = b.y + vy * b.restitution;
182
+ } else if (b.y + b.radius > this.height) {
183
+ b.y = this.height - b.radius;
184
+ b.oldY = b.y + vy * b.restitution;
185
+ }
186
+ }
187
+
188
+ for (let i = 0; i < this.bodies.length; i++) {
189
+ const b1 = this.bodies[i];
190
+ for (let j = i + 1; j < this.bodies.length; j++) {
191
+ const b2 = this.bodies[j];
192
+ const dx = b2.x - b1.x;
193
+ const dy = b2.y - b1.y;
194
+ const distSq = dx * dx + dy * dy;
195
+ const minDist = b1.radius + b2.radius;
196
+
197
+ if (distSq < minDist * minDist && distSq > 0) {
198
+ const dist = Math.sqrt(distSq);
199
+ const overlap = (minDist - dist) * 0.5;
200
+ const nx = dx / dist;
201
+ const ny = dy / dist;
202
+
203
+ if (!b1.isBeingDragged) {
204
+ b1.x -= nx * overlap;
205
+ b1.y -= ny * overlap;
206
+ }
207
+ if (!b2.isBeingDragged) {
208
+ b2.x += nx * overlap;
209
+ b2.y += ny * overlap;
210
+ }
211
+
212
+ if (b1.onCollision) b1.onCollision(b2);
213
+ if (b2.onCollision) b2.onCollision(b1);
214
+ }
215
+ }
216
+ }
217
+
218
+ for (let i = 0; i < this.bodies.length; i++) {
219
+ const b = this.bodies[i];
220
+ if (b.isBeingDragged) continue;
221
+
222
+ for (let j = 0; j < this.staticColliders.length; j++) {
223
+ const peg = this.staticColliders[j];
224
+ const dx = b.x - peg.x;
225
+ const dy = b.y - peg.y;
226
+ const distSq = dx * dx + dy * dy;
227
+ const minDist = b.radius + peg.radius;
228
+
229
+ if (distSq < minDist * minDist && distSq > 0) {
230
+ const dist = Math.sqrt(distSq);
231
+ const nx = dx / dist;
232
+ const ny = dy / dist;
233
+ const overlap = minDist - dist;
234
+
235
+ b.x += nx * overlap;
236
+ b.y += ny * overlap;
237
+
238
+ const vx = b.x - b.oldX;
239
+ const vy = b.y - b.oldY;
240
+ b.oldX = b.x + vx * b.restitution * 0.8;
241
+ b.oldY = b.y + vy * b.restitution * 0.8;
242
+
243
+ if (b.onCollision) b.onCollision(peg);
244
+ }
245
+ }
246
+ }
247
+
248
+ for (let i = this.particles.length - 1; i >= 0; i--) {
249
+ const p = this.particles[i];
250
+ p.x += p.vx;
251
+ p.y += p.vy;
252
+ p.vy += 0.15;
253
+ p.alpha -= p.decay;
254
+ if (p.alpha <= 0) {
255
+ this.particles.splice(i, 1);
256
+ }
257
+ }
258
+ }
259
+
260
+ render() {
261
+ this.ctx.clearRect(0, 0, this.width, this.height);
262
+
263
+ for (const peg of this.staticColliders) {
264
+ this.ctx.beginPath();
265
+ this.ctx.arc(peg.x, peg.y, peg.radius, 0, Math.PI * 2);
266
+ this.ctx.fillStyle = peg.color;
267
+ this.ctx.fill();
268
+ this.ctx.strokeStyle = '#475569';
269
+ this.ctx.lineWidth = 2;
270
+ this.ctx.stroke();
271
+ }
272
+
273
+ for (const b of this.bodies) {
274
+ this.ctx.save();
275
+ this.ctx.beginPath();
276
+ this.ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
277
+ this.ctx.fillStyle = b.color;
278
+ this.ctx.fill();
279
+ this.ctx.lineWidth = b.isBeingDragged ? 4 : 2;
280
+ this.ctx.strokeStyle = b.isBeingDragged ? '#fbbf24' : '#1e293b';
281
+ this.ctx.stroke();
282
+
283
+ if (b.emoji) {
284
+ this.ctx.font = `${Math.floor(b.radius * 1.1)}px sans-serif`;
285
+ this.ctx.textAlign = 'center';
286
+ this.ctx.textBaseline = 'middle';
287
+ this.ctx.fillText(b.emoji, b.x, b.y);
288
+ } else if (b.label) {
289
+ this.ctx.font = '12px system-ui, -apple-system, sans-serif';
290
+ this.ctx.fillStyle = '#ffffff';
291
+ this.ctx.textAlign = 'center';
292
+ this.ctx.textBaseline = 'middle';
293
+ this.ctx.fillText(b.label, b.x, b.y);
294
+ }
295
+ this.ctx.restore();
296
+ }
297
+
298
+ for (const p of this.particles) {
299
+ this.ctx.save();
300
+ this.ctx.globalAlpha = p.alpha;
301
+ this.ctx.beginPath();
302
+ this.ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
303
+ this.ctx.fillStyle = p.color;
304
+ this.ctx.fill();
305
+ this.ctx.restore();
306
+ }
307
+ }
308
+
309
+ start() {
310
+ if (this.isRunning) return;
311
+ this.isRunning = true;
312
+ const loop = () => {
313
+ if (!this.isRunning) return;
314
+ this.step();
315
+ this.render();
316
+ this.animFrameId = requestAnimationFrame(loop);
317
+ };
318
+ this.animFrameId = requestAnimationFrame(loop);
319
+ }
320
+
321
+ stop() {
322
+ this.isRunning = false;
323
+ if (this.animFrameId) {
324
+ cancelAnimationFrame(this.animFrameId);
325
+ this.animFrameId = null;
326
+ }
327
+ }
328
+ }
329
+
330
+ // --- GAME 1: PARTYPLATFORM "PARTYPULSE AUX ROULETTE" ---
331
+ export class PartyPulseAuxRoulette {
332
+ constructor(canvas, options = {}) {
333
+ this.canvas = typeof canvas === 'string' ? document.getElementById(canvas) : canvas;
334
+ this.ctx = this.canvas.getContext('2d');
335
+ this.items = options.items || ['Afrobeats', 'Hip Hop', '90s R&B', 'Amapiano', 'Pop Anthems', 'Reggae'];
336
+ this.colors = ['#ec4899', '#8b5cf6', '#3b82f6', '#10b981', '#f59e0b', '#ef4444'];
337
+ this.angle = 0;
338
+ this.velocity = 0;
339
+ this.isSpinning = false;
340
+ this.onSelected = options.onSelected || null;
341
+ this.size = Math.min(this.canvas.width, this.canvas.height);
342
+
343
+ this._initInteraction();
344
+ this.render();
345
+ }
346
+
347
+ _initInteraction() {
348
+ this.canvas.addEventListener('click', () => {
349
+ if (!this.isSpinning) {
350
+ this.spin();
351
+ }
352
+ });
353
+ }
354
+
355
+ spin(force = null) {
356
+ this.isSpinning = true;
357
+ this.velocity = force || (Math.random() * 0.35 + 0.45);
358
+ const tick = () => {
359
+ this.angle += this.velocity;
360
+ this.velocity *= 0.985;
361
+ this.render();
362
+
363
+ if (this.velocity > 0.002) {
364
+ requestAnimationFrame(tick);
365
+ } else {
366
+ this.isSpinning = false;
367
+ this.velocity = 0;
368
+ const sliceAngle = (Math.PI * 2) / this.items.length;
369
+ const normalized = (Math.PI * 2 - (this.angle % (Math.PI * 2))) % (Math.PI * 2);
370
+ const index = Math.floor(normalized / sliceAngle) % this.items.length;
371
+ if (this.onSelected) {
372
+ this.onSelected(this.items[index]);
373
+ }
374
+ }
375
+ };
376
+ requestAnimationFrame(tick);
377
+ }
378
+
379
+ render() {
380
+ const cx = this.canvas.width / 2;
381
+ const cy = this.canvas.height / 2;
382
+ const radius = (this.size / 2) - 15;
383
+ const sliceAngle = (Math.PI * 2) / this.items.length;
384
+
385
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
386
+
387
+ for (let i = 0; i < this.items.length; i++) {
388
+ const start = this.angle + i * sliceAngle;
389
+ const end = start + sliceAngle;
390
+ this.ctx.beginPath();
391
+ this.ctx.moveTo(cx, cy);
392
+ this.ctx.arc(cx, cy, radius, start, end);
393
+ this.ctx.fillStyle = this.colors[i % this.colors.length];
394
+ this.ctx.fill();
395
+ this.ctx.lineWidth = 2;
396
+ this.ctx.strokeStyle = '#ffffff';
397
+ this.ctx.stroke();
398
+
399
+ this.ctx.save();
400
+ this.ctx.translate(cx, cy);
401
+ this.ctx.rotate(start + sliceAngle / 2);
402
+ this.ctx.textAlign = 'right';
403
+ this.ctx.fillStyle = '#ffffff';
404
+ this.ctx.font = 'bold 14px system-ui, sans-serif';
405
+ this.ctx.fillText(this.items[i], radius - 20, 5);
406
+ this.ctx.restore();
407
+ }
408
+
409
+ this.ctx.beginPath();
410
+ this.ctx.arc(cx, cy, 24, 0, Math.PI * 2);
411
+ this.ctx.fillStyle = '#0f172a';
412
+ this.ctx.fill();
413
+ this.ctx.lineWidth = 3;
414
+ this.ctx.strokeStyle = '#fbbf24';
415
+ this.ctx.stroke();
416
+
417
+ this.ctx.beginPath();
418
+ this.ctx.moveTo(cx, cy - radius - 8);
419
+ this.ctx.lineTo(cx - 10, cy - radius + 12);
420
+ this.ctx.lineTo(cx + 10, cy - radius + 12);
421
+ this.ctx.closePath();
422
+ this.ctx.fillStyle = '#fbbf24';
423
+ this.ctx.fill();
424
+ }
425
+ }
426
+
427
+ // --- GAME 2: FAMILYPLATFORM "KINSHIP QUEST PINBALL" ---
428
+ export class KinshipQuestPinball {
429
+ constructor(canvas, options = {}) {
430
+ this.physics = new CanvasPhysicsEngine(canvas, {
431
+ gravity: { x: 0, y: 0.35 },
432
+ friction: 0.985,
433
+ restitution: 0.8
434
+ });
435
+ this.onScore = options.onScore || null;
436
+ this._buildField();
437
+ this.physics.start();
438
+ }
439
+
440
+ _buildField() {
441
+ const rows = 4;
442
+ const cols = 5;
443
+ const spacingX = this.physics.width / (cols + 1);
444
+ const spacingY = 50;
445
+
446
+ for (let r = 0; r < rows; r++) {
447
+ const offset = (r % 2 === 0) ? 0 : spacingX * 0.5;
448
+ for (let c = 0; c < cols; c++) {
449
+ const x = spacingX * (c + 1) + offset - (r % 2 === 0 ? 0 : spacingX * 0.25);
450
+ const y = 80 + r * spacingY;
451
+ if (x > 20 && x < this.physics.width - 20) {
452
+ this.physics.addStaticPeg(x, y, 7, '#38bdf8');
453
+ }
454
+ }
455
+ }
456
+ }
457
+
458
+ dropChoreBall(choreName, emoji = '⭐') {
459
+ const startX = this.physics.width / 2 + (Math.random() * 40 - 20);
460
+ const ball = this.physics.addCircle({
461
+ x: startX,
462
+ y: 25,
463
+ radius: 18,
464
+ emoji: emoji,
465
+ color: '#f43f5e',
466
+ onCollision: (other) => {
467
+ if (other.isStatic) {
468
+ this.physics.addParticleBurst(ball.x, ball.y, 4, ['#facc15', '#38bdf8']);
469
+ }
470
+ }
471
+ });
472
+
473
+ let ticks = 0;
474
+ const checker = setInterval(() => {
475
+ ticks++;
476
+ if (!this.physics.isRunning || ticks > 150) {
477
+ clearInterval(checker);
478
+ return;
479
+ }
480
+ if (ball.y >= this.physics.height - 25) {
481
+ clearInterval(checker);
482
+ this.physics.addParticleBurst(ball.x, ball.y, 30);
483
+ if (this.onScore) {
484
+ const bucket = Math.floor((ball.x / this.physics.width) * 3) + 1;
485
+ this.onScore({ chore: choreName, points: bucket * 50 });
486
+ }
487
+ }
488
+ }, 100);
489
+ }
490
+ }
491
+
492
+ // --- GAME 3: IDECIDE "DILEMMA SLINGSHOT & CHOICE ARENA" ---
493
+ export class DilemmaSlingshotArena {
494
+ constructor(canvas, options = {}) {
495
+ this.physics = new CanvasPhysicsEngine(canvas, {
496
+ gravity: { x: 0, y: 0.05 },
497
+ friction: 0.992,
498
+ restitution: 0.85
499
+ });
500
+ this.wells = options.wells || [
501
+ { label: 'High Reward', x: this.physics.width * 0.25, y: this.physics.height * 0.3, weight: 1.2, color: '#10b981' },
502
+ { label: 'Low Risk', x: this.physics.width * 0.75, y: this.physics.height * 0.3, weight: 1.0, color: '#3b82f6' },
503
+ { label: 'Fast Speed', x: this.physics.width * 0.5, y: this.physics.height * 0.75, weight: 0.8, color: '#f59e0b' }
504
+ ];
505
+ this.onEquilibrium = options.onEquilibrium || null;
506
+ this._initArena();
507
+ this.physics.start();
508
+ }
509
+
510
+ _initArena() {
511
+ const originalRender = this.physics.render.bind(this.physics);
512
+ this.physics.render = () => {
513
+ originalRender();
514
+ const ctx = this.physics.ctx;
515
+
516
+ for (const well of this.wells) {
517
+ ctx.save();
518
+ ctx.beginPath();
519
+ ctx.arc(well.x, well.y, 38, 0, Math.PI * 2);
520
+ ctx.fillStyle = well.color + '22';
521
+ ctx.fill();
522
+ ctx.strokeStyle = well.color;
523
+ ctx.lineWidth = 2;
524
+ ctx.setLineDash([4, 4]);
525
+ ctx.stroke();
526
+
527
+ ctx.font = 'bold 12px sans-serif';
528
+ ctx.fillStyle = well.color;
529
+ ctx.textAlign = 'center';
530
+ ctx.fillText(well.label, well.x, well.y + 4);
531
+ ctx.restore();
532
+ }
533
+ };
534
+
535
+ const originalStep = this.physics.step.bind(this.physics);
536
+ this.physics.step = () => {
537
+ for (const b of this.physics.bodies) {
538
+ if (b.isBeingDragged) continue;
539
+ for (const well of this.wells) {
540
+ const dx = well.x - b.x;
541
+ const dy = well.y - b.y;
542
+ const dist = Math.sqrt(dx * dx + dy * dy);
543
+ if (dist > 15 && dist < 250) {
544
+ const force = (well.weight * 0.45) / (dist * 0.05);
545
+ b.x += (dx / dist) * force;
546
+ b.y += (dy / dist) * force;
547
+ }
548
+ }
549
+ }
550
+ originalStep();
551
+ };
552
+ }
553
+
554
+ addDecisionOption(name, emoji = '🎯') {
555
+ return this.physics.addCircle({
556
+ x: this.physics.width / 2 + (Math.random() * 60 - 30),
557
+ y: this.physics.height / 2 + (Math.random() * 60 - 30),
558
+ radius: 26,
559
+ emoji: emoji,
560
+ label: name,
561
+ color: '#6366f1',
562
+ isDraggable: true,
563
+ onRelease: (body) => {
564
+ this.physics.addParticleBurst(body.x, body.y, 15);
565
+ }
566
+ });
567
+ }
568
+ }