euv-engine 0.28.21

A high-performance 2D game engine built on the euv framework, featuring ECS, fixed-timestep game loop, canvas rendering, physics, collision detection, sprite animation, and audio.
Documentation
use super::*;

fn reflective(target: &Object, key: &str, value: &JsValue) {
    let installer: js_sys::Function = js_sys::Function::new_with_args(
        "target, key, value",
        "Object.defineProperty(target, key, { value: value, writable: true, configurable: true });",
    );
    let outcome: Result<JsValue, JsValue> = installer.call3(
        &JsValue::NULL,
        target.as_ref(),
        &JsValue::from_str(key),
        value,
    );
    assert!(outcome.is_ok(), "the stand-in must accept {key}");
}

fn image(width: f64, height: f64) -> HtmlImageElement {
    let element: Object = Object::new();
    reflective(element.as_ref(), "width", &JsValue::from_f64(width));
    reflective(element.as_ref(), "height", &JsValue::from_f64(height));
    element.unchecked_into()
}

fn recording_context() -> (CanvasRenderingContext2d, Object) {
    let context: Object = Object::new();
    reflective(context.as_ref(), "__log", Array::new().as_ref());
    let arm: js_sys::Function = js_sys::Function::new_with_args(
        "target",
        "target.setTransform = function() { target.__log.push(['setTransform', Array.from(arguments)]); }; target.drawImage = function() { target.__log.push(['drawImage', Array.from(arguments)]); };",
    );
    let armed: Result<JsValue, JsValue> = arm.call1(&JsValue::NULL, context.as_ref());
    assert!(
        armed.is_ok(),
        "the sprite draw recorder must install cleanly"
    );
    let typed: CanvasRenderingContext2d = context.clone().unchecked_into();
    (typed, context)
}

fn ops_after(log: &Object, from: u32) -> Vec<String> {
    let entries: Array = Reflect::get(log, &JsValue::from_str("__log"))
        .expect("log readable")
        .unchecked_into();
    (from..entries.length())
        .map(|index: u32| {
            let entry: Array = entries.get(index).unchecked_into();
            entry.get(0).as_string().unwrap_or_default()
        })
        .collect()
}

fn args_after(log: &Object, op: &str, from: u32) -> Array {
    let entries: Array = Reflect::get(log, &JsValue::from_str("__log"))
        .expect("log readable")
        .unchecked_into();
    for index in from..entries.length() {
        let entry: Array = entries.get(index).unchecked_into();
        if entry.get(0).as_string().as_deref() == Some(op) {
            return entry.get(1).unchecked_into();
        }
    }
    panic!(
        "{op} was never called after index {from}; saw {:?}",
        ops_after(log, from)
    );
}

fn log_len(log: &Object) -> u32 {
    Reflect::get(log, &JsValue::from_str("__log"))
        .expect("log readable")
        .unchecked_into::<Array>()
        .length()
}

fn sheet() -> SpriteSheet {
    SpriteSheet::from_image(image(64.0, 32.0), 16.0, 16.0)
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records a CanvasRenderingContext2d stand-in, which only exists under wasm"
)]
fn a_frame_is_drawn_between_a_transform_and_its_reset() {
    let (context, log): (CanvasRenderingContext2d, Object) = recording_context();
    let subject: SpriteSheet = sheet();
    let before: u32 = log_len(&log);

    subject.draw_frame(&context, 0, &Transform2D::default());

    assert_eq!(
        ops_after(&log, before),
        vec![
            String::from("setTransform"),
            String::from("drawImage"),
            String::from("setTransform"),
        ],
        "the transform is composed, the sprite is drawn, then the context is handed back"
    );
    let reset: Array = args_after(&log, "setTransform", before + 2);
    assert_eq!(
        reset.get(0).as_f64(),
        Some(1.0),
        "the reset is the identity matrix, so a later draw is not scaled by this sprite"
    );
    assert_eq!(reset.get(1).as_f64(), Some(0.0), "with no shear");
    assert_eq!(reset.get(4).as_f64(), Some(0.0), "and no translation");
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records a CanvasRenderingContext2d stand-in, which only exists under wasm"
)]
fn the_destination_is_centred_on_the_transform_position() {
    let (context, log): (CanvasRenderingContext2d, Object) = recording_context();
    let subject: SpriteSheet = sheet();
    let before: u32 = log_len(&log);

    subject.draw_frame(&context, 0, &Transform2D::default());

    let draw: Array = args_after(&log, "drawImage", before);
    assert_eq!(
        draw.get(5).as_f64(),
        Some(-8.0),
        "a 16 wide frame is offset by half its width so the sprite straddles its position"
    );
    assert_eq!(
        draw.get(6).as_f64(),
        Some(-8.0),
        "and half its height on the other axis"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records a CanvasRenderingContext2d stand-in, which only exists under wasm"
)]
fn the_source_rectangle_follows_the_frame_index() {
    let (context, log): (CanvasRenderingContext2d, Object) = recording_context();
    let subject: SpriteSheet = sheet();

    let first: u32 = log_len(&log);
    subject.draw_frame(&context, 1, &Transform2D::default());
    let first_draw: Array = args_after(&log, "drawImage", first);

    let second: u32 = log_len(&log);
    subject.draw_frame(&context, 2, &Transform2D::default());
    let second_draw: Array = args_after(&log, "drawImage", second);

    assert_eq!(
        first_draw.get(1).as_f64(),
        Some(16.0),
        "frame one of a 16 pixel grid starts one column in"
    );
    assert_eq!(
        second_draw.get(1).as_f64(),
        Some(32.0),
        "and frame two starts two columns in"
    );
    assert_eq!(
        second_draw.get(2).as_f64(),
        Some(0.0),
        "a frame index within the first row does not move down"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records a CanvasRenderingContext2d stand-in, which only exists under wasm"
)]
fn the_transform_rotation_reaches_the_matrix() {
    let (context, log): (CanvasRenderingContext2d, Object) = recording_context();
    let subject: SpriteSheet = sheet();
    let before: u32 = log_len(&log);
    let mut transform: Transform2D = Transform2D::default();
    transform.rotate(PI / 2.0);

    subject.draw_frame(&context, 0, &transform);

    let matrix: Array = args_after(&log, "setTransform", before);
    let a: f64 = matrix.get(0).as_f64().unwrap_or(-1.0);
    let b: f64 = matrix.get(1).as_f64().unwrap_or(-1.0);
    assert!(
        a.abs() < 1e-9,
        "a quarter turn maps the x axis onto the y axis, so a is cos(90) = 0, got {a}"
    );
    assert!(
        (b - 1.0).abs() < 1e-9,
        "and the x axis now points along y, so b is sin(90) = 1, got {b}"
    );
}