PageSourceSearch

https://pupwife.com/scripts/effects/cursorTrailHandlers.js

js pupwife.com collected 2026-09-30 15:06:10 UTC 42,621 bytes, 1,182 lines download raw bytes

1/**
2 * Extended cursor trail handlers (particles + verlet rope).
3 * Loaded alongside cursorTrails.js — registers on window.PupwifeCursorTrailHandlers.
4 */
5(function (global) {
6    'use strict';
7
8    var HANDLER_TYPES = {
9        tardigrade: true,
10        seashells: true,
11        seaweed: true,
12        pearlBubbles: true,
13        bioluminescence: true,
14        snowballFight: true,
15        valentineCandy: true,
16        ropeYarn: true,
17        tetrominoes: true,
18        pokerChips: true,
19        xpOrbs: true,
20        pixelHearts: true,
21        paperClips: true,
22        stickyNotes: true,
23        popcorn: true,
24        gummyWoims: true,
25        shootingStars: true,
26        snails: true,
27        moths: true,
28        bats: true,
29        frogs: true,
30        woims: true,
31        equalizerBars: true,
32        glitterSparkles: true,
33        moonDust: true
34    };
35
36    var TETRO_KEYS = ['I', 'O', 'T', 'S', 'Z', 'J', 'L'];
37    var TETRO_COLORS = {
38        I: '#00e5e5', O: '#e5e500', T: '#b040e0', S: '#40e040', Z: '#e04040', J: '#4040e0', L: '#e0a040'
39    };
40    var TETRO_CELLS = {
41        I: [[0, 0], [1, 0], [2, 0], [3, 0]],
42        O: [[0, 0], [1, 0], [0, 1], [1, 1]],
43        T: [[0, 0], [1, 0], [2, 0], [1, 1]],
44        S: [[1, 0], [2, 0], [0, 1], [1, 1]],
45        Z: [[0, 0], [1, 0], [1, 1], [2, 1]],
46        J: [[0, 0], [0, 1], [1, 1], [2, 1]],
47        L: [[2, 0], [0, 1], [1, 1], [2, 1]]
48    };
49    var CHIP_COLORS = ['#c62828', '#1565c0', '#2e7d32', '#6a1b9a', '#ef6c00'];
50    var STICKY_TEXTS = ['!', '?', 'hi', 'todo', 'uwu', 'note', 'ok', 'hmm'];
51
52    var VALENTINE_WORDS = [
53        'LOVE', 'XOXO', 'CUTIE', 'BFF', 'HUG', 'SWEET', 'PUP', 'UWU', ':3C',
54        'HEY', 'SMILE', 'MOOD', 'YEET', 'HAI', 'COZY', 'LUCKY', 'TRUE'
55    ];
56
57    var rope = {
58        points: [],
59        segmentLen: 10,
60        maxPoints: 28
61    };
62
63    function isHandlerType(type) {
64        return !!HANDLER_TYPES[type];
65    }
66
67    function onTrailChange(type) {
68        rope.points = [];
69    }
70
71    function spawnTardigrade(x, y, ctx) {
72        if (Math.random() > 0.55) return;
73        ctx.particles.push({
74            x: x + (Math.random() - 0.5) * 12,
75            y: y + (Math.random() - 0.5) * 12,
76            vx: (Math.random() - 0.5) * 1.2,
77            vy: -0.2 - Math.random() * 0.6,
78            life: 1,
79            maxLife: 2.5 + Math.random() * 1.5,
80            size: 10 + Math.random() * 8,
81            wiggle: Math.random() * Math.PI * 2,
82            hue: 200 + Math.random() * 40,
83            type: 'tardigrade'
84        });
85    }
86
87    function spawnSeashell(x, y, ctx) {
88        if (Math.random() > 0.5) return;
89        ctx.particles.push({
90            x: x + (Math.random() - 0.5) * 14,
91            y: y + (Math.random() - 0.5) * 14,
92            vx: (Math.random() - 0.5) * 1.5,
93            vy: 0.4 + Math.random() * 1.2,
94            life: 1,
95            maxLife: 3 + Math.random() * 2,
96            size: 10 + Math.random() * 10,
97            rotation: Math.random() * Math.PI * 2,
98            rotationSpeed: (Math.random() - 0.5) * 0.12,
99            spiral: 0.4 + Math.random() * 0.3,
100            color: `hsl(${25 + Math.random() * 20}, ${45 + Math.random() * 25}%, ${72 + Math.random() * 15}%)`,
101            type: 'seashell'
102        });
103    }
104
105    function spawnSeaweed(x, y, ctx) {
106        if (Math.random() > 0.45) return;
107        var segments = 4 + Math.floor(Math.random() * 3);
108        ctx.particles.push({
109            x: x + (Math.random() - 0.5) * 10,
110            y: y + (Math.random() - 0.5) * 10,
111            vx: (Math.random() - 0.5) * 0.4,
112            vy: -0.1 - Math.random() * 0.3,
113            life: 1,
114            maxLife: 2.5 + Math.random() * 1.5,
115            size: 14 + Math.random() * 12,
116            segments: segments,
117            sway: Math.random() * Math.PI * 2,
118            swaySpeed: 0.08 + Math.random() * 0.06,
119            color: `hsl(${120 + Math.random() * 50}, ${50 + Math.random() * 30}%, ${35 + Math.random() * 20}%)`,
120            type: 'seaweed'
121        });
122    }
123
124    function spawnPearlBubble(x, y, ctx) {
125        if (Math.random() > 0.4) return;
126        ctx.particles.push({
127            x: x + (Math.random() - 0.5) * 10,
128            y: y + (Math.random() - 0.5) * 10,
129            vx: (Math.random() - 0.5) * 0.3,
130            vy: -0.6 - Math.random() * 1.2,
131            life: 1,
132            maxLife: 1.8 + Math.random() * 1.2,
133            size: 3 + Math.random() * 6,
134            popped: false,
135            type: 'pearlBubble'
136        });
137    }
138
139    function spawnBioluminescence(x, y, ctx) {
140        var speed = Math.sqrt(ctx.mouseVelX * ctx.mouseVelX + ctx.mouseVelY * ctx.mouseVelY);
141        if (speed < 2 && Math.random() > 0.3) return;
142        ctx.particles.push({
143            x: x + (Math.random() - 0.5) * 16,
144            y: y + (Math.random() - 0.5) * 16,
145            vx: (Math.random() - 0.5) * 0.8,
146            vy: (Math.random() - 0.5) * 0.8,
147            life: 1,
148            maxLife: 0.8 + Math.random() * 1.2,
149            size: 2 + Math.random() * 4,
150            flash: Math.random() * Math.PI * 2,
151            flashSpeed: 0.2 + Math.random() * 0.25,
152            intensity: Math.min(1, speed / 12),
153            color: `hsl(${170 + Math.random() * 80}, 90%, ${55 + Math.random() * 25}%)`,
154            type: 'bioluminescence'
155        });
156    }
157
158    function spawnSnowball(x, y, ctx) {
159        if (Math.random() > 0.5) return;
160        ctx.particles.push({
161            x: x + (Math.random() - 0.5) * 14,
162            y: y + (Math.random() - 0.5) * 14,
163            vx: (Math.random() - 0.5) * 3,
164            vy: (Math.random() - 0.5) * 2,
165            life: 1,
166            maxLife: 2 + Math.random() * 1.5,
167            size: 8 + Math.random() * 10,
168            rotation: Math.random() * Math.PI * 2,
169            rotationSpeed: (Math.random() - 0.5) * 0.2,
170            bounce: 0.35,
171            gravity: 0.35,
172            splatted: false,
173            type: 'snowball'
174        });
175    }
176
177    function spawnValentineCandy(x, y, ctx) {
178        if (Math.random() > 0.5) return;
179        var word = VALENTINE_WORDS[Math.floor(Math.random() * VALENTINE_WORDS.length)];
180        ctx.particles.push({
181            x: x + (Math.random() - 0.5) * 16,
182            y: y + (Math.random() - 0.5) * 16,
183            vx: (Math.random() - 0.5) * 1.5,
184            vy: 0.3 + Math.random() * 1,
185            life: 1,
186            maxLife: 3 + Math.random() * 2,
187            size: 11 + word.length * 1.2,
188            rotation: (Math.random() - 0.5) * 0.4,
189            rotationSpeed: (Math.random() - 0.5) * 0.04,
190            word: word,
191            candyHue: 330 + Math.random() * 30,
192            type: 'valentineCandy'
193        });
194    }
195
196    function spawnRopeYarn(x, y) {
197        if (rope.points.length === 0) {
198            rope.points.push({ x: x, y: y, ox: x, oy: y });
199            return;
200        }
201        var last = rope.points[rope.points.length - 1];
202        var dx = x - last.x;
203        var dy = y - last.y;
204        if (dx * dx + dy * dy < 36) return;
205        rope.points.push({ x: x, y: y, ox: x, oy: y });
206        if (rope.points.length > rope.maxPoints) {
207            rope.points.shift();
208        }
209    }
210
211    function pushParticle(ctx, p) {
212        ctx.particles.push(p);
213    }
214
215    function spawnTetromino(x, y, ctx) {
216        if (Math.random() > 0.55) return;
217        var key = TETRO_KEYS[Math.floor(Math.random() * TETRO_KEYS.length)];
218        pushParticle(ctx, {
219            x: x + (Math.random() - 0.5) * 12,
220            y: y + (Math.random() - 0.5) * 12,
221            vx: (Math.random() - 0.5) * 2,
222            vy: 0.5 + Math.random() * 1.5,
223            life: 1,
224            maxLife: 2.5 + Math.random() * 1.5,
225            size: 5 + Math.random() * 2,
226            rotation: Math.random() * Math.PI * 2,
227            rotationSpeed: (Math.random() - 0.5) * 0.08,
228            tetroKey: key,
229            color: TETRO_COLORS[key],
230            type: 'tetromino'
231        });
232    }
233
234    function spawnPokerChip(x, y, ctx) {
235        if (Math.random() > 0.5) return;
236        pushParticle(ctx, {
237            x: x + (Math.random() - 0.5) * 14,
238            y: y + (Math.random() - 0.5) * 14,
239            vx: (Math.random() - 0.5) * 1.5,
240            vy: 0.3 + Math.random() * 1,
241            life: 1,
242            maxLife: 2.5 + Math.random() * 1.5,
243            size: 10 + Math.random() * 6,
244            rotation: Math.random() * Math.PI * 2,
245            rotationSpeed: (Math.random() - 0.5) * 0.15,
246            chipColor: CHIP_COLORS[Math.floor(Math.random() * CHIP_COLORS.length)],
247            type: 'pokerChip'
248        });
249    }
250
251    function spawnXpOrb(x, y, ctx) {
252        if (Math.random() > 0.45) return;
253        pushParticle(ctx, {
254            x: x + (Math.random() - 0.5) * 12,
255            y: y + (Math.random() - 0.5) * 12,
256            vx: (Math.random() - 0.5) * 1.2,
257            vy: -0.4 - Math.random() * 0.8,
258            life: 1,
259            maxLife: 1.5 + Math.random() * 1,
260            size: 8 + Math.random() * 6,
261            pulse: Math.random() * Math.PI * 2,
262            type: 'xpOrb'
263        });
264    }
265
266    function spawnPixelHeart(x, y, ctx) {
267        if (Math.random() > 0.5) return;
268        pushParticle(ctx, {
269            x: x + (Math.random() - 0.5) * 12,
270            y: y + (Math.random() - 0.5) * 12,
271            vx: (Math.random() - 0.5) * 1,
272            vy: -0.3 - Math.random() * 0.6,
273            life: 1,
274            maxLife: 2 + Math.random() * 1.5,
275            size: 4 + Math.floor(Math.random() * 3),
276            heartHue: Math.random() > 0.5 ? 350 : 200,
277            type: 'pixelHeart'
278        });
279    }
280
281    function spawnPaperClip(x, y, ctx) {
282        if (Math.random() > 0.5) return;
283        pushParticle(ctx, {
284            x: x + (Math.random() - 0.5) * 14,
285            y: y + (Math.random() - 0.5) * 14,
286            vx: (Math.random() - 0.5) * 1.5,
287            vy: 0.2 + Math.random() * 0.8,
288            life: 1,
289            maxLife: 2.5 + Math.random() * 1.5,
290            size: 12 + Math.random() * 6,
291            rotation: (Math.random() - 0.5) * 0.8,
292            rotationSpeed: (Math.random() - 0.5) * 0.04,
293            type: 'paperClip'
294        });
295    }
296
297    function spawnStickyNote(x, y, ctx) {
298        if (Math.random() > 0.5) return;
299        pushParticle(ctx, {
300            x: x + (Math.random() - 0.5) * 16,
301            y: y + (Math.random() - 0.5) * 16,
302            vx: (Math.random() - 0.5) * 1,
303            vy: 0.2 + Math.random() * 0.6,
304            life: 1,
305            maxLife: 3 + Math.random() * 2,
306            size: 14 + Math.random() * 6,
307            rotation: (Math.random() - 0.5) * 0.5,
308            rotationSpeed: (Math.random() - 0.5) * 0.03,
309            text: STICKY_TEXTS[Math.floor(Math.random() * STICKY_TEXTS.length)],
310            noteHue: 48 + Math.random() * 12,
311            type: 'stickyNote'
312        });
313    }
314
315    function spawnPopcorn(x, y, ctx) {
316        if (Math.random() > 0.45) return;
317        pushParticle(ctx, {
318            x: x + (Math.random() - 0.5) * 16,
319            y: y + (Math.random() - 0.5) * 16,
320            vx: (Math.random() - 0.5) * 2.5,
321            vy: -1.5 - Math.random() * 2,
322            life: 1,
323            maxLife: 1.5 + Math.random() * 1,
324            size: 5 + Math.random() * 7,
325            bob: Math.random() * Math.PI * 2,
326            type: 'popcorn'
327        });
328    }
329
330    function spawnGummyWoim(x, y, ctx) {
331        if (Math.random() > 0.5) return;
332        var segs = 5 + Math.floor(Math.random() * 3);
333        pushParticle(ctx, {
334            x: x + (Math.random() - 0.5) * 12,
335            y: y + (Math.random() - 0.5) * 12,
336            vx: (Math.random() - 0.5) * 1.5,
337            vy: 0.3 + Math.random() * 0.8,
338            life: 1,
339            maxLife: 2.5 + Math.random() * 1.5,
340            size: 16 + Math.random() * 10,
341            segments: segs,
342            wiggle: Math.random() * Math.PI * 2,
343            wormHue: Math.random() * 360,
344            type: 'gummyWoim'
345        });
346    }
347
348    function spawnShootingStar(x, y, ctx) {
349        var speed = Math.sqrt(ctx.mouseVelX * ctx.mouseVelX + ctx.mouseVelY * ctx.mouseVelY);
350        if (speed < 1.5 && Math.random() > 0.25) return;
351        var angle = speed > 0.5 ? Math.atan2(ctx.mouseVelY, ctx.mouseVelX) : Math.random() * Math.PI * 2;
352        pushParticle(ctx, {
353            x: x,
354            y: y,
355            vx: Math.cos(angle) * (2 + speed * 0.15),
356            vy: Math.sin(angle) * (2 + speed * 0.15),
357            life: 1,
358            maxLife: 0.6 + Math.random() * 0.5,
359            size: 3 + Math.random() * 4,
360            tailLen: 20 + Math.random() * 30,
361            angle: angle,
362            type: 'shootingStar'
363        });
364    }
365
366    function spawnSnail(x, y, ctx) {
367        if (Math.random() > 0.5) return;
368        pushParticle(ctx, {
369            x: x + (Math.random() - 0.5) * 10,
370            y: y + (Math.random() - 0.5) * 10,
371            vx: (Math.random() - 0.5) * 0.6,
372            vy: 0.15 + Math.random() * 0.4,
373            life: 1,
374            maxLife: 3 + Math.random() * 2,
375            size: 10 + Math.random() * 6,
376            slime: true,
377            wiggle: Math.random() * Math.PI * 2,
378            type: 'snail'
379        });
380    }
381
382    function spawnMoth(x, y, ctx) {
383        if (Math.random() > 0.45) return;
384        pushParticle(ctx, {
385            x: x + (Math.random() - 0.5) * 20,
386            y: y + (Math.random() - 0.5) * 20,
387            vx: (Math.random() - 0.5) * 0.8,
388            vy: (Math.random() - 0.5) * 0.8,
389            life: 1,
390            maxLife: 2 + Math.random() * 1.5,
391            size: 10 + Math.random() * 8,
392            flutter: Math.random() * Math.PI * 2,
393            wingHue: 40 + Math.random() * 30,
394            type: 'moth'
395        });
396    }
397
398    function spawnBat(x, y, ctx) {
399        if (Math.random() > 0.5) return;
400        pushParticle(ctx, {
401            x: x + (Math.random() - 0.5) * 16,
402            y: y + (Math.random() - 0.5) * 16,
403            vx: (Math.random() - 0.5) * 2,
404            vy: -0.3 - Math.random() * 0.8,
405            life: 1,
406            maxLife: 2 + Math.random() * 1.5,
407            size: 12 + Math.random() * 8,
408            flap: Math.random() * Math.PI * 2,
409            type: 'bat'
410        });
411    }
412
413    function spawnFrog(x, y, ctx) {
414        if (Math.random() > 0.55) return;
415        pushParticle(ctx, {
416            x: x + (Math.random() - 0.5) * 12,
417            y: y + (Math.random() - 0.5) * 12,
418            vx: (Math.random() - 0.5) * 1.5,
419            vy: -2 - Math.random() * 2,
420            life: 1,
421            maxLife: 2 + Math.random() * 1,
422            size: 10 + Math.random() * 6,
423            hopPhase: 0,
424            grounded: false,
425            gravity: 0.25,
426            type: 'frog'
427        });
428    }
429
430    function spawnWoim(x, y, ctx) {
431        if (Math.random() > 0.5) return;
432        pushParticle(ctx, {
433            x: x + (Math.random() - 0.5) * 12,
434            y: y + (Math.random() - 0.5) * 12,
435            vx: (Math.random() - 0.5) * 0.8,
436            vy: 0.2 + Math.random() * 0.5,
437            life: 1,
438            maxLife: 2.5 + Math.random() * 1.5,
439            size: 14 + Math.random() * 8,
440            segments: 4 + Math.floor(Math.random() * 3),
441            wiggle: Math.random() * Math.PI * 2,
442            type: 'woim'
443        });
444    }
445
446    function spawnEqualizerBar(x, y, ctx) {
447        if (Math.random() > 0.4) return;
448        var bars = [];
449        for (var b = 0; b < 5; b++) {
450            bars.push(0.3 + Math.random() * 0.7);
451        }
452        pushParticle(ctx, {
453            x: x + (Math.random() - 0.5) * 10,
454            y: y + (Math.random() - 0.5) * 10,
455            vx: (Math.random() - 0.5) * 0.5,
456            vy: -0.1 - Math.random() * 0.3,
457            life: 1,
458            maxLife: 1.2 + Math.random() * 0.8,
459            size: 18 + Math.random() * 8,
460            bars: bars,
461            barPhase: Math.random() * Math.PI * 2,
462            barHue: 260 + Math.random() * 80,
463            type: 'equalizerBar'
464        });
465    }
466
467    function spawnGlitterSparkle(x, y, ctx) {
468        if (Math.random() > 0.35) return;
469        pushParticle(ctx, {
470            x: x + (Math.random() - 0.5) * 14,
471            y: y + (Math.random() - 0.5) * 14,
472            vx: (Math.random() - 0.5) * 1.2,
473            vy: (Math.random() - 0.5) * 1.2,
474            life: 1,
475            maxLife: 0.5 + Math.random() * 0.6,
476            size: 3 + Math.random() * 5,
477            twinkle: Math.random() * Math.PI * 2,
478            color: `hsl(${40 + Math.random() * 40}, 90%, ${70 + Math.random() * 20}%)`,
479            type: 'glitterSparkle'
480        });
481    }
482
483    function spawnMoonDust(x, y, ctx) {
484        if (Math.random() > 0.4) return;
485        pushParticle(ctx, {
486            x: x + (Math.random() - 0.5) * 18,
487            y: y + (Math.random() - 0.5) * 18,
488            vx: (Math.random() - 0.5) * 0.4,
489            vy: 0.15 + Math.random() * 0.5,
490            life: 1,
491            maxLife: 2 + Math.random() * 2,
492            size: 2 + Math.random() * 4,
493            drift: Math.random() * Math.PI * 2,
494            type: 'moonDust'
495        });
496    }
497
498    var spawners = {
499        tardigrade: spawnTardigrade,
500        seashells: spawnSeashell,
501        seaweed: spawnSeaweed,
502        pearlBubbles: spawnPearlBubble,
503        bioluminescence: spawnBioluminescence,
504        snowballFight: spawnSnowball,
505        valentineCandy: spawnValentineCandy,
506        ropeYarn: spawnRopeYarn,
507        tetrominoes: spawnTetromino,
508        pokerChips: spawnPokerChip,
509        xpOrbs: spawnXpOrb,
510        pixelHearts: spawnPixelHeart,
511        paperClips: spawnPaperClip,
512        stickyNotes: spawnStickyNote,
513        popcorn: spawnPopcorn,
514        gummyWoims: spawnGummyWoim,
515        shootingStars: spawnShootingStar,
516        snails: spawnSnail,
517        moths: spawnMoth,
518        bats: spawnBat,
519        frogs: spawnFrog,
520        woims: spawnWoim,
521        equalizerBars: spawnEqualizerBar,
522        glitterSparkles: spawnGlitterSparkle,
523        moonDust: spawnMoonDust
524    };
525
526    function spawn(type, x, y, ctx) {
527        if (spawners[type]) spawners[type](x, y, ctx);
528    }
529
530    function updateRopeYarn(mouseX, mouseY, deltaTime) {
531        if (rope.points.length === 0) return;
532        var head = rope.points[rope.points.length - 1];
533        head.x = mouseX;
534        head.y = mouseY;
535
536        for (var i = 0; i < rope.points.length; i++) {
537            var p = rope.points[i];
538            var vx = p.x - p.ox;
539            var vy = p.y - p.oy;
540            p.ox = p.x;
541            p.oy = p.y;
542            p.x += vx * 0.92;
543            p.y += vy * 0.92 + 0.15 * deltaTime;
544        }
545
546        for (var iter = 0; iter < 3; iter++) {
547            for (var j = 1; j < rope.points.length; j++) {
548                var a = rope.points[j - 1];
549                var b = rope.points[j];
550                var dx = b.x - a.x;
551                var dy = b.y - a.y;
552                var dist = Math.sqrt(dx * dx + dy * dy) || 0.001;
553                var diff = (dist - rope.segmentLen) / dist;
554                var offX = dx * diff * 0.5;
555                var offY = dy * diff * 0.5;
556                if (j < rope.points.length - 1) {
557                    a.x += offX;
558                    a.y += offY;
559                }
560                b.x -= offX;
561                b.y -= offY;
562            }
563        }
564    }
565
566    function drawRopeYarn(ctx2d, canvas) {
567        if (rope.points.length < 2) return;
568        ctx2d.save();
569        ctx2d.lineCap = 'round';
570        ctx2d.lineJoin = 'round';
571        for (var i = 1; i < rope.points.length; i++) {
572            var t = i / rope.points.length;
573            var a = rope.points[i - 1];
574            var b = rope.points[i];
575            ctx2d.strokeStyle = `rgba(${200 + t * 30}, ${140 + t * 40}, ${220 - t * 30}, ${0.35 + t * 0.45})`;
576            ctx2d.lineWidth = 5 - t * 2;
577            ctx2d.beginPath();
578            ctx2d.moveTo(a.x, a.y);
579            ctx2d.lineTo(b.x, b.y);
580            ctx2d.stroke();
581        }
582        ctx2d.restore();
583    }
584
585    function updateFrame(trailType, state) {
586        if (trailType === 'ropeYarn') {
587            updateRopeYarn(state.lastMouseX, state.lastMouseY, state.deltaTime);
588            drawRopeYarn(state.ctx, state.canvas);
589        }
590    }
591
592    function updateParticle(p, deltaTime, state) {
593        if (p.type === 'tardigrade') {
594            p.wiggle += 0.14 * deltaTime;
595            p.vy += 0.03 * deltaTime;
596        }
597        if (p.type === 'seaweed') {
598            p.sway += p.swaySpeed * deltaTime;
599            p.vx += Math.sin(p.sway) * 0.06 * deltaTime;
600        }
601        if (p.type === 'pearlBubble' && !p.popped && p.life < 0.22) {
602            p.popped = true;
603            p.vx = 0;
604            p.vy = 0;
605        }
606        if (p.type === 'bioluminescence') {
607            p.flash += p.flashSpeed * deltaTime * (1 + p.intensity);
608        }
609        if (p.type === 'snowball' && !p.splatted) {
610            p.vy += p.gravity * deltaTime;
611            if (p.y >= state.canvas.height - p.size - 10) {
612                p.splatted = true;
613                p.vy = 0;
614                p.vx *= 0.3;
615                p.life = Math.min(p.life, 0.35);
616            }
617        }
618        if (p.type === 'valentineCandy') {
619            p.vy += 0.05 * deltaTime;
620        }
621        if (p.type === 'popcorn') {
622            p.bob += 0.2 * deltaTime;
623            p.vy += 0.08 * deltaTime;
624        }
625        if (p.type === 'gummyWoim' || p.type === 'woim') {
626            p.wiggle += 0.16 * deltaTime;
627        }
628        if (p.type === 'snail') {
629            p.wiggle += 0.1 * deltaTime;
630            p.vx += Math.sin(p.wiggle) * 0.04 * deltaTime;
631        }
632        if (p.type === 'moth' && state.lastMouseX !== undefined) {
633            p.flutter += 0.22 * deltaTime;
634            var mx = state.lastMouseX - p.x;
635            var my = state.lastMouseY - p.y;
636            var dist = Math.sqrt(mx * mx + my * my) || 1;
637            p.vx += (mx / dist) * 0.04 * deltaTime;
638            p.vy += (my / dist) * 0.04 * deltaTime;
639            p.vx += Math.sin(p.flutter) * 0.06 * deltaTime;
640        }
641        if (p.type === 'bat') {
642            p.flap += 0.35 * deltaTime;
643            p.vy += Math.sin(p.flap) * 0.05 * deltaTime;
644        }
645        if (p.type === 'frog' && !p.grounded) {
646            p.vy += p.gravity * deltaTime;
647            if (p.y >= state.canvas.height - p.size - 12) {
648                p.grounded = true;
649                p.vy = -1.2 - Math.random() * 0.8;
650                p.vx = (Math.random() - 0.5) * 2;
651            }
652        }
653        if (p.type === 'equalizerBar') {
654            p.barPhase += 0.18 * deltaTime;
655            for (var bi = 0; bi < p.bars.length; bi++) {
656                p.bars[bi] = 0.25 + Math.abs(Math.sin(p.barPhase + bi * 0.9)) * 0.75;
657            }
658        }
659        if (p.type === 'glitterSparkle') {
660            p.twinkle += 0.25 * deltaTime;
661        }
662        if (p.type === 'moonDust') {
663            p.drift += 0.08 * deltaTime;
664            p.vx += Math.sin(p.drift) * 0.03 * deltaTime;
665            p.vy += 0.02 * deltaTime;
666        }
667        if (p.type === 'xpOrb') {
668            p.pulse += 0.14 * deltaTime;
669        }
670    }
671
672    function draw(ctx2d, p) {
673        switch (p.type) {
674            case 'tardigrade':
675                drawTardigrade(ctx2d, p);
676                break;
677            case 'seashell':
678                drawSeashell(ctx2d, p);
679                break;
680            case 'seaweed':
681                drawSeaweed(ctx2d, p);
682                break;
683            case 'pearlBubble':
684                drawPearlBubble(ctx2d, p);
685                break;
686            case 'bioluminescence':
687                drawBioluminescence(ctx2d, p);
688                break;
689            case 'snowball':
690                drawSnowball(ctx2d, p);
691                break;
692            case 'valentineCandy':
693                drawValentineCandy(ctx2d, p);
694                break;
695            case 'tetromino':
696                drawTetromino(ctx2d, p);
697                break;
698            case 'pokerChip':
699                drawPokerChip(ctx2d, p);
700                break;
701            case 'xpOrb':
702                drawXpOrb(ctx2d, p);
703                break;
704            case 'pixelHeart':
705                drawPixelHeart(ctx2d, p);
706                break;
707            case 'paperClip':
708                drawPaperClip(ctx2d, p);
709                break;
710            case 'stickyNote':
711                drawStickyNote(ctx2d, p);
712                break;
713            case 'popcorn':
714                drawPopcorn(ctx2d, p);
715                break;
716            case 'gummyWoim':
717                drawGummyWoim(ctx2d, p);
718                break;
719            case 'shootingStar':
720                drawShootingStar(ctx2d, p);
721                break;
722            case 'snail':
723                drawSnail(ctx2d, p);
724                break;
725            case 'moth':
726                drawMoth(ctx2d, p);
727                break;
728            case 'bat':
729                drawBat(ctx2d, p);
730                break;
731            case 'frog':
732                drawFrog(ctx2d, p);
733                break;
734            case 'woim':
735                drawWoim(ctx2d, p);
736                break;
737            case 'equalizerBar':
738                drawEqualizerBar(ctx2d, p);
739                break;
740            case 'glitterSparkle':
741                drawGlitterSparkle(ctx2d, p);
742                break;
743            case 'moonDust':
744                drawMoonDust(ctx2d, p);
745                break;
746        }
747    }
748
749    function drawTardigrade(ctx2d, p) {
750        var s = p.size;
751        var wobble = Math.sin(p.wiggle) * 0.15;
752        ctx2d.fillStyle = `hsla(${p.hue}, 35%, 62%, ${p.life * 0.9})`;
753        ctx2d.beginPath();
754        ctx2d.ellipse(0, 0, s * 0.55, s * 0.4, wobble, 0, Math.PI * 2);
755        ctx2d.fill();
756        ctx2d.fillStyle = `rgba(40, 30, 50, ${p.life * 0.8})`;
757        for (var leg = 0; leg < 4; leg++) {
758            var side = leg < 2 ? -1 : 1;
759            var lx = (leg % 2) * s * 0.25 - s * 0.12;
760            ctx2d.fillRect(lx, side * s * 0.35, s * 0.12, s * 0.22);
761        }
762        ctx2d.fillStyle = `rgba(30, 20, 40, ${p.life})`;
763        ctx2d.beginPath();
764        ctx2d.arc(s * 0.2, -s * 0.08, s * 0.08, 0, Math.PI * 2);
765        ctx2d.fill();
766    }
767
768    function drawSeashell(ctx2d, p) {
769        ctx2d.strokeStyle = p.color;
770        ctx2d.lineWidth = 2;
771        ctx2d.beginPath();
772        for (var t = 0; t <= 1; t += 0.05) {
773            var angle = t * Math.PI * 3;
774            var r = p.size * p.spiral * t;
775            var px = Math.cos(angle) * r;
776            var py = Math.sin(angle) * r;
777            if (t === 0) ctx2d.moveTo(px, py);
778            else ctx2d.lineTo(px, py);
779        }
780        ctx2d.stroke();
781    }
782
783    function drawSeaweed(ctx2d, p) {
784        ctx2d.strokeStyle = p.color;
785        ctx2d.lineWidth = 3;
786        ctx2d.lineCap = 'round';
787        var segH = p.size / p.segments;
788        var px = 0;
789        var py = 0;
790        ctx2d.beginPath();
791        ctx2d.moveTo(px, py);
792        for (var i = 1; i <= p.segments; i++) {
793            var sway = Math.sin(p.sway + i * 0.6) * p.size * 0.15;
794            px = sway;
795            py = i * segH;
796            ctx2d.lineTo(px, py);
797        }
798        ctx2d.stroke();
799    }
800
801    function drawPearlBubble(ctx2d, p) {
802        if (p.popped) {
803            var popSize = (1 - p.life) * p.size * 3;
804            ctx2d.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.6})`;
805            ctx2d.lineWidth = 1.5;
806            for (var i = 0; i < 6; i++) {
807                var ang = (i / 6) * Math.PI * 2;
808                ctx2d.beginPath();
809                ctx2d.moveTo(0, 0);
810                ctx2d.lineTo(Math.cos(ang) * popSize, Math.sin(ang) * popSize);
811                ctx2d.stroke();
812            }
813            return;
814        }
815        var g = ctx2d.createRadialGradient(-p.size * 0.2, -p.size * 0.2, 0, 0, 0, p.size);
816        g.addColorStop(0, `rgba(255, 255, 255, ${p.life * 0.95})`);
817        g.addColorStop(0.5, `rgba(220, 240, 255, ${p.life * 0.5})`);
818        g.addColorStop(1, `rgba(180, 220, 255, 0)`);
819        ctx2d.fillStyle = g;
820        ctx2d.beginPath();
821        ctx2d.arc(0, 0, p.size, 0, Math.PI * 2);
822        ctx2d.fill();
823    }
824
825    function drawBioluminescence(ctx2d, p) {
826        var bright = 0.4 + Math.sin(p.flash) * 0.5 * p.intensity;
827        ctx2d.shadowBlur = 10 * bright;
828        ctx2d.shadowColor = p.color;
829        ctx2d.fillStyle = p.color;
830        ctx2d.globalAlpha = bright * p.life;
831        ctx2d.beginPath();
832        ctx2d.arc(0, 0, p.size, 0, Math.PI * 2);
833        ctx2d.fill();
834        ctx2d.globalAlpha = 1;
835        ctx2d.shadowBlur = 0;
836    }
837
838    function drawSnowball(ctx2d, p) {
839        if (p.splatted) {
840            ctx2d.fillStyle = `rgba(240, 248, 255, ${p.life * 0.7})`;
841            ctx2d.beginPath();
842            ctx2d.ellipse(0, p.size * 0.2, p.size * 1.2, p.size * 0.45, 0, 0, Math.PI * 2);
843            ctx2d.fill();
844            return;
845        }
846        ctx2d.fillStyle = `rgba(245, 250, 255, ${p.life * 0.95})`;
847        ctx2d.beginPath();
848        ctx2d.arc(0, 0, p.size, 0, Math.PI * 2);
849        ctx2d.fill();
850    }
851
852    function drawValentineCandy(ctx2d, p) {
853        var w = p.size * 1.1;
854        var h = p.size * 0.85;
855        ctx2d.fillStyle = `hsl(${p.candyHue}, 55%, 88%)`;
856        ctx2d.strokeStyle = `hsl(${p.candyHue}, 45%, 70%)`;
857        ctx2d.lineWidth = 1.5;
858        ctx2d.beginPath();
859        ctx2d.moveTo(0, -h * 0.35);
860        ctx2d.bezierCurveTo(w * 0.5, -h * 0.5, w * 0.55, h * 0.1, 0, h * 0.45);
861        ctx2d.bezierCurveTo(-w * 0.55, h * 0.1, -w * 0.5, -h * 0.5, 0, -h * 0.35);
862        ctx2d.fill();
863        ctx2d.stroke();
864        ctx2d.fillStyle = `hsl(${p.candyHue}, 40%, 35%)`;
865        ctx2d.font = `bold ${Math.max(7, p.size * 0.38)}px Montserrat, sans-serif`;
866        ctx2d.textAlign = 'center';
867        ctx2d.textBaseline = 'middle';
868        ctx2d.fillText(p.word, 0, h * 0.05);
869    }
870
871    function drawTetromino(ctx2d, p) {
872        var cell = p.size;
873        var cells = TETRO_CELLS[p.tetroKey] || TETRO_CELLS.T;
874        var minX = 0;
875        var minY = 0;
876        var maxX = 0;
877        var maxY = 0;
878        cells.forEach(function (c) {
879            minX = Math.min(minX, c[0]);
880            minY = Math.min(minY, c[1]);
881            maxX = Math.max(maxX, c[0]);
882            maxY = Math.max(maxY, c[1]);
883        });
884        var ox = -((minX + maxX) / 2) * cell;
885        var oy = -((minY + maxY) / 2) * cell;
886        ctx2d.fillStyle = p.color;
887        ctx2d.strokeStyle = 'rgba(0,0,0,0.25)';
888        ctx2d.lineWidth = 0.5;
889        cells.forEach(function (c) {
890            var px = ox + c[0] * cell;
891            var py = oy + c[1] * cell;
892            ctx2d.fillRect(px - cell / 2, py - cell / 2, cell - 1, cell - 1);
893            ctx2d.strokeRect(px - cell / 2, py - cell / 2, cell - 1, cell - 1);
894        });
895    }
896
897    function drawPokerChip(ctx2d, p) {
898        var r = p.size;
899        ctx2d.fillStyle = p.chipColor;
900        ctx2d.beginPath();
901        ctx2d.arc(0, 0, r, 0, Math.PI * 2);
902        ctx2d.fill();
903        ctx2d.strokeStyle = '#fff';
904        ctx2d.lineWidth = 2;
905        ctx2d.beginPath();
906        ctx2d.arc(0, 0, r * 0.65, 0, Math.PI * 2);
907        ctx2d.stroke();
908        for (var i = 0; i < 8; i++) {
909            var a = (i / 8) * Math.PI * 2;
910            ctx2d.fillStyle = 'rgba(255,255,255,0.85)';
911            ctx2d.fillRect(Math.cos(a) * r * 0.85 - 1.5, Math.sin(a) * r * 0.85 - 3, 3, 6);
912        }
913    }
914
915    function drawXpOrb(ctx2d, p) {
916        var glow = 0.6 + Math.sin(p.pulse) * 0.35;
917        var r = p.size * (0.9 + glow * 0.15);
918        ctx2d.shadowBlur = 12 * glow;
919        ctx2d.shadowColor = '#7cfc00';
920        var g = ctx2d.createRadialGradient(0, 0, 0, 0, 0, r);
921        g.addColorStop(0, `rgba(220, 255, 180, ${p.life * glow})`);
922        g.addColorStop(0.5, `rgba(120, 220, 60, ${p.life * glow * 0.7})`);
923        g.addColorStop(1, 'rgba(60, 160, 30, 0)');
924        ctx2d.fillStyle = g;
925        ctx2d.beginPath();
926        ctx2d.arc(0, 0, r, 0, Math.PI * 2);
927        ctx2d.fill();
928        ctx2d.shadowBlur = 0;
929        ctx2d.fillStyle = `rgba(255, 255, 220, ${p.life * 0.9})`;
930        ctx2d.font = `bold ${Math.max(7, p.size * 0.55)}px Montserrat, sans-serif`;
931        ctx2d.textAlign = 'center';
932        ctx2d.textBaseline = 'middle';
933        ctx2d.fillText('XP', 0, 0);
934    }
935
936    function drawPixelHeart(ctx2d, p) {
937        var u = p.size;
938        var grid = [
939            [0, 1, 1, 0, 1, 1, 0],
940            [1, 1, 1, 1, 1, 1, 1],
941            [1, 1, 1, 1, 1, 1, 1],
942            [0, 1, 1, 1, 1, 1, 0],
943            [0, 0, 1, 1, 1, 0, 0],
944            [0, 0, 0, 1, 0, 0, 0]
945        ];
946        ctx2d.fillStyle = `hsl(${p.heartHue}, 80%, 55%)`;
947        for (var row = 0; row < grid.length; row++) {
948            for (var col = 0; col < grid[row].length; col++) {
949                if (grid[row][col]) {
950                    ctx2d.fillRect((col - 3) * u, (row - 2.5) * u, u - 0.5, u - 0.5);
951                }
952            }
953        }
954    }
955
956    function drawPaperClip(ctx2d, p) {
957        var s = p.size;
958        ctx2d.strokeStyle = `rgba(140, 150, 165, ${p.life * 0.95})`;
959        ctx2d.lineWidth = 2.5;
960        ctx2d.lineCap = 'round';
961        ctx2d.beginPath();
962        ctx2d.moveTo(-s * 0.15, s * 0.35);
963        ctx2d.lineTo(-s * 0.15, -s * 0.25);
964        ctx2d.arc(-s * 0.05, -s * 0.25, s * 0.12, Math.PI, 0, true);
965        ctx2d.lineTo(s * 0.15, s * 0.15);
966        ctx2d.arc(s * 0.05, s * 0.15, s * 0.12, 0, Math.PI, true);
967        ctx2d.lineTo(-s * 0.05, -s * 0.05);
968        ctx2d.stroke();
969    }
970
971    function drawStickyNote(ctx2d, p) {
972        var w = p.size;
973        var h = p.size * 0.9;
974        ctx2d.fillStyle = `hsl(${p.noteHue}, 85%, 78%)`;
975        ctx2d.strokeStyle = `hsla(${p.noteHue}, 40%, 50%, 0.5)`;
976        ctx2d.lineWidth = 1;
977        ctx2d.fillRect(-w / 2, -h / 2, w, h);
978        ctx2d.strokeRect(-w / 2, -h / 2, w, h);
979        ctx2d.fillStyle = 'rgba(60, 50, 30, 0.55)';
980        ctx2d.font = `${Math.max(8, w * 0.38)}px Montserrat, sans-serif`;
981        ctx2d.textAlign = 'center';
982        ctx2d.textBaseline = 'middle';
983        ctx2d.fillText(p.text, 0, 0);
984        ctx2d.beginPath();
985        ctx2d.moveTo(-w * 0.3, h * 0.15);
986        ctx2d.lineTo(-w * 0.1, h * 0.2);
987        ctx2d.stroke();
988    }
989
990    function drawPopcorn(ctx2d, p) {
991        var bob = Math.sin(p.bob) * 2;
992        ctx2d.fillStyle = `rgba(255, 252, 245, ${p.life * 0.95})`;
993        for (var i = 0; i < 3; i++) {
994            var ox = (i - 1) * p.size * 0.35;
995            var oy = bob + (i % 2) * -p.size * 0.15;
996            ctx2d.beginPath();
997            ctx2d.arc(ox, oy, p.size * 0.45, 0, Math.PI * 2);
998            ctx2d.fill();
999        }
1000        ctx2d.fillStyle = `rgba(255, 235, 200, ${p.life * 0.8})`;
1001        ctx2d.beginPath();
1002        ctx2d.arc(0, bob + p.size * 0.1, p.size * 0.25, 0, Math.PI * 2);
1003        ctx2d.fill();
1004    }
1005
1006    function drawGummyWoim(ctx2d, p) {
1007        var segLen = p.size / p.segments;
1008        ctx2d.lineCap = 'round';
1009        ctx2d.lineJoin = 'round';
1010        for (var i = 0; i < p.segments; i++) {
1011            var t = i / p.segments;
1012            var hue = (p.wormHue + i * 25) % 360;
1013            ctx2d.strokeStyle = `hsla(${hue}, 85%, 55%, ${p.life * 0.9})`;
1014            ctx2d.lineWidth = 5 - t;
1015            var x1 = Math.sin(p.wiggle + i * 0.7) * p.size * 0.2;
1016            var y1 = i * segLen - p.size / 2;
1017            var x2 = Math.sin(p.wiggle + (i + 1) * 0.7) * p.size * 0.2;
1018            var y2 = (i + 1) * segLen - p.size / 2;
1019            ctx2d.beginPath();
1020            ctx2d.moveTo(x1, y1);
1021            ctx2d.lineTo(x2, y2);
1022            ctx2d.stroke();
1023        }
1024    }
1025
1026    function drawShootingStar(ctx2d, p) {
1027        var len = p.tailLen * p.life;
1028        ctx2d.strokeStyle = `rgba(255, 255, 240, ${p.life * 0.9})`;
1029        ctx2d.lineWidth = p.size * 0.5;
1030        ctx2d.lineCap = 'round';
1031        ctx2d.beginPath();
1032        ctx2d.moveTo(0, 0);
1033        ctx2d.lineTo(-Math.cos(p.angle) * len, -Math.sin(p.angle) * len);
1034        ctx2d.stroke();
1035        ctx2d.fillStyle = `rgba(255, 255, 255, ${p.life})`;
1036        ctx2d.beginPath();
1037        ctx2d.arc(0, 0, p.size, 0, Math.PI * 2);
1038        ctx2d.fill();
1039    }
1040
1041    function drawSnail(ctx2d, p) {
1042        var s = p.size;
1043        if (p.slime) {
1044            ctx2d.fillStyle = `rgba(180, 220, 160, ${p.life * 0.25})`;
1045            ctx2d.beginPath();
1046            ctx2d.ellipse(0, s * 0.35, s * 0.7, s * 0.2, 0, 0, Math.PI * 2);
1047            ctx2d.fill();
1048        }
1049        ctx2d.fillStyle = `hsl(30, 50%, 72%)`;
1050        ctx2d.beginPath();
1051        ctx2d.arc(-s * 0.15, s * 0.1, s * 0.55, 0, Math.PI * 2);
1052        ctx2d.fill();
1053        ctx2d.strokeStyle = `hsla(25, 40%, 45%, ${p.life})`;
1054        ctx2d.lineWidth = 1.5;
1055        ctx2d.beginPath();
1056        ctx2d.arc(-s * 0.15, s * 0.05, s * 0.35, 0, Math.PI * 2);
1057        ctx2d.stroke();
1058        ctx2d.fillStyle = `hsla(120, 35%, 75%, ${p.life})`;
1059        ctx2d.beginPath();
1060        ctx2d.ellipse(s * 0.25, s * 0.15, s * 0.35, s * 0.2, Math.sin(p.wiggle) * 0.2, 0, Math.PI * 2);
1061        ctx2d.fill();
1062        ctx2d.fillStyle = `rgba(40,30,30,${p.life})`;
1063        ctx2d.beginPath();
1064        ctx2d.arc(s * 0.42, s * 0.05, s * 0.06, 0, Math.PI * 2);
1065        ctx2d.fill();
1066    }
1067
1068    function drawMoth(ctx2d, p) {
1069        var flap = Math.sin(p.flutter) * 0.35;
1070        ctx2d.fillStyle = `hsla(${p.wingHue}, 25%, 75%, ${p.life * 0.85})`;
1071        ctx2d.beginPath();
1072        ctx2d.ellipse(-p.size * 0.35, 0, p.size * 0.4, p.size * 0.55 + flap * p.size, -0.3, 0, Math.PI * 2);
1073        ctx2d.ellipse(p.size * 0.35, 0, p.size * 0.4, p.size * 0.55 - flap * p.size, 0.3, 0, Math.PI * 2);
1074        ctx2d.fill();
1075        ctx2d.fillStyle = `rgba(50, 40, 35, ${p.life})`;
1076        ctx2d.beginPath();
1077        ctx2d.ellipse(0, 0, p.size * 0.12, p.size * 0.2, 0, 0, Math.PI * 2);
1078        ctx2d.fill();
1079    }
1080
1081    function drawBat(ctx2d, p) {
1082        var flap = Math.sin(p.flap) * 0.4;
1083        ctx2d.fillStyle = `rgba(35, 30, 45, ${p.life * 0.9})`;
1084        ctx2d.beginPath();
1085        ctx2d.moveTo(0, 0);
1086        ctx2d.quadraticCurveTo(-p.size * 0.6, -p.size * (0.5 + flap), -p.size * 0.9, p.size * 0.1);
1087        ctx2d.quadraticCurveTo(-p.size * 0.35, p.size * 0.15, 0, p.size * 0.05);
1088        ctx2d.quadraticCurveTo(p.size * 0.35, p.size * 0.15, p.size * 0.9, p.size * 0.1);
1089        ctx2d.quadraticCurveTo(p.size * 0.6, -p.size * (0.5 - flap), 0, 0);
1090        ctx2d.fill();
1091    }
1092
1093    function drawFrog(ctx2d, p) {
1094        var s = p.size;
1095        ctx2d.fillStyle = `hsl(120, 55%, 45%)`;
1096        ctx2d.beginPath();
1097        ctx2d.ellipse(0, 0, s * 0.55, s * 0.4, 0, 0, Math.PI * 2);
1098        ctx2d.fill();
1099        ctx2d.fillStyle = `hsl(120, 50%, 38%)`;
1100        ctx2d.beginPath();
1101        ctx2d.arc(-s * 0.25, -s * 0.25, s * 0.18, 0, Math.PI * 2);
1102        ctx2d.arc(s * 0.25, -s * 0.25, s * 0.18, 0, Math.PI * 2);
1103        ctx2d.fill();
1104        ctx2d.fillStyle = `rgba(20, 40, 20, ${p.life})`;
1105        ctx2d.beginPath();
1106        ctx2d.arc(-s * 0.25, -s * 0.25, s * 0.07, 0, Math.PI * 2);
1107        ctx2d.arc(s * 0.25, -s * 0.25, s * 0.07, 0, Math.PI * 2);
1108        ctx2d.fill();
1109    }
1110
1111    function drawWoim(ctx2d, p) {
1112        var segLen = p.size / p.segments;
1113        ctx2d.lineCap = 'round';
1114        ctx2d.strokeStyle = `hsla(15, 45%, 55%, ${p.life * 0.9})`;
1115        ctx2d.lineWidth = 4;
1116        ctx2d.beginPath();
1117        for (var i = 0; i <= p.segments; i++) {
1118            var px = Math.sin(p.wiggle + i * 0.8) * p.size * 0.15;
1119            var py = i * segLen - p.size / 2;
1120            if (i === 0) ctx2d.moveTo(px, py);
1121            else ctx2d.lineTo(px, py);
1122        }
1123        ctx2d.stroke();
1124        ctx2d.fillStyle = `rgba(40, 25, 20, ${p.life})`;
1125        ctx2d.beginPath();
1126        var hx = Math.sin(p.wiggle) * p.size * 0.15;
1127        ctx2d.arc(hx, -p.size / 2 + 2, 3, 0, Math.PI * 2);
1128        ctx2d.fill();
1129    }
1130
1131    function drawEqualizerBar(ctx2d, p) {
1132        var barW = p.size / (p.bars.length * 1.6);
1133        var gap = barW * 0.4;
1134        var totalW = p.bars.length * (barW + gap);
1135        var startX = -totalW / 2;
1136        for (var i = 0; i < p.bars.length; i++) {
1137            var h = p.bars[i] * p.size;
1138            var x = startX + i * (barW + gap);
1139            ctx2d.fillStyle = `hsla(${p.barHue + i * 12}, 75%, 60%, ${p.life * 0.9})`;
1140            ctx2d.fillRect(x, -h / 2, barW, h);
1141        }
1142    }
1143
1144    function drawGlitterSparkle(ctx2d, p) {
1145        var bright = 0.5 + Math.sin(p.twinkle) * 0.5;
1146        ctx2d.strokeStyle = p.color;
1147        ctx2d.lineWidth = 1.5;
1148        ctx2d.globalAlpha = bright * p.life;
1149        var s = p.size;
1150        ctx2d.beginPath();
1151        ctx2d.moveTo(-s, 0);
1152        ctx2d.lineTo(s, 0);
1153        ctx2d.moveTo(0, -s);
1154        ctx2d.lineTo(0, s);
1155        ctx2d.moveTo(-s * 0.7, -s * 0.7);
1156        ctx2d.lineTo(s * 0.7, s * 0.7);
1157        ctx2d.moveTo(s * 0.7, -s * 0.7);
1158        ctx2d.lineTo(-s * 0.7, s * 0.7);
1159        ctx2d.stroke();
1160        ctx2d.globalAlpha = 1;
1161    }
1162
1163    function drawMoonDust(ctx2d, p) {
1164        ctx2d.shadowBlur = 6;
1165        ctx2d.shadowColor = 'rgba(220, 230, 255, 0.6)';
1166        ctx2d.fillStyle = `rgba(230, 235, 255, ${p.life * 0.75})`;
1167        ctx2d.beginPath();
1168        ctx2d.arc(0, 0, p.size, 0, Math.PI * 2);
1169        ctx2d.fill();
1170        ctx2d.shadowBlur = 0;
1171    }
1172
1173    global.PupwifeCursorTrailHandlers = {
1174        HANDLER_TYPES: HANDLER_TYPES,
1175        isHandlerType: isHandlerType,
1176        spawn: spawn,
1177        updateFrame: updateFrame,
1178        updateParticle: updateParticle,
1179        draw: draw,
1180        onTrailChange: onTrailChange
1181    };
1182})(window);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.