svelte-confetti-explosion 0.0.1 → 0.0.5
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.
- package/ConfettiExplosion.svelte +302 -0
- package/ConfettiExplosion.svelte.d.ts +104 -0
- package/{src/lib/index.ts → index.d.ts} +0 -0
- package/index.js +1 -0
- package/package.json +9 -11
- package/.prettierrc +0 -5
- package/pnpm-lock.yaml +0 -924
- package/src/app.html +0 -12
- package/src/global.d.ts +0 -1
- package/src/lib/ConfettiExplosion.svelte +0 -404
- package/src/routes/index.svelte +0 -22
- package/src/routes/page.svelte +0 -5
- package/static/favicon.png +0 -0
- package/svelte.config.js +0 -23
- package/tsconfig.json +0 -31
@@ -0,0 +1,302 @@
|
|
1
|
+
<script context="module">const ROTATION_SPEED_MIN = 200; // minimum possible duration of single particle full rotation
|
2
|
+
const ROTATION_SPEED_MAX = 800; // maximum possible duration of single particle full rotation
|
3
|
+
const CRAZY_PARTICLES_FREQUENCY = 0.1; // 0-1 frequency of crazy curvy unpredictable particles
|
4
|
+
const CRAZY_PARTICLE_CRAZINESS = 0.3; // 0-1 how crazy these crazy particles are
|
5
|
+
const BEZIER_MEDIAN = 0.5; // utility for mid-point bezier curves, to ensure smooth motion paths
|
6
|
+
const FORCE = 0.5; // 0-1 roughly the vertical force at which particles initially explode
|
7
|
+
const SIZE = 12; // max height for particle rectangles, diameter for particle circles
|
8
|
+
const FLOOR_HEIGHT = 800; // pixels the particles will fall from initial explosion point
|
9
|
+
const FLOOR_WIDTH = 1600; // horizontal spread of particles in pixels
|
10
|
+
const PARTICLE_COUNT = 150;
|
11
|
+
const DURATION = 3500;
|
12
|
+
const COLORS = ['#FFC700', '#FF0000', '#2E3191', '#41BBC7'];
|
13
|
+
const createParticles = (count, colors) => {
|
14
|
+
const increment = 360 / count;
|
15
|
+
return Array.from({ length: count }, (_, i) => ({
|
16
|
+
color: colors[i % colors.length],
|
17
|
+
degree: i * increment,
|
18
|
+
}));
|
19
|
+
};
|
20
|
+
const waitFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
21
|
+
// From here: https://stackoverflow.com/a/11832950
|
22
|
+
function round(num, precision = 2) {
|
23
|
+
return Math.round((num + Number.EPSILON) * 10 ** precision) / 10 ** precision;
|
24
|
+
}
|
25
|
+
function arraysEqual(a, b) {
|
26
|
+
if (a === b)
|
27
|
+
return true;
|
28
|
+
if (a == null || b == null)
|
29
|
+
return false;
|
30
|
+
if (a.length !== b.length)
|
31
|
+
return false;
|
32
|
+
for (let i = 0; i < a.length; i++)
|
33
|
+
if (a[i] !== b[i])
|
34
|
+
return false;
|
35
|
+
return true;
|
36
|
+
}
|
37
|
+
const mapRange = (value, x1, y1, x2, y2) => ((value - x1) * (y2 - x2)) / (y1 - x1) + x2;
|
38
|
+
const rotate = (degree, amount) => {
|
39
|
+
const result = degree + amount;
|
40
|
+
return result > 360 ? result - 360 : result;
|
41
|
+
};
|
42
|
+
const coinFlip = () => Math.random() > 0.5;
|
43
|
+
// avoid this for circles, as it will have no visual effect
|
44
|
+
const zAxisRotation = [0, 0, 1];
|
45
|
+
const rotationTransforms = [
|
46
|
+
// dual axis rotations (a bit more realistic)
|
47
|
+
[1, 1, 0],
|
48
|
+
[1, 0, 1],
|
49
|
+
[0, 1, 1],
|
50
|
+
// single axis rotations (a bit dumber)
|
51
|
+
[1, 0, 0],
|
52
|
+
[0, 1, 0],
|
53
|
+
zAxisRotation,
|
54
|
+
];
|
55
|
+
const shouldBeCircle = (rotationIndex) => !arraysEqual(rotationTransforms[rotationIndex], zAxisRotation) && coinFlip();
|
56
|
+
const isUndefined = (value) => typeof value === 'undefined';
|
57
|
+
const error = (message) => {
|
58
|
+
console.error(message);
|
59
|
+
};
|
60
|
+
function validate(particleCount, duration, colors, particleSize, force, floorHeight, floorWidth) {
|
61
|
+
const isSafeInteger = Number.isSafeInteger;
|
62
|
+
if (!isUndefined(particleCount) && isSafeInteger(particleCount) && particleCount < 0) {
|
63
|
+
error('particleCount must be a positive integer');
|
64
|
+
return false;
|
65
|
+
}
|
66
|
+
if (!isUndefined(duration) && isSafeInteger(duration) && duration < 0) {
|
67
|
+
error('duration must be a positive integer');
|
68
|
+
return false;
|
69
|
+
}
|
70
|
+
if (!isUndefined(colors) && !Array.isArray(colors)) {
|
71
|
+
error('colors must be an array of strings');
|
72
|
+
return false;
|
73
|
+
}
|
74
|
+
if (!isUndefined(particleSize) && isSafeInteger(particleSize) && particleSize < 0) {
|
75
|
+
error('particleSize must be a positive integer');
|
76
|
+
return false;
|
77
|
+
}
|
78
|
+
if (!isUndefined(force) && isSafeInteger(force) && (force < 0 || force > 1)) {
|
79
|
+
error('force must be a positive integer and should be within 0 and 1');
|
80
|
+
return false;
|
81
|
+
}
|
82
|
+
if (!isUndefined(floorHeight) &&
|
83
|
+
typeof floorHeight === 'number' &&
|
84
|
+
isSafeInteger(floorHeight) &&
|
85
|
+
floorHeight < 0) {
|
86
|
+
error('floorHeight must be a positive integer');
|
87
|
+
return false;
|
88
|
+
}
|
89
|
+
if (!isUndefined(floorWidth) &&
|
90
|
+
typeof floorWidth === 'number' &&
|
91
|
+
isSafeInteger(floorWidth) &&
|
92
|
+
floorWidth < 0) {
|
93
|
+
error('floorWidth must be a positive integer');
|
94
|
+
return false;
|
95
|
+
}
|
96
|
+
return true;
|
97
|
+
}
|
98
|
+
</script>
|
99
|
+
|
100
|
+
<script >import { onMount } from 'svelte';
|
101
|
+
/**
|
102
|
+
* Number of confetti particles to create
|
103
|
+
*
|
104
|
+
* @default 150
|
105
|
+
*
|
106
|
+
* @example
|
107
|
+
*
|
108
|
+
* ```svelte
|
109
|
+
* <ConfettiExplosion particleCount={200} />
|
110
|
+
* ```
|
111
|
+
*/
|
112
|
+
export let particleCount = PARTICLE_COUNT;
|
113
|
+
/**
|
114
|
+
* Duration of the animation in milliseconds
|
115
|
+
*
|
116
|
+
* @default 3500
|
117
|
+
*
|
118
|
+
* @example
|
119
|
+
*
|
120
|
+
* ```svelte
|
121
|
+
* <ConfettiExplosion duration={5000} />
|
122
|
+
* ```
|
123
|
+
*/
|
124
|
+
export let duration = DURATION;
|
125
|
+
/**
|
126
|
+
* Colors to use for the confetti particles. Pass string array of colors. Can use hex colors, named colors,
|
127
|
+
* CSS Variables, literally anything valid in plain CSS.
|
128
|
+
*
|
129
|
+
* @default ['#FFC700', '#FF0000', '#2E3191', '#41BBC7']
|
130
|
+
*
|
131
|
+
* @example
|
132
|
+
*
|
133
|
+
* ```svelte
|
134
|
+
* <ConfettiExplosion colors={['var(--yellow)', 'var(--red)', '#2E3191', '#41BBC7']} />
|
135
|
+
* ```
|
136
|
+
*/
|
137
|
+
export let colors = COLORS;
|
138
|
+
/**
|
139
|
+
* Size of the confetti particles in pixels
|
140
|
+
*
|
141
|
+
* @default 12
|
142
|
+
*
|
143
|
+
* @example
|
144
|
+
*
|
145
|
+
* ```svelte
|
146
|
+
* <ConfettiExplosion particleSize={20} />
|
147
|
+
* ```
|
148
|
+
*/
|
149
|
+
export let particleSize = SIZE;
|
150
|
+
/**
|
151
|
+
* Force of the confetti particles. Between 0 and 1. 0 is no force, 1 is maximum force.
|
152
|
+
*
|
153
|
+
* @default 0.5
|
154
|
+
*
|
155
|
+
* @example
|
156
|
+
*
|
157
|
+
* ```svelte
|
158
|
+
* <ConfettiExplosion force={0.8} />
|
159
|
+
* ```
|
160
|
+
*/
|
161
|
+
export let force = FORCE;
|
162
|
+
/**
|
163
|
+
* Height of the floor in pixels. Confetti will only fall within this height.
|
164
|
+
*
|
165
|
+
* @default 800
|
166
|
+
*
|
167
|
+
* @example
|
168
|
+
*
|
169
|
+
* ```svelte
|
170
|
+
* <ConfettiExplosion floorHeight={500} />
|
171
|
+
* ```
|
172
|
+
*/
|
173
|
+
export let floorHeight = FLOOR_HEIGHT;
|
174
|
+
/**
|
175
|
+
* Width of the floor in pixels. Confetti will only fall within this width.
|
176
|
+
*
|
177
|
+
* @default 1600
|
178
|
+
*
|
179
|
+
* @example
|
180
|
+
*
|
181
|
+
* ```svelte
|
182
|
+
* <ConfettiExplosion floorWidth={1000} />
|
183
|
+
* ```
|
184
|
+
*/
|
185
|
+
export let floorWidth = FLOOR_WIDTH;
|
186
|
+
/**
|
187
|
+
* Whether or not destroy all confetti nodes after the `duration` period has passed. By default it destroys all nodes, to preserve memory.
|
188
|
+
*
|
189
|
+
* @default true
|
190
|
+
*
|
191
|
+
* @example
|
192
|
+
*
|
193
|
+
* ```svelte
|
194
|
+
* <ConfettiExplosion shouldDestroyAfterDone={false} />
|
195
|
+
* ```
|
196
|
+
*/
|
197
|
+
export let shouldDestroyAfterDone = true;
|
198
|
+
let isVisible = true;
|
199
|
+
$: particles = createParticles(particleCount, colors);
|
200
|
+
$: isValid = validate(particleCount, duration, colors, particleSize, force, floorHeight, floorWidth);
|
201
|
+
onMount(async () => {
|
202
|
+
await waitFor(duration);
|
203
|
+
if (shouldDestroyAfterDone) {
|
204
|
+
isVisible = false;
|
205
|
+
}
|
206
|
+
});
|
207
|
+
function confettiStyles(node, { degree }) {
|
208
|
+
// Get x landing point for it
|
209
|
+
const landingPoint = mapRange(Math.abs(rotate(degree, 90) - 180), 0, 180, -floorWidth / 2, floorWidth / 2);
|
210
|
+
// Crazy calculations for generating styles
|
211
|
+
const rotation = Math.random() * (ROTATION_SPEED_MAX - ROTATION_SPEED_MIN) + ROTATION_SPEED_MIN;
|
212
|
+
const rotationIndex = Math.round(Math.random() * (rotationTransforms.length - 1));
|
213
|
+
const durationChaos = duration - Math.round(Math.random() * 1000);
|
214
|
+
const shouldBeCrazy = Math.random() < CRAZY_PARTICLES_FREQUENCY;
|
215
|
+
const isCircle = shouldBeCircle(rotationIndex);
|
216
|
+
// x-axis disturbance, roughly the distance the particle will initially deviate from its target
|
217
|
+
const x1 = shouldBeCrazy ? round(Math.random() * CRAZY_PARTICLE_CRAZINESS, 2) : 0;
|
218
|
+
const x2 = x1 * -1;
|
219
|
+
const x3 = x1;
|
220
|
+
// x-axis arc of explosion, so 90deg and 270deg particles have curve of 1, 0deg and 180deg have 0
|
221
|
+
const x4 = round(Math.abs(mapRange(Math.abs(rotate(degree, 90) - 180), 0, 180, -1, 1)), 4);
|
222
|
+
// roughly how fast particle reaches end of its explosion curve
|
223
|
+
const y1 = round(Math.random() * BEZIER_MEDIAN, 4);
|
224
|
+
// roughly maps to the distance particle goes before reaching free-fall
|
225
|
+
const y2 = round(Math.random() * force * (coinFlip() ? 1 : -1), 4);
|
226
|
+
// roughly how soon the particle transitions from explosion to free-fall
|
227
|
+
const y3 = BEZIER_MEDIAN;
|
228
|
+
// roughly the ease of free-fall
|
229
|
+
const y4 = round(Math.max(mapRange(Math.abs(degree - 180), 0, 180, force, -force), 0), 4);
|
230
|
+
const setCSSVar = (key, val) => node.style.setProperty(key, val + '');
|
231
|
+
setCSSVar('--x-landing-point', `${landingPoint}px`);
|
232
|
+
setCSSVar('--duration-chaos', `${durationChaos}ms`);
|
233
|
+
setCSSVar('--x1', `${x1}`);
|
234
|
+
setCSSVar('--x2', `${x2}`);
|
235
|
+
setCSSVar('--x3', `${x3}`);
|
236
|
+
setCSSVar('--x4', `${x4}`);
|
237
|
+
setCSSVar('--y1', `${y1}`);
|
238
|
+
setCSSVar('--y2', `${y2}`);
|
239
|
+
setCSSVar('--y3', `${y3}`);
|
240
|
+
setCSSVar('--y4', `${y4}`);
|
241
|
+
// set --width and --height here
|
242
|
+
setCSSVar('--width', `${isCircle ? particleSize : Math.round(Math.random() * 4) + particleSize / 2}px`);
|
243
|
+
setCSSVar('--height', (isCircle ? particleSize : Math.round(Math.random() * 2) + particleSize) + 'px');
|
244
|
+
setCSSVar('--rotation', `${rotationTransforms[rotationIndex].join()}`);
|
245
|
+
setCSSVar('--rotation-duration', `${rotation}ms`);
|
246
|
+
setCSSVar('--border-radius', `${isCircle ? '50%' : '0'}`);
|
247
|
+
}
|
248
|
+
</script>
|
249
|
+
|
250
|
+
{#if isVisible && isValid}
|
251
|
+
<div class="container" style="--floor-height: {floorHeight}px;">
|
252
|
+
{#each particles as { color, degree }}
|
253
|
+
<div class="particle" use:confettiStyles={{ degree }}>
|
254
|
+
<div style="--bgcolor: {color};" />
|
255
|
+
</div>
|
256
|
+
{/each}
|
257
|
+
</div>
|
258
|
+
{/if}
|
259
|
+
|
260
|
+
<style >@keyframes y-axis {
|
261
|
+
to {
|
262
|
+
transform: translate3d(0, var(--floor-height), 0);
|
263
|
+
}
|
264
|
+
}
|
265
|
+
@keyframes x-axis {
|
266
|
+
to {
|
267
|
+
transform: translate3d(var(--x-landing-point), 0, 0);
|
268
|
+
}
|
269
|
+
}
|
270
|
+
@keyframes rotation {
|
271
|
+
to {
|
272
|
+
transform: rotate3d(var(--rotation), 360deg);
|
273
|
+
}
|
274
|
+
}
|
275
|
+
.container {
|
276
|
+
width: 0;
|
277
|
+
height: 0;
|
278
|
+
overflow: visible;
|
279
|
+
position: relative;
|
280
|
+
z-index: 1200;
|
281
|
+
}
|
282
|
+
|
283
|
+
.particle {
|
284
|
+
animation: x-axis var(--duration-chaos) forwards cubic-bezier(var(--x1), var(--x2), var(--x3), var(--x4));
|
285
|
+
}
|
286
|
+
.particle div {
|
287
|
+
position: absolute;
|
288
|
+
top: 0;
|
289
|
+
left: 0;
|
290
|
+
animation: y-axis var(--duration-chaos) forwards cubic-bezier(var(--y1), var(--y2), var(--y3), var(--y4));
|
291
|
+
width: var(--width);
|
292
|
+
height: var(--height);
|
293
|
+
}
|
294
|
+
.particle div:before {
|
295
|
+
display: block;
|
296
|
+
height: 100%;
|
297
|
+
width: 100%;
|
298
|
+
content: "";
|
299
|
+
background-color: var(--bgcolor);
|
300
|
+
animation: rotation var(--rotation-duration) infinite linear;
|
301
|
+
border-radius: var(--border-radius);
|
302
|
+
}</style>
|
@@ -0,0 +1,104 @@
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
2
|
+
declare const __propDef: {
|
3
|
+
props: {
|
4
|
+
/**
|
5
|
+
* Number of confetti particles to create
|
6
|
+
*
|
7
|
+
* @default 150
|
8
|
+
*
|
9
|
+
* @example
|
10
|
+
*
|
11
|
+
* ```svelte
|
12
|
+
* <ConfettiExplosion particleCount={200} />
|
13
|
+
* ```
|
14
|
+
*/ particleCount?: number;
|
15
|
+
/**
|
16
|
+
* Duration of the animation in milliseconds
|
17
|
+
*
|
18
|
+
* @default 3500
|
19
|
+
*
|
20
|
+
* @example
|
21
|
+
*
|
22
|
+
* ```svelte
|
23
|
+
* <ConfettiExplosion duration={5000} />
|
24
|
+
* ```
|
25
|
+
*/ duration?: number;
|
26
|
+
/**
|
27
|
+
* Colors to use for the confetti particles. Pass string array of colors. Can use hex colors, named colors,
|
28
|
+
* CSS Variables, literally anything valid in plain CSS.
|
29
|
+
*
|
30
|
+
* @default ['#FFC700', '#FF0000', '#2E3191', '#41BBC7']
|
31
|
+
*
|
32
|
+
* @example
|
33
|
+
*
|
34
|
+
* ```svelte
|
35
|
+
* <ConfettiExplosion colors={['var(--yellow)', 'var(--red)', '#2E3191', '#41BBC7']} />
|
36
|
+
* ```
|
37
|
+
*/ colors?: string[];
|
38
|
+
/**
|
39
|
+
* Size of the confetti particles in pixels
|
40
|
+
*
|
41
|
+
* @default 12
|
42
|
+
*
|
43
|
+
* @example
|
44
|
+
*
|
45
|
+
* ```svelte
|
46
|
+
* <ConfettiExplosion particleSize={20} />
|
47
|
+
* ```
|
48
|
+
*/ particleSize?: number;
|
49
|
+
/**
|
50
|
+
* Force of the confetti particles. Between 0 and 1. 0 is no force, 1 is maximum force.
|
51
|
+
*
|
52
|
+
* @default 0.5
|
53
|
+
*
|
54
|
+
* @example
|
55
|
+
*
|
56
|
+
* ```svelte
|
57
|
+
* <ConfettiExplosion force={0.8} />
|
58
|
+
* ```
|
59
|
+
*/ force?: number;
|
60
|
+
/**
|
61
|
+
* Height of the floor in pixels. Confetti will only fall within this height.
|
62
|
+
*
|
63
|
+
* @default 800
|
64
|
+
*
|
65
|
+
* @example
|
66
|
+
*
|
67
|
+
* ```svelte
|
68
|
+
* <ConfettiExplosion floorHeight={500} />
|
69
|
+
* ```
|
70
|
+
*/ floorHeight?: number;
|
71
|
+
/**
|
72
|
+
* Width of the floor in pixels. Confetti will only fall within this width.
|
73
|
+
*
|
74
|
+
* @default 1600
|
75
|
+
*
|
76
|
+
* @example
|
77
|
+
*
|
78
|
+
* ```svelte
|
79
|
+
* <ConfettiExplosion floorWidth={1000} />
|
80
|
+
* ```
|
81
|
+
*/ floorWidth?: number;
|
82
|
+
/**
|
83
|
+
* Whether or not destroy all confetti nodes after the `duration` period has passed. By default it destroys all nodes, to preserve memory.
|
84
|
+
*
|
85
|
+
* @default true
|
86
|
+
*
|
87
|
+
* @example
|
88
|
+
*
|
89
|
+
* ```svelte
|
90
|
+
* <ConfettiExplosion shouldDestroyAfterDone={false} />
|
91
|
+
* ```
|
92
|
+
*/ shouldDestroyAfterDone?: boolean;
|
93
|
+
};
|
94
|
+
events: {
|
95
|
+
[evt: string]: CustomEvent<any>;
|
96
|
+
};
|
97
|
+
slots: {};
|
98
|
+
};
|
99
|
+
export declare type ConfettiExplosionProps = typeof __propDef.props;
|
100
|
+
export declare type ConfettiExplosionEvents = typeof __propDef.events;
|
101
|
+
export declare type ConfettiExplosionSlots = typeof __propDef.slots;
|
102
|
+
export default class ConfettiExplosion extends SvelteComponentTyped<ConfettiExplosionProps, ConfettiExplosionEvents, ConfettiExplosionSlots> {
|
103
|
+
}
|
104
|
+
export {};
|
File without changes
|
package/index.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export { default as ConfettiExplosion } from './ConfettiExplosion.svelte';
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "svelte-confetti-explosion",
|
3
|
-
"version": "0.0.
|
4
|
-
"svelte": "
|
3
|
+
"version": "0.0.5",
|
4
|
+
"svelte": "index.js",
|
5
5
|
"devDependencies": {
|
6
6
|
"@sveltejs/kit": "^1.0.0-next.197",
|
7
7
|
"prettier": "^2.4.1",
|
@@ -15,14 +15,12 @@
|
|
15
15
|
"typescript": "^4.5.2"
|
16
16
|
},
|
17
17
|
"type": "module",
|
18
|
-
"
|
19
|
-
"
|
20
|
-
|
21
|
-
|
22
|
-
"
|
23
|
-
"
|
24
|
-
"
|
25
|
-
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. .",
|
26
|
-
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
18
|
+
"dependencies": {
|
19
|
+
"svelte-confetti-explosion": "0.0.4"
|
20
|
+
},
|
21
|
+
"exports": {
|
22
|
+
"./package.json": "./package.json",
|
23
|
+
"./ConfettiExplosion.svelte": "./ConfettiExplosion.svelte",
|
24
|
+
".": "./index.js"
|
27
25
|
}
|
28
26
|
}
|