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::*;

const GL_NAMES: &str = r#"[
    "enable","disable","depthFunc","depthMask","blendFuncSeparate",
    "blendEquationSeparate","cullFace","frontFace","colorMask","scissor",
    "viewport","activeTexture","bindTexture","useProgram","drawElements",
    "drawArraysInstanced","drawElementsInstanced","readPixels","clear",
    "clearColor","clearDepth","pixelStorei","texImage2D","texSubImage2D",
    "createTexture","bindTexture","texParameteri","deleteTexture",
    "isContextLost","getError","finish","flush","generateMipmap","bindBufferBase"
]"#;

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 recording_context(context_lost: bool) -> (WebGl2RenderingContext, 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, names, lost",
        "for (const name of JSON.parse(names)) { target[name] = function() { target.__log.push([name, Array.from(arguments)]); if (name === 'isContextLost') return lost; if (name.indexOf('create') === 0) return { __gl: name }; if (name === 'getError') return 0; return null; }; }",
    );
    let armed: Result<JsValue, JsValue> = arm.call3(
        &JsValue::NULL,
        context.as_ref(),
        &JsValue::from_str(GL_NAMES),
        &JsValue::from_bool(context_lost),
    );
    assert!(armed.is_ok(), "the backend recorder must install cleanly");
    let typed: WebGl2RenderingContext = context.clone().unchecked_into();
    (typed, context)
}

fn canvas_element() -> HtmlCanvasElement {
    let element: Object = Object::new();
    element.unchecked_into()
}

fn backend(context_lost: bool) -> (WebGl2Backend, Object) {
    let (context, log): (WebGl2RenderingContext, Object) = recording_context(context_lost);
    let subject: WebGl2Backend = WebGl2Backend::from_init(WebGl2BackendInit {
        canvas: canvas_element(),
        context,
        width: 800,
        height: 600,
        clear_color: Color::new(0.1, 0.2, 0.3, 1.0),
    });
    (subject, log)
}

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}");
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records a WebGl2RenderingContext stand-in, which only exists under wasm"
)]
fn a_fresh_backend_reports_a_live_context_and_no_context_loss() {
    let (subject, _log): (WebGl2Backend, Object) = backend(false);

    assert!(
        !subject.is_context_lost(),
        "a freshly constructed backend holds a live context, so the per-frame check reads false"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records a WebGl2RenderingContext stand-in, which only exists under wasm"
)]
fn a_lost_context_is_reported_so_the_renderer_can_rebuild() {
    let (subject, _log): (WebGl2Backend, Object) = backend(true);

    assert!(
        subject.is_context_lost(),
        "every GL call against a lost context is a silent no-op, so this has to reach the caller"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records a WebGl2RenderingContext stand-in, which only exists under wasm"
)]
fn resizing_pushes_the_new_viewport_to_the_driver() {
    let (mut subject, log): (WebGl2Backend, Object) = backend(false);
    let before: u32 = Reflect::get(&log, &JsValue::from_str("__log"))
        .expect("log readable")
        .unchecked_into::<Array>()
        .length();

    subject.resize(1280, 720);

    let fresh: Vec<String> = ops_after(&log, before);
    assert_eq!(
        fresh,
        vec![String::from("viewport")],
        "resizing issues exactly the viewport call and nothing else"
    );
    let args: Array = args_after(&log, "viewport", before);
    assert_eq!(
        args.get(2).as_f64(),
        Some(1280.0),
        "the new width reaches the driver"
    );
    assert_eq!(args.get(3).as_f64(), Some(720.0), "and the new height");
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records a WebGl2RenderingContext stand-in, which only exists under wasm"
)]
fn invalidating_state_seeds_the_shadow_from_the_new_viewport() {
    let (mut subject, _log): (WebGl2Backend, Object) = backend(false);

    subject.invalidate_state(640, 480);

    assert!(
        !subject.is_context_lost(),
        "invalidating the shadow is a bookkeeping reset, not a context event"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "records a WebGl2RenderingContext stand-in, which only exists under wasm"
)]
fn the_default_state_a_fresh_backend_starts_with_disables_depth_testing() {
    let fresh: GlRenderState = GlRenderState::context_defaults(800, 600);

    assert!(
        !fresh.get_depth().get_enabled(),
        "a fresh GL context has depth testing off, so the shadow must not claim it is on"
    );
    assert!(
        fresh.get_scissor().is_none(),
        "scissoring starts off, and the off case is an absent rectangle rather than a zero one"
    );
    assert_eq!(
        *fresh.get_color_mask().get_bits(),
        0xf,
        "all four channels are writable until a draw says otherwise"
    );
}