All files / web/src/components/flowchart AnimatedMathDisplay.tsx

0% Statements 0/90
0% Branches 0/1
0% Functions 0/1
0% Lines 0/90

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91                                                                                                                                                                                     
'use client'

import { useRef, useEffect, useState } from 'react'
import { css } from '../../../styled-system/css'
import { MathDisplay } from './MathDisplay'

interface AnimatedMathDisplayProps {
  /** Math expression string to render (e.g., "52 − 37" or "3 2/9 − 1 1/2") */
  expression: string
  /** Font size for the display */
  size?: 'sm' | 'md' | 'lg' | 'xl'
  /** Animation duration in ms (default: 700 to match dice animation) */
  duration?: number
}

/**
 * AnimatedMathDisplay - MathML display with crossfade transitions
 *
 * When the expression changes, the old expression fades out while
 * the new expression fades in (inverse opacity). Uses CSS Grid to
 * stack layers independently.
 */
export function AnimatedMathDisplay({
  expression,
  size = 'lg',
  duration = 700,
}: AnimatedMathDisplayProps) {
  const prevExpressionRef = useRef<string>(expression)

  const [layers, setLayers] = useState<Array<{ expression: string; opacity: number; id: number }>>([
    { expression, opacity: 1, id: 0 },
  ])

  const idCounter = useRef(1)

  useEffect(() => {
    if (expression !== prevExpressionRef.current) {
      const oldExpression = prevExpressionRef.current
      prevExpressionRef.current = expression
      const newId = idCounter.current++

      // Add new layer at opacity 0, keep old layer at opacity 1
      setLayers([
        { expression: oldExpression, opacity: 1, id: newId - 1 },
        { expression, opacity: 0, id: newId },
      ])

      // After a frame, trigger the crossfade
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          setLayers([
            { expression: oldExpression, opacity: 0, id: newId - 1 },
            { expression, opacity: 1, id: newId },
          ])
        })
      })

      // After animation, remove the old layer
      const timeout = setTimeout(() => {
        setLayers([{ expression, opacity: 1, id: newId }])
      }, duration + 50)

      return () => clearTimeout(timeout)
    }
  }, [expression, duration])

  return (
    <span
      data-testid="animated-math-display"
      data-expression={expression}
      className={css({
        display: 'inline-grid',
        placeItems: 'center',
      })}
    >
      {layers.map((layer) => (
        <span
          key={layer.id}
          style={{
            gridArea: '1 / 1',
            opacity: layer.opacity,
            transition: `opacity ${duration}ms ease-in-out`,
          }}
        >
          <MathDisplay expression={layer.expression} size={size} />
        </span>
      ))}
    </span>
  )
}