@solidrt/2d 0.0.69 → 0.0.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -6
- package/package.json +3 -3
- package/src/components/sprite-layer.tsx +13 -1
- package/src/font.ts +2 -3
- package/src/text-layout.ts +21 -27
- package/src/text.ts +2 -3
package/AGENTS.md
CHANGED
|
@@ -603,10 +603,10 @@ for a zoom animation that would re-rasterize every label per frame.
|
|
|
603
603
|
Layout is `layoutText` (text-layout.ts, pure): `\n` breaks, `maxWidth`
|
|
604
604
|
wraps with core's greedy rule, `align` per line, `anchor`
|
|
605
605
|
(start/middle/end, as d-text) and `anchorY` (top/middle/baseline/bottom)
|
|
606
|
-
place the run's point, `
|
|
607
|
-
run reports `width`, `height`, `ascent`
|
|
608
|
-
defaults to the face's; glyphs scale their
|
|
609
|
-
font.size`.
|
|
606
|
+
place the run's point, `lineHeight` as named; `letterSpacing` is shaped
|
|
607
|
+
into the units by the engine. The run reports `width`, `height`, `ascent`
|
|
608
|
+
and `lines`. A run's `fontSize` defaults to the face's; glyphs scale their
|
|
609
|
+
cells by `fontSize / font.size`.
|
|
610
610
|
|
|
611
611
|
### Pure pieces
|
|
612
612
|
|
|
@@ -720,8 +720,16 @@ sits, `viewport` is the nearest view (the `<SpriteLayer>`'s own, or
|
|
|
720
720
|
inside a `<View2d>` that view), what a `<Camera2d>` drives, and
|
|
721
721
|
`pointer` that view's feed (the owner's `pointer` prop, null without
|
|
722
722
|
one); read under `output={false}` outside a `<View2d>` `viewport` throws. A FILL-mode
|
|
723
|
-
|
|
724
|
-
|
|
723
|
+
leaf is size-contained (`contain="size"`, the CSS layout prop every
|
|
724
|
+
element takes): it adds nothing to its ancestors' sizes, exactly as an
|
|
725
|
+
empty `<view>` at 100% would, so a column of header, layer pane
|
|
726
|
+
(`flexGrow: 1`) and footer needs no `minHeight={0}`, and a pane with no
|
|
727
|
+
definite height does not come out square. The leaf then has no size of
|
|
728
|
+
its own at all: a parent with no size leaves it empty, the view is
|
|
729
|
+
invisible, and the component warns once with the stack of the component
|
|
730
|
+
that built it, naming the fix (a size, or flexGrow in a sized column). A
|
|
731
|
+
FILL-mode view is 1x1 until the first layout: `ref` fires at mount,
|
|
732
|
+
before it, so `viewport.size()` reads 1x1 there. Sprites added and positioned from
|
|
725
733
|
`ref` (the pool, the opening scene) are fine - the first layout resizes
|
|
726
734
|
the view before the first paint - but anything that needs the real size
|
|
727
735
|
(centering on the view) waits for `onLayout`, or works in a `designSize`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/2d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.70",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
"AGENTS.md"
|
|
16
16
|
],
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@solidrt/test": "0.0.
|
|
18
|
+
"@solidrt/test": "0.0.70"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@solidjs/signals": "2.0.0-rc.13",
|
|
22
|
-
"@solidrt/core": "0.0.
|
|
22
|
+
"@solidrt/core": "0.0.70"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEffect, displayScale, getBoundingBoxViewport, getLayoutBox, onCleanup, onLayout, untrack } from "@solidrt/core"
|
|
1
|
+
import { createEffect, displayScale, getBoundingBoxViewport, getLayoutBox, onCleanup, onLayout, untrack, warnOnce } from "@solidrt/core"
|
|
2
2
|
import type { Element, ParentComponent, PointerFeed, TextureId } from "@solidrt/core"
|
|
3
3
|
import type { BlendMode } from "@solidrt/core/gpu"
|
|
4
4
|
import type { Atlas } from "../atlas.ts"
|
|
@@ -314,7 +314,18 @@ export let SpriteLayer: ParentComponent<SpriteLayerProps> = props => {
|
|
|
314
314
|
// one onLayout applies size then oversample in order; setSize no-ops
|
|
315
315
|
// when nothing changed.
|
|
316
316
|
if (fill) {
|
|
317
|
+
let warnEmpty = warnOnce()
|
|
317
318
|
onLayout(() => {
|
|
319
|
+
// The leaf adds no size of its own (contain="size" below), so a
|
|
320
|
+
// parent with no size leaves it empty: nothing renders, and nothing
|
|
321
|
+
// on screen says why.
|
|
322
|
+
let box = leaf && getLayoutBox(leaf)
|
|
323
|
+
if (box && (box.width <= 0 || box.height <= 0)) {
|
|
324
|
+
warnEmpty(
|
|
325
|
+
`SpriteLayer: the fill leaf laid out ${box.width} x ${box.height}, so the view is invisible. ` +
|
|
326
|
+
`It adds no size of its own: give its parent a size (width/height, or flexGrow in a sized column), as on the web.`,
|
|
327
|
+
)
|
|
328
|
+
}
|
|
318
329
|
let size = viewSize()
|
|
319
330
|
if (size) view.setSize(size.width, size.height)
|
|
320
331
|
})
|
|
@@ -354,6 +365,7 @@ export let SpriteLayer: ParentComponent<SpriteLayerProps> = props => {
|
|
|
354
365
|
src={view.texture}
|
|
355
366
|
width={fill ? "100%" : props.width}
|
|
356
367
|
height={fill ? "100%" : props.height}
|
|
368
|
+
contain={fill ? "size" : undefined}
|
|
357
369
|
onPointerDown={events ? viewHandlers.onPointerDown : undefined}
|
|
358
370
|
onPointerMove={events ? viewHandlers.onPointerMove : undefined}
|
|
359
371
|
onPointerUp={events ? viewHandlers.onPointerUp : undefined}
|
package/src/font.ts
CHANGED
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
// (`Atlas.sdf`), and "mask", coverage at the face's exact size for drawing
|
|
15
15
|
// 1:1.
|
|
16
16
|
import { getOwner, onCleanup } from "@solidrt/core"
|
|
17
|
-
import type { PreparedText } from "@solidrt/core"
|
|
18
17
|
import { createFont, destroyFont, fontAtlas, glyphCells, prepareText, requestGlyphs } from "flux:font"
|
|
19
|
-
import type { FontAtlasOptions, FontFace, FontId, FontPrepareOptions, GlyphCell } from "flux:font"
|
|
18
|
+
import type { FontAtlasOptions, FontFace, FontId, FontPreparedText, FontPrepareOptions, GlyphCell } from "flux:font"
|
|
20
19
|
import type { Atlas } from "./atlas.ts"
|
|
21
20
|
import type { Frame } from "./frames.ts"
|
|
22
21
|
|
|
@@ -68,7 +67,7 @@ export type SpriteFont = {
|
|
|
68
67
|
readonly fontSize: number
|
|
69
68
|
/** Shape `text` on the engine (see flux:font prepareText): every unit
|
|
70
69
|
* carries its glyphs, for `layoutText`. */
|
|
71
|
-
prepare(text: string, options?: FontPrepareOptions):
|
|
70
|
+
prepare(text: string, options?: FontPrepareOptions): FontPreparedText
|
|
72
71
|
/** The glyph's frame and placement, or null while its cell is not in
|
|
73
72
|
* the atlas (request it with `request`). */
|
|
74
73
|
glyph(id: number): Glyph | null
|
package/src/text-layout.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// unit placed whole), restated here rather than imported because core
|
|
7
7
|
// loads the gui modules and this file is checked headless on the flux
|
|
8
8
|
// binary (tests/text-layout.test.ts), like frames.ts. No GPU or GUI
|
|
9
|
-
// imports: the
|
|
10
|
-
|
|
9
|
+
// imports: the flux:font import is a type. Letter spacing is the
|
|
10
|
+
// engine's, shaped into the units (`prepareText`'s `letterSpacing`).
|
|
11
|
+
import type { FontPreparedText, FontTextUnit } from "flux:font"
|
|
11
12
|
|
|
12
13
|
/** The anchor of the run's x: where the point sits on each line's
|
|
13
14
|
* extent (SVG's text-anchor, d-text's `anchor`). */
|
|
@@ -28,8 +29,6 @@ export type TextLayoutOptions = {
|
|
|
28
29
|
anchor?: TextAnchor
|
|
29
30
|
/** Default "top". */
|
|
30
31
|
anchorY?: TextAnchorY
|
|
31
|
-
/** Added after every glyph, run pixels (CSS letter-spacing); default 0. */
|
|
32
|
-
letterSpacing?: number
|
|
33
32
|
/** Baseline to baseline, run pixels; default each line's own ascent
|
|
34
33
|
* plus descent. */
|
|
35
34
|
lineHeight?: number
|
|
@@ -65,24 +64,18 @@ export type TextLayout = {
|
|
|
65
64
|
|
|
66
65
|
/**
|
|
67
66
|
* Lay out a prepared text (from `SpriteFont.prepare`) into glyph
|
|
68
|
-
* placements.
|
|
69
|
-
* Impeller, which exposes none).
|
|
67
|
+
* placements.
|
|
70
68
|
*/
|
|
71
|
-
export function layoutText(prepared:
|
|
69
|
+
export function layoutText(prepared: FontPreparedText, opts?: TextLayoutOptions): TextLayout {
|
|
72
70
|
let units = prepared.units
|
|
73
71
|
let maxWidth = opts?.maxWidth ?? Infinity
|
|
74
72
|
if (!(maxWidth > 0)) throw new Error(`layoutText: maxWidth must be positive, got ${maxWidth}`)
|
|
75
|
-
let letterSpacing = opts?.letterSpacing ?? 0
|
|
76
|
-
if (!Number.isFinite(letterSpacing)) throw new Error(`layoutText: letterSpacing must be finite, got ${letterSpacing}`)
|
|
77
|
-
for (let i = 0; i < units.length; i++) {
|
|
78
|
-
if (units[i]!.glyphs === undefined) throw new Error(`layoutText: unit ${i} ("${units[i]!.text}") carries no glyphs; prepare the text on a sprite font`)
|
|
79
|
-
}
|
|
80
73
|
// Break into lines: the greedy pass, lines as unit ranges.
|
|
81
74
|
type Range = { from: number; to: number; ascent: number; descent: number; width: number }
|
|
82
75
|
let ranges: Range[] = []
|
|
83
76
|
let cursor = 0
|
|
84
77
|
while (cursor < units.length) {
|
|
85
|
-
ranges.push(nextLine(units, cursor, maxWidth
|
|
78
|
+
ranges.push(nextLine(units, cursor, maxWidth))
|
|
86
79
|
cursor = ranges[ranges.length - 1]!.to
|
|
87
80
|
}
|
|
88
81
|
// A text with no units (empty) still has one empty line of no height.
|
|
@@ -115,25 +108,26 @@ export function layoutText(prepared: PreparedText, opts?: TextLayoutOptions): Te
|
|
|
115
108
|
let pen = line.x
|
|
116
109
|
for (let i = line.from; i < line.to; i++) {
|
|
117
110
|
let unit = units[i]!
|
|
118
|
-
for (let g of unit.glyphs
|
|
119
|
-
glyphs.push({ id: g.id, x: pen + g.x, y: line.y + g.y })
|
|
120
|
-
pen += letterSpacing
|
|
121
|
-
}
|
|
111
|
+
for (let g of unit.glyphs) glyphs.push({ id: g.id, x: pen + g.x, y: line.y + g.y })
|
|
122
112
|
pen += unit.advance
|
|
123
113
|
}
|
|
124
114
|
}
|
|
125
115
|
return { glyphs, lines, width: runWidth, height, ascent: firstAscent }
|
|
126
116
|
}
|
|
127
117
|
|
|
118
|
+
// Slack a line's width may be exceeded by and still fit, logical px: core's
|
|
119
|
+
// FIT_SLACK, the same value, for the same reason (a width that came back
|
|
120
|
+
// from a layout box is a float ulp off the width the text measured).
|
|
121
|
+
const FIT_SLACK = 1 / 64
|
|
122
|
+
|
|
128
123
|
// Ink width of the wrap unit starting at `index` (core's unitInk): its
|
|
129
|
-
// advance through every glued piece after it, plus the last piece's ink
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
let ink = units[index]!.width + letterSpacing * units[index]!.glyphs!.length
|
|
124
|
+
// advance through every glued piece after it, plus the last piece's ink.
|
|
125
|
+
function unitInk(units: FontTextUnit[], index: number): number {
|
|
126
|
+
let ink = units[index]!.width
|
|
133
127
|
let advance = 0
|
|
134
128
|
for (let j = index + 1; j < units.length && units[j]!.glue; j++) {
|
|
135
|
-
advance += units[j - 1]!.advance
|
|
136
|
-
ink = advance + units[j]!.width
|
|
129
|
+
advance += units[j - 1]!.advance
|
|
130
|
+
ink = advance + units[j]!.width
|
|
137
131
|
}
|
|
138
132
|
return ink
|
|
139
133
|
}
|
|
@@ -142,15 +136,15 @@ function unitInk(units: TextUnit[], index: number, letterSpacing: number): numbe
|
|
|
142
136
|
// layoutNextLine): units go on while the pen plus the unit's ink stays
|
|
143
137
|
// within the width; a hard break ends the line; a unit wider than the
|
|
144
138
|
// line on its own goes on whole.
|
|
145
|
-
function nextLine(units:
|
|
139
|
+
function nextLine(units: FontTextUnit[], cursor: number, width: number) {
|
|
146
140
|
let pen = 0
|
|
147
141
|
let ascent = 0
|
|
148
142
|
let descent = 0
|
|
149
143
|
let i = cursor
|
|
150
144
|
while (i < units.length) {
|
|
151
145
|
let unit = units[i]!
|
|
152
|
-
if (i > cursor && !unit.glue && pen + unitInk(units, i
|
|
153
|
-
pen += unit.advance
|
|
146
|
+
if (i > cursor && !unit.glue && pen + unitInk(units, i) > width + FIT_SLACK) break
|
|
147
|
+
pen += unit.advance
|
|
154
148
|
if (unit.ascent > ascent) ascent = unit.ascent
|
|
155
149
|
if (unit.descent > descent) descent = unit.descent
|
|
156
150
|
i++
|
|
@@ -158,6 +152,6 @@ function nextLine(units: TextUnit[], cursor: number, width: number, letterSpacin
|
|
|
158
152
|
}
|
|
159
153
|
let last = units[i - 1]!
|
|
160
154
|
// The ink width: trailing whitespace off, like the unit's own `width`.
|
|
161
|
-
let lineWidth = pen -
|
|
155
|
+
let lineWidth = pen - last.advance + last.width
|
|
162
156
|
return { from: cursor, to: i, ascent, descent, width: Math.max(lineWidth, 0) }
|
|
163
157
|
}
|
package/src/text.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type TextStyle = {
|
|
|
23
23
|
fontSize?: number
|
|
24
24
|
/** Baseline to baseline in layer pixels; default each line's own box. */
|
|
25
25
|
lineHeight?: number
|
|
26
|
-
/**
|
|
26
|
+
/** Extra advance after every character, layer pixels (CSS letter-spacing); default the font's. */
|
|
27
27
|
letterSpacing?: number
|
|
28
28
|
/** Wrap at this width; unset, only `\n` breaks. */
|
|
29
29
|
maxWidth?: number
|
|
@@ -149,13 +149,12 @@ function relayout(run: RunState, style: TextStyle): void {
|
|
|
149
149
|
run._style = style
|
|
150
150
|
run.text = style.text
|
|
151
151
|
let fontSize = style.fontSize ?? run.font.fontSize
|
|
152
|
-
let prepared = run.font.prepare(style.text, { fontSize, lineHeight: 0 })
|
|
152
|
+
let prepared = run.font.prepare(style.text, { fontSize, lineHeight: 0, letterSpacing: style.letterSpacing })
|
|
153
153
|
run._layout = layoutText(prepared, {
|
|
154
154
|
maxWidth: style.maxWidth,
|
|
155
155
|
align: style.align,
|
|
156
156
|
anchor: style.anchor,
|
|
157
157
|
anchorY: style.anchorY,
|
|
158
|
-
letterSpacing: style.letterSpacing,
|
|
159
158
|
lineHeight: style.lineHeight,
|
|
160
159
|
})
|
|
161
160
|
run.width = run._layout.width
|