///|
fn copy_canvas(canvas : Canvas) -> Canvas {
  let copy = Canvas::new(
    width=canvas.width,
    height=canvas.height,
    fill=rgb(0, 0, 0),
  )
  for i, color in canvas.pixels {
    copy.pixels[i] = color
  }
  copy
}

///|
fn clear_canvas_rect(
  canvas : Canvas,
  left : Int,
  top : Int,
  width : Int,
  height : Int,
  fill : Rgba,
) -> Unit {
  for y in 0.. Unit {
  let source = frame.canvas
  let (left, top) = frame.offset()
  for y in 0.. ()
        Some(color) => {
          let matches_transparent = match frame.transparent {
            None => false
            Some(t) => color == t
          }
          let transparent = color.alpha() == 0 || matches_transparent
          if !transparent {
            ignore(canvas.set(x=left + x, y=top + y, color~))
          }
        }
      }
    }
  }
}

///|
/// Compose frames in display order and apply GIF disposal semantics.
///
/// The returned canvases are independent snapshots, so callers can render or
/// compare them without mutating later frames. `Background` clears only the
/// frame rectangle; `Previous` restores the canvas from before the frame.
pub fn compose(animation : Animation) -> Array[Canvas] {
  let current = Canvas::new(
    width=animation.width,
    height=animation.height,
    fill=animation.background,
  )
  let rendered : Array[Canvas] = []
  for frame in animation.frames {
    let before = copy_canvas(current)
    draw_frame(current, frame)
    rendered.push(copy_canvas(current))
    let (left, top) = frame.offset()
    match frame.disposal {
      Keep => ()
      Background =>
        clear_canvas_rect(
          current,
          left,
          top,
          frame.canvas.width,
          frame.canvas.height,
          animation.background,
        )
      Previous =>
        for i, color in before.pixels {
          current.pixels[i] = color
        }
    }
  }
  rendered
}

///|
pub fn Canvas::copy(self : Canvas) -> Canvas {
  copy_canvas(self)
}