Repository navigation
Text: round outline corners by default, lineJoin / miterLimit, and no cut outline - #1823
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- UICheckbox: a checkbox with its label, drawn or skinned, a radio button with a `group`; `onChange` only on user input - UISlider: horizontal or vertical, drag, track click, wheel, step, caption and value text, drawn or skinned; `onChange` and `onChangeEnd` - UITextButton: `onClick` setting on a completed click, atlas `skin` with lowered pressed art, `enabled`, `pressedColor`, CSS fonts - UIPanel: nine-slice `skin`, `title`, `draggable`, `layout` (column/row/grid, padding, gap, align, autoSize) and `scroll` (wheel, drag with momentum, scrollbar, scrollTo/scrollToChild) - UIBaseElement: cancelPress/onCancel, so a drag on a button in a scrolling panel scrolls instead of clicking - Input: children cropped by a clipping container, or scrolled out of a panel, no longer get pointer events there - The UI example uses the widgets, with volume sliders wired to audio and a scrolling level list; skill sections and changelog entries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- UIBaseElement: a draggable element pressed with no hover first, as a touch tap is, no longer throws, and drags by touch - UIPanel: a draggable panel is moved by its title or frame, not through a widget on it, so dragging it by a button no longer clicks the button - UIPanel: no fling while the pointer is still down; autoSize counts the title and frame from a zero height; a scrolling panel inside another scrolls alone; emptying or destroying a panel does not lay it out per child or call onScroll - UISlider: the wheel to the right raises it; a drag also ends on a mouse or touch release, or a release over the slider - UICheckbox: as wide as its longer label, whichever it is - UITextButton: a font given as fontData, an image, or with only its image loaded is a bitmap font as before; pressedColor defaults to hoverOnColor; bitmapText can be assigned - 10 regression tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
API, from a consistency review against the existing UI widgets: - UITextButton: width/height, fontSize (pixels, bitmap fonts too), labelFillStyle and borderColor, the names the other widgets use; the old names still work. Settings are no longer written back. Colours take any Paint. pressedColor left undefined follows hoverOnColor. onClickCallback (was clickCallback). bitmapText keeps its BitmapText type, deprecated - UICheckbox: boxSize, boxHoverColor; labelOn/labelOff settable and refitting; group changes apply at once; skin hoverOn/hoverOff and disabledOn/disabledOff - UISlider: valueFormat(value, ratio) (was format), ratio, settable min/max/step, thumbHoverColor, thumbBorderColor, thumbBorderWidth - UIBaseElement: enabled shared by every UI element; keepsDrag public - ProgressBar: onChange also gets the bar - Paint exported; @category UI on the UI settings types Fixes: - align "stretch" only resizes buttons and panels, not a BitmapText (whose resize is a scale) or a checkbox or slider - a drag ends only on its own pointer's release, in the event family the engine listens to, not on another finger or button - a non-blocking scrolling panel inside another scrolls alone - a draggable panel is not moved through a widget at any depth, and the check no longer mixes coordinate spaces in the world - a draggable element in the world no longer jumps by the camera position - a stale scroll press is dropped; skin insets of 0 match the art - slider steps in exponent notation, and a range that is not a whole number of steps, snap correctly Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
shadowColor, shadowOffsetX, shadowOffsetY and shadowBlur. The soft edge is a few stacked translucent rounded rectangles, so it looks the same on Canvas, WebGL and WebGPU; it follows the corner radius, goes under a skin too, and is drawn outside the bounds so it never takes the pointer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…game (#1805) A touch fires pointerover and pointerenter before its pointerdown, and pointerout and pointerleave after its pointerup. The plugin only kept the down, move, up and cancel from the engine, so the widget under a button got onOver as the button was pressed (the Alex 4 menu arrow jumped to the line under B). The plugin now also captures the four hover events and stops them for a touch that lands on, or lifted from, a control. A mouse hovering over the controls is left alone, as its moves are. Fixes #1805 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
UIFocus (off until UIFocus.enable()): one widget has the focus; the arrows, the D-pad or the left stick move it to the nearest widget that way, edge to edge; Enter, Space or A press it as a click does; left/right step a slider; Escape or B cancel. The pad is read directly (input.readGamepads, real or virtual), so the game's pad bindings are untouched. The ring shows after keyboard or pad input and hides when the mouse moves; the first key after the mouse only shows it. A UIPanel with trapFocus keeps the focus in and restores it after, with onBack on cancel. Focused widgets in a scrolling panel scroll into view, with room for the ring. Any UIBaseElement can take part: focusable, focusNeighbors, onNavigate, onFocus/onBlur, focusPress/focusRelease. Buttons, checkboxes and sliders do. hoverScale and pressScale on UIBaseElement, and as settings on every widget: an eased bounce applied to the drawing only, so hit areas and layouts stay put. The UI example turns on both, with a hint line for the controls. Skill sections and changelog entries; 13 new tests (87 in ui-widgets). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Arrows or the D-pad move to the nearest widget, Enter, Space or A press it, sliders step, a ring shows the focus, and a focused widget in a scrolling panel scrolls into view. Removed: trapFocus/onBack, cancel, focusNeighbors, onFocus/onBlur, hover moving the focus, the first-key rule, the stick with repeat, configurable keys and ring sizes. Most games use the mouse or touch; this covers a menu driven by keys or a pad. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- a press held by Enter or A is called off when the focus moves, the widget is removed or navigation is disabled, instead of staying down - a focused widget only bounces while the focus ring shows - the bounce eases over about 0.2 s; it snapped in a frame - Enter goes to the focused button: another bound to Enter waits while the ring shows elsewhere - a pad's A held as navigation is enabled is not a press - a widget made focusable after it was added takes part - navigation skips widgets inside a hidden or transparent container - an arrow from a focused widget disabled since moves on from where it is - docs: the bounce is on UIBaseElement; Enter and bindKey; accept calls onClick() with no event Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
UIFocus is gone. A widget takes the keyboard and gamepad focus with focus(); from then on the arrows or the D-pad move it, Enter, Space or A press it, sliders step. Nothing listens until a widget has the focus, and the listeners go when none does (blur(), or the focused widget leaving the scene), so the keys and the pad are the game's until then. On UIBaseElement: focus(), blur(), focused, focusable, onNavigate, the static focusOwner and focusRingColor. The example focuses its first widget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Three bugs the new tests caught: - a slider's default value text kept the old decimals after `step` changed - Enter on a focused button the pointer already held clicked it on release - a virtual gamepad sharing a slot with a real one was not read The stub-subclass tests now use the real button. New tests cover the shadow under a faded parent, scrollbar geometry, vertical and skinned sliders, cancelled and two-finger drags, skin regions and bitmap font sizes, radio drawing and groups, sideways and line-mode wheel scrolling, wheel pass-through at the end of a panel, fling limits, grid stretch, row autoSize, clearChildren, the focus ring, and focus teardown. The clipping test now clips through a real container. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Dates the 1.0.2 changelog section: a touch on the controls no longer reaches the game as a hover (#1805). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- a "which widget for what" table - one complete options menu: skinned panel, column layout, checkbox, slider, button, drop shadow, bounce, keyboard / gamepad focus - code for drop shadows, a scrolling level list, radio groups, skinned checkboxes and sliders, stepped and vertical sliders, and a custom focusable widget - symptom rows for focus, bindKey vs focus, and shadow corners - drops a reference to the UI example's old hand-made panel Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Class docs in sections (look, label, state and onChange, radio groups, keyboard and gamepad; range, input, text, look), every setting with its unit and default, and examples for the common uses: a settings toggle, a label that follows the state, a radio group, skins, restoring saved settings, a volume slider, stepped and vertical sliders, percent text, a slider in a scrolling panel. Three docs that contradicted the code now match it (trackThickness with a skin, the checkbox fontSize minimum, onChangeEnd on arrow steps). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
UIPanel's class doc is rewritten in sections (background, title, dragging, blocking, layout, scrolling, drop shadow, idle float, things to know), with examples for a drawn dialog, a skinned options menu, a scrolling level grid, a draggable window, a drop shadow and a HUD frame. Every setting has its unit and default; clearChildren is now documented. ProgressBar's doc is tidied, with examples for a loading bar, a health bar, a colour that follows the value, a vertical bar, a drop shadow, and reacting when the bar empties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
UIBaseElement's class doc covers the pointer callbacks, enabled, dragging, the bounce and keyboard / gamepad focus, with examples for a widget of your own, a draggable token, a keyboard and pad menu, and an opaque dialog. UITextButton documents every setting with its unit and default (the old names as aliases), with examples for drawn, skinned, bindKey, disabled and shared-style buttons. UISpriteElement gets a real class doc and examples. Several docs that contradicted the code now match it (onMove, onRelease, enabled on a custom element, bindKey while disabled). Fix: a held bindKey press that is called off (the window losing focus, the button removed or disabled) now calls onCancel(), as a pointer press does; it called neither onRelease nor onCancel, so work started on the press never stopped. Test added; the skill's bindKey rules follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Its onClick() and onRelease() returned false, stopping every press and release at the sprite, while every other UI element lets the event carry on to what is under it unless a handler returns false. They now return true. A subclass that relied on the old default returns false itself; the examples already do. onActivateEvent / onDeactivateEvent now call super. Test with real pointer events; docs, skill and changelog updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
UIBaseElement's onClick() and onRelease() now return false, as UISpriteElement's always did (the previous commit's flip of UISpriteElement is undone). A click on a button, a checkbox, a slider or a widget of your own no longer also reaches the game behind it; a handler returns true to let the press through. Hover is unchanged, and a non-blocking UIPanel still lets presses on its background through. Tests with real pointer events for buttons, checkboxes, sprite elements, a handler letting the press through, and a non-blocking panel. Docs, both skills and the changelog follow; two example buttons that returned true now return false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
It was a Sprite carrying its own copy of the pointer handling, which had drifted from UIBaseElement's. It now extends UIBaseElement and holds its image as `sprite`, so it shares the one implementation and gains what the other widgets have: enabled (drawn at half opacity when off), cancelPress / onCancel, hoverScale / pressScale, dragging and keyboard / gamepad focus. Kept for existing code: (x, y) is still where the image's anchor point goes, and setRegion (which also resizes the element), addAnimation, setCurrentAnimation and isCurrentAnimation stay on the element. Other Sprite members move to `sprite`; UISpriteElementSettings is exported. The matter platformer's touch controls follow: the button passes its anchorPoint in the settings, and the joypad's knob is a child instead of being drawn by hand. Tests cover placement, clicks, disabled drawing, setRegion, the bounce, cancel and focus; docs, skill and changelog updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
17 tests of what a game relies on: Sprite settings and size, placement (centred by default, or by anchorPoint in the settings), presses, releases, hover, a press ended by leaving, isClickable, removal, the press stopping at it or let through, a subclass overriding the callbacks as the demos' buttons do, and the setRegion / animation / opacity shortcuts. They only use what the Sprite-based element and the UIBaseElement-based one both offer, and pass on both, and on master's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…es the canvas A pointerup cleared the pointer a region was tracking, so the region forgot it was under the pointer: leaving straight after a click never called onOut (a button stayed lit until the pointer came back over it), and the next move over it called onOver a second time. The release now keeps the pointer; the press still ends as before. The browser's pointerleave, sent when the pointer leaves the canvas or a finger lifts, now releases every region holding that pointer, wherever the exit is reported. It used to reach only a region at that point, which for a mouse leaving the canvas is the edge, far from most widgets. Touch is unchanged: a tap hovers while the finger is down, not after. Tests for leaving after a click, moving on over it, repeated clicks, a tap with new pointer ids, and leaving the canvas; the UISpriteElement tests check the hover strictly again, and pass on the old and new element. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- UICheckbox#toggle() on a checked radio button leaves it checked, as a click does, instead of leaving its group with nothing checked - UIPanel#scrollTo() (and scrollToChild, scrollX / scrollY) stops a fling still coasting; the panel's own fling, drag, wheel and relayout scroll through a private method that leaves it alone - UISlider lays its texts out in one place, again whenever they change: a vertical slider widens for a longer value text (valueFormat, min, max or step set later), a horizontal one moves the value below the track when it would run into the caption, and the new labelText sets the caption later Tests for each, failing before the fix, and one for the wheel over a slider in a scrolling panel (it moves the slider, not the panel), which had none. Docs, skill and changelog updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
A checkbox joined its group only once in the scene, so before a menu was added several radio buttons could be checked at once and UICheckbox.checkedIn() answered undefined. A checkbox now joins its group when made (or when its group changes) and leaves it when destroyed. Groups hold their members through WeakRef, so a checkbox dropped without being destroyed can still be collected. A checkbox taken out of the world but kept alive stays in its group. Tests for a group before the scene, and for leaving on destroy while staying in the group when kept alive; both fail before the fix. Docs, skill and changelog updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…eElement follows its frame - A pointerleave (the pointer leaving the canvas, or a finger lifting) now goes to every registered region holding that pointer, not only the candidates the spatial query finds near the exit point, which once the quadtree splits misses a hovered region in another quadrant - UISpriteElement follows its sprite whenever the image's size or anchor changes: through setRegion, setCurrentAnimation, sprite.setRegion() or an animation frame of another size. The element takes the new size and the image's anchor point stays in place on screen, as a Sprite's pos does, so the hit area matches the artwork Regression tests: a crowded world with a split index and the exit in another quadrant; a region of another size keeping the anchor; a frame set on the sprite itself followed on the next update and hit where the smaller image is. Each fails before the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
The kept shortcut dropped Sprite's third argument, so existing calls with it no longer compiled in TypeScript, and in JavaScript reset the frame timer instead of keeping it. The compatibility test checks the argument reaches the sprite, on the old element and the new. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- pointercancel cleared the region's pointer and only called the cancel callback; the browser's leave after it then no longer matched, so a cancelled touch left a button highlighted. A cancel now also leaves the region (hover cleared, onOut called). - A legacy touchend kept the touch's id like a mouse release does, but touch events send no leave after it, so the region stayed occupied and later mouse hover could not enter it. A touchend now leaves the region, as a lifted finger hovers nothing. Test for the cancel; the legacy touch path is only used where pointer events are missing, so the browser tests cannot reach it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…d WebGPU (#1816) On WebGL and WebGPU a clipRect made while another clip was active replaced it, where Canvas's context.clip() intersects with it. A clipping container nested in another, or a scrolling UIPanel in a clipping parent, drew past the outer clip wherever it stuck out of it. clipRect now intersects the new screen box with the active scissor on both GPU renderers; no overlap gives an empty scissor (nothing draws). save() / restore() already store and re-apply the scissor, so the outer clip comes back as before. A clip under a non-finite transform (a ColorLayer's infinite size) keeps the active clip instead of dropping it, as a clip can only narrow. enableScissor still sets the box outright. The clipping example gets a third panel: an inner clipping container sticking out of the outer one, its fill shown only where both overlap. Tests: nested clipping containers read back in pixels on Canvas (the reference) and WebGL, the scissor overlap, restore and an empty overlap on WebGL, and WebGPU's clipRect on a stand-in without a device; they fail before the fix. A save/restore test that pinned the old replacing behaviour now expects the overlap. The UIPanel docs and the skill drop the limitation they described. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
FadeEffect counted itself complete as soon as its colour's alpha read as the target. The alpha is stored as a 32-bit float, so on some durations a value one step short of the end already rounds to it; the camera then destroyed the effect, which stopped the tween before it called onComplete. fadeIn / fadeOut callbacks, the fade state transition and a fading level trigger could all hang on it. The fade is now complete when its tween has finished running, after the tween has called onComplete. Tests: a sweep of fade durations at 60 fps, both directions, frame by frame as the loop runs it, checks onComplete fires exactly once before the effect goes (10 durations missed it before the fix); and a fade whose alpha already reads as the target stays until its tween ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
viewport.fadeIn() / fadeOut() driven the way a game runs them: each frame a TICK, so the application updates (its stage removes completed camera effects) and then the tween steps. A sweep of durations at 60 fps checks onComplete fires once before the fade goes; a chain of scene changes on fadeIn callbacks, as the Alex 4 outro does, reaches its end; and the fade stays on the camera until its tween has called onComplete. Against the code before the fix, fadeIn missed its callback at 600, 800, 850, 900 and 950 ms, and the chain stopped after the first scene. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…id (#1809) level.load() seeds the GUID counter with the map's nextobjectid. The TMX format makes it optional, and with it missing the counter started at 0: objects added without an id took GUIDs equal to the map objects' own (<level>-<id>). The builtin detector keys contacts by GUID, so two pairs shared a key and the second contact in a step was dropped as a duplicate; in Alex 4, Alex fell through the boss. Without nextobjectid, TMXTileMap now uses one past the highest object id in the map, objects in group layers at any depth included (1 for a map with none). Tests: the map's own value is kept; without it the value is one past the highest id, through nested groups, and 1 with no objects; and runtime GUIDs never meet those of the map's objects. Four fail before the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…ex() alone The hex alpha precision fix also made toHex8() round alpha instead of truncating it, which changes what it returns for every colour whose alpha is not a whole byte (0.5 gave #...7F and would give #...80). The fix does not need it: with parseHex() keeping the exact byte / 255, toHex8() already writes every 8-bit and 4-bit hex alpha back unchanged, which the round-trip tests check. So toHex8() is restored and its expectations with it; the public API and toHex8() output stay as they were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- transition-second-change: the spec steps the engine's own game loop itself, one frame at a time, instead of waiting on browser animation frames, which a busy browser can hold back for seconds. The tests still all fail on the code before #1787. - drawmesh_bench: runs only when asked for, with BENCH=1, and then with the time it needs. It measures rather than tests, and it ties up the shared GPU process for minutes, timing out the specs beside it. - text-ink-clipping: allows the one row a face's hinting can paint short of the ink box the browser reports, and checks the unstroked glyph leaves the canvas edges blank, which is what a cut leaves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
"comes back from an idle suspension the browser never acknowledged" gave the resume and the clip a fixed 20 ms, which a full parallel run can outlast. It now waits for them: an engine wedged as in the bug never starts the clip at all, so it still fails there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
… cut outline
An outlined Text was stroked with the canvas's pointed ("miter") corners,
which can reach up to miterLimit (10) half line widths past a glyph. The
bake left room for half a line width, so the tips of sharp glyphs such as
"A", "V" or "M" were cut flat at the top, and it left nothing to the sides,
so the outline on the left of the first glyph and past the end of a line
was cut too.
- Outlines have round corners by default, the same width all around.
- `lineJoin` ("round", "bevel", "miter") and `miterLimit` settings choose
others; a limit the canvas would refuse falls back to its default, 10.
- The bake leaves room for the outline on all four sides, for whichever
corners are set (`inkPadLeft` / `inkPadRight` beside the existing top and
bottom), and draws back by the same amount: the glyphs land on the same
screen pixels and the bounds are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…d reuse - the Text constructor gets an example of every corner option, and miterLimit one of a live change; both properties say a change shows from the next setText() - lineJoin is typed as its three values rather than any string - the skill lists the options in a table, with an example of each - tests: corners changed on a live label, the room each style takes, an invisible outline taking none, and a recycled label not keeping the corners of the one before Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
An outlined
Text(lineWidth> 0) could have its outline cut off at the edges of its texture:"miter"corners, which can point up tomiterLimit(10) half line widths past a glyph. The bake left room for only half a line width, so the tips of sharp glyphs such as "A", "V" or "M" were cut flat.This PR:
lineJoinandmiterLimitsettings choose other corners:"round"(default),"bevel", or"miter"for pointed corners as a plain canvas draws them, withmiterLimitcapping the points.lineJoinis typed as its three values; amiterLimitthe canvas would refuse (zero, negative, infinite, NaN) falls back to 10. As with the other style properties, a change takes effect on the nextsetText().TextMetrics#inkPadLeft/inkPadRightbeside the existinginkPadTop/inkPadBottom), and the image is drawn back by the same amount, so the text lands on the same screen pixels and its bounds are unchanged. Pointed corners at the default limit cost a bigger texture: a 72px "AVWM" with an 8px outline bakes at 320×192 instead of 256×96.API: additive only. Nothing is removed or renamed. The one visible change is the default corner look: set
lineJoin: "miter"to keep pointed corners, now drawn whole.Docs:
Textconstructor gets an example of every option, and both properties have their own docs and examplesmelonjs-ui-and-textskill gets a table of the options with an example of eachTests (
tests/text-outline-join.spec.js, 21 tests; 14 fail on the old code):tests/text_bucket.spec.jsnow counts the side padding in the canvas width, as it already counted the top and bottom padding in the height.Type of change
Checklist
pnpm lintpasses)pnpm testpasses)pnpm build)Related issues
Found while fixing #1817.
Generated by Claude Code