earthbound-battle-backgrounds 0.1.0

Emulate and render the battle backgrounds from EarthBound / Mother 2.
Documentation
use std::cell::RefCell;
use std::f64::consts::PI as π;
use std::rc::Rc;
const R: usize = 0;
const G: usize = 1;
const B: usize = 2;
const A: usize = 3;

fn mod_(n: i16, m: i16) -> i16 {
    ((n % m) + m) % m
}

use crate::engine::{SNES_HEIGHT, SNES_WIDTH};
use crate::rom::distortion_effect::{
    DistortionEffect, HORIZONTAL, HORIZONTAL_INTERLACED, VERTICAL,
};

#[allow(non_snake_case)]
#[derive(Debug)]
pub struct Distorter {
    bitmap: Rc<RefCell<Vec<i16>>>,
    pub effect: Rc<RefCell<DistortionEffect>>,
    C1: f64,
    C2: f64,
    C3: f64,
    amplitude: f64,
    frequency: f64,
    compression: i16,
    speed: f64,
}

impl Distorter {
    pub fn new(bitmap: Rc<RefCell<Vec<i16>>>) -> Self {
        // There is some redundancy here: 'effect' is currently what is used
        // in computing frames, although really there should be a list of
        // four different effects ('dist') which are used in sequence.
        Distorter {
            bitmap,
            effect: Rc::new(RefCell::new(DistortionEffect::new(0))),
            // NOTE: Another discrepancy from Java: These values should be "short" and must have a specific precision. This seems to affect backgrounds with distortEffect === HORIZONTAL
            C1: 1. / 512.,
            C2: 8. * π / (1024. * 256.),
            C3: π / 60.,
            amplitude: 0.,
            compression: 0,
            frequency: 0.,
            speed: 0.,
        }
    }

    fn set_offset_constants(&mut self, ticks: u64, effect: &DistortionEffect) {
        let amplitude = effect.amplitude();
        let amplitude_acceleration = effect.amplitude_acceleration();
        let compression = effect.compression();
        let compression_acceleration = effect.compression_acceleration();
        let frequency = effect.frequency();
        let frequency_acceleration = effect.frequency_acceleration();
        let speed = effect.speed();
        // Compute "current" values of amplitude, frequency and compression.
        let t2 = ticks * 2;
        self.amplitude = self.C1 * f64::from(amplitude + amplitude_acceleration * t2 as i16);
        self.frequency = self.C2 * f64::from(frequency + frequency_acceleration * t2 as i16);
        self.compression = 1 + (compression + compression_acceleration * t2 as i16) / 256;
        self.speed = self.C3 * f64::from(speed) * ticks as f64;
    }

    #[allow(non_snake_case)]
    fn S(&self, y: i16) -> i16 {
        (f64::from(self.amplitude)
            * (f64::from(self.frequency) * f64::from(y) + f64::from(self.speed)).sin())
        .round() as i16
    }

    pub fn overlay_frame(
        &mut self,
        dst: &mut [u8],
        letterbox: u8,
        ticks: u64,
        alpha: f32,
        erase: bool,
    ) {
        let bitmap = self.bitmap.clone();
        self.compute_frame(
            dst,
            &bitmap.borrow(),
            letterbox,
            ticks,
            alpha,
            erase,
            self.effect.clone(),
        );
    }

    /// Evaluates the distortion effect at the given destination line and
    /// time value and returns the computed offset value.
    /// If the distortion mode is horizontal, this offset should be interpreted
    /// as the number of pixels to offset the given line's starting x position.
    /// If the distortion mode is vertical, this offset should be interpreted as
    /// the y-coordinate of the line from the source bitmap to draw at the given
    /// y-coordinate in the destination bitmap.
    /// @param y
    ///   The y-coordinate of the destination line to evaluate for
    /// @param t
    ///   The number of ticks since beginning animation
    /// @return
    ///   The distortion offset for the given (y, t) coordinates
    fn get_applied_offset(&self, y: i16, distortion_effect: u8) -> i16 {
        let s = self.S(y);
        match distortion_effect {
            HORIZONTAL => s,
            HORIZONTAL_INTERLACED => {
                if y % 2 == 0 {
                    -s
                } else {
                    s
                }
            }
            VERTICAL => {
                // Compute L
                mod_(s + y * self.compression, 256)
            }
            _ => s,
        }
    }

