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

Code before:
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')"
    />
  </div>
</template>


Review comment (bug):
~1. add `z--1` back (you can see the visual differences in /modules page, etc.)~
  update: i know what's the problem, the transition should be on top of the UI but the final icon should not, so applying z--1 here will be wrong. let's leave this first, i'll fix it on further PRs
2. `left-0 top-0 h-full w-full` --> `inset-0`
3. `pointer-events-none` inherits, so just adding for the parent div will be ok, no need to add them for all children

Code after:

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"
    />
  </div>
</template>