All files / web/src/components/toys/coordinate-plane/challenge viewportAnimation.ts

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

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                                                                                                                                                                     
import type { CoordinatePlaneState } from '../types'
import type { ViewportTarget } from './types'
import type { WordProblem } from '../wordProblems/types'

const PADDING_FACTOR = 0.3 // 30% padding around the bounding box

/** Maximum ratio between ppuX and ppuY to avoid extreme visual distortion */
const MAX_PPU_RATIO = 12

/**
 * Compute the target viewport that shows the problem's relevant geometry.
 *
 * Uses independent axis scaling so high-slope problems (e.g., 53 miles/hour)
 * are viewable — the Y axis gets a different scale than X.
 *
 * Returns a ViewportTarget with potentially different ppuX and ppuY.
 */
export function computeViewportTarget(
  problem: WordProblem,
  canvasWidth: number,
  canvasHeight: number,
  _currentState: CoordinatePlaneState
): ViewportTarget {
  // Collect points of interest
  const points: { x: number; y: number }[] = [
    { x: 0, y: 0 }, // origin is always useful context
    { x: 0, y: problem.equation.intercept.num / problem.equation.intercept.den }, // y-intercept
    { x: problem.answer.x, y: problem.answer.y }, // solution point
  ]

  // For two-point problems, include the given points
  if (problem.difficulty === 4) {
    const nums = problem.answer
    points.push({ x: nums.x, y: nums.y })
  }

  // Compute bounding box
  let minX = Infinity,
    maxX = -Infinity,
    minY = Infinity,
    maxY = -Infinity
  for (const p of points) {
    minX = Math.min(minX, p.x)
    maxX = Math.max(maxX, p.x)
    minY = Math.min(minY, p.y)
    maxY = Math.max(maxY, p.y)
  }

  // Add padding
  const rangeX = maxX - minX || 2
  const rangeY = maxY - minY || 2
  const padX = rangeX * PADDING_FACTOR
  const padY = rangeY * PADDING_FACTOR
  minX -= padX
  maxX += padX
  minY -= padY
  maxY += padY

  // Reserve space for the card (roughly 1/3 of the viewport on one side)
  const expandedRangeX = (maxX - minX) * 1.4
  const expandedRangeY = (maxY - minY) * 1.4

  // Compute independent PPU for each axis
  let ppuX = canvasWidth / expandedRangeX
  let ppuY = canvasHeight / expandedRangeY

  // Clamp the ratio to avoid extreme distortion
  const ratio = ppuX / ppuY
  if (ratio > MAX_PPU_RATIO) {
    // X is too zoomed in relative to Y — pull X back
    ppuX = ppuY * MAX_PPU_RATIO
  } else if (ratio < 1 / MAX_PPU_RATIO) {
    // Y is too zoomed in relative to X — pull Y back
    ppuY = ppuX * MAX_PPU_RATIO
  }

  // Center on the midpoint of the bounding box
  const cx = (minX + maxX) / 2
  const cy = (minY + maxY) / 2

  return { cx, cy, ppuX, ppuY }
}