chore: rebase to main with translation ports
This commit is contained in:
parent
768fa93c41
commit
8024c6ac40
134 changed files with 15456 additions and 10650 deletions
86
Examples/Web/Host.cs
Normal file
86
Examples/Web/Host.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices.JavaScript;
|
||||
|
||||
namespace Examples.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Browser host: owns the single raylib window and the "current" example. The frame loop is
|
||||
/// driven from JavaScript (main.js, via requestAnimationFrame) since a blocking C# loop would
|
||||
/// freeze the page. Methods marked [JSExport] are called from main.js.
|
||||
/// </summary>
|
||||
public partial class Host
|
||||
{
|
||||
public const int screenWidth = 800;
|
||||
public const int screenHeight = 450;
|
||||
|
||||
// Examples discovered from the assembly, minus desktop-only ones (see ExampleRegistry).
|
||||
private static readonly List<IExample> _examples = new(ExampleRegistry.BrowserExamples);
|
||||
|
||||
private static IExample _current;
|
||||
|
||||
public static void Main()
|
||||
{
|
||||
InitWindow(screenWidth, screenHeight, "raylib-cs web examples");
|
||||
SetTargetFPS(60);
|
||||
|
||||
SetExample(_examples[0].Name);
|
||||
}
|
||||
|
||||
/// <summary>Render one frame of the current example (called every requestAnimationFrame tick).</summary>
|
||||
[JSExport]
|
||||
public static void UpdateFrame()
|
||||
{
|
||||
try
|
||||
{
|
||||
_current?.Update();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Don't let one misbehaving example kill the whole page; log and stop driving it.
|
||||
Console.WriteLine($"[Examples.Web] '{_current?.Name}' threw during Update: {ex}");
|
||||
_current = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Switch the active example by name (called from the nav dropdown).</summary>
|
||||
[JSExport]
|
||||
public static void SetExample(string name)
|
||||
{
|
||||
var next = _examples.Find(e => e.Name == name);
|
||||
if (next == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_current?.Unload();
|
||||
_current = next;
|
||||
_current.Init();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Examples.Web] '{next.Name}' failed to initialize: {ex}");
|
||||
_current = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Newline-separated example names, used to populate the nav dropdown.</summary>
|
||||
[JSExport]
|
||||
public static string GetExampleNames()
|
||||
{
|
||||
return string.Join("\n", _examples.ConvertAll(e => e.Name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map browser CSS mouse coordinates to the fixed 800x450 framebuffer when the canvas
|
||||
/// is CSS-scaled (integer/fit/native display modes in main.js).
|
||||
/// </summary>
|
||||
[JSExport]
|
||||
public static void SetMouseScaleFromDisplay(float scaleX, float scaleY)
|
||||
{
|
||||
SetMouseOffset(0, 0);
|
||||
SetMouseScale(scaleX, scaleY);
|
||||
}
|
||||
}
|
||||
118
Examples/Web/README.md
Normal file
118
Examples/Web/README.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Examples/Web — raylib-cs in the browser (WebAssembly)
|
||||
|
||||
A small browser app that runs raylib examples in the browser via WebAssembly, with a dropdown to
|
||||
switch between them. It exists to **prove the `Raylib-cs` NuGet package's `browser-wasm` support
|
||||
works end-to-end**: the package's `buildTransitive/Raylib-cs.targets` automatically links the
|
||||
shipped `runtimes/browser-wasm/native/raylib.a` (and adds `-sUSE_GLFW=3`) into the .NET wasm
|
||||
runtime. The browser host lives in `Examples/Web`, while `Examples.csproj` remains the single
|
||||
examples project.
|
||||
|
||||
The browser-wasm configuration is enabled only when publishing `Examples` with
|
||||
`RuntimeIdentifier=browser-wasm`, so normal solution builds do not require the `wasm-tools`
|
||||
workload.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK
|
||||
- `dotnet workload install wasm-tools`
|
||||
- The `Raylib-cs` package matching `$(RaylibCsVersion)` (see `Directory.Build.props`) — from
|
||||
nuget.org, or built locally into the repo's `./nuget` feed (`dotnet pack Raylib-cs -c Release --output nuget`).
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
dotnet publish Examples -f net10.0 -r browser-wasm -c Release
|
||||
# -> Examples/bin/Release/net10.0/browser-wasm/AppBundle/
|
||||
```
|
||||
|
||||
### Toolchain caveat
|
||||
|
||||
If the link step fails with:
|
||||
|
||||
```
|
||||
wasm-opt: Unknown option '--enable-bulk-memory-opt'
|
||||
```
|
||||
|
||||
your installed `wasm-tools` workload (Binaryen) is out of sync with the SDK — emscripten passes a
|
||||
feature flag the bundled `wasm-opt` doesn't understand (commonly caused by a stale workload band
|
||||
or a conflicting system EMSDK on PATH/`$EMSDK`). Proper fix:
|
||||
|
||||
```bash
|
||||
dotnet workload update
|
||||
```
|
||||
|
||||
`Examples.csproj` defaults browser-wasm publishes to unoptimized native linking so the standard
|
||||
publish command works on affected local toolchains. A fully optimized publish can override those
|
||||
MSBuild properties after updating the workload/toolchain.
|
||||
|
||||
## Run
|
||||
|
||||
WebAssembly must be served over HTTP (not `file://`):
|
||||
|
||||
```bash
|
||||
dotnet serve -d Examples/bin/Release/net10.0/browser-wasm/AppBundle # dotnet tool install -g dotnet-serve
|
||||
# or: npx http-server Examples/bin/Release/net10.0/browser-wasm/AppBundle
|
||||
```
|
||||
|
||||
Open the printed URL and use the **Example** dropdown to switch examples.
|
||||
|
||||
## Canvas scaling modes
|
||||
|
||||
The browser host keeps raylib's internal render buffer fixed to `800x450` and applies display
|
||||
scaling in CSS. Use the **Scale** dropdown (or `?scale=` query param) to choose behavior:
|
||||
|
||||
- `native` (default): exact `800x450` CSS pixels (1x), no resizing.
|
||||
- `integer`: largest whole-number multiple that fits the viewport, centered with letterboxing.
|
||||
Best for pixel-perfect presentation.
|
||||
- `fit`: fills available viewport while preserving aspect ratio (can be fractional, less crisp on
|
||||
some DPI/zoom combinations).
|
||||
|
||||
Scaling is computed in **device pixels first** and then converted back to CSS pixels using current
|
||||
`devicePixelRatio`, which decouples the modes from needing a specific browser zoom level on
|
||||
125%/150% OS scale displays.
|
||||
|
||||
`main.js` also shows runtime diagnostics in the status bar (`DPR`, CSS size, backing size, scale)
|
||||
to help debug OS scaling / browser zoom behavior across monitors.
|
||||
|
||||
### Manual validation matrix (expected behavior)
|
||||
|
||||
- OS scale `100%`, browser zoom `100%`: `integer` should appear crisp and stable (`Scale 1.00` or
|
||||
higher if viewport allows).
|
||||
- OS scale `125%` and `150%`, browser zoom `100%`: `integer` should stay crisp (no subpixel CSS
|
||||
scaling); perceived physical size changes with OS scale are expected.
|
||||
- Browser zoom `80%`, `100%`, `125%`: `integer` should continue using whole-number scaling;
|
||||
`fit` may show softening at non-integer effective scales.
|
||||
- `native` mode should always report `CSS 800x450` and `Scale 1.00`.
|
||||
- During window resize, there should be no frame-by-frame jitter because scale updates run on
|
||||
resize/mode changes, not per animation frame.
|
||||
|
||||
## How it works
|
||||
|
||||
A browser can't run raylib's blocking `while (!WindowShouldClose())` loop (it would freeze the
|
||||
page), so frames are driven from JavaScript:
|
||||
|
||||
- `Host.Main()` calls `InitWindow` once and selects the default example (`main.js` runs it via
|
||||
`runMain()`).
|
||||
- `main.js` binds the page `<canvas>` to the runtime, then calls `Host.UpdateFrame()` every
|
||||
`requestAnimationFrame` tick. `UpdateFrame`, `SetExample`, and `GetExampleNames` are `[JSExport]`.
|
||||
- Each base example implements `IExample` (`Init` / `Update` / `Unload`) in `#if BROWSER` partials.
|
||||
The host owns the window;
|
||||
examples never call `InitWindow`/`CloseWindow`.
|
||||
|
||||
## Adding more examples
|
||||
|
||||
For a new desktop example (`../Core/...`, `../Shaders/...`, etc.), add a browser partial
|
||||
(`*.Browser.cs`) that implements `IExample` and mirrors the split of its monolithic `Main`:
|
||||
|
||||
```
|
||||
Main() { <setup>; while(!WindowShouldClose()){ <body> } <cleanup> }
|
||||
-> Init() { <setup, minus InitWindow/SetTargetFPS> } // loop-spanning locals become fields
|
||||
Update(){ <body> } // keep BeginDrawing..EndDrawing
|
||||
Unload(){ <cleanup, minus CloseWindow> }
|
||||
```
|
||||
|
||||
Then add `new YourExample()` to the `Examples` list in `Host.cs`.
|
||||
|
||||
Examples that load files (`resources/...`) also need those assets in the wasm virtual filesystem.
|
||||
`Examples.csproj` bundles `../resources/` into the browser app when publishing with
|
||||
`RuntimeIdentifier=browser-wasm`.
|
||||
70
Examples/Web/index.html
Normal file
70
Examples/Web/index.html
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>raylib-cs — browser examples</title>
|
||||
<link rel="icon" type="image/x-icon" href="./favicon.ico"/>
|
||||
<link rel="modulepreload" href="./main.js"/>
|
||||
<link rel="modulepreload" href="./_framework/dotnet.js"/>
|
||||
<style>
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0.75rem;
|
||||
box-sizing: border-box;
|
||||
background: #1d1d1d;
|
||||
color: #eaeaea;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
h1 { font-size: 1.1rem; font-weight: 600; }
|
||||
#toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 0.25rem; }
|
||||
#examples { font-size: 1rem; padding: .3rem .5rem; }
|
||||
#scaleMode { font-size: 0.95rem; padding: .2rem .4rem; }
|
||||
#viewport {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
background: #111;
|
||||
}
|
||||
canvas {
|
||||
border: 1px solid #444;
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
width: 800px;
|
||||
height: 450px;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
background: #000;
|
||||
}
|
||||
#status { color: #888; font-size: .85rem; margin: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>raylib-cs running in the browser (WebAssembly)</h1>
|
||||
<div id="toolbar">
|
||||
<label for="examples">Example: </label>
|
||||
<select id="examples"><option>loading…</option></select>
|
||||
<label for="scaleMode"> Scale: </label>
|
||||
<select id="scaleMode">
|
||||
<option value="integer">Integer</option>
|
||||
<option value="native">Native (1x)</option>
|
||||
<option value="fit">Fit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="viewport">
|
||||
<canvas id="canvas" width="800" height="450" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
|
||||
</div>
|
||||
<div id="status">Initializing .NET + raylib…</div>
|
||||
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
157
Examples/Web/main.js
Normal file
157
Examples/Web/main.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { dotnet } from './_framework/dotnet.js'
|
||||
import {
|
||||
CANVAS_WIDTH,
|
||||
CANVAS_HEIGHT,
|
||||
normalizeScaleMode,
|
||||
computeDisplaySize
|
||||
} from './scaleUtils.js'
|
||||
|
||||
const status = document.getElementById('status');
|
||||
const select = document.getElementById('examples');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const viewport = document.getElementById('viewport');
|
||||
const scaleModeSelect = document.getElementById('scaleMode');
|
||||
|
||||
function readScaleModeFromQuery() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return normalizeScaleMode(params.get('scale'));
|
||||
}
|
||||
|
||||
function writeScaleModeToQuery(mode) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('scale', mode);
|
||||
const query = params.toString();
|
||||
const nextUrl = query.length > 0 ? `${window.location.pathname}?${query}` : window.location.pathname;
|
||||
window.history.replaceState(null, '', nextUrl);
|
||||
}
|
||||
|
||||
let scaleMode = readScaleModeFromQuery();
|
||||
let Host = null;
|
||||
|
||||
function ensureBackingBufferSize() {
|
||||
if (canvas.width !== CANVAS_WIDTH) {
|
||||
canvas.width = CANVAS_WIDTH;
|
||||
}
|
||||
|
||||
if (canvas.height !== CANVAS_HEIGHT) {
|
||||
canvas.height = CANVAS_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
function updateScaleDiagnostics(displayScale, cssWidth, cssHeight, deviceWidth, deviceHeight, dpr) {
|
||||
status.textContent =
|
||||
`Running. Example: ${scaleMode} scaling | DPR ${dpr.toFixed(2)} | ` +
|
||||
`CSS ${cssWidth.toFixed(2)}x${cssHeight.toFixed(2)} | ` +
|
||||
`Device ${deviceWidth}x${deviceHeight} | Backing ${canvas.width}x${canvas.height} | ` +
|
||||
`Scale ${displayScale.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function syncMouseScale() {
|
||||
if (!Host) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cssWidth = canvas.clientWidth;
|
||||
const cssHeight = canvas.clientHeight;
|
||||
if (cssWidth <= 0 || cssHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map CSS/display mouse coords to the fixed backing buffer (canvas.width/height).
|
||||
const scaleX = canvas.width / cssWidth;
|
||||
const scaleY = canvas.height / cssHeight;
|
||||
Host.SetMouseScaleFromDisplay(scaleX, scaleY);
|
||||
}
|
||||
|
||||
function applyDisplayScale() {
|
||||
ensureBackingBufferSize();
|
||||
|
||||
const viewportWidth = viewport.clientWidth || CANVAS_WIDTH;
|
||||
const viewportHeight = viewport.clientHeight || CANVAS_HEIGHT;
|
||||
const dprRaw = window.devicePixelRatio ?? 1;
|
||||
const display = computeDisplaySize(scaleMode, viewportWidth, viewportHeight, dprRaw);
|
||||
|
||||
canvas.style.width = `${display.cssWidth}px`;
|
||||
canvas.style.height = `${display.cssHeight}px`;
|
||||
|
||||
// Center canvas inside viewport, leaving letterboxing around it.
|
||||
viewport.style.justifyContent = 'center';
|
||||
viewport.style.alignItems = 'center';
|
||||
|
||||
syncMouseScale();
|
||||
|
||||
updateScaleDiagnostics(
|
||||
display.scale,
|
||||
display.cssWidth,
|
||||
display.cssHeight,
|
||||
display.deviceWidth,
|
||||
display.deviceHeight,
|
||||
display.dpr
|
||||
);
|
||||
}
|
||||
|
||||
function setScaleMode(mode, updateQuery) {
|
||||
const nextMode = normalizeScaleMode(mode);
|
||||
if (nextMode === scaleMode && !updateQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
scaleMode = nextMode;
|
||||
if (scaleModeSelect) {
|
||||
scaleModeSelect.value = scaleMode;
|
||||
}
|
||||
|
||||
if (updateQuery) {
|
||||
writeScaleModeToQuery(scaleMode);
|
||||
}
|
||||
|
||||
applyDisplayScale();
|
||||
}
|
||||
|
||||
if (scaleModeSelect) {
|
||||
scaleModeSelect.value = scaleMode;
|
||||
scaleModeSelect.addEventListener('change', () => {
|
||||
setScaleMode(scaleModeSelect.value, true);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', applyDisplayScale);
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', applyDisplayScale);
|
||||
}
|
||||
|
||||
applyDisplayScale();
|
||||
|
||||
const { getAssemblyExports, getConfig, runMain } = await dotnet
|
||||
.withDiagnosticTracing(false)
|
||||
.create();
|
||||
|
||||
const config = getConfig();
|
||||
const exports = await getAssemblyExports(config.mainAssemblyName);
|
||||
Host = exports.Examples.Web.Host;
|
||||
|
||||
// raylib's GLFW/emscripten backend renders into this canvas.
|
||||
dotnet.instance.Module['canvas'] = canvas;
|
||||
|
||||
// Runs Host.Main() -> InitWindow + default example Init(). Must come after the canvas is bound.
|
||||
await runMain();
|
||||
applyDisplayScale();
|
||||
|
||||
// Populate the navigation dropdown from the registered examples.
|
||||
select.innerHTML = '';
|
||||
for (const name of Host.GetExampleNames().split('\n').filter(n => n.length > 0)) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = name;
|
||||
opt.textContent = name;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
select.addEventListener('change', () => Host.SetExample(select.value));
|
||||
|
||||
setScaleMode(scaleMode, false);
|
||||
|
||||
// Drive raylib one frame per animation tick (never block the browser).
|
||||
function mainLoop() {
|
||||
Host.UpdateFrame();
|
||||
requestAnimationFrame(mainLoop);
|
||||
}
|
||||
requestAnimationFrame(mainLoop);
|
||||
67
Examples/Web/scaleUtils.js
Normal file
67
Examples/Web/scaleUtils.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
export const CANVAS_WIDTH = 800;
|
||||
export const CANVAS_HEIGHT = 450;
|
||||
export const SCALE_MODES = new Set(['integer', 'native', 'fit']);
|
||||
export const DEFAULT_SCALE_MODE = 'native';
|
||||
|
||||
export function normalizeScaleMode(mode) {
|
||||
if (!mode) {
|
||||
return DEFAULT_SCALE_MODE;
|
||||
}
|
||||
|
||||
const normalized = String(mode).toLowerCase();
|
||||
return SCALE_MODES.has(normalized) ? normalized : DEFAULT_SCALE_MODE;
|
||||
}
|
||||
|
||||
export function computeDisplaySize(mode, viewportWidth, viewportHeight, dpr = 1) {
|
||||
const safeDpr = Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
|
||||
|
||||
if (viewportWidth <= 0 || viewportHeight <= 0) {
|
||||
return {
|
||||
cssWidth: CANVAS_WIDTH / safeDpr,
|
||||
cssHeight: CANVAS_HEIGHT / safeDpr,
|
||||
deviceWidth: CANVAS_WIDTH,
|
||||
deviceHeight: CANVAS_HEIGHT,
|
||||
scale: 1,
|
||||
dpr: safeDpr
|
||||
};
|
||||
}
|
||||
|
||||
// Convert viewport from CSS pixels to device pixels first so scaling is DPI/zoom aware.
|
||||
const viewportDeviceWidth = Math.max(1, Math.floor(viewportWidth * safeDpr));
|
||||
const viewportDeviceHeight = Math.max(1, Math.floor(viewportHeight * safeDpr));
|
||||
|
||||
let deviceWidth;
|
||||
let deviceHeight;
|
||||
let scale;
|
||||
|
||||
if (mode === 'native') {
|
||||
// 1 source pixel -> 1 physical device pixel.
|
||||
scale = 1;
|
||||
deviceWidth = CANVAS_WIDTH;
|
||||
deviceHeight = CANVAS_HEIGHT;
|
||||
} else if (mode === 'fit') {
|
||||
const fitRatio = Math.min(viewportDeviceWidth / CANVAS_WIDTH, viewportDeviceHeight / CANVAS_HEIGHT);
|
||||
const safeRatio = Number.isFinite(fitRatio) && fitRatio > 0 ? fitRatio : 1;
|
||||
scale = safeRatio;
|
||||
deviceWidth = Math.max(1, Math.floor(CANVAS_WIDTH * safeRatio));
|
||||
deviceHeight = Math.max(1, Math.floor(CANVAS_HEIGHT * safeRatio));
|
||||
} else {
|
||||
// integer mode
|
||||
const integerScale = Math.max(1, Math.floor(Math.min(
|
||||
viewportDeviceWidth / CANVAS_WIDTH,
|
||||
viewportDeviceHeight / CANVAS_HEIGHT
|
||||
)));
|
||||
scale = integerScale;
|
||||
deviceWidth = CANVAS_WIDTH * integerScale;
|
||||
deviceHeight = CANVAS_HEIGHT * integerScale;
|
||||
}
|
||||
|
||||
return {
|
||||
cssWidth: deviceWidth / safeDpr,
|
||||
cssHeight: deviceHeight / safeDpr,
|
||||
deviceWidth,
|
||||
deviceHeight,
|
||||
scale,
|
||||
dpr: safeDpr
|
||||
};
|
||||
}
|
||||
92
Examples/Web/scaleUtils.test.js
Normal file
92
Examples/Web/scaleUtils.test.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
CANVAS_WIDTH,
|
||||
CANVAS_HEIGHT,
|
||||
normalizeScaleMode,
|
||||
computeDisplaySize
|
||||
} from './scaleUtils.js';
|
||||
|
||||
describe('normalizeScaleMode', () => {
|
||||
it('returns native for null, undefined, and empty string', () => {
|
||||
assert.equal(normalizeScaleMode(null), 'native');
|
||||
assert.equal(normalizeScaleMode(undefined), 'native');
|
||||
assert.equal(normalizeScaleMode(''), 'native');
|
||||
});
|
||||
|
||||
it('normalizes allowlisted values case-insensitively', () => {
|
||||
assert.equal(normalizeScaleMode('INTEGER'), 'integer');
|
||||
assert.equal(normalizeScaleMode('Fit'), 'fit');
|
||||
assert.equal(normalizeScaleMode('native'), 'native');
|
||||
});
|
||||
|
||||
it('returns native for unknown values', () => {
|
||||
assert.equal(normalizeScaleMode('stretch'), 'native');
|
||||
assert.equal(normalizeScaleMode('bogus'), 'native');
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeDisplaySize', () => {
|
||||
it('native mode uses 1:1 device pixels at scale 1', () => {
|
||||
const result = computeDisplaySize('native', 1200, 800, 1);
|
||||
|
||||
assert.equal(result.scale, 1);
|
||||
assert.equal(result.deviceWidth, CANVAS_WIDTH);
|
||||
assert.equal(result.deviceHeight, CANVAS_HEIGHT);
|
||||
assert.equal(result.cssWidth, CANVAS_WIDTH);
|
||||
assert.equal(result.cssHeight, CANVAS_HEIGHT);
|
||||
assert.equal(result.dpr, 1);
|
||||
});
|
||||
|
||||
it('integer mode scales by floored ratio with minimum scale 1', () => {
|
||||
const doubled = computeDisplaySize('integer', 1600, 900, 1);
|
||||
|
||||
assert.equal(doubled.scale, 2);
|
||||
assert.equal(doubled.deviceWidth, CANVAS_WIDTH * 2);
|
||||
assert.equal(doubled.deviceHeight, CANVAS_HEIGHT * 2);
|
||||
|
||||
const single = computeDisplaySize('integer', 900, 500, 1);
|
||||
|
||||
assert.equal(single.scale, 1);
|
||||
assert.equal(single.deviceWidth, CANVAS_WIDTH);
|
||||
assert.equal(single.deviceHeight, CANVAS_HEIGHT);
|
||||
});
|
||||
|
||||
it('fit mode scales proportionally and keeps device dimensions at least 1', () => {
|
||||
const result = computeDisplaySize('fit', 400, 225, 1);
|
||||
|
||||
assert.equal(result.scale, 0.5);
|
||||
assert.equal(result.deviceWidth, 400);
|
||||
assert.equal(result.deviceHeight, 225);
|
||||
});
|
||||
|
||||
it('fit mode falls back to scale 1 for zero or negative viewport', () => {
|
||||
const zero = computeDisplaySize('fit', 0, 0, 1);
|
||||
|
||||
assert.equal(zero.scale, 1);
|
||||
assert.equal(zero.deviceWidth, CANVAS_WIDTH);
|
||||
assert.equal(zero.deviceHeight, CANVAS_HEIGHT);
|
||||
|
||||
const negative = computeDisplaySize('fit', -100, -50, 1);
|
||||
|
||||
assert.equal(negative.scale, 1);
|
||||
assert.equal(negative.deviceWidth, CANVAS_WIDTH);
|
||||
assert.equal(negative.deviceHeight, CANVAS_HEIGHT);
|
||||
});
|
||||
|
||||
it('applies dpr to viewport before scaling and converts css size back', () => {
|
||||
const result = computeDisplaySize('native', 400, 225, 2);
|
||||
|
||||
assert.equal(result.dpr, 2);
|
||||
assert.equal(result.deviceWidth, CANVAS_WIDTH);
|
||||
assert.equal(result.deviceHeight, CANVAS_HEIGHT);
|
||||
assert.equal(result.cssWidth, CANVAS_WIDTH / 2);
|
||||
assert.equal(result.cssHeight, CANVAS_HEIGHT / 2);
|
||||
});
|
||||
|
||||
it('treats invalid dpr as 1', () => {
|
||||
assert.equal(computeDisplaySize('native', 800, 450, 0).dpr, 1);
|
||||
assert.equal(computeDisplaySize('native', 800, 450, NaN).dpr, 1);
|
||||
assert.equal(computeDisplaySize('native', 800, 450, Infinity).dpr, 1);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue