mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-11 07:08:26 +00:00
dpr
This commit is contained in:
@@ -960,6 +960,9 @@
|
||||
"rail_distance_desc": "How far zoomed out train tracks remain visible",
|
||||
"rail_thickness_label": "Train track thickness",
|
||||
"rail_thickness_desc": "How wide train tracks are drawn",
|
||||
"section_display": "Display",
|
||||
"dpr_scale_label": "Render resolution",
|
||||
"dpr_scale_desc": "Scales the render resolution — lower improves performance",
|
||||
"section_effects": "Effects",
|
||||
"reset_label": "Reset to defaults",
|
||||
"reset_desc": "Clear all graphics overrides"
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
createDebugGui,
|
||||
createRenderSettings,
|
||||
deepAssign,
|
||||
getDpr,
|
||||
GameView as WebGLGameView,
|
||||
} from "./render/gl";
|
||||
import { ALL_UNIT_TYPES, UnitState } from "./render/types";
|
||||
@@ -351,7 +352,7 @@ function mountWebGLFrameLoop(
|
||||
|
||||
const syncCamera = (): void => {
|
||||
const scale = transformHandler.scale;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = getDpr(view.getSettings());
|
||||
const centerX =
|
||||
transformHandler.offsetX +
|
||||
mapWidth / 2 +
|
||||
|
||||
@@ -50,6 +50,10 @@ const RAIL_THICKNESS_MIN = 0.5;
|
||||
const RAIL_THICKNESS_MAX = 3;
|
||||
const RAIL_THICKNESS_STEP = 0.1;
|
||||
|
||||
const DPR_SCALE_MIN = 0.25;
|
||||
const DPR_SCALE_MAX = 1;
|
||||
const DPR_SCALE_STEP = 0.05;
|
||||
|
||||
export class ShowGraphicsSettingsModalEvent {
|
||||
constructor(
|
||||
public readonly isVisible: boolean = true,
|
||||
@@ -233,6 +237,27 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
);
|
||||
}
|
||||
|
||||
private currentDprScale(): number {
|
||||
return (
|
||||
this.userSettings.graphicsOverrides().display?.dprScale ??
|
||||
renderDefaults.display.dprScale
|
||||
);
|
||||
}
|
||||
|
||||
private patchDisplay(patch: Partial<GraphicsOverrides["display"]>) {
|
||||
const current = this.userSettings.graphicsOverrides();
|
||||
this.userSettings.setGraphicsOverrides({
|
||||
...current,
|
||||
display: { ...current.display, ...patch },
|
||||
});
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private onDprScaleChange(event: Event) {
|
||||
const value = parseFloat((event.target as HTMLInputElement).value);
|
||||
this.patchDisplay({ dprScale: value });
|
||||
}
|
||||
|
||||
private onHighlightFillChange(event: Event) {
|
||||
const value = parseFloat((event.target as HTMLInputElement).value);
|
||||
this.patchMapOverlay({ highlightFillBrighten: value });
|
||||
@@ -339,6 +364,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
const territoryAlpha = this.currentTerritoryAlpha();
|
||||
const railDrawDistance = RAIL_ZOOM_MAX - this.currentRailMinZoom();
|
||||
const railThickness = this.currentRailThickness();
|
||||
const dprScale = this.currentDprScale();
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -650,6 +676,37 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-2"
|
||||
>
|
||||
${translateText("graphics_setting.section_display")}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded-sm text-white transition-colors"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
${translateText("graphics_setting.dpr_scale_label")}
|
||||
</div>
|
||||
<div class="text-sm text-slate-400">
|
||||
${translateText("graphics_setting.dpr_scale_desc")}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min=${DPR_SCALE_MIN}
|
||||
max=${DPR_SCALE_MAX}
|
||||
step=${DPR_SCALE_STEP}
|
||||
.value=${String(dprScale)}
|
||||
@input=${this.onDprScaleChange}
|
||||
class="w-full border border-slate-500 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-slate-400 w-12 text-right">
|
||||
${dprScale.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="px-3 py-1 text-xs font-semibold text-slate-400 uppercase tracking-wider mt-2"
|
||||
>
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
* ty = -offsetY * sy
|
||||
*/
|
||||
|
||||
import type { RenderSettings } from "./RenderSettings";
|
||||
import { getDpr } from "./utils/Dpr";
|
||||
|
||||
const MIN_ZOOM = 0.2;
|
||||
const MAX_ZOOM = 20;
|
||||
const DBLCLICK_MIN_ZOOM = 0.7;
|
||||
@@ -34,7 +37,11 @@ export class Camera {
|
||||
/** True until fitMap() has been called with valid canvas dimensions. */
|
||||
private needsInitialFit = true;
|
||||
|
||||
constructor(mapWidth: number, mapHeight: number) {
|
||||
constructor(
|
||||
mapWidth: number,
|
||||
mapHeight: number,
|
||||
private settings: RenderSettings,
|
||||
) {
|
||||
this.mapW = mapWidth;
|
||||
this.mapH = mapHeight;
|
||||
this.offsetX = mapWidth / 2;
|
||||
@@ -44,7 +51,7 @@ export class Camera {
|
||||
|
||||
/** Update canvas pixel dimensions. Triggers initial fitMap on first call. */
|
||||
resize(cssWidth: number, cssHeight: number): void {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = getDpr(this.settings);
|
||||
this.canvasW = Math.round(cssWidth * dpr);
|
||||
this.canvasH = Math.round(cssHeight * dpr);
|
||||
if (this.needsInitialFit) {
|
||||
@@ -163,7 +170,7 @@ export class Camera {
|
||||
|
||||
/** Convert screen pixel position to world coordinates. */
|
||||
screenToWorld(screenX: number, screenY: number): { x: number; y: number } {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = getDpr(this.settings);
|
||||
const ndcX = ((screenX * dpr) / this.canvasW) * 2 - 1;
|
||||
const ndcY = -(((screenY * dpr) / this.canvasH) * 2 - 1);
|
||||
const sx = (this.zoom * 2) / this.canvasW;
|
||||
@@ -176,7 +183,7 @@ export class Camera {
|
||||
|
||||
/** Convert world coordinates to screen pixel position (CSS pixels). */
|
||||
worldToScreen(worldX: number, worldY: number): { x: number; y: number } {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = getDpr(this.settings);
|
||||
return {
|
||||
x: (this.zoom * (worldX - this.offsetX)) / dpr + this.canvasW / (2 * dpr),
|
||||
y: (this.zoom * (worldY - this.offsetY)) / dpr + this.canvasH / (2 * dpr),
|
||||
|
||||
@@ -29,6 +29,11 @@ export const GraphicsOverridesSchema = z
|
||||
railThickness: z.number(),
|
||||
})
|
||||
.partial(),
|
||||
display: z
|
||||
.object({
|
||||
dprScale: z.number(),
|
||||
})
|
||||
.partial(),
|
||||
passEnabled: z
|
||||
.object({
|
||||
fx: z.boolean(),
|
||||
|
||||
@@ -48,6 +48,9 @@ export function applyGraphicsOverrides(
|
||||
if (overrides.railroad?.railThickness !== undefined) {
|
||||
settings.railroad.railThickness = overrides.railroad.railThickness;
|
||||
}
|
||||
if (overrides.display?.dprScale !== undefined) {
|
||||
settings.display.dprScale = overrides.display.dprScale;
|
||||
}
|
||||
if (overrides.passEnabled?.fx !== undefined) {
|
||||
settings.passEnabled.fx = overrides.passEnabled.fx;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,13 @@ export interface RenderSettings {
|
||||
enemyG: number;
|
||||
enemyB: number;
|
||||
};
|
||||
display: {
|
||||
/**
|
||||
* Multiplier on the native devicePixelRatio for the canvas backing
|
||||
* store. Below 1 renders fewer pixels (faster); above 1 supersamples.
|
||||
*/
|
||||
dprScale: number;
|
||||
};
|
||||
railroad: {
|
||||
railMinZoom: number;
|
||||
railFadeRange: number;
|
||||
|
||||
@@ -62,6 +62,7 @@ import { WorldTextPass } from "./passes/WorldTextPass";
|
||||
import { createRenderSettings, type RenderSettings } from "./RenderSettings";
|
||||
import { AffiliationPalette } from "./utils/Affiliation";
|
||||
import { buildTerrainRGBA, getPaletteSize } from "./utils/ColorUtils";
|
||||
import { getDpr } from "./utils/Dpr";
|
||||
import {
|
||||
createTexture2D,
|
||||
toScreen,
|
||||
@@ -168,6 +169,12 @@ export class GPURenderer {
|
||||
private localPlayerID = 0;
|
||||
private playerTeams = new Map<number, string>(); // smallID → team
|
||||
|
||||
// Last CSS size pushed via resize(), kept so a dprScale settings change can
|
||||
// re-create the canvas backing store at the new resolution mid-game.
|
||||
private cssWidth = 0;
|
||||
private cssHeight = 0;
|
||||
private appliedDprScale = 1;
|
||||
|
||||
// Alt-view: affiliation recoloring (space hold)
|
||||
private altView = false;
|
||||
// Grid-view: coordinate grid overlay (M toggle)
|
||||
@@ -215,7 +222,7 @@ export class GPURenderer {
|
||||
this.mapW = mapW;
|
||||
this.mapH = mapH;
|
||||
|
||||
this.camera = new Camera(mapW, mapH);
|
||||
this.camera = new Camera(mapW, mapH, this.settings);
|
||||
|
||||
// --- Terrain (static) ---
|
||||
const terrainRGBA = buildTerrainRGBA(terrainBytes, mapW, mapH);
|
||||
@@ -471,7 +478,7 @@ export class GPURenderer {
|
||||
this.barPass = new BarPass(gl, header, this.settings, config);
|
||||
this.worldTextPass = new WorldTextPass(gl, this.settings, config);
|
||||
this.worldTextPass.setMapWidth(this.mapW);
|
||||
this.radialMenuPass = new RadialMenuPass(gl);
|
||||
this.radialMenuPass = new RadialMenuPass(gl, this.settings);
|
||||
this.selectionBoxPass = new SelectionBoxPass(gl);
|
||||
this.moveIndicatorPass = new MoveIndicatorPass(gl, this.settings);
|
||||
this.nukeTrajectoryPass = new NukeTrajectoryPass(gl, this.settings);
|
||||
@@ -552,7 +559,10 @@ export class GPURenderer {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
resize(cssWidth: number, cssHeight: number): void {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
this.cssWidth = cssWidth;
|
||||
this.cssHeight = cssHeight;
|
||||
this.appliedDprScale = this.settings.display.dprScale;
|
||||
const dpr = getDpr(this.settings);
|
||||
this.canvas.width = Math.round(cssWidth * dpr);
|
||||
this.canvas.height = Math.round(cssHeight * dpr);
|
||||
this.camera.resize(cssWidth, cssHeight);
|
||||
@@ -1201,6 +1211,12 @@ export class GPURenderer {
|
||||
draw(): void {
|
||||
const now = performance.now();
|
||||
this.trackFps(now);
|
||||
if (
|
||||
this.appliedDprScale !== this.settings.display.dprScale &&
|
||||
this.cssWidth > 0
|
||||
) {
|
||||
this.resize(this.cssWidth, this.cssHeight);
|
||||
}
|
||||
this.uploadTextures();
|
||||
this.computeTextures();
|
||||
this.renderFrame();
|
||||
|
||||
@@ -7,6 +7,10 @@ import { toggle } from "./props/Toggle";
|
||||
|
||||
export function buildTree(s: RenderSettings, d: RenderSettings): DebugNode[] {
|
||||
return [
|
||||
folder("Display", [
|
||||
slider(s.display, "dprScale", d.display, 0.25, 1, 0.05, "DPR Scale"),
|
||||
]),
|
||||
|
||||
folder("Pass Enables", [
|
||||
toggle(s.passEnabled, "terrain", d.passEnabled),
|
||||
toggle(s.passEnabled, "territory", d.passEnabled),
|
||||
|
||||
@@ -20,6 +20,7 @@ export { createRenderSettings, dumpSettings } from "./RenderSettings";
|
||||
export type { RenderSettings } from "./RenderSettings";
|
||||
export { deepAssign, deepDiff } from "./SettingsUtils";
|
||||
export { buildTerrainRGBA, getPaletteSize } from "./utils/ColorUtils";
|
||||
export { getDpr } from "./utils/Dpr";
|
||||
export { buildNukeTrajectory, samRange } from "./utils/NukeTrajectory";
|
||||
export type { SAMInfo } from "./utils/NukeTrajectory";
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
*/
|
||||
|
||||
import type { RadialMenuItem } from "../Events";
|
||||
import type { RenderSettings } from "../RenderSettings";
|
||||
import { getDpr } from "../utils/Dpr";
|
||||
import { createProgram } from "../utils/GlUtils";
|
||||
|
||||
import arcFragSrc from "../shaders/radial-menu/arcs.frag.glsl?raw";
|
||||
@@ -150,7 +152,10 @@ export class RadialMenuPass {
|
||||
private savedItems: RadialMenuItem[] = [];
|
||||
private savedCenterItem: RadialMenuItem | null = null;
|
||||
|
||||
constructor(gl: WebGL2RenderingContext) {
|
||||
constructor(
|
||||
gl: WebGL2RenderingContext,
|
||||
private settings: RenderSettings,
|
||||
) {
|
||||
this.gl = gl;
|
||||
this.emojiMap = buildEmojiMap();
|
||||
|
||||
@@ -437,7 +442,7 @@ export class RadialMenuPass {
|
||||
if (this.items.length === 0 && !this.centerItem) return;
|
||||
|
||||
const gl = this.gl;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = getDpr(this.settings);
|
||||
const vw = gl.drawingBufferWidth;
|
||||
const vh = gl.drawingBufferHeight;
|
||||
const ax = this.anchorX * dpr;
|
||||
@@ -491,7 +496,7 @@ export class RadialMenuPass {
|
||||
centerHovered: boolean,
|
||||
): void {
|
||||
const gl = this.gl;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = getDpr(this.settings);
|
||||
const n = items.length;
|
||||
const hasCenter = centerItem !== null;
|
||||
const outerR = cfg.outerR * dpr;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import type { Config } from "../../../../core/configuration/Config";
|
||||
import type { BonusEvent, ConquestFx } from "../../types";
|
||||
import type { RenderSettings } from "../RenderSettings";
|
||||
import { getDpr } from "../utils/Dpr";
|
||||
import { createProgram } from "../utils/GlUtils";
|
||||
import type { GlyphTables } from "./name-pass/AtlasData";
|
||||
import { buildGlyphTables, parseAtlasData } from "./name-pass/AtlasData";
|
||||
@@ -399,7 +400,7 @@ export class WorldTextPass {
|
||||
let count = 0;
|
||||
// canvasW in Camera is cssWidth*dpr, so `zoom` is device-px-per-world-unit.
|
||||
// Multiply screen-relative scales by dpr to keep a constant CSS-pixel size.
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = getDpr(this.settings);
|
||||
|
||||
for (const popup of this.active) {
|
||||
const elapsed = now - popup.startMs;
|
||||
@@ -541,7 +542,7 @@ export class WorldTextPass {
|
||||
gl.useProgram(this.program);
|
||||
gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix);
|
||||
gl.uniform1f(this.uZoom, zoom);
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const dpr = getDpr(this.settings);
|
||||
gl.uniform1f(
|
||||
this.uMinScreenScale,
|
||||
this.settings.bonusPopup.minScreenScale * dpr,
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
"enemyG": 0,
|
||||
"enemyB": 0
|
||||
},
|
||||
"display": {
|
||||
"dprScale": 1
|
||||
},
|
||||
"railroad": {
|
||||
"railMinZoom": 4,
|
||||
"railFadeRange": 2,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { RenderSettings } from "../RenderSettings";
|
||||
|
||||
/**
|
||||
* Effective device pixel ratio: the native devicePixelRatio scaled by the
|
||||
* `display.dprScale` render setting (lower = cheaper rendering, higher =
|
||||
* supersampling). Everything that converts between CSS pixels and canvas
|
||||
* pixels must use this so the camera, hit-testing, and screen-anchored
|
||||
* sprites stay consistent.
|
||||
*/
|
||||
export function getDpr(settings: RenderSettings): number {
|
||||
return (window.devicePixelRatio || 1) * (settings.display?.dprScale ?? 1);
|
||||
}
|
||||
@@ -66,6 +66,17 @@ describe("GraphicsOverridesSchema", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("accepts partial display overrides", () => {
|
||||
const cases = [
|
||||
{ display: {} },
|
||||
{ display: { dprScale: 0.5 } },
|
||||
{ display: { dprScale: 2 } },
|
||||
];
|
||||
for (const c of cases) {
|
||||
expect(GraphicsOverridesSchema.safeParse(c).success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects wrong field types", () => {
|
||||
expect(
|
||||
GraphicsOverridesSchema.safeParse({ name: { nameScaleFactor: "big" } })
|
||||
@@ -94,6 +105,11 @@ describe("GraphicsOverridesSchema", () => {
|
||||
railroad: { railThickness: "wide" },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
GraphicsOverridesSchema.safeParse({
|
||||
display: { dprScale: "retina" },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -261,6 +277,14 @@ describe("applyGraphicsOverrides", () => {
|
||||
expect(z.railThickness).toBe(defaults.railThickness);
|
||||
});
|
||||
|
||||
test("applies dprScale override (including values below 1)", () => {
|
||||
expect(gen({ display: { dprScale: 0.5 } }).display.dprScale).toBe(0.5);
|
||||
expect(gen({ display: { dprScale: 2 } }).display.dprScale).toBe(2);
|
||||
expect(gen({ display: {} }).display.dprScale).toBe(
|
||||
createRenderSettings().display.dprScale,
|
||||
);
|
||||
});
|
||||
|
||||
test("classicIcons + name overrides compose independently", () => {
|
||||
const s = gen({
|
||||
name: { darkNames: true, nameScaleFactor: 0.9 },
|
||||
|
||||
Reference in New Issue
Block a user