Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FN-4]온보딩 뉴 디자인, 애니메이션 적용 #79

Merged
merged 4 commits into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion components/carousel/carousel.components.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cn } from '@/lib/client/utils'
import { MotionValue, useTransform, m, circOut } from 'framer-motion'
import React, { PropsWithChildren, useMemo } from 'react'
import React, { PropsWithChildren, useEffect, useMemo } from 'react'

export const DotButton = ({
index,
Expand Down
2 changes: 1 addition & 1 deletion components/carousel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const Carousel = <T,>({
<m.div className={cn(className)} {...fadeInProps}>
<div className="overflow-hidden grow flex flex-col" ref={viewportRef}>
<div
className="disabled-select flex grow relative"
className="disabled-select flex grow relative h-full"
ref={containerRef}
>
{slides.map((item, index) => (
Expand Down
2 changes: 1 addition & 1 deletion components/compositions/tree-card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const TreeCard = ({
<motion.div
id={id}
variants={fadeInProps.variants}
className={cn('w-[104px] h-[110px] cursor-pointer relative', {
className={cn('h-full aspect-[104/110] cursor-pointer relative', {
'preserve-3d': isFlipped,
})}
style={{ transformStyle: 'preserve-3d' }}
Expand Down
220 changes: 220 additions & 0 deletions components/confetti/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import {
HTMLAttributes,
MouseEventHandler,
PropsWithChildren,
forwardRef,
useEffect,
useRef,
} from 'react'
const randomNumBetween = (min: number, max: number) =>
Math.random() * (max - min) + min

const hexToRgb = (hex: string) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null
}

interface ParticleOptions {
x: number
y: number
deg: number
r?: number
spread?: number
}

class Particle {
public x: number
public y: number
public vx: number
public vy: number
public r: number

public width = 12
public height = 12
public opacity = 1

public widthDelta = randomNumBetween(0, 360)
public heightDelta = randomNumBetween(0, 360)
public rotation = randomNumBetween(0, 360)

constructor(
{
x,
y,
deg = 0,
r = randomNumBetween(10, 30),
spread = 15,
}: ParticleOptions,
private readonly friction = 0.89,
private readonly gravity = 0.5,
private readonly angle = (Math.PI / 180) * randomNumBetween(0, 360),

private readonly rotationDelta = randomNumBetween(-1, 1),
private readonly colors = ['#00BC68', '#E2F6E9', '#005E16', '#E2F5FF'],
private readonly color = hexToRgb(
colors[Math.floor(randomNumBetween(0, colors.length))],
)!,
private readonly shapes = ['circle', 'square'],
private readonly shape = shapes[
Math.floor(randomNumBetween(0, shapes.length))
],
) {
this.r = r
this.x = x
this.y = y

this.vx = this.r * Math.cos(this.angle)
this.vy = this.r * Math.sin(this.angle)
}

update() {
this.vy += this.gravity

this.vx *= this.friction
this.vy *= this.friction

this.x += this.vx
this.y += this.vy

this.opacity -= 0.005

this.widthDelta += 2
this.heightDelta += 2
this.rotation += this.rotationDelta
}

drawSquare(ctx: CanvasRenderingContext2D) {
ctx.fillRect(
this.x,
this.y,
this.width * Math.cos((Math.PI / 180) * this.widthDelta),
this.height * Math.sin((Math.PI / 180) * this.heightDelta),
)
}
drawCircle(ctx: CanvasRenderingContext2D) {
ctx.beginPath()
ctx.ellipse(
this.x,
this.y,
Math.abs(this.width * Math.cos((Math.PI / 180) * this.widthDelta)) / 2,
Math.abs(this.height * Math.sin((Math.PI / 180) * this.heightDelta)) / 2,
0,
0,
Math.PI * 2,
)
ctx.fill()
ctx.closePath()
}

draw(ctx: CanvasRenderingContext2D) {
ctx.translate(this.x + this.width * 1.2, this.y + this.height * 1.2)
ctx.rotate((Math.PI / 180) * this.rotation)
ctx.translate(-this.x - this.width * 1.2, -this.y - this.height * 1.2)
ctx.fillStyle = `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${this.opacity})`
switch (this.shape) {
case 'square':
this.drawSquare(ctx)
break
case 'circle':
this.drawCircle(ctx)
break
}

ctx.resetTransform()
}
}

const defaultOptions = {
fps: 60,
intervalTime: 1000,
dpr: typeof window !== 'undefined' && window?.devicePixelRatio > 1 ? 2 : 1,
}

interface ConfettiProps extends HTMLAttributes<HTMLCanvasElement> {}

const Confetti = (props: PropsWithChildren<ConfettiProps>) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const clickableRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (canvasRef.current && clickableRef.current) {
const { dpr, intervalTime, fps } = defaultOptions
const interval = intervalTime / fps
const ctx = canvasRef.current.getContext('2d')
if (ctx) {
let animationId: number
const canvas = canvasRef.current
const canvasWidth = canvas.clientWidth * dpr
const canvasHeight = canvas.clientHeight * dpr
canvas.style.width = canvasWidth + 'px'
canvas.style.height = canvasHeight + 'px'
canvas.width = canvasHeight
canvas.height = canvasHeight
ctx.scale(dpr, dpr)

const particles: Particle[] = []
const createConfetti = (event: MouseEvent) => {
if (!clickableRef.current) return
const count = 20
const deg = 0
const spread = -1

const po = clickableRef.current?.getBoundingClientRect()
const x = event.clientX + (po.width / 2) * dpr
const y = event.clientY - po.height * dpr
console.log()
for (let i = 0; i < count; i++) {
particles.push(new Particle({ x, y, deg, spread }))
}
}

const render = () => {
let now, delta
let then = Date.now()

const frame = () => {
animationId = requestAnimationFrame(frame)
now = Date.now()
delta = now - then
if (delta < interval) return
ctx.clearRect(0, 0, canvasWidth, canvasHeight)

for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update()
particles[i].draw(ctx)
if (particles[i].opacity < 0) {
particles.splice(i, 1)
}
}
then = now - (delta % interval)
}
animationId = requestAnimationFrame(frame)
}

render()
const clickable = clickableRef.current

clickable.addEventListener('click', createConfetti)

return () => {
cancelAnimationFrame(animationId)
clickable.removeEventListener('click', createConfetti)
}
}
}
}, [])
const { children, ...rest } = props
return (
<>
<div ref={clickableRef}>{props.children}</div>
<canvas {...rest} ref={canvasRef} />
</>
)
}

export default Confetti
1 change: 1 addition & 0 deletions components/icons/welcome-trees.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react'
const WelcomeTrees = () => {
return (
<svg
className="w-full h-full aspect-[295/147]"
width="295"
height="147"
viewBox="0 0 295 147"
Expand Down
Loading
Loading