Repository: moeru-ai/airi
File: apps/stage-web/src/components/IconAnimation.vue (Vue)

Code before:
      isAnimating.value = true
    })
  }
})

const centerPosition = computed(() => {
  const size = props.started ? 25 : props.iconSize
  return `calc(50vw - ${size / 2}rem), calc(50vh - ${size / 2}rem)`
})

const position = computed(() =>
  isAnimating.value
    ? (props.isReverse ? props.position : centerPosition.value)
    : (props.isReverse ? centerPosition.value : props.position),
)

const opacity = computed(() => {
  return isAnimating.value ? props.isReverse ? 0 : 1 : props.isReverse ? 1 : 0
})

const size = computed(() => {
  return isAnimating.value ? props.isReverse ? props.iconSize : 25 : props.isReverse ? 25 : props.iconSize
})

const textColor = computed(() => {
  return isAnimating.value ? props.isReverse ? props.textColor : 'text-white' : props.isReverse ? 'text-white' : props.textColor
})
</script>

<template>
  <div pointer-events-none fixed left-0 top-0 h-full w-full>
    <div
      bg-primary-500 pointer-events-none h-full w-full transition-opacity ease-linear :style="{
        opacity,
        transitionDuration: `${duration}ms`,
      }"
    />
    <div
      pointer-events-none
      fixed left-0 top-0 transition-all ease-in-out :style="{
        width: `${size}rem`,
        height: `${size}rem`,
        transform: `translate(${position})`,
        transitionDuration: `${duration}ms`,
      }" :class="[
        textColor,
        icon,
        { 'transition-all': isAnimating },
      ]"
      @transitionend="emit('animationEnded')"
    />

Review comment (suggestion):
this is too lengthy (and you'll need to repeat the values every time), consider using `xor`?
e.g.
```js
return (isAnimating.value ^ props.isReverse) ? 'text-white' : props.textColor
```

additionally, you might consider using this way:
```js
const from = { position: ..., opacity: ..., ... }
const to = { ... }

const cssProps = computed(() => (isAnimating.value ^ props.isReverse) ? from : to)
```
to further reduce repeated calculations

Code after:
    requestAnimationFrame(() => {
      isAnimating.value = true
    })
  }
})

const clsAndProps = computed(() => {
  return {
    opacity: isAnimating.value !== props.isReverse ? 1 : 0, // this equals to opacity: isAnimating.value ? props.isReverse ? 1 : 0 : props.isReverse ? 0 : 1
    size: isAnimating.value !== props.isReverse ? 25 : props.iconSize,
    position: isAnimating.value !== props.isReverse ? `calc(50dvw - 12.5rem), calc(50dvh - 12.5rem)` : props.position,
    textColor: isAnimating.value !== props.isReverse ? 'text-white' : props.textColor,
  }
})

const animationEndProps = ref<string[]>([])
const animationEnded = ref(false)
function handleAnimationEnded(e: TransitionEvent) {
  animationEndProps.value.push(e.propertyName)
  if (animationEndProps.value.includes('color') && animationEndProps.value.includes('width') && animationEndProps.value.includes('height') && animationEndProps.value.includes('transform')) {
    animationEnded.value = true
    emit('animationEnded')
  }
}
</script>

<template>
  <div
    pointer-events-none fixed w="100dvw" h="100dvh"
    :style="{
      zIndex: animationEnded ? zIndex : undefined,
    }"
  >
    <div
      bg-primary-500 fixed inset-0 transition-opacity ease-linear :style="{
        opacity: clsAndProps.opacity,
        transitionDuration: `${duration}ms`,
      }"
    />
    <div
      fixed inset-0 ease-in-out :style="{
        width: `${clsAndProps.size}rem`,
        height: `${clsAndProps.size}rem`,
        transform: `translate(${clsAndProps.position})`,
        transitionDuration: `${duration}ms`,
      }" :class="[
        clsAndProps.textColor,
        props.icon,
        { 'transition-all': isAnimating },
      ]"
      @transitionend="handleAnimationEnded"