@farcaster/snap 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/react/snap-view-core.js +107 -17
- package/dist/react-native/fireworks-overlay.d.ts +1 -0
- package/dist/react-native/fireworks-overlay.js +125 -0
- package/dist/react-native/snap-view-core.js +7 -2
- package/dist/schemas.d.ts +1 -0
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/react/snap-view-core.tsx +152 -28
- package/src/react-native/fireworks-overlay.tsx +176 -0
- package/src/react-native/snap-view-core.tsx +6 -1
package/dist/constants.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const SUPPORTED_SPEC_VERSIONS: readonly ["1.0", "2.0"];
|
|
|
5
5
|
export type SpecVersion = (typeof SUPPORTED_SPEC_VERSIONS)[number];
|
|
6
6
|
export declare const SNAP_PAYLOAD_HEADER: "X-Snap-Payload";
|
|
7
7
|
export declare const MEDIA_TYPE: "application/vnd.farcaster.snap+json";
|
|
8
|
-
export declare const EFFECT_VALUES: readonly ["confetti"];
|
|
8
|
+
export declare const EFFECT_VALUES: readonly ["confetti", "fireworks"];
|
|
9
9
|
export declare const POST_GRID_TAP_KEY: "grid_tap";
|
|
10
10
|
export declare const GRID_MIN_COLS = 2;
|
|
11
11
|
export declare const GRID_MAX_COLS = 32;
|
package/dist/constants.js
CHANGED
|
@@ -7,7 +7,7 @@ export const SUPPORTED_SPEC_VERSIONS = [
|
|
|
7
7
|
];
|
|
8
8
|
export const SNAP_PAYLOAD_HEADER = "X-Snap-Payload";
|
|
9
9
|
export const MEDIA_TYPE = "application/vnd.farcaster.snap+json";
|
|
10
|
-
export const EFFECT_VALUES = ["confetti"];
|
|
10
|
+
export const EFFECT_VALUES = ["confetti", "fireworks"];
|
|
11
11
|
// ─── Pixel grid ────────────────────────────────────────
|
|
12
12
|
export const POST_GRID_TAP_KEY = "grid_tap";
|
|
13
13
|
export const GRID_MIN_COLS = 2;
|
|
@@ -47,33 +47,119 @@ const CONFETTI_COLORS = [
|
|
|
47
47
|
"#EF4444",
|
|
48
48
|
"#06B6D4",
|
|
49
49
|
];
|
|
50
|
+
const FIREWORK_COLORS = [
|
|
51
|
+
"#FFD700",
|
|
52
|
+
"#FF6B6B",
|
|
53
|
+
"#4ECDC4",
|
|
54
|
+
"#C4A7E7",
|
|
55
|
+
"#F6C177",
|
|
56
|
+
"#EBBCBA",
|
|
57
|
+
"#9CCFD8",
|
|
58
|
+
"#fff",
|
|
59
|
+
];
|
|
50
60
|
function ConfettiOverlay() {
|
|
51
|
-
const pieces = useMemo(() => Array.from({ length: 80 }, (_, i) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
const pieces = useMemo(() => Array.from({ length: 80 }, (_, i) => {
|
|
62
|
+
const driftX = (Math.random() - 0.5) * 120;
|
|
63
|
+
return {
|
|
64
|
+
id: i,
|
|
65
|
+
left: Math.random() * 100,
|
|
66
|
+
delay: Math.random() * 1.2,
|
|
67
|
+
duration: 2.8 + Math.random() * 1.8,
|
|
68
|
+
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
|
|
69
|
+
size: 6 + Math.random() * 8,
|
|
70
|
+
rotation: Math.random() * 360,
|
|
71
|
+
isCircle: Math.random() > 0.6,
|
|
72
|
+
driftX,
|
|
73
|
+
driftMid: -driftX * 0.4,
|
|
74
|
+
};
|
|
75
|
+
}), []);
|
|
60
76
|
return (_jsxs("div", { style: {
|
|
61
77
|
position: "absolute",
|
|
62
78
|
inset: 0,
|
|
63
79
|
overflow: "hidden",
|
|
64
80
|
pointerEvents: "none",
|
|
65
81
|
zIndex: 20,
|
|
66
|
-
}, children: [pieces.map(({ id, left, delay, duration, color, size, rotation }) => (_jsx("div", { style: {
|
|
82
|
+
}, children: [pieces.map(({ id, left, delay, duration, color, size, rotation, isCircle, driftX, driftMid }) => (_jsx("div", { style: {
|
|
67
83
|
position: "absolute",
|
|
68
84
|
left: `${left}%`,
|
|
69
85
|
top: -20,
|
|
70
86
|
width: size,
|
|
71
|
-
height: size * 0.
|
|
87
|
+
height: isCircle ? size : size * 0.5,
|
|
72
88
|
backgroundColor: color,
|
|
73
|
-
borderRadius: 2,
|
|
74
|
-
transform: `
|
|
75
|
-
animation: `confettiFall ${duration}s
|
|
76
|
-
|
|
89
|
+
borderRadius: isCircle ? "50%" : 2,
|
|
90
|
+
transform: `rotateZ(${rotation}deg)`,
|
|
91
|
+
animation: `confettiFall ${duration}s cubic-bezier(0.25,0,0.75,1) ${delay}s forwards`,
|
|
92
|
+
"--dx": `${driftX}px`,
|
|
93
|
+
"--dm": `${driftMid}px`,
|
|
94
|
+
} }, id))), _jsx("style", { children: `@keyframes confettiFall{
|
|
95
|
+
0% {top:-20px;opacity:1;transform:rotateZ(0deg) rotateY(0deg) translateX(0)}
|
|
96
|
+
20% {transform:rotateZ(144deg) rotateY(60deg) translateX(var(--dm))}
|
|
97
|
+
40% {transform:rotateZ(288deg) rotateY(120deg) translateX(0)}
|
|
98
|
+
60% {opacity:1;transform:rotateZ(432deg) rotateY(200deg) translateX(calc(-1 * var(--dm)))}
|
|
99
|
+
80% {transform:rotateZ(576deg) rotateY(280deg) translateX(var(--dx))}
|
|
100
|
+
100%{top:110%;opacity:0;transform:rotateZ(720deg) rotateY(360deg) translateX(var(--dx))}
|
|
101
|
+
}` })] }));
|
|
102
|
+
}
|
|
103
|
+
function FireworksOverlay() {
|
|
104
|
+
const bursts = useMemo(() => Array.from({ length: 5 }, (_, b) => ({
|
|
105
|
+
id: b,
|
|
106
|
+
x: 15 + Math.random() * 70,
|
|
107
|
+
y: 10 + Math.random() * 50,
|
|
108
|
+
delay: b * 0.5 + Math.random() * 0.2,
|
|
109
|
+
particles: Array.from({ length: 24 }, (_, p) => {
|
|
110
|
+
const angle = (p / 24) * Math.PI * 2 + (Math.random() - 0.5) * 0.2;
|
|
111
|
+
const dist = 55 + Math.random() * 60;
|
|
112
|
+
return {
|
|
113
|
+
id: p,
|
|
114
|
+
vx: Math.cos(angle) * dist,
|
|
115
|
+
vy: Math.sin(angle) * dist,
|
|
116
|
+
color: FIREWORK_COLORS[Math.floor(Math.random() * FIREWORK_COLORS.length)],
|
|
117
|
+
size: 3 + Math.random() * 3,
|
|
118
|
+
};
|
|
119
|
+
}),
|
|
120
|
+
})), []);
|
|
121
|
+
return (_jsxs("div", { style: {
|
|
122
|
+
position: "absolute",
|
|
123
|
+
inset: 0,
|
|
124
|
+
overflow: "hidden",
|
|
125
|
+
pointerEvents: "none",
|
|
126
|
+
zIndex: 20,
|
|
127
|
+
}, children: [bursts.map(({ id: bid, x, y, delay, particles }) => (_jsxs("div", { children: [_jsx("div", { style: {
|
|
128
|
+
position: "absolute",
|
|
129
|
+
left: `${x}%`,
|
|
130
|
+
top: `${y}%`,
|
|
131
|
+
width: 12,
|
|
132
|
+
height: 12,
|
|
133
|
+
borderRadius: "50%",
|
|
134
|
+
backgroundColor: "#fff",
|
|
135
|
+
transform: "translate(-50%,-50%)",
|
|
136
|
+
animation: `fwFlash 0.4s ease-out ${delay}s both`,
|
|
137
|
+
opacity: 0,
|
|
138
|
+
} }), particles.map(({ id: pid, vx, vy, color, size }) => (_jsx("div", { style: {
|
|
139
|
+
position: "absolute",
|
|
140
|
+
left: `${x}%`,
|
|
141
|
+
top: `${y}%`,
|
|
142
|
+
width: size,
|
|
143
|
+
height: size,
|
|
144
|
+
borderRadius: "50%",
|
|
145
|
+
backgroundColor: color,
|
|
146
|
+
transform: "translate(-50%,-50%)",
|
|
147
|
+
animation: `fwBurst 1s cubic-bezier(0.2,0,0.8,1) ${delay}s forwards`,
|
|
148
|
+
opacity: 0,
|
|
149
|
+
"--vx": `${vx}px`,
|
|
150
|
+
"--vy": `${vy}px`,
|
|
151
|
+
} }, pid)))] }, bid))), _jsx("style", { children: `
|
|
152
|
+
@keyframes fwFlash{
|
|
153
|
+
0% {opacity:0;transform:translate(-50%,-50%) scale(0)}
|
|
154
|
+
25% {opacity:1;transform:translate(-50%,-50%) scale(2.5)}
|
|
155
|
+
100%{opacity:0;transform:translate(-50%,-50%) scale(5)}
|
|
156
|
+
}
|
|
157
|
+
@keyframes fwBurst{
|
|
158
|
+
0% {opacity:1;transform:translate(-50%,-50%) translate(0,0) scale(1)}
|
|
159
|
+
65% {opacity:1}
|
|
160
|
+
100%{opacity:0;transform:translate(-50%,-50%) translate(var(--vx),var(--vy)) scale(0)}
|
|
161
|
+
}
|
|
162
|
+
` })] }));
|
|
77
163
|
}
|
|
78
164
|
export function SnapLoadingOverlay({ appearance, accentHex, active, }) {
|
|
79
165
|
const isDark = appearance === "dark";
|
|
@@ -156,11 +242,15 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
156
242
|
setPageKey((k) => k + 1);
|
|
157
243
|
}, [spec]);
|
|
158
244
|
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
245
|
+
const showFireworks = snap.effects?.includes("fireworks") ?? false;
|
|
159
246
|
const [confettiKey, setConfettiKey] = useState(0);
|
|
247
|
+
const [fireworksKey, setFireworksKey] = useState(0);
|
|
160
248
|
useEffect(() => {
|
|
161
249
|
if (showConfetti)
|
|
162
250
|
setConfettiKey((k) => k + 1);
|
|
163
|
-
|
|
251
|
+
if (showFireworks)
|
|
252
|
+
setFireworksKey((k) => k + 1);
|
|
253
|
+
}, [showConfetti, showFireworks, snap]);
|
|
164
254
|
const accentName = snap.theme?.accent ?? "purple";
|
|
165
255
|
const accentHex = useMemo(() => resolveSnapPaletteHex(accentName, appearance), [accentName, appearance]);
|
|
166
256
|
const previewSurfaceStyle = useMemo(() => {
|
|
@@ -226,7 +316,7 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
226
316
|
break;
|
|
227
317
|
}
|
|
228
318
|
}, [handlers]);
|
|
229
|
-
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
319
|
+
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), showFireworks && _jsx(FireworksOverlay, {}, fireworksKey), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
230
320
|
applyStatePaths(stateRef.current, changes);
|
|
231
321
|
}, onAction: handleAction }, pageKey) }) })] }));
|
|
232
322
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function FireworksOverlay(): import("react").JSX.Element;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Animated, StyleSheet, View, useWindowDimensions } from "react-native";
|
|
4
|
+
const FIREWORK_COLORS = [
|
|
5
|
+
"#FFD700",
|
|
6
|
+
"#FF6B6B",
|
|
7
|
+
"#4ECDC4",
|
|
8
|
+
"#C4A7E7",
|
|
9
|
+
"#F6C177",
|
|
10
|
+
"#EBBCBA",
|
|
11
|
+
"#9CCFD8",
|
|
12
|
+
"#fff",
|
|
13
|
+
];
|
|
14
|
+
const BURST_COUNT = 5;
|
|
15
|
+
const PARTICLE_COUNT = 24;
|
|
16
|
+
function FireworkBurst({ burst }) {
|
|
17
|
+
const flashAnim = useRef(new Animated.Value(0)).current;
|
|
18
|
+
const burstAnim = useRef(new Animated.Value(0)).current;
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const composite = Animated.parallel([
|
|
21
|
+
Animated.timing(flashAnim, {
|
|
22
|
+
toValue: 1,
|
|
23
|
+
duration: 400,
|
|
24
|
+
useNativeDriver: true,
|
|
25
|
+
}),
|
|
26
|
+
Animated.timing(burstAnim, {
|
|
27
|
+
toValue: 1,
|
|
28
|
+
duration: 1000,
|
|
29
|
+
useNativeDriver: true,
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
composite.start();
|
|
33
|
+
return () => composite.stop();
|
|
34
|
+
}, [flashAnim, burstAnim]);
|
|
35
|
+
const flashOpacity = flashAnim.interpolate({
|
|
36
|
+
inputRange: [0, 0.25, 1],
|
|
37
|
+
outputRange: [0, 1, 0],
|
|
38
|
+
});
|
|
39
|
+
const flashScale = flashAnim.interpolate({
|
|
40
|
+
inputRange: [0, 0.25, 1],
|
|
41
|
+
outputRange: [0, 2.5, 5],
|
|
42
|
+
});
|
|
43
|
+
return (_jsxs(_Fragment, { children: [_jsx(Animated.View, { style: [
|
|
44
|
+
styles.flash,
|
|
45
|
+
{
|
|
46
|
+
left: burst.x - 6,
|
|
47
|
+
top: burst.y - 6,
|
|
48
|
+
opacity: flashOpacity,
|
|
49
|
+
transform: [{ scale: flashScale }],
|
|
50
|
+
},
|
|
51
|
+
] }), burst.particles.map((p) => {
|
|
52
|
+
const opacity = burstAnim.interpolate({
|
|
53
|
+
inputRange: [0, 0.65, 1],
|
|
54
|
+
outputRange: [1, 1, 0],
|
|
55
|
+
});
|
|
56
|
+
const translateX = burstAnim.interpolate({
|
|
57
|
+
inputRange: [0, 1],
|
|
58
|
+
outputRange: [0, p.vx],
|
|
59
|
+
});
|
|
60
|
+
const translateY = burstAnim.interpolate({
|
|
61
|
+
inputRange: [0, 1],
|
|
62
|
+
outputRange: [0, p.vy],
|
|
63
|
+
});
|
|
64
|
+
const scale = burstAnim.interpolate({
|
|
65
|
+
inputRange: [0, 1],
|
|
66
|
+
outputRange: [1, 0],
|
|
67
|
+
});
|
|
68
|
+
return (_jsx(Animated.View, { style: [
|
|
69
|
+
styles.particle,
|
|
70
|
+
{
|
|
71
|
+
left: burst.x - p.size / 2,
|
|
72
|
+
top: burst.y - p.size / 2,
|
|
73
|
+
width: p.size,
|
|
74
|
+
height: p.size,
|
|
75
|
+
backgroundColor: p.color,
|
|
76
|
+
opacity,
|
|
77
|
+
transform: [{ translateX }, { translateY }, { scale }],
|
|
78
|
+
},
|
|
79
|
+
] }, p.id));
|
|
80
|
+
})] }));
|
|
81
|
+
}
|
|
82
|
+
export function FireworksOverlay() {
|
|
83
|
+
const { width, height } = useWindowDimensions();
|
|
84
|
+
const bursts = useMemo(() => Array.from({ length: BURST_COUNT }, (_, b) => ({
|
|
85
|
+
id: b,
|
|
86
|
+
x: (0.15 + Math.random() * 0.7) * width,
|
|
87
|
+
y: (0.1 + Math.random() * 0.5) * height,
|
|
88
|
+
delay: b * 500 + Math.random() * 200,
|
|
89
|
+
particles: Array.from({ length: PARTICLE_COUNT }, (_, p) => {
|
|
90
|
+
const angle = (p / PARTICLE_COUNT) * Math.PI * 2 + (Math.random() - 0.5) * 0.2;
|
|
91
|
+
const dist = 55 + Math.random() * 60;
|
|
92
|
+
return {
|
|
93
|
+
id: p,
|
|
94
|
+
vx: Math.cos(angle) * dist,
|
|
95
|
+
vy: Math.sin(angle) * dist,
|
|
96
|
+
color: FIREWORK_COLORS[Math.floor(Math.random() * FIREWORK_COLORS.length)],
|
|
97
|
+
size: 3 + Math.random() * 3,
|
|
98
|
+
};
|
|
99
|
+
}),
|
|
100
|
+
})),
|
|
101
|
+
// stable on mount
|
|
102
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
|
+
[]);
|
|
104
|
+
const [mountedBursts, setMountedBursts] = useState([]);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const timers = bursts.map((burst, b) => setTimeout(() => {
|
|
107
|
+
setMountedBursts((prev) => [...prev, b]);
|
|
108
|
+
}, burst.delay));
|
|
109
|
+
return () => timers.forEach(clearTimeout);
|
|
110
|
+
}, [bursts]);
|
|
111
|
+
return (_jsx(View, { style: StyleSheet.absoluteFill, pointerEvents: "none", children: mountedBursts.map((b) => (_jsx(FireworkBurst, { burst: bursts[b] }, b))) }));
|
|
112
|
+
}
|
|
113
|
+
const styles = StyleSheet.create({
|
|
114
|
+
flash: {
|
|
115
|
+
position: "absolute",
|
|
116
|
+
width: 12,
|
|
117
|
+
height: 12,
|
|
118
|
+
borderRadius: 6,
|
|
119
|
+
backgroundColor: "#fff",
|
|
120
|
+
},
|
|
121
|
+
particle: {
|
|
122
|
+
position: "absolute",
|
|
123
|
+
borderRadius: 999,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
3
|
import { SnapCatalogView } from "./catalog-renderer.js";
|
|
4
4
|
import { ConfettiOverlay } from "./confetti-overlay.js";
|
|
5
|
+
import { FireworksOverlay } from "./fireworks-overlay.js";
|
|
5
6
|
import { useSnapTheme } from "./theme.js";
|
|
6
7
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
7
8
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
|
@@ -81,11 +82,15 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
81
82
|
setPageKey((k) => k + 1);
|
|
82
83
|
}, [spec]);
|
|
83
84
|
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
85
|
+
const showFireworks = snap.effects?.includes("fireworks") ?? false;
|
|
84
86
|
const [confettiKey, setConfettiKey] = useState(0);
|
|
87
|
+
const [fireworksKey, setFireworksKey] = useState(0);
|
|
85
88
|
useEffect(() => {
|
|
86
89
|
if (showConfetti)
|
|
87
90
|
setConfettiKey((k) => k + 1);
|
|
88
|
-
|
|
91
|
+
if (showFireworks)
|
|
92
|
+
setFireworksKey((k) => k + 1);
|
|
93
|
+
}, [showConfetti, showFireworks, snap]);
|
|
89
94
|
const handlersRef = useRef(handlers);
|
|
90
95
|
handlersRef.current = handlers;
|
|
91
96
|
const handleAction = useCallback((name, params) => {
|
|
@@ -147,7 +152,7 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
147
152
|
: loadingOverlay
|
|
148
153
|
: null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
149
154
|
applyStatePaths(stateRef.current, changes);
|
|
150
|
-
}, onAction: handleAction }, pageKey), showConfetti && _jsx(ConfettiOverlay, {}, confettiKey)] }));
|
|
155
|
+
}, onAction: handleAction }, pageKey), showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), showFireworks && _jsx(FireworksOverlay, {}, fireworksKey)] }));
|
|
151
156
|
}
|
|
152
157
|
export function SnapLoadingOverlay({ appearance, accentHex, }) {
|
|
153
158
|
return (_jsx(View, { style: [
|
package/dist/schemas.d.ts
CHANGED
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const SNAP_PAYLOAD_HEADER = "X-Snap-Payload" as const;
|
|
|
11
11
|
|
|
12
12
|
export const MEDIA_TYPE = "application/vnd.farcaster.snap+json" as const;
|
|
13
13
|
|
|
14
|
-
export const EFFECT_VALUES = ["confetti"] as const;
|
|
14
|
+
export const EFFECT_VALUES = ["confetti", "fireworks"] as const;
|
|
15
15
|
|
|
16
16
|
// ─── Pixel grid ────────────────────────────────────────
|
|
17
17
|
export const POST_GRID_TAP_KEY = "grid_tap" as const;
|
|
@@ -63,18 +63,106 @@ const CONFETTI_COLORS = [
|
|
|
63
63
|
"#06B6D4",
|
|
64
64
|
];
|
|
65
65
|
|
|
66
|
+
const FIREWORK_COLORS = [
|
|
67
|
+
"#FFD700",
|
|
68
|
+
"#FF6B6B",
|
|
69
|
+
"#4ECDC4",
|
|
70
|
+
"#C4A7E7",
|
|
71
|
+
"#F6C177",
|
|
72
|
+
"#EBBCBA",
|
|
73
|
+
"#9CCFD8",
|
|
74
|
+
"#fff",
|
|
75
|
+
];
|
|
76
|
+
|
|
66
77
|
function ConfettiOverlay() {
|
|
67
78
|
const pieces = useMemo(
|
|
68
79
|
() =>
|
|
69
|
-
Array.from({ length: 80 }, (_, i) =>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
Array.from({ length: 80 }, (_, i) => {
|
|
81
|
+
const driftX = (Math.random() - 0.5) * 120;
|
|
82
|
+
return {
|
|
83
|
+
id: i,
|
|
84
|
+
left: Math.random() * 100,
|
|
85
|
+
delay: Math.random() * 1.2,
|
|
86
|
+
duration: 2.8 + Math.random() * 1.8,
|
|
87
|
+
color:
|
|
88
|
+
CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
|
|
89
|
+
size: 6 + Math.random() * 8,
|
|
90
|
+
rotation: Math.random() * 360,
|
|
91
|
+
isCircle: Math.random() > 0.6,
|
|
92
|
+
driftX,
|
|
93
|
+
driftMid: -driftX * 0.4,
|
|
94
|
+
};
|
|
95
|
+
}),
|
|
96
|
+
[],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
style={{
|
|
102
|
+
position: "absolute",
|
|
103
|
+
inset: 0,
|
|
104
|
+
overflow: "hidden",
|
|
105
|
+
pointerEvents: "none",
|
|
106
|
+
zIndex: 20,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{pieces.map(
|
|
110
|
+
({ id, left, delay, duration, color, size, rotation, isCircle, driftX, driftMid }) => (
|
|
111
|
+
<div
|
|
112
|
+
key={id}
|
|
113
|
+
style={
|
|
114
|
+
{
|
|
115
|
+
position: "absolute",
|
|
116
|
+
left: `${left}%`,
|
|
117
|
+
top: -20,
|
|
118
|
+
width: size,
|
|
119
|
+
height: isCircle ? size : size * 0.5,
|
|
120
|
+
backgroundColor: color,
|
|
121
|
+
borderRadius: isCircle ? "50%" : 2,
|
|
122
|
+
transform: `rotateZ(${rotation}deg)`,
|
|
123
|
+
animation: `confettiFall ${duration}s cubic-bezier(0.25,0,0.75,1) ${delay}s forwards`,
|
|
124
|
+
"--dx": `${driftX}px`,
|
|
125
|
+
"--dm": `${driftMid}px`,
|
|
126
|
+
} as CSSProperties
|
|
127
|
+
}
|
|
128
|
+
/>
|
|
129
|
+
),
|
|
130
|
+
)}
|
|
131
|
+
<style>{`@keyframes confettiFall{
|
|
132
|
+
0% {top:-20px;opacity:1;transform:rotateZ(0deg) rotateY(0deg) translateX(0)}
|
|
133
|
+
20% {transform:rotateZ(144deg) rotateY(60deg) translateX(var(--dm))}
|
|
134
|
+
40% {transform:rotateZ(288deg) rotateY(120deg) translateX(0)}
|
|
135
|
+
60% {opacity:1;transform:rotateZ(432deg) rotateY(200deg) translateX(calc(-1 * var(--dm)))}
|
|
136
|
+
80% {transform:rotateZ(576deg) rotateY(280deg) translateX(var(--dx))}
|
|
137
|
+
100%{top:110%;opacity:0;transform:rotateZ(720deg) rotateY(360deg) translateX(var(--dx))}
|
|
138
|
+
}`}</style>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function FireworksOverlay() {
|
|
144
|
+
const bursts = useMemo(
|
|
145
|
+
() =>
|
|
146
|
+
Array.from({ length: 5 }, (_, b) => ({
|
|
147
|
+
id: b,
|
|
148
|
+
x: 15 + Math.random() * 70,
|
|
149
|
+
y: 10 + Math.random() * 50,
|
|
150
|
+
delay: b * 0.5 + Math.random() * 0.2,
|
|
151
|
+
particles: Array.from({ length: 24 }, (_, p) => {
|
|
152
|
+
const angle =
|
|
153
|
+
(p / 24) * Math.PI * 2 + (Math.random() - 0.5) * 0.2;
|
|
154
|
+
const dist = 55 + Math.random() * 60;
|
|
155
|
+
return {
|
|
156
|
+
id: p,
|
|
157
|
+
vx: Math.cos(angle) * dist,
|
|
158
|
+
vy: Math.sin(angle) * dist,
|
|
159
|
+
color:
|
|
160
|
+
FIREWORK_COLORS[
|
|
161
|
+
Math.floor(Math.random() * FIREWORK_COLORS.length)
|
|
162
|
+
],
|
|
163
|
+
size: 3 + Math.random() * 3,
|
|
164
|
+
};
|
|
165
|
+
}),
|
|
78
166
|
})),
|
|
79
167
|
[],
|
|
80
168
|
);
|
|
@@ -89,25 +177,57 @@ function ConfettiOverlay() {
|
|
|
89
177
|
zIndex: 20,
|
|
90
178
|
}}
|
|
91
179
|
>
|
|
92
|
-
{
|
|
93
|
-
<div
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
180
|
+
{bursts.map(({ id: bid, x, y, delay, particles }) => (
|
|
181
|
+
<div key={bid}>
|
|
182
|
+
<div
|
|
183
|
+
style={{
|
|
184
|
+
position: "absolute",
|
|
185
|
+
left: `${x}%`,
|
|
186
|
+
top: `${y}%`,
|
|
187
|
+
width: 12,
|
|
188
|
+
height: 12,
|
|
189
|
+
borderRadius: "50%",
|
|
190
|
+
backgroundColor: "#fff",
|
|
191
|
+
transform: "translate(-50%,-50%)",
|
|
192
|
+
animation: `fwFlash 0.4s ease-out ${delay}s both`,
|
|
193
|
+
opacity: 0,
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
{particles.map(({ id: pid, vx, vy, color, size }) => (
|
|
197
|
+
<div
|
|
198
|
+
key={pid}
|
|
199
|
+
style={
|
|
200
|
+
{
|
|
201
|
+
position: "absolute",
|
|
202
|
+
left: `${x}%`,
|
|
203
|
+
top: `${y}%`,
|
|
204
|
+
width: size,
|
|
205
|
+
height: size,
|
|
206
|
+
borderRadius: "50%",
|
|
207
|
+
backgroundColor: color,
|
|
208
|
+
transform: "translate(-50%,-50%)",
|
|
209
|
+
animation: `fwBurst 1s cubic-bezier(0.2,0,0.8,1) ${delay}s forwards`,
|
|
210
|
+
opacity: 0,
|
|
211
|
+
"--vx": `${vx}px`,
|
|
212
|
+
"--vy": `${vy}px`,
|
|
213
|
+
} as CSSProperties
|
|
214
|
+
}
|
|
215
|
+
/>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
107
218
|
))}
|
|
108
|
-
<style>{
|
|
109
|
-
|
|
110
|
-
|
|
219
|
+
<style>{`
|
|
220
|
+
@keyframes fwFlash{
|
|
221
|
+
0% {opacity:0;transform:translate(-50%,-50%) scale(0)}
|
|
222
|
+
25% {opacity:1;transform:translate(-50%,-50%) scale(2.5)}
|
|
223
|
+
100%{opacity:0;transform:translate(-50%,-50%) scale(5)}
|
|
224
|
+
}
|
|
225
|
+
@keyframes fwBurst{
|
|
226
|
+
0% {opacity:1;transform:translate(-50%,-50%) translate(0,0) scale(1)}
|
|
227
|
+
65% {opacity:1}
|
|
228
|
+
100%{opacity:0;transform:translate(-50%,-50%) translate(var(--vx),var(--vy)) scale(0)}
|
|
229
|
+
}
|
|
230
|
+
`}</style>
|
|
111
231
|
</div>
|
|
112
232
|
);
|
|
113
233
|
}
|
|
@@ -240,10 +360,13 @@ export function SnapViewCore({
|
|
|
240
360
|
}, [spec]);
|
|
241
361
|
|
|
242
362
|
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
363
|
+
const showFireworks = snap.effects?.includes("fireworks") ?? false;
|
|
243
364
|
const [confettiKey, setConfettiKey] = useState(0);
|
|
365
|
+
const [fireworksKey, setFireworksKey] = useState(0);
|
|
244
366
|
useEffect(() => {
|
|
245
367
|
if (showConfetti) setConfettiKey((k) => k + 1);
|
|
246
|
-
|
|
368
|
+
if (showFireworks) setFireworksKey((k) => k + 1);
|
|
369
|
+
}, [showConfetti, showFireworks, snap]);
|
|
247
370
|
|
|
248
371
|
const accentName = snap.theme?.accent ?? "purple";
|
|
249
372
|
|
|
@@ -326,6 +449,7 @@ export function SnapViewCore({
|
|
|
326
449
|
return (
|
|
327
450
|
<div style={{ position: "relative", width: "100%" }}>
|
|
328
451
|
{showConfetti && <ConfettiOverlay key={confettiKey} />}
|
|
452
|
+
{showFireworks && <FireworksOverlay key={fireworksKey} />}
|
|
329
453
|
{loadingOverlay === undefined ? (
|
|
330
454
|
<SnapLoadingOverlay
|
|
331
455
|
appearance={appearance}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Animated, StyleSheet, View, useWindowDimensions } from "react-native";
|
|
3
|
+
|
|
4
|
+
const FIREWORK_COLORS = [
|
|
5
|
+
"#FFD700",
|
|
6
|
+
"#FF6B6B",
|
|
7
|
+
"#4ECDC4",
|
|
8
|
+
"#C4A7E7",
|
|
9
|
+
"#F6C177",
|
|
10
|
+
"#EBBCBA",
|
|
11
|
+
"#9CCFD8",
|
|
12
|
+
"#fff",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const BURST_COUNT = 5;
|
|
16
|
+
const PARTICLE_COUNT = 24;
|
|
17
|
+
|
|
18
|
+
type BurstData = {
|
|
19
|
+
id: number;
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
particles: Array<{
|
|
23
|
+
id: number;
|
|
24
|
+
vx: number;
|
|
25
|
+
vy: number;
|
|
26
|
+
color: string;
|
|
27
|
+
size: number;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function FireworkBurst({ burst }: { burst: BurstData }) {
|
|
32
|
+
const flashAnim = useRef(new Animated.Value(0)).current;
|
|
33
|
+
const burstAnim = useRef(new Animated.Value(0)).current;
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const composite = Animated.parallel([
|
|
37
|
+
Animated.timing(flashAnim, {
|
|
38
|
+
toValue: 1,
|
|
39
|
+
duration: 400,
|
|
40
|
+
useNativeDriver: true,
|
|
41
|
+
}),
|
|
42
|
+
Animated.timing(burstAnim, {
|
|
43
|
+
toValue: 1,
|
|
44
|
+
duration: 1000,
|
|
45
|
+
useNativeDriver: true,
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
composite.start();
|
|
49
|
+
return () => composite.stop();
|
|
50
|
+
}, [flashAnim, burstAnim]);
|
|
51
|
+
|
|
52
|
+
const flashOpacity = flashAnim.interpolate({
|
|
53
|
+
inputRange: [0, 0.25, 1],
|
|
54
|
+
outputRange: [0, 1, 0],
|
|
55
|
+
});
|
|
56
|
+
const flashScale = flashAnim.interpolate({
|
|
57
|
+
inputRange: [0, 0.25, 1],
|
|
58
|
+
outputRange: [0, 2.5, 5],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<Animated.View
|
|
64
|
+
style={[
|
|
65
|
+
styles.flash,
|
|
66
|
+
{
|
|
67
|
+
left: burst.x - 6,
|
|
68
|
+
top: burst.y - 6,
|
|
69
|
+
opacity: flashOpacity,
|
|
70
|
+
transform: [{ scale: flashScale }],
|
|
71
|
+
},
|
|
72
|
+
]}
|
|
73
|
+
/>
|
|
74
|
+
{burst.particles.map((p) => {
|
|
75
|
+
const opacity = burstAnim.interpolate({
|
|
76
|
+
inputRange: [0, 0.65, 1],
|
|
77
|
+
outputRange: [1, 1, 0],
|
|
78
|
+
});
|
|
79
|
+
const translateX = burstAnim.interpolate({
|
|
80
|
+
inputRange: [0, 1],
|
|
81
|
+
outputRange: [0, p.vx],
|
|
82
|
+
});
|
|
83
|
+
const translateY = burstAnim.interpolate({
|
|
84
|
+
inputRange: [0, 1],
|
|
85
|
+
outputRange: [0, p.vy],
|
|
86
|
+
});
|
|
87
|
+
const scale = burstAnim.interpolate({
|
|
88
|
+
inputRange: [0, 1],
|
|
89
|
+
outputRange: [1, 0],
|
|
90
|
+
});
|
|
91
|
+
return (
|
|
92
|
+
<Animated.View
|
|
93
|
+
key={p.id}
|
|
94
|
+
style={[
|
|
95
|
+
styles.particle,
|
|
96
|
+
{
|
|
97
|
+
left: burst.x - p.size / 2,
|
|
98
|
+
top: burst.y - p.size / 2,
|
|
99
|
+
width: p.size,
|
|
100
|
+
height: p.size,
|
|
101
|
+
backgroundColor: p.color,
|
|
102
|
+
opacity,
|
|
103
|
+
transform: [{ translateX }, { translateY }, { scale }],
|
|
104
|
+
},
|
|
105
|
+
]}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function FireworksOverlay() {
|
|
114
|
+
const { width, height } = useWindowDimensions();
|
|
115
|
+
|
|
116
|
+
const bursts = useMemo<(BurstData & { delay: number })[]>(
|
|
117
|
+
() =>
|
|
118
|
+
Array.from({ length: BURST_COUNT }, (_, b) => ({
|
|
119
|
+
id: b,
|
|
120
|
+
x: (0.15 + Math.random() * 0.7) * width,
|
|
121
|
+
y: (0.1 + Math.random() * 0.5) * height,
|
|
122
|
+
delay: b * 500 + Math.random() * 200,
|
|
123
|
+
particles: Array.from({ length: PARTICLE_COUNT }, (_, p) => {
|
|
124
|
+
const angle =
|
|
125
|
+
(p / PARTICLE_COUNT) * Math.PI * 2 + (Math.random() - 0.5) * 0.2;
|
|
126
|
+
const dist = 55 + Math.random() * 60;
|
|
127
|
+
return {
|
|
128
|
+
id: p,
|
|
129
|
+
vx: Math.cos(angle) * dist,
|
|
130
|
+
vy: Math.sin(angle) * dist,
|
|
131
|
+
color:
|
|
132
|
+
FIREWORK_COLORS[
|
|
133
|
+
Math.floor(Math.random() * FIREWORK_COLORS.length)
|
|
134
|
+
]!,
|
|
135
|
+
size: 3 + Math.random() * 3,
|
|
136
|
+
};
|
|
137
|
+
}),
|
|
138
|
+
})),
|
|
139
|
+
// stable on mount
|
|
140
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
141
|
+
[],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const [mountedBursts, setMountedBursts] = useState<number[]>([]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const timers = bursts.map((burst, b) =>
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
setMountedBursts((prev) => [...prev, b]);
|
|
150
|
+
}, burst.delay),
|
|
151
|
+
);
|
|
152
|
+
return () => timers.forEach(clearTimeout);
|
|
153
|
+
}, [bursts]);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
157
|
+
{mountedBursts.map((b) => (
|
|
158
|
+
<FireworkBurst key={b} burst={bursts[b]!} />
|
|
159
|
+
))}
|
|
160
|
+
</View>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const styles = StyleSheet.create({
|
|
165
|
+
flash: {
|
|
166
|
+
position: "absolute",
|
|
167
|
+
width: 12,
|
|
168
|
+
height: 12,
|
|
169
|
+
borderRadius: 6,
|
|
170
|
+
backgroundColor: "#fff",
|
|
171
|
+
},
|
|
172
|
+
particle: {
|
|
173
|
+
position: "absolute",
|
|
174
|
+
borderRadius: 999,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
@@ -2,6 +2,7 @@ import type { Spec } from "@json-render/core";
|
|
|
2
2
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
3
|
import { SnapCatalogView } from "./catalog-renderer";
|
|
4
4
|
import { ConfettiOverlay } from "./confetti-overlay";
|
|
5
|
+
import { FireworksOverlay } from "./fireworks-overlay";
|
|
5
6
|
import { useSnapTheme } from "./theme";
|
|
6
7
|
import {
|
|
7
8
|
type ReactNode,
|
|
@@ -128,10 +129,13 @@ export function SnapViewCoreInner({
|
|
|
128
129
|
}, [spec]);
|
|
129
130
|
|
|
130
131
|
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
132
|
+
const showFireworks = snap.effects?.includes("fireworks") ?? false;
|
|
131
133
|
const [confettiKey, setConfettiKey] = useState(0);
|
|
134
|
+
const [fireworksKey, setFireworksKey] = useState(0);
|
|
132
135
|
useEffect(() => {
|
|
133
136
|
if (showConfetti) setConfettiKey((k) => k + 1);
|
|
134
|
-
|
|
137
|
+
if (showFireworks) setFireworksKey((k) => k + 1);
|
|
138
|
+
}, [showConfetti, showFireworks, snap]);
|
|
135
139
|
|
|
136
140
|
const handlersRef = useRef(handlers);
|
|
137
141
|
handlersRef.current = handlers;
|
|
@@ -213,6 +217,7 @@ export function SnapViewCoreInner({
|
|
|
213
217
|
onAction={handleAction}
|
|
214
218
|
/>
|
|
215
219
|
{showConfetti && <ConfettiOverlay key={confettiKey} />}
|
|
220
|
+
{showFireworks && <FireworksOverlay key={fireworksKey} />}
|
|
216
221
|
</View>
|
|
217
222
|
);
|
|
218
223
|
}
|