    fn compute_frame(
        &mut self,
        destination_bitmap: &mut [u8],
        source_bitmap: &[i16],
        letterbox: u8,
        ticks: u64,
        alpha: f32,
        erase: bool,
        effect: Rc<RefCell<DistortionEffect>>,
    ) {
        let distortion_effect = effect.borrow().type_();
        let new_bitmap = destination_bitmap;
        let old_bitmap = source_bitmap;
        // TODO: Hardcoding is bad
        let dst_stride = 1024;
        let src_stride = 1024;

        // Given the list of 4 distortions and the tick count, decide which
        // effect to use:
        // Basically, we have 4 effects, each possibly with a duration.
        // Evaluation order is: 1, 2, 3, 0
        // If the first effect is null, control transitions to the second effect.
        // If the first and second effects are null, no effect occurs.
        // If any other effect is null, the sequence is truncated.
        // If a non-null effect has a zero duration, it will not be switched
        // away from.
        // Essentially, this configuration sets up a precise and repeating
        // sequence of between 0 and 4 different distortion effects. Once we
        // compute the sequence, computing the particular frame of which distortion
        // to use becomes easy; simply mod the tick count by the total duration
        // of the effects that are used in the sequence, then check the remainder
        // against the cumulative durations of each effect.
        // I guess the trick is to be sure that my description above is correct.
        // Heh.
        let mut b_pos: usize;
        let mut s_pos: usize;
        let mut dx;
        self.set_offset_constants(ticks, &effect.borrow());
        for y in 0..usize::from(SNES_HEIGHT) {
            let offset = self.get_applied_offset(y as i16, distortion_effect);
            #[allow(non_snake_case)]
            let L = if distortion_effect == VERTICAL {
                offset
            } else {
                y as i16
            };
            for x in 0..usize::from(SNES_WIDTH) {
                b_pos = x * 4 + y * dst_stride;
                if y < letterbox.into() || y > usize::from(SNES_HEIGHT) - usize::from(letterbox) {
                    new_bitmap[b_pos + R] = 0;
                    new_bitmap[b_pos + G] = 0;
                    new_bitmap[b_pos + B] = 0;
                    new_bitmap[b_pos + A] = 255;
                    continue;
                }
                dx = x;
                if distortion_effect == HORIZONTAL || distortion_effect == HORIZONTAL_INTERLACED {
                    dx = mod_(x as i16 + offset, SNES_WIDTH as i16) as usize;
                }
                s_pos = dx * 4 + L as usize * src_stride;
                // Either copy or add to the destination bitmap.
                if erase {
                    new_bitmap[b_pos + R] = (alpha * f32::from(old_bitmap[s_pos + R])) as u8;
                    new_bitmap[b_pos + G] = (alpha * f32::from(old_bitmap[s_pos + G])) as u8;
                    new_bitmap[b_pos + B] = (alpha * f32::from(old_bitmap[s_pos + B])) as u8;
                    new_bitmap[b_pos + A] = 255;
                } else {
                    new_bitmap[b_pos + R] += (alpha * f32::from(old_bitmap[s_pos + R])) as u8;
                    new_bitmap[b_pos + G] += (alpha * f32::from(old_bitmap[s_pos + G])) as u8;
                    new_bitmap[b_pos + B] += (alpha * f32::from(old_bitmap[s_pos + B])) as u8;
                    new_bitmap[b_pos + A] = 255;
                }
            }
        }
    }
}