0.3.1-0-pre.4
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/apps/game-snake-poc-tauri/frontend/main.html -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 2 -->
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
@@ -23,7 +23,7 @@
|
||||
<output id="score">Score : 0</output>
|
||||
<output id="length">Longueur : 0</output>
|
||||
</div>
|
||||
<div class="game-stage">
|
||||
<div id="game-stage" class="game-stage">
|
||||
<canvas id="game" width="360" height="600" tabindex="0" aria-label="Zone de jeu Snake"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/apps/game-snake-poc-tauri/frontend/ts/game.ts
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
import init, { SnakeWasmGame } from "@snake-wasm";
|
||||
import type { RuntimeDescriptor } from "./bridge";
|
||||
@@ -13,6 +13,8 @@ export type SnakeDirection = "left" | "right" | "up" | "down";
|
||||
|
||||
export interface SnakeTauriSession {
|
||||
queueDirection(direction: SnakeDirection, source: SnakeInputSource): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
renderNow(): void;
|
||||
refreshDeviceClass(): void;
|
||||
dispose(): void;
|
||||
@@ -92,6 +94,7 @@ export async function startGame(
|
||||
let previous = performance.now();
|
||||
let accumulator = 0;
|
||||
let animationFrame: number | null = null;
|
||||
let running = false;
|
||||
let disposed = false;
|
||||
|
||||
function renderNow(): void {
|
||||
@@ -101,8 +104,16 @@ export async function startGame(
|
||||
render(game, canvas, renderingContext, score, length);
|
||||
}
|
||||
|
||||
function schedule(): void {
|
||||
if (!running || disposed || animationFrame !== null) {
|
||||
return;
|
||||
}
|
||||
animationFrame = window.requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
function frame(now: number): void {
|
||||
if (disposed) {
|
||||
animationFrame = null;
|
||||
if (!running || disposed) {
|
||||
return;
|
||||
}
|
||||
const elapsed = Math.min(Math.max(0, now - previous), MAX_FRAME_ELAPSED_MILLIS);
|
||||
@@ -113,11 +124,35 @@ export async function startGame(
|
||||
accumulator -= FRAME_MILLIS;
|
||||
}
|
||||
renderNow();
|
||||
animationFrame = window.requestAnimationFrame(frame);
|
||||
schedule();
|
||||
}
|
||||
|
||||
function pause(): void {
|
||||
if (!running || disposed) {
|
||||
return;
|
||||
}
|
||||
running = false;
|
||||
accumulator = 0;
|
||||
if (animationFrame !== null) {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
animationFrame = null;
|
||||
}
|
||||
frontendInfo("Snake Tauri runtime paused");
|
||||
}
|
||||
|
||||
function resume(): void {
|
||||
if (running || disposed) {
|
||||
return;
|
||||
}
|
||||
previous = performance.now();
|
||||
accumulator = 0;
|
||||
running = true;
|
||||
schedule();
|
||||
frontendInfo("Snake Tauri runtime resumed");
|
||||
}
|
||||
|
||||
renderNow();
|
||||
animationFrame = window.requestAnimationFrame(frame);
|
||||
resume();
|
||||
return {
|
||||
queueDirection(direction: SnakeDirection, source: SnakeInputSource): void {
|
||||
if (disposed) {
|
||||
@@ -126,6 +161,8 @@ export async function startGame(
|
||||
provenance.recordInput(source);
|
||||
queueDirection(game, direction);
|
||||
},
|
||||
pause,
|
||||
resume,
|
||||
renderNow,
|
||||
refreshDeviceClass(): void {
|
||||
if (!disposed) {
|
||||
@@ -136,12 +173,10 @@ export async function startGame(
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
pause();
|
||||
disposed = true;
|
||||
if (animationFrame !== null) {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
animationFrame = null;
|
||||
}
|
||||
game.free();
|
||||
frontendInfo("Snake Tauri runtime disposed");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
60
crates/apps/game-snake-poc-tauri/frontend/ts/lifecycle.ts
Normal file
60
crates/apps/game-snake-poc-tauri/frontend/ts/lifecycle.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// file: crates/apps/game-snake-poc-tauri/frontend/ts/lifecycle.ts
|
||||
// version: 1
|
||||
|
||||
import type { SnakeTauriSession } from "./game";
|
||||
import { frontendInfo } from "./logging";
|
||||
|
||||
export interface SnakeTauriLifecycleBinding {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export function bindTauriLifecycle(session: SnakeTauriSession, stage: HTMLElement): SnakeTauriLifecycleBinding {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
session.refreshDeviceClass();
|
||||
session.renderNow();
|
||||
});
|
||||
|
||||
function synchronizeVisibility(): void {
|
||||
if (document.visibilityState === "hidden") {
|
||||
session.pause();
|
||||
frontendInfo("Snake Tauri lifecycle: visibility hidden");
|
||||
return;
|
||||
}
|
||||
session.resume();
|
||||
session.refreshDeviceClass();
|
||||
session.renderNow();
|
||||
frontendInfo("Snake Tauri lifecycle: visibility visible");
|
||||
}
|
||||
|
||||
function pageHide(event: PageTransitionEvent): void {
|
||||
if (event.persisted) {
|
||||
session.pause();
|
||||
} else {
|
||||
session.dispose();
|
||||
}
|
||||
frontendInfo(`Snake Tauri lifecycle: page hide persisted=${event.persisted}`);
|
||||
}
|
||||
|
||||
function pageShow(event: PageTransitionEvent): void {
|
||||
session.resume();
|
||||
session.refreshDeviceClass();
|
||||
session.renderNow();
|
||||
frontendInfo(`Snake Tauri lifecycle: page show persisted=${event.persisted}`);
|
||||
}
|
||||
|
||||
resizeObserver.observe(stage);
|
||||
document.addEventListener("visibilitychange", synchronizeVisibility);
|
||||
window.addEventListener("pagehide", pageHide);
|
||||
window.addEventListener("pageshow", pageShow);
|
||||
synchronizeVisibility();
|
||||
|
||||
return {
|
||||
dispose(): void {
|
||||
resizeObserver.disconnect();
|
||||
document.removeEventListener("visibilitychange", synchronizeVisibility);
|
||||
window.removeEventListener("pagehide", pageHide);
|
||||
window.removeEventListener("pageshow", pageShow);
|
||||
session.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
// file: crates/apps/game-snake-poc-tauri/frontend/ts/main.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import "../css/main.css";
|
||||
import { loadSnakeTauriAssets } from "./assets";
|
||||
import { getRuntimeDescriptor, reportFrontendReady } from "./bridge";
|
||||
import { startGame } from "./game";
|
||||
import { bindInputs } from "./input";
|
||||
import { bindTauriLifecycle } from "./lifecycle";
|
||||
import { frontendError, frontendInfo, initializeFrontendTracing } from "./logging";
|
||||
|
||||
function requiredElement<T extends HTMLElement>(identifier: string): T {
|
||||
@@ -21,6 +22,7 @@ async function bootstrap(): Promise<void> {
|
||||
frontendInfo("Snake Tauri frontend bootstrap started");
|
||||
const descriptor = await getRuntimeDescriptor();
|
||||
const canvas = requiredElement<HTMLCanvasElement>("game");
|
||||
const stage = requiredElement<HTMLElement>("game-stage");
|
||||
const score = requiredElement<HTMLOutputElement>("score");
|
||||
const length = requiredElement<HTMLOutputElement>("length");
|
||||
const provenance = requiredElement<HTMLOutputElement>("runtime-provenance");
|
||||
@@ -37,11 +39,7 @@ async function bootstrap(): Promise<void> {
|
||||
status.textContent = "Chargement WASM…";
|
||||
const session = await startGame(canvas, score, length, provenance, descriptor);
|
||||
bindInputs(session, buttons);
|
||||
window.addEventListener("resize", () => {
|
||||
session.refreshDeviceClass();
|
||||
session.renderNow();
|
||||
});
|
||||
window.addEventListener("pagehide", () => session.dispose(), { once: true });
|
||||
bindTauriLifecycle(session, stage);
|
||||
|
||||
document.body.dataset.runtimeState = "ready";
|
||||
status.textContent = "Prêt";
|
||||
|
||||
Reference in New Issue
Block a user