Last active
February 27, 2025 15:39
-
-
Save 0xsommer/3cb879fee4dadaa3dfe3e00e06a76a78 to your computer and use it in GitHub Desktop.
Exploding button
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { motion } from 'framer-motion'; | |
const particleCount = 80; | |
const colors = ['#FFFFFF', '#FFFFFF']; // White, Yellow, Blue | |
const generateParticles = () => { | |
return Array.from({ length: particleCount }).map((_, i) => { | |
const angle = (i / particleCount) * 360; | |
const radius = Math.random() * 200 + 100; | |
return { | |
x: Math.cos(angle * Math.PI / 180) * radius, | |
y: Math.sin(angle * Math.PI / 180) * radius, | |
rotation: Math.random() * 720 - 360, | |
scale: Math.random() * 1 + 0.5, | |
color: colors[Math.floor(Math.random() * colors.length)], | |
}; | |
}); | |
}; | |
export default function Explosion() { | |
const particles = generateParticles(); | |
return ( | |
<motion.div | |
className="absolute inset-0 pointer-events-none z-50"> | |
{particles.map((particle, index) => ( | |
<motion.div | |
key={index} | |
className="absolute left-1/2 top-1/2 w-3 h-3 rounded-full" | |
initial={{ x: 0, y: 0, opacity: 1, scale: 0 }} | |
animate={{ | |
x: particle.x, | |
y: particle.y, | |
opacity: 0, | |
scale: particle.scale, | |
rotate: particle.rotation, | |
}} | |
transition={{ | |
duration: 1.2, // Increased from 1.2 to 1.5 for longer travel time | |
ease: "easeOut", | |
}} | |
style={{ backgroundColor: '#FFFFFF' }} | |
/> | |
))} | |
{/* Multiple shockwaves */} | |
{colors.map((color, index) => ( | |
<motion.div | |
key={`shockwave-${index}`} | |
className="absolute left-1/2 top-1/2 rounded-full" | |
initial={{ width: 0, height: 0, borderWidth: 80, opacity: 1 }} | |
animate={{ | |
width: '250%', // Increased from 300% to 400% | |
height: '250%', // Increased from 300% to 400% | |
borderWidth: 0, | |
opacity: 0 | |
}} | |
transition={{ | |
duration: 2, | |
delay: index * 0.2, | |
type: "spring", | |
}} | |
style={{ | |
transform: 'translate(-50%, -50%)', | |
border: 'solid', | |
borderColor: `${color}`, | |
backgroundColor: 'transparent', | |
}} | |
/> | |
))} | |
</motion.div> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use client' | |
import React, { useState, useEffect } from 'react'; | |
import Skeleton from "@/app/kanon/components/skeleton"; | |
import IntroText from "@/app/kanon/components/intro-text"; | |
import FragmentFullWidth from "@/app/kanon/components/fragment-full-width"; | |
import metaData from './meta.json'; | |
import Button from "./components/button"; | |
import { motion, useAnimationControls } from "framer-motion"; | |
import IPhoneFrame from "@/app/kanon/components/IPhoneFrame"; | |
const Page: React.FC = () => { | |
const introText = [ | |
"This experiment showcases a smooth onboarding experience for a crypto wallet application.", | |
"The design focuses on simplicity and user-friendliness, guiding users through the initial setup process." | |
]; | |
const [loadFinished, setLoadFinished] = useState(false); | |
const controls = useAnimationControls(); | |
useEffect(() => { | |
if (loadFinished) { | |
controls.start({ | |
scale: 0.8, | |
transition: { type: "spring", duration: 0.2 } | |
}).then(() => { | |
setTimeout(() => { | |
controls.start({ | |
scale: 1, | |
transition: { type: "spring", stiffness: 200, damping: 10 } | |
}); | |
}, 100); | |
}); | |
} | |
}, [loadFinished, controls]); | |
return ( | |
<Skeleton | |
title={metaData[0].title} | |
description={metaData[0].summary} | |
image={metaData[0].image} | |
slug={metaData[0].slug} | |
publishedAt={metaData[0].publishedAt} | |
> | |
<IntroText copy={introText} /> | |
<IPhoneFrame className="scale-100"> | |
<div className="w-full flex flex-col items-center gap-16 px-4 py-80 bg-zinc-800"> | |
<motion.div | |
animate={controls} | |
initial={{ scale: 1 }} | |
onDoubleClick={() => setLoadFinished(false)} | |
> | |
<Button setLoadFinished={setLoadFinished} loadFinished={loadFinished}/> | |
</motion.div> | |
</div> | |
</IPhoneFrame> | |
</Skeleton> | |
); | |
}; | |
export default Page; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { motion, useAnimation } from 'framer-motion'; | |
import { useEffect, useState } from 'react'; | |
interface ParticlesProps { | |
isPressed: boolean; | |
customTransition: { | |
duration: number; | |
ease: any; | |
}; | |
totalDuration: number; // New prop for total animation duration | |
} | |
// Customizable constants | |
const PARTICLE_COUNT = 150; | |
const MIN_PARTICLE_SIZE = 5; | |
const MAX_PARTICLE_SIZE = 10; | |
const MIN_PARTICLE_DISTANCE = 150; | |
const MAX_PARTICLE_DISTANCE = 250; | |
const MIN_ANIMATION_DURATION_RATIO = 0.2; | |
const MAX_ANIMATION_DURATION_RATIO = 0.4; | |
const MIN_FADE_OUT_POINT = 0.7; | |
const MAX_FADE_OUT_POINT = 0.9; | |
const FADE_IN_DURATION_RATIO = 0.05; | |
const getInitialPosition = () => { | |
const angle = Math.random() * 2 * Math.PI; | |
const radius = MIN_PARTICLE_DISTANCE + Math.random() * (MAX_PARTICLE_DISTANCE - MIN_PARTICLE_DISTANCE); | |
return { | |
x: Math.cos(angle) * radius, | |
y: Math.sin(angle) * radius, | |
size: MIN_PARTICLE_SIZE + Math.random() * (MAX_PARTICLE_SIZE - MIN_PARTICLE_SIZE) | |
}; | |
}; | |
export default function Particles({ isPressed, customTransition, totalDuration }: ParticlesProps) { | |
const particlesControl = useAnimation(); | |
const [particles, setParticles] = useState(() => | |
Array.from({ length: PARTICLE_COUNT }, getInitialPosition) | |
); | |
useEffect(() => { | |
if (isPressed) { | |
particlesControl.start(i => { | |
const delayRatio = i / PARTICLE_COUNT; | |
const delay = delayRatio * totalDuration * 0.5; // Use half of total duration for delays | |
const durationRatio = MIN_ANIMATION_DURATION_RATIO + Math.random() * (MAX_ANIMATION_DURATION_RATIO - MIN_ANIMATION_DURATION_RATIO); | |
const duration = totalDuration * durationRatio; | |
const fadeOutPoint = MIN_FADE_OUT_POINT + Math.random() * (MAX_FADE_OUT_POINT - MIN_FADE_OUT_POINT); | |
return { | |
x: 0, | |
y: 0, | |
opacity: [0, 1, 1, 0], | |
transition: { | |
x: { ...customTransition, delay, duration }, | |
y: { ...customTransition, delay, duration }, | |
opacity: { | |
times: [0, FADE_IN_DURATION_RATIO, fadeOutPoint, 1], | |
duration: duration, | |
delay | |
} | |
}, | |
}; | |
}); | |
} else { | |
particlesControl.start(i => ({ | |
x: particles[i].x, | |
y: particles[i].y, | |
opacity: 0, | |
transition: { duration: totalDuration * 0.05 } // Quick fade out | |
})); | |
} | |
}, [isPressed, particlesControl, customTransition, particles, totalDuration]); | |
return ( | |
<> | |
{particles.map((particle, i) => ( | |
<motion.div | |
key={i} | |
className="absolute rounded-full bg-white" | |
style={{ | |
width: particle.size, | |
height: particle.size, | |
}} | |
initial={{ x: particle.x, y: particle.y, opacity: 0 }} | |
animate={particlesControl} | |
custom={i} | |
/> | |
))} | |
</> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment