์๋ ํ๋ฉด์ ๋ฐคํฐ์๋ค.
Vue๋ฅผ ๊ณต๋ถํ๊ธฐ ์ํด์ ๋ง๋ ๊ฑฐ๋ผ ๊ทธ๋ค์ง ์ด์์ง ์์๋ค.
์ด๋ ํ๋ก ํธ๊ฐ ์ฒ์์ด์๋์ง๋ผ ๋ถ์กฑํ ์ ์ด ๋ง์๋๋ฐ
๊ทธ๋์ ์ฑ์ฅํ ๋งํผ ๊ธฐ๋ฅ์ ์ถ๊ฐํด์ ์ฝ๋๋ฅผ ์์ ํด๋ณด์
1. ์ถฉ๊ฒฉ์ ์ธ ๋ฐคํฐ ๊ธฐ์กด ํ๋ฉด

๋ค์๋ด๋ ๋์ถฉ๊ฒฉ ๋น์ฃผ์ผ์ด๋ค. ๋ง์น ๊ณต์ฐ๋น์์ ๋ง๋ ๋น์ฃผ์ผ์ด๋ค. ์๊ฐํด๋ณด๋ฉด ํ ๋ผ๋ ๊ฑฐ๋ถ์ด๋ ์๋ ํ์์ ๊ฒฝ์ฃผํ๋ ๊ฒ ๊ฐ์๋ฐ... ๊ณผ๊ฑฐ์ ๋๋ ๊ณต์ ์ฑ์ ์ํด ๊ฑฐ๋ถ์ด๋ฅผ ๋ฐ๋ค์์ ๊ฒฝ์ฃผ์ํจ๊ฑธ๊น?ใ ใ ใ ใ ์ฌ์ค ๋๋ด์ด๊ณ ์ด๋ ๊ทธ๋ฐ ์๊ฐ ์์๋ค. ํ๋ก ํธ๊ฐ ์ ๊ธฐํด์ ์ด๊ฒ์ ๊ฒ ํด๋ณด๋ ๊ทธ๋ฅ ์ด์ํ ์ฌ๋์ด์๋ ๊ฒ!
์๋ฌดํผ ์ด์ ํฌํธํด๋ฆฌ์ค๋ฅผ ์์ฑํด์ผํด์ ์ด ๋ฐคํฐ ํ๋ก์ ํธ๋ฅผ ๋ฃ์ด์ผํ๋๋ฐ ์ด๋ ๊ฒ ๋ฃ์ ์ ์์ ๊ฒ ๊ฐ๋ค. ๋น์ฃผ์ผ ์ ๊ทธ๋ ์ด๋ ์ํค๊ณ , ๊ธฐ๋ฅ๋ ๊ฐ์ ํด๋ณด๊ฒ ๋ค.
2. ์ธ๊ด ๊ฐ์
- ์์ ์


๋ด ์ถ์ต์ ํ๋ก์ ํธ๊ฐ ๋ ๊ฒ ๊ฐ๋ค. ๋์ ๋ฏธ๊ฐ ํ์ค์ ์๋ ค์ฃผ๋...ใ (๋๋ฆ ๊ท์ฌ์ด ๊ฒ ๊ฐ๊ธฐ๋ ํ๊ณ )
- ์์ ํ


๋ด ๋ฏธ๊ฐ์ ํ๊ณ๋ค. ์ต๋ํ ๋ ธ๋ ฅํ๋ค. ์ฝ๊ฐ ์์ฌํ๊ณผ ๋ฌด์ง๊ฐ๋ก ์ฌ์ด์ ์๊ฐ ๊ฐ๋ค ใ ใ ใ ์ด๋ฏธ์ง๋ GPTํํ ๋ง๋ค์ด๋ฌ๋ผ๊ณ ํ๋ค. ์ ์๋ GPT๊ฐ ๋ง๋ค์ด์ค๊ฑด๋ฐ ์๋ ๋ณด๋ค ์ฌํด ๊ธฐ๋ฅ์ด ๋ ์ข์์ ธ์ ๊ท์ฝ๊ฒ ๋ง๋ค์ด์คฌ๋ค. (๊ทผ๋ฐ ๋๋ผ์ด๊ฑด ์ ๊ฑฐ ๋ง๋ค๋ ์ง์ง ์์ฒญ ๋ฟ๋ฏํดํ์ใ ใ ใ ใ ใ GPT ๊ทธ๋ฆผ ๊ทธ๋ ค์ฃผ๋๊ฒ๋ ๋ง์กฑํ๋ค๋๊ฒ ์๊น ใ ใ ) ๊ทธ๋ฆฌ๊ณ ์ด์ ๊ฑฐ๋ถ์ด๋ ๋ ์์ ๊ฒฝ์ฃผํ๋ค. ์ต๊น์ ์ธ์์ด ์์๋ ์ ์ด๋ค.
3. ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถ๊ฐ

"canvas-confetti": "^1.9.4",
"gsap": "^3.15.0"
๋ ์๋๊ฐ์๋ ํํ์ ์ํด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํ๋ค. (์์งํ ์์ฒญ๋ ์ฐจ์ด๊ฐ ์์ง ์๋ค)
canvas-confetti๋ ์น๋ฆฌ ํ์ ๋ ํญ์ฃฝ ํจ๊ณผ๋ฅผ ๋ฃ๊ธฐ ์ํด ์ถ๊ฐํ๋ค. ๊ทธ๋ฆฌ๊ณ ์๋ ํ ๋ผ์ ๊ฑฐ๋ถ์ด ์ด๋์ css keyframe์ผ๋ก ์งํํ๋๋ฐ gsap์ ์ถ๊ฐํด์ JS ์ ๋๋ฉ์ด์ ์ผ๋ก ์ ์ดํ๋ค.
4. ๊ฒฝ์ฃผ ๋ก์ง ๋ณ๊ฒฝ
- ๊ธฐ์กด CSS keyframe ๋ฐฉ์
data() {
return {
rabbitRunning: false,
rabbitStopping: false,
turtleRunning: false,
}
}
๊ฑฐ๋ถ์ด๋ ํญ์ ์ผ์ ํ ์๋๋ก ๋ฌ๋ฆฐ๋ค. ๊ฒ์์ ์น๋ฆฌ์ฌ๋ถ๋ ํ ๋ผ์๊ฒ ๋ฌ๋ ค์๋ ์ ์ด๋ค. rabbitRunnig์ ํ ๋ผ๊ฐ ๋๋ฌด์์ ์ ๋นํ ์ฌ๋ ๋ฒ์ ์ด๊ณ rabbitStopping์ ํ ๋ผ๊ฐ ๋๋ฌด์์ ๋ง์ด ์ฌ์์ ๋ ๋ฒ์ ์ด๋ค. ์ด๋ ์ด ์ ์ ๊ธฐ๋ณธ ์ํ๊ฐ์ false๋ก ์์ง์์ด ์๋๋ก ํ๊ณ , ์์ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ํ๊ฐ์ด ๋ฐ๋๋๋ก ํ๋ค.
gameStart() {
const currentSeconds = new Date().getSeconds()
if (currentSeconds % 2 === 0) {
this.rabbitRunning = true
} else {
this.rabbitStopping = true
}
this.turtleRunning = true
}
ํ์ฌ ์ด๋ฅผ ๊ฐ์ ธ์์ ์ด๋ฅผ 2๋ก ๋๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋๋จธ์ง๊ฐ 0์ด๋ฉด(์ง์) ํ ๋ผ๊ฐ ์ด๊ธฐ๋๋ก ํ๋ค. 0์ด ์๋๋ฉด ํ ๋ผ๊ฐ ์ง๋๋ก ์ฝ๋๋ฅผ ์์ฑํ๋ค.
@keyframes rabbit-run {
0% {
left: -10%;
}
40% {
left: 50%;
}
60% {
left: 50%;
}
100% {
left: 90%;
}
}
.running-rabbit.run {
animation: rabbit-run 5s linear forwards;
}
.running-rabbit.stop {
animation: rabbit-run 7s linear forwards;
}
์ฒซ ์์์ ์ผ์ชฝ์์ -10 ์์น์์ ์์ํ๋ค. ํ๋ฉด์์ผ๋ก ๊ทธ๋ฅ ์ผ์ชฝ ๋์ ์๋๋ก ๋ณด์ธ๋ค. ํ ๋ผ๊ฐ ์ค๊ฐ์ฏค ์ค๋ฉด ๋ฉ์ถ๋ค. ์ด ๋ถ๊ทผ์ด ๋๋ฌด๊ฐ ์๋ ๋ถ๊ทผ์ด๋ค. ํ ๋ผ๋ 60%์์ ๋ค์ ์์ง์ด๋๋ฐ running-rabbit.run์ธ ๊ฒฝ์ฐ ์ด ๋ชจ๋ ์งํ์ 5์ด์ ๋๋ด๋๋ก ํ๊ณ running-rabbit.stop์ 7์ด์ ๋๋ด๋๋ก ํ๋ค.
@keyframes turtle-run {
0% {
left: -10%;
}
100% {
left: 90%;
}
}
.running-turtle.run {
animation: turtle-run 6s linear forwards;
}
๊ฑฐ๋ถ์ด ์ฝ๋๋ ๋ ๊ฐ๋จํ๋ค. ๋ฌด์กฐ๊ฑด left -10์์ 90๊น์ง ๊ฐ๋ ๊ฑธ 6์ด์ ๋๋ด๋๋ก ์ค์ ํ๋ค.
- GSAP
<img ref="rabbitEl" src="@/assets/rabbit2.png" class="runner runner-rabbit" />
<img ref="turtleEl" src="@/assets/turtle2.png" class="runner runner-turtle" />
const rabbitEl = ref(null)
const turtleEl = ref(null)
const {
countdown,
message,
reset,
running,
start,
winner,
} = useRaceGame({ rabbitEl, turtleEl })
ํ ๋ผ, ๊ฑฐ๋ถ์ด ์ด๋ฏธ์ง์ ref๋ฅผ ๋ถ์ด๊ณ ์ด DOM ์์๋ฅผ useRaceGame(jsํ์ผ)์ ๋๊ฒผ๋ค. ์ด์ ์ ๋๋ฉ์ด์ ์ CSS ํด๋์ค๊ฐ ์๋๋ผ JS์์ ์ง์ ์คํํ๊ฒ ๋๋ค.
function pickWinner() {
const currentSeconds = new Date().getSeconds()
return currentSeconds % 2 === 0 ? 'rabbit' : 'turtle'
}
๋จผ์ ์น์๋ฅผ ๋ฝ์. ๋ก์ง์ ์ฒ์ ์ง ๊ฑฐ๋ ๋๊ฐ๋ค. ํ์ฌ ์ด๊ฐ ์ง์๋ฉด rabbit์ ์น๋ฆฌ, ํ์๋ฉด turtle์ ์น๋ฆฌ์ด๋ค.
function run(nextWinner) {
const finishX = () => window.innerWidth * 0.82
timeline = gsap.timeline({
defaults: {
ease: 'power1.inOut',
},
})
๊ฒฐ์น์ ์์น๋ finishX, ํ๋ฉด ์ ์ฒด ๋๋น(window.innerWidth)์์ 82% ์ง์ ์ ๊ฒฐ์น์ ์ผ๋ก ์ค์ ํ๋ค. ํจ์ () => ํํ๋ก ์์ฑํด์ ์ฐฝ์ ํฌ๊ธฐ๊ฐ ๋ณํด๋ ํญ์ ๋์ ์ผ๋ก ๋ง์ถฐ์ง๋๋ก ํ๋๋ฐ, ์ ๋ ์ง ๋ชจ๋ฅด๊ฒ ๋ค. (๋ด๊ฒ ํ๋ฉด์ด ํ๋์ด๋ค!)
gsap.timeline( )์ ์ฌ๋ฌ ์ ๋๋ฉ์ด์ ์ ์ฐ๊ฒฐํด์ ์ฌ์ํ๊ธฐ ์ํด ํ์๋ผ์ธ์ ๋ง๋ค์๋ค. ๊ธฐ๋ณธ ์์ง์์ธ ease ์์ฑ๊ฐ์ธ power1.inOut์ ์ฌ์ฉํ๋ค. ์ด๊ฑด ๋์ค์ gsap ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์์ฑํ๋ฉด์ ๋ ์์ธํ ์ค๋ช ํ๊ฒ ๋ค.
if (nextWinner === 'rabbit') {
timeline
.to(rabbitEl.value, {
x: () => window.innerWidth * 0.42,
y: -10,
rotate: -4,
duration: 2,
ease: 'power2.out',
}, 0)
.to(rabbitEl.value, {
y: 4,
rotate: -8,
duration: 0.65,
ease: 'sine.inOut',
})
.to(rabbitEl.value, {
y: -8,
rotate: 3,
duration: 0.3,
ease: 'power2.out',
})
.to(rabbitEl.value, {
x: finishX,
y: -12,
rotate: 3,
duration: 2.2,
ease: 'power3.out',
onComplete: () => end('rabbit'),
})
.to(turtleEl.value, {
x: () => window.innerWidth * 0.62,
y: 5,
duration: 5.8,
}, 0)
}
ํ ๋ผ๊ฐ ์ด๊ธธ๋ ์ฝ๋์ด๋ค. ์ถ๋ฐํ์๋ง์ ํ๋ฉด์ 42% ์ง์ ๊น์ง ๋ฐ์ด๊ฐ๋ค. ์ด๊ฒ 2์ด ๊ฑธ๋ฆฌ๋๋ก ํ๋ค. ๋๋ฌด ๋ถ๊ทผ๊น์ง ๊ฐ๋ฉด ๋ฉ์ถฐ์ ์ฝ๊ฐ์ ์์ง์์ ์ฃผ์๋ค. ์ด์ง ๋ชธ์ ํ๋๋ ๋์์ธ๋ฐ, ๋ณ ๋ค๋ฅธ ์๋ฏธ๋ ์๊ณ ๋๋ฆ ์์ด๋ณด์ด๊ณ ์ถ์ด์ ์ถ๊ฐํ๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฒฐ์น์ ์ผ๋ก ๋น ๋ฅด๊ฒ ๋ฌ๋ จ๊ฐ๋ค. ์ด๋ power3.out์ ์ผ๋ค. ๋์ฐฉํ๋ฉด onComplete๊ฐ ๋ฐ๋ํด์ end('rabbit') ํจ์๋ฅผ ๋ถ๋ฅด๊ณ ๊ฒฝ์ฃผ๊ฐ ๋๋๋ค.
๊ฑฐ๋ถ์ด๋ ํ ๋ผ์ ๋์์ ์ถ๋ฐํ์ง๋ง 5.8์ด๋์ ํ๋ฉด์ 62% ์ง์ ๊น์ง ๊ฐ๋๋ก ์ค์ ํ๋ค. ๊ฒฐ์น์ ์ ๋ฟ์ง ๋ชปํ๊ณ ๋๋๋ค.
else {
timeline
.to(rabbitEl.value, {
x: () => window.innerWidth * 0.38,
y: -10,
duration: 2,
ease: 'power2.out',
}, 0)
.to(rabbitEl.value, {
rotate: -8,
y: 2,
duration: 1.1,
ease: 'sine.inOut',
yoyo: true,
repeat: 1,
})
.to(rabbitEl.value, {
x: () => window.innerWidth * 0.68,
y: -6,
rotate: 2,
duration: 2.8,
})
.to(turtleEl.value, {
x: finishX,
y: 2,
duration: 5.7,
ease: 'power1.out',
onComplete: () => end('turtle'),
}, 0)
}
}
๊ฑฐ๋ถ์ด๊ฐ ์ด๊ธธ๋ ์ฝ๋์ด๋ค. ํ ๋ผ๊ฐ ํ๋ฉด์ 38% ์ง์ ๊น์ง ๊ฐ๋ค. ์๋ง ์ด ์ง์ ์ด ๋๋ฌด ๋ถ๊ทผ์ผ ๊ฒ์ด๋ค. ์ด ๋ค์ ์ฝ๋์ yoyo: true๊ฐ ์๋๋ฐ, ๋๋ฌด ๋ถ๊ทผ์์ ํ ๋ผ์ ์์ง์์ด ์ญ์ฌ์๋๋๋ก ํ ๊ฒ์ด๋ค. ๊ฒ์์ ์งํํ ๋ ๋ณด๋ฉด ํ ๋ผ๊ฐ ์ด๊ธธ๋ ํ ๋ผ์ ๊ธฐ์ธ๊ธฐ๊ฐ ํ ๋ฒ ์์ง์ธ๋ค. ๊ทธ๋ฐ๋ฐ ๊ฑฐ๋ถ์ด๊ฐ ์ด๊ธธ ๋ ํ ๋ผ์ ์์ง์์ด 2๋ฒ ๋ฐ์ํ๋ค. ์ด๊ฒ yoyo๋ฅผ trueํด์ ๊ทธ๋ ๋ค. repeat์ 1๋ก ์ค์ ํด์ ํ๋ฒ๋ง ์ญ์ฌ์ ๋๋๋ก ํ๋ค. ๊ทธ๋ค์ ํ ๋ผ๋ ๋ค์ ๊ฒฐ์น์ ์ ํฅํด ์์ง์ด์ง๋ง 68%์์ ๋ฉ์ถ๊ฒ ๋ ๊ฒ์ด๋ค.
๊ฑฐ๋ถ์ด๋ 5.7ํฌ ๋์ ํ๊ฒฐ๊ฐ์ ๊ฑธ์์ผ๋ก ๊ฒฐ์น์ ์ ํฅํด ์์ง์ธ๋ค.
5. ์ด๊ธด ๋๋ฌผ ํญ์ฃฝ
const origin = nextWinner === 'rabbit'
? { x: 0.88, y: 0.25 }
: { x: 0.88, y: 0.72 }
์ฐ์นํ ๋๋ฌผ์ด ์๋ ์์น์์ ํญ์ฃฝ์ด ํฐ์ง๋๋ก ์ขํ๋ฅผ ๊ณ์ฐํ๋ค. ๊ฒฐ์น์ ์ ํ๋ฉด์ 88% ์ง์ ์ ์๋ค. (์ผ์ชฝ์ ๊ธฐ์ค์ผ๋ก 88%๋งํผ ๋จ์ด์ ธ ์๋ค๋ ๊ฒ) ํ ๋ผ๊ฐ ์ด๊ธฐ๋ฉด A : B ์ค A๊ฐ ๋๋ค. (true๋ฉด A, false๋ฉด B) ๊ทธ๋์ ํ ๋ผ๊ฐ ์ด๊ธฐ๋ฉด y์ถ์ด ํ๋ฉด์ 25%(์๊ฐ ๊ธฐ์ค) ์ง์ ์์ ํญ์ฃฝ์ด ํฐ์ง๊ณ ํ ๋ผ๊ฐ ์ง๋ฉด ๊ฑฐ๋ถ์ด๊ฐ ์๋ ๋ถ๊ทผ์ธ 72% ์ง์ ์์ ํฐ์ง๋ค.
confetti({
particleCount: 110,
spread: 70,
startVelocity: 36,
origin,
colors: ['#d692c8', '#8caa97', '#f8d0e5', '#fbfbb4', '#ffffff'],
})
ํญ์ฃฝ์ ์ด์๊ฒ ๋ง๋ค์๋ค. ์ด ๋ถ๋ถ์ ์์ง์๊ณผ ์์์ ์ค์ ํ ๋ถ๋ถ์ด๋ค. particleCount๋ 110, ๊ฝ๊ฐ๋ฃจ์ ๊ฐ์๋ฅผ ์๋ฏธํ๋ค. spread: 70์ ๊ฝ๊ฐ๋ฃจ๊ฐ ํผ์ง๋ ๊ฐ๋์ด๋ค. ๊ทธ๋ฆฌ๊ณ startVelocity๋ ํญ์ฃฝ์ด ์ฒ์ ํฐ์ ธ ๋๊ฐ ๋์ ์ด๊ธฐ ์๋์ด๋ค. ์ซ์๊ฐ ํด์๋ก ๋ ๋ฉ๋ฆฌ ๊ฐํ๊ฒ ๋๊ฐ๋ค. origin์ ์๊น ์์์ ๊ณ์ฐํ ์น์์ ์ขํ๋ฅผ ๋ฃ๋ ๋ถ๋ถ์ด๋ค. colors๋ ๊ฝ๊ฐ๋ฃจ ์์ ์กฐํฉ์ด ๋๋ค.
https://www.npmjs.com/package/canvas-confetti