Created
June 22, 2021 13:11
-
-
Save teidesu/6f28d81f0606b33f569f81a7d0d18561 to your computer and use it in GitHub Desktop.
Play-Pause animated icon for Vue.
This file contains 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
<template> | |
<svg | |
ref="svg" | |
height="100%" | |
width="100%" | |
viewBox="0 0 24 24" | |
class="play-pause-icon" | |
> | |
<path ref="path1" d=""></path> | |
<path ref="path2" d=""></path> | |
</svg> | |
</template> | |
<script lang="ts"> | |
import { Component, Prop, Ref, Vue, Watch } from 'vue-property-decorator' | |
type FlatPath = number[] | |
const pause1: FlatPath = [ | |
20, 4, | |
20, 20, | |
15, 20, | |
15, 4 | |
] | |
const pause2: FlatPath = [ | |
4, 4, | |
4, 20, | |
9, 20, | |
9, 4 | |
] | |
const play1: FlatPath = [ | |
12, 4, | |
20, 20, | |
12, 20, | |
12, 4 | |
] | |
const play2: FlatPath = [ | |
12, 4, | |
4, 20, | |
12, 20, | |
12, 4 | |
] | |
@Component({ | |
name: 'PlayPauseIcon' | |
}) | |
export default class PlayPauseIcon extends Vue { | |
@Prop({ default: 'pause', validator: (v) => ['pause', 'play'].indexOf(v) > -1 }) readonly state!: 'pause' | 'play' | |
@Prop({ type: Number, default: 300 }) readonly duration!: number | |
@Ref() svg!: SVGElement | |
@Ref() path1!: SVGPathElement | |
@Ref() path2!: SVGPathElement | |
private pendingAnimation?: number | |
private pendingAnimationPos?: number | |
private updatePath(first: FlatPath, second: FlatPath) { | |
this.path1.setAttribute('d', PlayPauseIcon.buildPath(first)) | |
this.path2.setAttribute('d', PlayPauseIcon.buildPath(second)) | |
} | |
private static interp(t: number): number { | |
return t < .5 ? 2 * t * t : -1 + ( | |
4 - 2 * t | |
) * t | |
} | |
private static interpPath(fr: FlatPath, to: FlatPath, pos: number) { | |
if (fr.length !== to.length) { | |
throw Error('Can\'t interpolate paths with different sizes.') | |
} | |
const ret: FlatPath = [] | |
for (let i = 0; i < fr.length; i++) { | |
ret.push(( | |
to[i] - fr[i] | |
) * pos + fr[i]) | |
} | |
return ret | |
} | |
private static buildPath(path: FlatPath): string { | |
let positions = [] | |
for (let i = 0; i < path.length; i += 2) { | |
positions.push(path[i] + ' ' + path[i + 1]) | |
} | |
return 'M ' + positions.join(' L ') + ' Z' | |
} | |
jumpTo(state: 'pause' | 'play') { | |
if (state === 'pause') { | |
this.updatePath(pause1, pause2) | |
this.svg.style.transform = 'rotate(0deg)' | |
} else if (state === 'play') { | |
this.updatePath(play1, play2) | |
this.svg.style.transform = 'rotate(90deg)' | |
} else { | |
throw Error('Invalid state: ' + state) | |
} | |
} | |
@Watch('state') | |
animateTo(state: 'pause' | 'play', old?: 'pause' | 'play') { | |
if (state === this.state && !old) { | |
return | |
} | |
if (['pause', 'play'].indexOf(state) === -1) { | |
throw Error('Invalid state: ' + state) | |
} | |
if (this.pendingAnimation) cancelAnimationFrame(this.pendingAnimation) | |
const skip = this.pendingAnimationPos ? 1 - this.pendingAnimationPos : 0 | |
const start = performance.now() | |
const animate = (now: number) => { | |
let frac = Math.min(1, (now - start) / this.duration + skip) | |
this.pendingAnimationPos = frac | |
let pos = PlayPauseIcon.interp(frac) | |
if (frac < 1) { | |
if (state === 'pause') { | |
this.updatePath( | |
PlayPauseIcon.interpPath(play1, pause1, pos), | |
PlayPauseIcon.interpPath(play2, pause2, pos) | |
) | |
} else { | |
this.updatePath( | |
PlayPauseIcon.interpPath(pause1, play1, pos), | |
PlayPauseIcon.interpPath(pause2, play2, pos) | |
) | |
} | |
this.svg.style.transform = 'rotate(' + ((state === 'pause' ? 1 - pos : pos) * 90) + 'deg)' | |
this.pendingAnimation = requestAnimationFrame(animate) | |
} else { | |
this.svg.style.transform = 'rotate(' + (state === 'pause' ? 0 : 90) + 'deg)' | |
this.pendingAnimationPos = undefined | |
if (state === 'pause') { | |
this.updatePath(pause1, pause2) | |
} else { | |
this.updatePath(play1, play2) | |
} | |
} | |
} | |
this.pendingAnimation = requestAnimationFrame(animate) | |
} | |
mounted() { | |
this.jumpTo(this.state) | |
} | |
} | |
</script> | |
<style> | |
.play-pause-icon { | |
transform-origin: center; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment