kurita-0: client/src/HeartCounter.tsx
import isEqual from 'lodash/isEqual'
import sample from 'lodash/sample'
import React, {useLayoutEffect, useRef, useState} from 'react'
import styled, {keyframes} from 'styled-components'
import Emoji from './Emoji'
import useTime from './useTime'
const heartFlyTime = 1000
const maxHearts = 50
const heartEmojis = ['❤', '💚', '💙', '💜']
const HeartCounter: React.FC<{
count: number
}> = ({count}) => {
const [lastCount, setLastCount] = useState(null)
const [hearts, setHearts] = useState([])
const now = useTime(500)
const nowMs = now.getTime()
const containerRef = useRef(null)
const [size, setSize] = useState([])
const [width, height] = size
useLayoutEffect(
() => {
const box = containerRef.current.getBoundingClientRect()
setSize([box.width, box.height])
},
[],
)
const newHearts = hearts.filter(([emoji, seed, delay, endMs]) => endMs + heartFlyTime > nowMs)
if (count > lastCount && lastCount !== null) {
const actualCount = count - lastCount
const logCount = Math.log(count - lastCount) / Math.log(1.5)
const newCount = Math.max(Math.min(actualCount, 1), logCount)
for (let i = 0; i < newCount; i++) {
if (newHearts.length >= maxHearts) {
break
}
const delay = Math.random()
newHearts.push([
sample(heartEmojis),
Math.random(),
delay,
nowMs + heartFlyTime + delay * heartFlyTime,
])
}
}
if (count != lastCount) {
setLastCount(count)
}
if (!isEqual(hearts, newHearts)) {
setHearts(newHearts)
}
return (
<Fill ref={containerRef} style={{marginBottom: 32, marginTop: -32}}>
{newHearts.map(([emoji, seed, delay, endMs]) => {
const style = {
left: width * seed,
top: height,
opacity: 0,
animationDelay: `${delay * heartFlyTime}ms`,
}
return (
<AnimatedEmoji key={seed} emoji={emoji} size={24} style={style} />
)
})}
</Fill>
)
}
const Fill = styled.div`
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
`
const rise = keyframes`
0% {
transform: translate(0px, 0px);
opacity: 0;
}
25% {
transform: translate(-25px, -50px);
opacity: 1;
}
50% {
transform: translate(0px, -100px);
}
75% {
transform: translate(25px, -150px);
}
100% {
transform: translate(0px, -200px);
opacity: 0;
}
`
const AnimatedEmoji = styled(Emoji)`
position: absolute;
animation: ${rise} ${heartFlyTime}ms linear;
`
export default HeartCounter