Skip to content

Instantly share code, notes, and snippets.

@teidesu
Created June 22, 2021 13:11
Show Gist options
  • Save teidesu/6f28d81f0606b33f569f81a7d0d18561 to your computer and use it in GitHub Desktop.
Save teidesu/6f28d81f0606b33f569f81a7d0d18561 to your computer and use it in GitHub Desktop.
Play-Pause animated icon for Vue.
<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