Skip to content

Instantly share code, notes, and snippets.

@maremarismaria
Created August 7, 2023 14:42
Show Gist options
  • Save maremarismaria/cd736535fd2279fd3741a18707b93d8f to your computer and use it in GitHub Desktop.
Save maremarismaria/cd736535fd2279fd3741a18707b93d8f to your computer and use it in GitHub Desktop.
[React][TSX][SVG] Animating a level bar: how to use refs for forcing React to re-render when the SVG defs are updated by the State
import * as React from "react"
export interface Props {
xpLevel: number,
xpNextLevel: number
}
export interface State {
animateLevelBar: boolean
}
export default class LevelBar extends React.Component<Props, State> {
// Hack for forcing React to re-render the entire SVG by changing the key on every render.
// This is necessary for updating the level bar animation, because it is in the SVG defs
SVGKey = 0
previousLevel: React.MutableRefObject<number | null> = React.createRef()
state = {
animateLevelBar: false,
} as State
ids = {
filledBar: "filled-bar"
}
styles = {
emptyBar: {
fill: "#5D540F",
},
text: {
fill: "#e7d200",
fontFamily: "'Signika', cursive",
fontSize: "9px",
fontWeight: 300,
},
} as Record<string, React.CSSProperties>
componentDidMount() {
this.previousLevel.current = this.props.xpLevel
}
componentDidUpdate(prevProps: Readonly<Props>) {
this.previousLevel.current = prevProps.xpLevel
if (this.props.xpLevel !== prevProps.xpLevel) {
this.setState(() => ({
animateLevelBar: true,
}))
}
}
getNormalizedLevel = (xpLevel: number, xpNextLevel: number) => {
if (xpNextLevel === 0) {
return 0
}
const value = (xpLevel / xpNextLevel) * 100
return (value / 100) < 0.5
? Math.ceil(value) / 100
: Math.floor(value) / 100
}
renderLevelBar = (previousLevel: number, level: number) => {
if (this.state.animateLevelBar) {
return (
<>
<stop offset="0" stopColor="#e7d200">
<animate dur="2s" attributeName="offset" fill="freeze" from={`${previousLevel}`} to={`${level}`} />
</stop>
<stop offset="0" stopColor="transparent">
<animate dur="2s" attributeName="offset" fill="freeze" from={`${previousLevel}`} to={`${level}`} />
</stop>
</>
)
}
return (
<>
<stop offset={`${level}`} stopColor="#e7d200"/>
<stop offset={`${level}`} stopColor="transparent"/>
</>
)
}
render() {
const { filledBar } = this.ids
const { text, emptyBar } = this.styles
const { xpLevel, xpNextLevel } = this.props
const level = this.getNormalizedLevel(xpLevel, xpNextLevel)
const previousLevel = this.getNormalizedLevel(this.previousLevel.current, xpNextLevel)
return (
<svg
xmlns={"http://www.w3.org/2000/svg"}
xmlnsXlink={"http://www.w3.org/1999/xlink"}
key={this.SVGKey++}
>
<defs>
<linearGradient id={filledBar}>
{ this.renderLevelBar(previousLevel, level) }
</linearGradient>
</defs>
<path style={emptyBar} d={"M12,40.5H162v-6H18l-6,6Z"} />
<path fill={`url(#${filledBar})`} d={"M12,40.5H162v-6H18l-6,6Z"}/>
<text style={text} x={"54%"} y={"28%"} dominantBaseline={"middle"} textAnchor={"end"}>
<tspan>{ xpLevel }</tspan>
<tspan>/</tspan>
<tspan>{ xpNextLevel }</tspan>
</text>
</svg>
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment