Created
August 7, 2023 14:42
-
-
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
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 * 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