From 414afecb0219e1f4f55b585eaaf633a7efdcf065 Mon Sep 17 00:00:00 2001 From: Brandon Corfman Date: Sun, 5 Jul 2026 22:55:40 -0400 Subject: [PATCH] redid canvas presentation --- docs/getting-started/pattern-demo.md | 6 +++--- src/phaser/EditorScene.ts | 5 +++-- src/phaser/GameScene.ts | 5 +++-- src/phaser/textureFiltering.ts | 23 +++++++++++++++++++++++ tests/phaser/textureFiltering.test.ts | 22 +++++++++++++++++++++- 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/docs/getting-started/pattern-demo.md b/docs/getting-started/pattern-demo.md index fa1de1a..2816c99 100644 --- a/docs/getting-started/pattern-demo.md +++ b/docs/getting-started/pattern-demo.md @@ -13,9 +13,9 @@ This walkthrough recreates the `Pattern Demo` scene in PhaserForge. It assumes y ## Before You Start - Open PhaserForge and sign in if needed. -- If you are continuing from older work, reset to a new empty scene from `Project -> Startup & Reset`. +- If you are continuing from older work, reset to a new empty scene from the Project Tree in the upper left. Click `Manage -> Create New`. - Stay in the same signed-in project flow you established during cloud account setup. -- Set the scene world size to `800 x 600` before you begin placing ships. +- Set the scene world size to `800 x 600` and then the `Fit` button to recenter the canvas to full size before you begin placing ships. Assets dock add menu with the demo pack import option @@ -23,7 +23,7 @@ This walkthrough recreates the `Pattern Demo` scene in PhaserForge. It assumes y Success check: - The canvas is empty and the scene graph does not show leftover sprites or formations. -- The World Size is `800 x 600`, as shown in the Viewport panel (Figure 1). +- The World Size is `800 x 600`, as shown in the Viewport panel (Figure 1), and the viewport is fully fit to the canvas. ## Import the Demo Pack Assets diff --git a/src/phaser/EditorScene.ts b/src/phaser/EditorScene.ts index 462967f..5dd754e 100644 --- a/src/phaser/EditorScene.ts +++ b/src/phaser/EditorScene.ts @@ -48,7 +48,7 @@ import { import { registerSceneGetter, unregisterSceneGetter } from '../testing/testBridge'; import { resolvePointerModifier } from './inputModifiers'; import { getPreferredTextResolution } from './textResolution'; -import { applyProjectTextureFilter } from './textureFiltering'; +import { applyProjectCanvasRenderMode, applyProjectTextureFilter } from './textureFiltering'; import type { ViewState } from '../util/viewStateStorage'; const PLACEHOLDER_TEXTURE_KEY = '__phaserforge:placeholder-1x1'; @@ -155,7 +155,7 @@ export class EditorScene extends Phaser.Scene { create(): void { this.cameras.main.setBackgroundColor('#0c0f1a'); - this.cameras.main.roundPixels = true; + applyProjectCanvasRenderMode(this.game.canvas, this.cameras.main, this.project ? getProjectRenderMode(this.project) : 'pixel-art'); this.bindSceneListeners(); EventBus.emit('current-scene-ready', this); this.lastViewportSize = { width: this.scale.width, height: this.scale.height }; @@ -840,6 +840,7 @@ export class EditorScene extends Phaser.Scene { this.clearScene(); this.project = project; this.mode = mode; + applyProjectCanvasRenderMode(this.game.canvas, this.cameras.main, project ? getProjectRenderMode(project) : 'pixel-art'); this.varsService = new BasicVarsService({ counters: project?.counters, collections: project?.collections }); this.compiled = compileScene(sceneSpec, { opRegistry: this.opRegistry, vars: this.varsService }); this.referenceCompiled = referenceSceneSpec ? compileScene(referenceSceneSpec, { opRegistry: this.opRegistry, vars: this.varsService }) : undefined; diff --git a/src/phaser/GameScene.ts b/src/phaser/GameScene.ts index ba7e7ab..aec33d0 100644 --- a/src/phaser/GameScene.ts +++ b/src/phaser/GameScene.ts @@ -21,7 +21,7 @@ import type { TriggerZoneSpec } from '../model/types'; import { assetSourceKey, resolveAssetSourceUrl } from '../cloud/assetUrls'; import type { ViewState } from '../util/viewStateStorage'; import { getProjectRenderMode } from '../model/projectPixelScale'; -import { applyProjectTextureFilter } from './textureFiltering'; +import { applyProjectCanvasRenderMode, applyProjectTextureFilter } from './textureFiltering'; const PLACEHOLDER_TEXTURE_KEY = '__phaserforge:placeholder-1x1'; const EMPTY_SCENE_SPEC: SceneSpec = { id: '', entities: {}, groups: {}, attachments: {}, behaviors: {}, actions: {}, conditions: {} }; @@ -155,7 +155,7 @@ export class GameScene extends Phaser.Scene { create(): void { // Match editor canvas background so offscreen space is consistent between edit and preview. this.cameras.main.setBackgroundColor('#0c0f1a'); - this.cameras.main.roundPixels = true; + applyProjectCanvasRenderMode(this.game.canvas, this.cameras.main, this.project ? getProjectRenderMode(this.project) : 'pixel-art'); this.audioService = new BasicAudioService(this.sound as any); this.inputService = new BasicInputService({ getGamepads: () => (typeof navigator !== 'undefined' && navigator.getGamepads ? Array.from(navigator.getGamepads()) : []), @@ -204,6 +204,7 @@ export class GameScene extends Phaser.Scene { public loadSceneSpec(projectOrScene: ProjectSpec | SceneSpec, maybeScene?: SceneSpec): void { const project = maybeScene ? (projectOrScene as ProjectSpec) : undefined; this.project = project; + applyProjectCanvasRenderMode(this.game.canvas, this.cameras.main, project ? getProjectRenderMode(project) : 'pixel-art'); this.varsService = new BasicVarsService({ counters: project?.counters, collections: project?.collections }); const sceneSpec = (maybeScene ?? projectOrScene) as GameSceneSpec; if (project) { diff --git a/src/phaser/textureFiltering.ts b/src/phaser/textureFiltering.ts index e73e9ca..801fd58 100644 --- a/src/phaser/textureFiltering.ts +++ b/src/phaser/textureFiltering.ts @@ -4,6 +4,16 @@ type TextureLike = { setFilter?: (mode: number) => void; }; +type CanvasLike = { + style?: { + imageRendering?: string; + }; +}; + +type CameraLike = { + roundPixels?: boolean; +}; + type TextureManagerLike = { exists: (key: string) => boolean; get: (key: string) => TextureLike | null | undefined; @@ -22,3 +32,16 @@ export function applyProjectTextureFilter( const texture = textures.get(key); texture?.setFilter?.(renderMode === 'smooth-2d' ? LINEAR_FILTER_MODE : NEAREST_FILTER_MODE); } + +export function applyProjectCanvasRenderMode( + canvas: CanvasLike | null | undefined, + camera: CameraLike | null | undefined, + renderMode: ProjectRenderMode = 'pixel-art', +): void { + if (canvas?.style) { + canvas.style.imageRendering = renderMode === 'smooth-2d' ? 'auto' : 'pixelated'; + } + if (camera) { + camera.roundPixels = renderMode !== 'smooth-2d'; + } +} diff --git a/tests/phaser/textureFiltering.test.ts b/tests/phaser/textureFiltering.test.ts index 722d8be..dab8184 100644 --- a/tests/phaser/textureFiltering.test.ts +++ b/tests/phaser/textureFiltering.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { applyProjectTextureFilter } from '../../src/phaser/textureFiltering'; +import { applyProjectCanvasRenderMode, applyProjectTextureFilter } from '../../src/phaser/textureFiltering'; describe('applyProjectTextureFilter', () => { it('sets the filter mode to nearest for pixel-art projects', () => { @@ -31,4 +31,24 @@ describe('applyProjectTextureFilter', () => { expect(setFilter).not.toHaveBeenCalled(); }); + + it('uses browser smoothing and subpixel camera placement for smooth-2d projects', () => { + const canvas = { style: { imageRendering: 'pixelated' } }; + const camera = { roundPixels: true }; + + applyProjectCanvasRenderMode(canvas, camera, 'smooth-2d'); + + expect(canvas.style.imageRendering).toBe('auto'); + expect(camera.roundPixels).toBe(false); + }); + + it('uses pixelated canvas scaling and rounded camera positions for pixel-art projects', () => { + const canvas = { style: { imageRendering: 'auto' } }; + const camera = { roundPixels: false }; + + applyProjectCanvasRenderMode(canvas, camera, 'pixel-art'); + + expect(canvas.style.imageRendering).toBe('pixelated'); + expect(camera.roundPixels).toBe(true); + }); });