svelte-confetti-explosion 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
package/src/app.html ADDED
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="/favicon.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ %svelte.head%
8
+ </head>
9
+ <body>
10
+ <div id="svelte">%svelte.body%</div>
11
+ </body>
12
+ </html>
@@ -0,0 +1 @@
1
+ /// <reference types="@sveltejs/kit" />
@@ -0,0 +1,404 @@
1
+ <script lang="ts" context="module">
2
+ type Particle = {
3
+ color: string; // color of particle
4
+ degree: number; // vector direction, between 0-360 (0 being straight up ↑)
5
+ };
6
+
7
+ type Rotate3dTransform = [number, number, number];
8
+
9
+ const ROTATION_SPEED_MIN = 200; // minimum possible duration of single particle full rotation
10
+ const ROTATION_SPEED_MAX = 800; // maximum possible duration of single particle full rotation
11
+ const CRAZY_PARTICLES_FREQUENCY = 0.1; // 0-1 frequency of crazy curvy unpredictable particles
12
+ const CRAZY_PARTICLE_CRAZINESS = 0.3; // 0-1 how crazy these crazy particles are
13
+ const BEZIER_MEDIAN = 0.5; // utility for mid-point bezier curves, to ensure smooth motion paths
14
+
15
+ const FORCE = 0.5; // 0-1 roughly the vertical force at which particles initially explode
16
+ const SIZE = 12; // max height for particle rectangles, diameter for particle circles
17
+ const FLOOR_HEIGHT = 800; // pixels the particles will fall from initial explosion point
18
+ const FLOOR_WIDTH = 1600; // horizontal spread of particles in pixels
19
+ const PARTICLE_COUNT = 150;
20
+ const DURATION = 3500;
21
+ const COLORS = ['#FFC700', '#FF0000', '#2E3191', '#41BBC7'];
22
+
23
+ const createParticles = (count: number, colors: string[]): Particle[] => {
24
+ const increment = 360 / count;
25
+ return Array.from({ length: count }, (_, i) => ({
26
+ color: colors[i % colors.length],
27
+ degree: i * increment,
28
+ }));
29
+ };
30
+
31
+ const waitFor = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
32
+
33
+ // From here: https://stackoverflow.com/a/11832950
34
+ function round(num: number, precision: number = 2) {
35
+ return Math.round((num + Number.EPSILON) * 10 ** precision) / 10 ** precision;
36
+ }
37
+
38
+ function arraysEqual<ItemType>(a: ItemType[], b: ItemType[]) {
39
+ if (a === b) return true;
40
+ if (a == null || b == null) return false;
41
+ if (a.length !== b.length) return false;
42
+
43
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
44
+
45
+ return true;
46
+ }
47
+
48
+ const mapRange = (value: number, x1: number, y1: number, x2: number, y2: number) =>
49
+ ((value - x1) * (y2 - x2)) / (y1 - x1) + x2;
50
+
51
+ const rotate = (degree: number, amount: number) => {
52
+ const result = degree + amount;
53
+ return result > 360 ? result - 360 : result;
54
+ };
55
+
56
+ const coinFlip = () => Math.random() > 0.5;
57
+
58
+ // avoid this for circles, as it will have no visual effect
59
+ const zAxisRotation: Rotate3dTransform = [0, 0, 1];
60
+
61
+ const rotationTransforms: Rotate3dTransform[] = [
62
+ // dual axis rotations (a bit more realistic)
63
+ [1, 1, 0],
64
+ [1, 0, 1],
65
+ [0, 1, 1],
66
+ // single axis rotations (a bit dumber)
67
+ [1, 0, 0],
68
+ [0, 1, 0],
69
+ zAxisRotation,
70
+ ];
71
+
72
+ const shouldBeCircle = (rotationIndex: number) =>
73
+ !arraysEqual(rotationTransforms[rotationIndex], zAxisRotation) && coinFlip();
74
+
75
+ const isUndefined = (value: any) => typeof value === 'undefined';
76
+
77
+ const error = (message: string) => {
78
+ console.error(message);
79
+ };
80
+
81
+ function validate(
82
+ particleCount: number,
83
+ duration: number,
84
+ colors: string[],
85
+ particleSize: number,
86
+ force: number,
87
+ floorHeight: number,
88
+ floorWidth: number
89
+ ) {
90
+ const isSafeInteger = Number.isSafeInteger;
91
+ if (!isUndefined(particleCount) && isSafeInteger(particleCount) && particleCount < 0) {
92
+ error('particleCount must be a positive integer');
93
+ return false;
94
+ }
95
+
96
+ if (!isUndefined(duration) && isSafeInteger(duration) && duration < 0) {
97
+ error('duration must be a positive integer');
98
+ return false;
99
+ }
100
+
101
+ if (!isUndefined(colors) && !Array.isArray(colors)) {
102
+ error('colors must be an array of strings');
103
+ return false;
104
+ }
105
+
106
+ if (!isUndefined(particleSize) && isSafeInteger(particleSize) && particleSize < 0) {
107
+ error('particleSize must be a positive integer');
108
+ return false;
109
+ }
110
+
111
+ if (!isUndefined(force) && isSafeInteger(force) && (force < 0 || force > 1)) {
112
+ error('force must be a positive integer and should be within 0 and 1');
113
+ return false;
114
+ }
115
+
116
+ if (
117
+ !isUndefined(floorHeight) &&
118
+ typeof floorHeight === 'number' &&
119
+ isSafeInteger(floorHeight) &&
120
+ floorHeight < 0
121
+ ) {
122
+ error('floorHeight must be a positive integer');
123
+ return false;
124
+ }
125
+
126
+ if (
127
+ !isUndefined(floorWidth) &&
128
+ typeof floorWidth === 'number' &&
129
+ isSafeInteger(floorWidth) &&
130
+ floorWidth < 0
131
+ ) {
132
+ error('floorWidth must be a positive integer');
133
+ return false;
134
+ }
135
+
136
+ return true;
137
+ }
138
+ </script>
139
+
140
+ <script lang="ts">
141
+ import { onMount } from 'svelte';
142
+
143
+ /**
144
+ * Number of confetti particles to create
145
+ *
146
+ * @default 150
147
+ *
148
+ * @example
149
+ *
150
+ * ```svelte
151
+ * <ConfettiExplosion particleCount={200} />
152
+ * ```
153
+ */
154
+ export let particleCount = PARTICLE_COUNT;
155
+
156
+ /**
157
+ * Duration of the animation in milliseconds
158
+ *
159
+ * @default 3500
160
+ *
161
+ * @example
162
+ *
163
+ * ```svelte
164
+ * <ConfettiExplosion duration={5000} />
165
+ * ```
166
+ */
167
+ export let duration = DURATION;
168
+
169
+ /**
170
+ * Colors to use for the confetti particles. Pass string array of colors. Can use hex colors, named colors,
171
+ * CSS Variables, literally anything valid in plain CSS.
172
+ *
173
+ * @default ['#FFC700', '#FF0000', '#2E3191', '#41BBC7']
174
+ *
175
+ * @example
176
+ *
177
+ * ```svelte
178
+ * <ConfettiExplosion colors={['var(--yellow)', 'var(--red)', '#2E3191', '#41BBC7']} />
179
+ * ```
180
+ */
181
+ export let colors = COLORS;
182
+
183
+ /**
184
+ * Size of the confetti particles in pixels
185
+ *
186
+ * @default 12
187
+ *
188
+ * @example
189
+ *
190
+ * ```svelte
191
+ * <ConfettiExplosion particleSize={20} />
192
+ * ```
193
+ */
194
+ export let particleSize = SIZE;
195
+
196
+ /**
197
+ * Force of the confetti particles. Between 0 and 1. 0 is no force, 1 is maximum force.
198
+ *
199
+ * @default 0.5
200
+ *
201
+ * @example
202
+ *
203
+ * ```svelte
204
+ * <ConfettiExplosion force={0.8} />
205
+ * ```
206
+ */
207
+ export let force = FORCE;
208
+
209
+ /**
210
+ * Height of the floor in pixels. Confetti will only fall within this height.
211
+ *
212
+ * @default 800
213
+ *
214
+ * @example
215
+ *
216
+ * ```svelte
217
+ * <ConfettiExplosion floorHeight={500} />
218
+ * ```
219
+ */
220
+ export let floorHeight = FLOOR_HEIGHT;
221
+
222
+ /**
223
+ * Width of the floor in pixels. Confetti will only fall within this width.
224
+ *
225
+ * @default 1600
226
+ *
227
+ * @example
228
+ *
229
+ * ```svelte
230
+ * <ConfettiExplosion floorWidth={1000} />
231
+ * ```
232
+ */
233
+ export let floorWidth = FLOOR_WIDTH;
234
+
235
+ /**
236
+ * Whether or not destroy all confetti nodes after the `duration` period has passed. By default it destroys all nodes, to preserve memory.
237
+ *
238
+ * @default true
239
+ *
240
+ * @example
241
+ *
242
+ * ```svelte
243
+ * <ConfettiExplosion shouldDestroyAfterDone={false} />
244
+ * ```
245
+ */
246
+ export let shouldDestroyAfterDone = true;
247
+
248
+ let isVisible = true;
249
+
250
+ $: particles = createParticles(particleCount, colors);
251
+
252
+ $: isValid = validate(
253
+ particleCount,
254
+ duration,
255
+ colors,
256
+ particleSize,
257
+ force,
258
+ floorHeight,
259
+ floorWidth
260
+ );
261
+
262
+ onMount(async () => {
263
+ await waitFor(duration);
264
+
265
+ if (shouldDestroyAfterDone) {
266
+ isVisible = false;
267
+ }
268
+ });
269
+
270
+ function confettiStyles(node: HTMLDivElement, { degree }: { degree: number }) {
271
+ // Get x landing point for it
272
+ const landingPoint = mapRange(
273
+ Math.abs(rotate(degree, 90) - 180),
274
+ 0,
275
+ 180,
276
+ -floorWidth / 2,
277
+ floorWidth / 2
278
+ );
279
+
280
+ // Crazy calculations for generating styles
281
+ const rotation = Math.random() * (ROTATION_SPEED_MAX - ROTATION_SPEED_MIN) + ROTATION_SPEED_MIN;
282
+ const rotationIndex = Math.round(Math.random() * (rotationTransforms.length - 1));
283
+ const durationChaos = duration - Math.round(Math.random() * 1000);
284
+ const shouldBeCrazy = Math.random() < CRAZY_PARTICLES_FREQUENCY;
285
+ const isCircle = shouldBeCircle(rotationIndex);
286
+
287
+ // x-axis disturbance, roughly the distance the particle will initially deviate from its target
288
+ const x1 = shouldBeCrazy ? round(Math.random() * CRAZY_PARTICLE_CRAZINESS, 2) : 0;
289
+ const x2 = x1 * -1;
290
+ const x3 = x1;
291
+ // x-axis arc of explosion, so 90deg and 270deg particles have curve of 1, 0deg and 180deg have 0
292
+ const x4 = round(Math.abs(mapRange(Math.abs(rotate(degree, 90) - 180), 0, 180, -1, 1)), 4);
293
+
294
+ // roughly how fast particle reaches end of its explosion curve
295
+ const y1 = round(Math.random() * BEZIER_MEDIAN, 4);
296
+ // roughly maps to the distance particle goes before reaching free-fall
297
+ const y2 = round(Math.random() * force * (coinFlip() ? 1 : -1), 4);
298
+ // roughly how soon the particle transitions from explosion to free-fall
299
+ const y3 = BEZIER_MEDIAN;
300
+ // roughly the ease of free-fall
301
+ const y4 = round(Math.max(mapRange(Math.abs(degree - 180), 0, 180, force, -force), 0), 4);
302
+
303
+ const setCSSVar = (key: string, val: string | number) => node.style.setProperty(key, val + '');
304
+
305
+ setCSSVar('--x-landing-point', `${landingPoint}px`);
306
+
307
+ setCSSVar('--duration-chaos', `${durationChaos}ms`);
308
+
309
+ setCSSVar('--x1', `${x1}`);
310
+ setCSSVar('--x2', `${x2}`);
311
+ setCSSVar('--x3', `${x3}`);
312
+ setCSSVar('--x4', `${x4}`);
313
+
314
+ setCSSVar('--y1', `${y1}`);
315
+ setCSSVar('--y2', `${y2}`);
316
+ setCSSVar('--y3', `${y3}`);
317
+ setCSSVar('--y4', `${y4}`);
318
+
319
+ // set --width and --height here
320
+ setCSSVar(
321
+ '--width',
322
+ `${isCircle ? particleSize : Math.round(Math.random() * 4) + particleSize / 2}px`
323
+ );
324
+ setCSSVar(
325
+ '--height',
326
+ (isCircle ? particleSize : Math.round(Math.random() * 2) + particleSize) + 'px'
327
+ );
328
+
329
+ setCSSVar('--rotation', `${rotationTransforms[rotationIndex].join()}`);
330
+ setCSSVar('--rotation-duration', `${rotation}ms`);
331
+ setCSSVar('--border-radius', `${isCircle ? '50%' : '0'}`);
332
+ }
333
+ </script>
334
+
335
+ {#if isVisible && isValid}
336
+ <div class="container" style="--floor-height: {floorHeight}px;">
337
+ {#each particles as { color, degree }}
338
+ <div class="particle" use:confettiStyles={{ degree }}>
339
+ <div style="--bgcolor: {color};" />
340
+ </div>
341
+ {/each}
342
+ </div>
343
+ {/if}
344
+
345
+ <style lang="scss">
346
+ @keyframes y-axis {
347
+ to {
348
+ transform: translate3d(0, var(--floor-height), 0);
349
+ }
350
+ }
351
+
352
+ @keyframes x-axis {
353
+ to {
354
+ transform: translate3d(var(--x-landing-point), 0, 0);
355
+ }
356
+ }
357
+
358
+ @keyframes rotation {
359
+ to {
360
+ transform: rotate3d(var(--rotation), 360deg);
361
+ }
362
+ }
363
+
364
+ .container {
365
+ width: 0;
366
+ height: 0;
367
+
368
+ overflow: visible;
369
+
370
+ position: relative;
371
+ z-index: 1200;
372
+ }
373
+
374
+ .particle {
375
+ animation: x-axis var(--duration-chaos) forwards
376
+ cubic-bezier(var(--x1), var(--x2), var(--x3), var(--x4));
377
+
378
+ div {
379
+ position: absolute;
380
+ top: 0;
381
+ left: 0;
382
+
383
+ animation: y-axis var(--duration-chaos) forwards
384
+ cubic-bezier(var(--y1), var(--y2), var(--y3), var(--y4));
385
+
386
+ width: var(--width);
387
+ height: var(--height);
388
+
389
+ &:before {
390
+ display: block;
391
+
392
+ height: 100%;
393
+ width: 100%;
394
+
395
+ content: '';
396
+ background-color: var(--bgcolor);
397
+
398
+ animation: rotation var(--rotation-duration) infinite linear;
399
+
400
+ border-radius: var(--border-radius);
401
+ }
402
+ }
403
+ }
404
+ </style>
@@ -0,0 +1 @@
1
+ export { default as ConfettiExplosion } from './ConfettiExplosion.svelte';
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import { ConfettiExplosion } from '$lib';
3
+ </script>
4
+
5
+ <div>
6
+ <ConfettiExplosion particleCount={100} force={0.3} />
7
+ </div>
8
+
9
+ <style lang="scss">
10
+ :global(body) {
11
+ overflow: hidden;
12
+ }
13
+
14
+ div {
15
+ position: absolute;
16
+ top: 20%;
17
+ left: 40%;
18
+ right: 0;
19
+ bottom: 0;
20
+ background: rgba(255, 255, 255, 0.5);
21
+ }
22
+ </style>
@@ -0,0 +1,5 @@
1
+ <script>
2
+ import ConfettiExplosion from '$lib/ConfettiExplosion.svelte';
3
+ </script>
4
+
5
+ <ConfettiExplosion force={0.31} particleCount={10} />
Binary file
@@ -0,0 +1,23 @@
1
+ import preprocess from 'svelte-preprocess';
2
+
3
+ /** @type {import('@sveltejs/kit').Config} */
4
+ const config = {
5
+ // Consult https://github.com/sveltejs/svelte-preprocess
6
+ // for more information about preprocessors
7
+ preprocess: preprocess(),
8
+
9
+ kit: {
10
+ // hydrate the <div id="svelte"> element in src/app.html
11
+ target: '#svelte',
12
+
13
+ package: {
14
+ emitTypes: true,
15
+ dir: 'package',
16
+ exports: (file) => {
17
+ return file === 'index.ts';
18
+ },
19
+ },
20
+ },
21
+ };
22
+
23
+ export default config;
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "moduleResolution": "node",
4
+ "module": "es2020",
5
+ "lib": ["es2020", "DOM"],
6
+ "target": "es2020",
7
+ /**
8
+ svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
9
+ to enforce using \`import type\` instead of \`import\` for Types.
10
+ */
11
+ "importsNotUsedAsValues": "error",
12
+ "isolatedModules": true,
13
+ "resolveJsonModule": true,
14
+ /**
15
+ To have warnings/errors of the Svelte compiler at the correct position,
16
+ enable source maps by default.
17
+ */
18
+ "sourceMap": true,
19
+ "esModuleInterop": true,
20
+ "skipLibCheck": true,
21
+ "forceConsistentCasingInFileNames": true,
22
+ "baseUrl": ".",
23
+ "allowJs": true,
24
+ "checkJs": true,
25
+ "paths": {
26
+ "$lib": ["src/lib"],
27
+ "$lib/*": ["src/lib/*"]
28
+ }
29
+ },
30
+ "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
31
+ }