Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cspell/project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ devicerestored
diegetic
dppx
eamodio
enterkeyhint
Erdokovy
esbenp
evented
Expand Down Expand Up @@ -83,6 +84,7 @@ implementability
impluse
Infima
inigo
inputmode
interactable
interactables
isampler
Expand All @@ -101,6 +103,7 @@ letterboxing
lifecycles
lowp
matterjs
maxlength
mediump
Menlo
mesher
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ this step by step for bug fixes.
/ecs # Entity-Component-System core
/events # Event system
/finite-state-machine # FSM implementation
/input # Input handling
/input # Input handling (keyboard, mouse, gamepad, hidden-DOM text entry)
/lifecycle # Lifecycle management
/math # Math utilities
/particles # Particle system
Expand All @@ -116,7 +116,7 @@ this step by step for bug fixes.
/storage # StorageBackend (localStorage, memory) and createPersistentState: typed records kept outside the game
/text # MSDF font atlas loading and text rendering
/timer # Timer utilities
/ui # Retained-mode UI (anchored rect tree layout, canvases, panels, labels, buttons, focus navigation, toggles, sliders, progress bars, dropdowns, layout groups, content size/aspect ratio fitters)
/ui # Retained-mode UI (anchored rect tree layout, canvases, panels, labels, buttons, focus navigation, toggles, sliders, progress bars, dropdowns, text inputs, layout groups, content size/aspect ratio fitters)
/utilities # General utilities
index.ts # Main exports

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **rendering:** `getCameraView(world, camera, renderContext)` (and `computeCameraView(camera, position, renderContext)` for systems that already hold the camera's components) returns what a camera sees: the world area it shows (`bounds`, `size`, accounting for its position and zoom), its `pixelsPerUnit` in CSS pixels, and `worldToViewport`/`viewportToWorld` conversions to and from CSS pixels on the canvas. To place something drawn by one camera over something drawn by another, convert through the viewport: `hudView.viewportToWorld(gameView.worldToViewport(position))`
- **text:** Rich text tags. `TextEcsComponent.text` (and `shapeText`) parse `<b>...</b>` and `<color=#rrggbb>...</color>` (`#rgb`, `#rgba` and `#rrggbbaa` too) to style part of a string. Tags are stripped before shaping, so they never change kerning or wrapping. `<b>` draws a synthetic bold from the same atlas, thickening each glyph by `FAUX_BOLD_EMBOLDEN` ems per side and widening its advance to match. A `<color>` replaces the text's `color`, alpha included. Markup that isn't a valid tag, such as `HP < 50%` or `<i>`, is drawn as literal text, so a string that happens to contain `<b>` or `<color=#...>` now renders styled. `parseRichText` is public, and `GlyphQuad` gains `color` and `embolden`
- **text:** The engine's default font can be imported through a bundler from `@forge-game-engine/forge/fonts/default/default.json` and `@forge-game-engine/forge/fonts/default/default.png`, so you no longer need to copy it out of `node_modules`
- **ui:** Text input fields. `createTextInput(world, parent, { renderContext, sprite, fillSprite, fontAtlas, size })` builds a single-line field the player types into through a hidden DOM `<input>`, so keyboard layouts, IME composition, paste, phone keyboards and screen readers work. Its `TextInputEcsComponent` holds the filtered `value` (characters the font can't draw are dropped, then an optional `filter` and `maxLength` apply), `isEditing`, and `onValueChanged`/`onSubmit`/`onCancel` events; `setTextInputValue` and `editTextInput` change it from code. Editing is separate from UI focus: it starts on a click, a tap (focusing inside the tap, so phone keyboards open) or `submitInput` while focused, and ends on Enter, Escape or blur. `registerUiSystems` registers the new `createUiTextInputEcsSystem`
- **input:** `createTextEntry(container)` creates the hidden-input primitive text fields are built on, for game-drawn text that isn't a UI field (a chat box, a debug console): it exposes the value, selection and IME composition, and reports Enter/Escape/changes/blur through `takeEvents()`
- **text:** `TextEcsComponent.richText` (and `shapeText`'s `richText` option), default `true`. Set it to `false` to draw text exactly as written, tags included, for text a player typed. Text fields' labels set it to `false`
- **text:** `shapeText` returns `caretStops`, the position of every UTF-16 boundary in the shaped text (whitespace and missing glyphs included), and `TextMeshEcsComponent.caretStops` carries them
- **ui:** `raycastUiCanvas(world, canvasEntity, renderContext, viewportPosition)` returns the topmost interactable under a pointer position on a canvas, the same hit test `createUiRaycastEcsSystem` runs each tick

#### Changed

Expand All @@ -42,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **ui:** A `'screenPixels'`-unit size or margin, and `UiSafeAreaEcsComponent` insets, now follow the canvas camera's `zoom`, so they keep their on-screen size on a world-space canvas whose camera zooms. A screen-space canvas's root rect now fills its camera's view, so it follows a moved or zoomed UI camera
- **rendering:** Render targets can be canvas-sized. `createRenderTarget(renderContext, size, format?)` takes the render context instead of `gl`, and a `size` that's either `'canvas'` or a fixed `{ width, height }`. A canvas-sized target is resized by `RenderContext.resize` along with the canvas, so a camera's target always matches it: replace `createRenderTarget(renderContext.gl, renderContext.width, renderContext.height)` with `createRenderTarget(renderContext, 'canvas')` and delete any code that resized camera targets to follow the canvas. `RenderTarget.resize` throws for a canvas-sized target. `RenderTarget`'s `swapBuffers()`, `resize(width, height)` and `dispose()` no longer take `gl`, and `width`/`height` are read-only. `PingPongTarget` changes the same way: `new PingPongTarget(renderContext, size, format?)`, `resize(width, height)` and `dispose()`. The render context keeps a reference to each canvas-sized target until it's disposed, so `dispose()` targets you stop using. Screen-space UI canvases now render into a canvas-sized target, and `createUiLayoutEcsSystem` no longer resizes their camera's target. `RenderContext.maxPixelRatio` can be set at runtime, for example from a graphics quality setting: it re-applies the last resize at the new cap (or is stored for the next resize while the canvas has no size), so remove any cast you used to write it and the `resize` call that followed. `RenderContext`'s `width`, `height`, `cssWidth`, `cssHeight` and `pixelRatio` are now read-only, changed only by `resize`
- **text:** `FontAtlasCache.getOrLoad` takes the URL of both atlas files, `getOrLoad({ metricsUrl, imageUrl })`, instead of finding the image next to the JSON, so atlases imported through Vite, webpack or another bundler that renames files now load. Pass the URLs your bundler gives you for the `.json` and `.png` (with Vite, `import metricsUrl from './my-font.json?url'` and `import imageUrl from './my-font.png'`), and look loaded atlases up with `get(metricsUrl)`. `getOrLoad` rejects if the image's size doesn't match the JSON's `atlasSize`, or if one `metricsUrl` is requested with two different image URLs. Concurrent calls for the same atlas now share one load. `FontAtlasCache` no longer implements `AssetCache` and its `load` method and `assets` map are no longer public; call `getOrLoad` instead. `FontAtlasData` and `FontAtlasFileData` no longer have an `atlasImage` field and `forge-generate-font-atlas` no longer writes one; existing JSON files that have it still load
- **ui:** `resolveCanvasPointerPosition` takes the pointer's viewport position (CSS pixels, Y-down) as its last argument instead of a `UiPointerSource`. Pass `pointerSource.position`
- **rendering:** Bloom, Gaussian blur and tone mapping no longer copy their result back into the camera's render target, saving one full-screen draw per effect per frame and a full-resolution scratch buffer each. A `RenderTarget` now has two color buffers: the new `beginPostProcessPass(renderContext, target)` makes the other one current, binds and clears it, and returns the texture holding the target's contents, so a custom screen effect is that call plus `drawFullscreenQuad` with a material that samples the returned texture (see "Writing a post-processing effect" in the Multipass Rendering guide). Delete your effect's scratch target, copy material and copy-back draw. `RenderTarget.colorTexture` and `RenderTarget.framebuffer` are now read-only getters that change whenever a post-processing pass runs on the target, so read them when you draw instead of keeping them from setup. Bloom with `passes: 0` now draws nothing, like the blur, instead of adding its unblurred highlights; set `passes` to `0` to turn bloom off without changing `intensity`

#### Removed
Expand All @@ -52,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

#### Fixed

- **input:** `KeyboardInputSource` ignores keys typed into an editable element (`<input>`, `<textarea>`, `<select>`, `contentEditable`, including inside a shadow root): they don't count as held and don't dispatch bindings, and a release of a key it never saw go down is ignored. Typing into an HTML form next to the game no longer moves the player, while a key held before typing started is still released. `KeyboardInputSource` no longer implements `Resettable` (its `reset()` only cleared unused state), so remove any `inputManager.addResettable(keyboardInputSource)` call
- **rendering:** `Material.setUniform` (and `setColorUniform`/`setVectorUniform`) accepts every uniform its shaders declare, including one the GLSL compiler removed because nothing reads it. Which uniforms a compiler removes depends on the GPU and driver, so a game could throw on some phones (for example when a `mediump` hash folded to zero there and took the only reader of `u_time` with it) and run fine everywhere else. A removed uniform's value is checked against its declared type and size and stored, and simply has nothing to upload. Setting a name neither shader declares still throws, now with `Uniform "<name>" is not declared in material "<vertex>" + "<fragment>". Declared uniforms: ...`. A uniform array's size must be an integer literal, a `#define` or a `const int`, and a uniform declared twice must use the same type and size in both places, or creating the `Material` throws. New export: `parseUniformDeclarations`, and `ForgeShaderSource.uniformDeclarations` returns a shader's declared uniforms

## [0.25.8] - 2026-10-03
Expand Down
2 changes: 1 addition & 1 deletion design/input-action-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
| **Kind** | Defect |
| **Found in** | Galactic Journey demo: `src/input/create-inputs.ts` (three `actionResetTypes.noReset` axes), `src/run/run-input.system.ts` (`shootInput.endHold()`) |
| **Engine version at time of writing** | `0.25.8` |
| **Related** | [`text-input-field.md`](./text-input-field.md) (keyboard source changes) |
| **Related** | the text input field, which made the keyboard source ignore keys typed into editable elements |

## 0. Targeted modules

Expand Down
Loading
Loading