@weasel-js/paint 1.2.0
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/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/index.d.ts +288 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @weasel-js/paint
|
|
2
|
+
|
|
3
|
+
Paint vocabulary for weasel: `FillStyle`, `Stroke`, gradients, dashes. Plain
|
|
4
|
+
data — nothing here draws anything.
|
|
5
|
+
|
|
6
|
+
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
7
|
+
scene-graph canvas kit for React. See the
|
|
8
|
+
[API reference](https://orochi235.github.io/weasel/api/).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @weasel-js/paint
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import type { FillStyle, Stroke } from '@weasel-js/paint';
|
|
20
|
+
|
|
21
|
+
const fill: FillStyle = { color: '#c0ffee' };
|
|
22
|
+
const stroke: Stroke = { paint: { color: '#000' }, width: 2, join: 'round' };
|
|
23
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A registered texture, named by the registry that holds it.
|
|
3
|
+
*
|
|
4
|
+
* The handle is here rather than beside the registry because `FillStyle`'s
|
|
5
|
+
* pattern variant names it, and paint is a leaf: the registry lives in the
|
|
6
|
+
* renderer and imports this back.
|
|
7
|
+
*/
|
|
8
|
+
interface TextureHandle {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* FillStyle and Stroke types — the unified shape for "what color or texture
|
|
14
|
+
* paints these pixels," modeled on SVG's paint-server concept.
|
|
15
|
+
*
|
|
16
|
+
* - `FillStyle` is a tagged union: solid color, pattern, or gradient. Used
|
|
17
|
+
* wherever a kit option previously took `fillStyle: string`.
|
|
18
|
+
* - `Stroke` pairs a `FillStyle` with structural stroke parameters (width, dash,
|
|
19
|
+
* line cap/join, alignment).
|
|
20
|
+
* - These types are consumed by the GL renderer's DrawCommand path fills
|
|
21
|
+
* and strokes.
|
|
22
|
+
*
|
|
23
|
+
* The 2D `applyPaint` / `applyStroke` / `renderFilledRegion` helpers that
|
|
24
|
+
* formerly lived alongside these types were deleted with the 2D backend in
|
|
25
|
+
* Step 10. `alignedStrokeRect` survives as a pure geometry helper used by
|
|
26
|
+
* path tessellation and the selection overlay.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Color/texture strategy for fills (and, via `Stroke.paint`, strokes).
|
|
31
|
+
*
|
|
32
|
+
* `fill` is optional and defaults to `'solid'` — `{ color: '#abc' }` is
|
|
33
|
+
* equivalent to `{ fill: 'solid', color: '#abc' }`. Pattern paints must set
|
|
34
|
+
* `fill: 'pattern'` explicitly.
|
|
35
|
+
*
|
|
36
|
+
* The `'pattern'` variant's payload is either a `TilePatternSpec` — plain
|
|
37
|
+
* data naming one of the built-in tiles, which survives serialization — or a
|
|
38
|
+
* `TextureHandle` for a tile the consumer built itself via
|
|
39
|
+
* `createTilePattern()`. A handle is a session-scoped registry key, so a
|
|
40
|
+
* paint carrying one cannot be persisted or exported; prefer the spec.
|
|
41
|
+
*
|
|
42
|
+
* `units` on a pattern names the space the tile's origin and scale live in,
|
|
43
|
+
* not the space of any geometry (a pattern has none). Under `'bounds'` the
|
|
44
|
+
* tile anchors to the painted node's box, so the pattern travels with the
|
|
45
|
+
* node and a resize reveals more tiles rather than stretching them;
|
|
46
|
+
* `fillInPoseFrame` resolves that to a `'local'` paint with an explicit
|
|
47
|
+
* `origin` before the renderer sees it.
|
|
48
|
+
*/
|
|
49
|
+
type FillStyle = {
|
|
50
|
+
fill?: 'solid';
|
|
51
|
+
color: string;
|
|
52
|
+
opacity?: number;
|
|
53
|
+
} | {
|
|
54
|
+
fill: 'pattern';
|
|
55
|
+
pattern: TextureHandle | TilePatternSpec;
|
|
56
|
+
units?: GradientUnits;
|
|
57
|
+
origin?: {
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
};
|
|
61
|
+
opacity?: number;
|
|
62
|
+
} | {
|
|
63
|
+
fill: 'linear-gradient';
|
|
64
|
+
from: {
|
|
65
|
+
x: number;
|
|
66
|
+
y: number;
|
|
67
|
+
};
|
|
68
|
+
to: {
|
|
69
|
+
x: number;
|
|
70
|
+
y: number;
|
|
71
|
+
};
|
|
72
|
+
stops: GradStop[];
|
|
73
|
+
units?: GradientUnits;
|
|
74
|
+
opacity?: number;
|
|
75
|
+
} | {
|
|
76
|
+
fill: 'radial-gradient';
|
|
77
|
+
center: {
|
|
78
|
+
x: number;
|
|
79
|
+
y: number;
|
|
80
|
+
};
|
|
81
|
+
radius: number;
|
|
82
|
+
stops: GradStop[];
|
|
83
|
+
units?: GradientUnits;
|
|
84
|
+
opacity?: number;
|
|
85
|
+
} | {
|
|
86
|
+
fill: 'conic-gradient';
|
|
87
|
+
center: {
|
|
88
|
+
x: number;
|
|
89
|
+
y: number;
|
|
90
|
+
};
|
|
91
|
+
angle: number;
|
|
92
|
+
stops: GradStop[];
|
|
93
|
+
units?: GradientUnits;
|
|
94
|
+
opacity?: number;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Which coordinate space a gradient's geometry (`from`/`to`, `center`,
|
|
98
|
+
* `radius`) is expressed in. SVG's `gradientUnits`, plus an option for
|
|
99
|
+
* paints that are themselves viewport furniture.
|
|
100
|
+
*
|
|
101
|
+
* - `'bounds'`: fractions of the painted node's bounding box, `0..1` on each
|
|
102
|
+
* axis — SVG `objectBoundingBox`. Resolved by the node painter, before the
|
|
103
|
+
* renderer sees it, so the paint follows the node through moves, resizes
|
|
104
|
+
* and rotation. What a gradient on a scene node wants.
|
|
105
|
+
* - `'local'`: the coordinate space the geometry was handed to the renderer
|
|
106
|
+
* in — the enclosing group's frame. For draw commands a consumer builds
|
|
107
|
+
* itself, where "the coordinates I just wrote" is the useful frame.
|
|
108
|
+
* - `'world'`: scene coordinates. The paint stays put under pan and zoom
|
|
109
|
+
* while the geometry moves through it. SVG `userSpaceOnUse`. Requires the
|
|
110
|
+
* renderer to have been handed a view matrix; falls back to `'screen'`
|
|
111
|
+
* when it has not.
|
|
112
|
+
* - `'screen'`: CSS pixels of the drawing surface. For overlays and
|
|
113
|
+
* viewport-fixed washes that should not move with the content at all.
|
|
114
|
+
*
|
|
115
|
+
* Defaults to `'screen'`, which is the behavior every gradient had before
|
|
116
|
+
* this field existed.
|
|
117
|
+
*/
|
|
118
|
+
type GradientUnits = 'bounds' | 'local' | 'world' | 'screen';
|
|
119
|
+
/**
|
|
120
|
+
* A built-in tile, described as plain data. The serializable half of the
|
|
121
|
+
* pattern paint: `resolvePatternSpec()` turns one into a `TextureHandle`
|
|
122
|
+
* at paint time, memoized so identical specs share a texture.
|
|
123
|
+
*
|
|
124
|
+
* `size` is the tile's edge length, and doubles as its extent in paint
|
|
125
|
+
* space — a bigger `size` rasterizes a bigger tile rather than magnifying
|
|
126
|
+
* a small one, which is why there is no separate scale field.
|
|
127
|
+
*/
|
|
128
|
+
interface TilePatternSpec {
|
|
129
|
+
tile: 'hatch' | 'crosshatch' | 'dots' | 'chunks';
|
|
130
|
+
color: string;
|
|
131
|
+
/** `chunks` only — omit for a transparent tile background. */
|
|
132
|
+
bg?: string;
|
|
133
|
+
size?: number;
|
|
134
|
+
/** `hatch` / `crosshatch`. */
|
|
135
|
+
lineWidth?: number;
|
|
136
|
+
/** `dots`. */
|
|
137
|
+
radius?: number;
|
|
138
|
+
/** `chunks`. */
|
|
139
|
+
density?: number;
|
|
140
|
+
chunkSize?: number;
|
|
141
|
+
seed?: number;
|
|
142
|
+
}
|
|
143
|
+
/** A single color stop within a gradient. `offset` is in 0..1. */
|
|
144
|
+
interface GradStop {
|
|
145
|
+
offset: number;
|
|
146
|
+
color: string;
|
|
147
|
+
}
|
|
148
|
+
/** The gradient members of `FillStyle`, as one type. What a gradient editor
|
|
149
|
+
* edits, and what the gradient-specific helpers accept. */
|
|
150
|
+
type GradientFill = Extract<FillStyle, {
|
|
151
|
+
fill: 'linear-gradient' | 'radial-gradient' | 'conic-gradient';
|
|
152
|
+
}>;
|
|
153
|
+
/** `GradientFill['fill']` — the three gradient discriminants on their own. */
|
|
154
|
+
type GradientKind = GradientFill['fill'];
|
|
155
|
+
/**
|
|
156
|
+
* Where a stroke sits relative to the geometric edge it strokes.
|
|
157
|
+
*
|
|
158
|
+
* - `'center'` (default): canvas-native — half the stroke width sits inside
|
|
159
|
+
* the geometry, half outside.
|
|
160
|
+
* - `'inner'`: the entire stroke lies inside the geometry. The outer edge of
|
|
161
|
+
* the stroke coincides with the geometric edge.
|
|
162
|
+
* - `'outer'`: the entire stroke lies outside the geometry. The inner edge
|
|
163
|
+
* of the stroke coincides with the geometric edge.
|
|
164
|
+
*
|
|
165
|
+
* Mirrors the (proposed) SVG `stroke-alignment` property. Honoring `inner`
|
|
166
|
+
* or `outer` is the renderer's responsibility — for axis-aligned rects, the
|
|
167
|
+
* kit shifts coordinates by `width / 2`. For arbitrary paths, renderers
|
|
168
|
+
* typically use a stencil mask of the stroked path against the geometry.
|
|
169
|
+
*/
|
|
170
|
+
type StrokeAlign = 'center' | 'inner' | 'outer';
|
|
171
|
+
/** Stroke style: a FillStyle plus structural line parameters. */
|
|
172
|
+
interface Stroke {
|
|
173
|
+
paint: FillStyle;
|
|
174
|
+
/** World units, or `{ px }` for screen pixels — resolved against the
|
|
175
|
+
* accumulated transform scale at draw time, so it holds its on-screen
|
|
176
|
+
* thickness as the view zooms. */
|
|
177
|
+
width?: number | {
|
|
178
|
+
px: number;
|
|
179
|
+
};
|
|
180
|
+
/** Per `CanvasRenderingContext2D.setLineDash` — empty/omitted = solid. */
|
|
181
|
+
dash?: number[];
|
|
182
|
+
cap?: 'butt' | 'round' | 'square';
|
|
183
|
+
join?: 'miter' | 'round' | 'bevel';
|
|
184
|
+
/**
|
|
185
|
+
* Miter join fallback threshold. When the miter length exceeds
|
|
186
|
+
* `miterLimit * width / 2`, the join falls back to a bevel. Default 4,
|
|
187
|
+
* matching SVG — which is also what the kit's own serializer implies when
|
|
188
|
+
* it omits the attribute for an unset field. Canvas2D's 10 lets an acute
|
|
189
|
+
* corner throw a spike four times the half-width.
|
|
190
|
+
*/
|
|
191
|
+
miterLimit?: number;
|
|
192
|
+
/** Where the stroke sits relative to the geometric edge. Default `'center'`. */
|
|
193
|
+
align?: StrokeAlign;
|
|
194
|
+
/**
|
|
195
|
+
* Per-anchor RGBA, flat (length = 4 × countPathAnchors(path)). Each
|
|
196
|
+
* value in 0..1. Arc-length interpolated across the tessellated ribbon
|
|
197
|
+
* between consecutive anchors. When set, `paint` is still required —
|
|
198
|
+
* its `opacity` (and color, as a placeholder) flow through the shader.
|
|
199
|
+
*/
|
|
200
|
+
vertexColors?: number[];
|
|
201
|
+
/**
|
|
202
|
+
* Per-anchor stroke width (length = `countPathAnchors(path)`). When set,
|
|
203
|
+
* the tessellator interpolates half-widths along each segment to produce
|
|
204
|
+
* a tapered ribbon. `width` is used as the fallback for any anchor whose
|
|
205
|
+
* entry is missing or non-finite. Pressure-driven pencil strokes use
|
|
206
|
+
* this; pair with `pressureToWidth` to derive widths from stylus input.
|
|
207
|
+
*
|
|
208
|
+
* Joins between adjacent segments whose widths differ by more than
|
|
209
|
+
* `varyingWidthJoinThreshold` (default 1.5×) are forced to bevel
|
|
210
|
+
* regardless of the `join` setting — miter math is unstable when widths
|
|
211
|
+
* vary across the corner; smooth round joins with mismatched widths
|
|
212
|
+
* are a future enhancement.
|
|
213
|
+
*/
|
|
214
|
+
vertexWidths?: number[];
|
|
215
|
+
/**
|
|
216
|
+
* Max width ratio (greater / lesser) at which a non-bevel join is
|
|
217
|
+
* preserved when `vertexWidths` causes adjacent segments to differ.
|
|
218
|
+
* Beyond this ratio the join falls back to bevel. Default 1.5. Ignored
|
|
219
|
+
* when `vertexWidths` is absent.
|
|
220
|
+
*/
|
|
221
|
+
varyingWidthJoinThreshold?: number;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Inflate (positive) or deflate (negative) a rect to honor `align` when
|
|
225
|
+
* stroking it. Returns the rect to pass to a stroked-rect renderer. `width`
|
|
226
|
+
* is the stroke width (defaults to 1 to match canvas).
|
|
227
|
+
*
|
|
228
|
+
* Pure geometry helper — no rendering side effects. Used by path
|
|
229
|
+
* tessellation and the selection overlay to produce a rect whose
|
|
230
|
+
* center-aligned stroke visually coincides with the requested
|
|
231
|
+
* inner/outer-aligned stroke of the original rect.
|
|
232
|
+
*/
|
|
233
|
+
declare function alignedStrokeRect(rect: {
|
|
234
|
+
x: number;
|
|
235
|
+
y: number;
|
|
236
|
+
width: number;
|
|
237
|
+
height: number;
|
|
238
|
+
}, align: StrokeAlign, width?: number): {
|
|
239
|
+
x: number;
|
|
240
|
+
y: number;
|
|
241
|
+
width: number;
|
|
242
|
+
height: number;
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* The named line styles a `Stroke.dash` array reads as.
|
|
246
|
+
*
|
|
247
|
+
* `custom` is what an imported array that matches no preset reads as — it is
|
|
248
|
+
* reportable, not authorable: there is no array it maps back to.
|
|
249
|
+
*/
|
|
250
|
+
type StrokeDashStyle = 'solid' | 'dashed' | 'dotted' | 'custom';
|
|
251
|
+
/**
|
|
252
|
+
* Dash and gap lengths of the presets, **as multiples of the stroke width**.
|
|
253
|
+
*
|
|
254
|
+
* SVG dash lengths are absolute, so a fixed pattern is a different style at
|
|
255
|
+
* every width: `[6, 3]` is dots on a hairline and a railroad on a 20px
|
|
256
|
+
* stroke. Scaling by the width is what makes "dashed" one style.
|
|
257
|
+
*/
|
|
258
|
+
declare const STROKE_DASH_RATIOS: {
|
|
259
|
+
readonly dashed: readonly [3, 2];
|
|
260
|
+
readonly dotted: readonly [1, 2];
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* The `Stroke.dash` array for a named style at `width`, or `undefined` for
|
|
264
|
+
* `solid` — which is stored as no dash at all, not as an empty pattern.
|
|
265
|
+
*
|
|
266
|
+
* `custom` has no array of its own and returns `undefined`; a caller that
|
|
267
|
+
* offers it as a choice should refuse the choice rather than call this.
|
|
268
|
+
*/
|
|
269
|
+
declare function dashForStrokeStyle(style: StrokeDashStyle, width: number | {
|
|
270
|
+
px: number;
|
|
271
|
+
} | undefined): number[] | undefined;
|
|
272
|
+
/**
|
|
273
|
+
* The style a stored `dash` reads as at `width`. Absent or empty is `solid`;
|
|
274
|
+
* an array matching neither preset is `custom`.
|
|
275
|
+
*/
|
|
276
|
+
declare function strokeDashStyleOf(dash: readonly number[] | undefined, width: number | {
|
|
277
|
+
px: number;
|
|
278
|
+
} | undefined): StrokeDashStyle;
|
|
279
|
+
/** Region a fill is clipped to. */
|
|
280
|
+
interface Region {
|
|
281
|
+
x: number;
|
|
282
|
+
y: number;
|
|
283
|
+
w: number;
|
|
284
|
+
h: number;
|
|
285
|
+
shape: 'rectangle' | 'circle';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export { type FillStyle, type GradStop, type GradientFill, type GradientKind, type GradientUnits, type Region, STROKE_DASH_RATIOS, type Stroke, type StrokeAlign, type StrokeDashStyle, type TextureHandle, type TilePatternSpec, alignedStrokeRect, dashForStrokeStyle, strokeDashStyleOf };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/paint.ts
|
|
2
|
+
function alignedStrokeRect(rect, align, width = 1) {
|
|
3
|
+
if (align === "center") return rect;
|
|
4
|
+
const sign = align === "inner" ? -1 : 1;
|
|
5
|
+
const d = sign * width / 2;
|
|
6
|
+
return {
|
|
7
|
+
x: rect.x - d,
|
|
8
|
+
y: rect.y - d,
|
|
9
|
+
width: rect.width + 2 * d,
|
|
10
|
+
height: rect.height + 2 * d
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
var STROKE_DASH_RATIOS = {
|
|
14
|
+
dashed: [3, 2],
|
|
15
|
+
dotted: [1, 2]
|
|
16
|
+
};
|
|
17
|
+
function dashWidth(width) {
|
|
18
|
+
const w = typeof width === "object" ? width.px : width ?? 1;
|
|
19
|
+
return Number.isFinite(w) && w > 0 ? w : 1;
|
|
20
|
+
}
|
|
21
|
+
function dashForStrokeStyle(style, width) {
|
|
22
|
+
if (style !== "dashed" && style !== "dotted") return void 0;
|
|
23
|
+
const w = dashWidth(width);
|
|
24
|
+
return STROKE_DASH_RATIOS[style].map((r) => r * w);
|
|
25
|
+
}
|
|
26
|
+
function strokeDashStyleOf(dash, width) {
|
|
27
|
+
if (dash === void 0 || dash.length === 0 || dash.every((v) => v === 0)) return "solid";
|
|
28
|
+
const w = dashWidth(width);
|
|
29
|
+
for (const style of ["dashed", "dotted"]) {
|
|
30
|
+
const preset = STROKE_DASH_RATIOS[style];
|
|
31
|
+
if (dash.length === preset.length && preset.every((r, i) => Math.abs(dash[i] - r * w) <= w * 1e-3)) {
|
|
32
|
+
return style;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return "custom";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { STROKE_DASH_RATIOS, alignedStrokeRect, dashForStrokeStyle, strokeDashStyleOf };
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
40
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/paint.ts"],"names":[],"mappings":";AA6LO,SAAS,iBAAA,CACd,IAAA,EACA,KAAA,EACA,KAAA,GAAQ,CAAA,EACiD;AACzD,EAAA,IAAI,KAAA,KAAU,UAAU,OAAO,IAAA;AAG/B,EAAA,MAAM,IAAA,GAAO,KAAA,KAAU,OAAA,GAAU,EAAA,GAAK,CAAA;AACtC,EAAA,MAAM,CAAA,GAAK,OAAO,KAAA,GAAS,CAAA;AAC3B,EAAA,OAAO;AAAA,IACL,CAAA,EAAG,KAAK,CAAA,GAAI,CAAA;AAAA,IACZ,CAAA,EAAG,KAAK,CAAA,GAAI,CAAA;AAAA,IACZ,KAAA,EAAO,IAAA,CAAK,KAAA,GAAQ,CAAA,GAAI,CAAA;AAAA,IACxB,MAAA,EAAQ,IAAA,CAAK,MAAA,GAAS,CAAA,GAAI;AAAA,GAC5B;AACF;AAiBO,IAAM,kBAAA,GAAqB;AAAA,EAChC,MAAA,EAAQ,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,EACb,MAAA,EAAQ,CAAC,CAAA,EAAG,CAAC;AACf;AAIA,SAAS,UAAU,KAAA,EAAoD;AACrE,EAAA,MAAM,IAAI,OAAO,KAAA,KAAU,QAAA,GAAW,KAAA,CAAM,KAAM,KAAA,IAAS,CAAA;AAC3D,EAAA,OAAO,OAAO,QAAA,CAAS,CAAC,CAAA,IAAK,CAAA,GAAI,IAAI,CAAA,GAAI,CAAA;AAC3C;AASO,SAAS,kBAAA,CACd,OACA,KAAA,EACsB;AACtB,EAAA,IAAI,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,QAAA,EAAU,OAAO,MAAA;AACrD,EAAA,MAAM,CAAA,GAAI,UAAU,KAAK,CAAA;AACzB,EAAA,OAAO,mBAAmB,KAAK,CAAA,CAAE,IAAI,CAAC,CAAA,KAAM,IAAI,CAAC,CAAA;AACnD;AAMO,SAAS,iBAAA,CACd,MACA,KAAA,EACiB;AACjB,EAAA,IAAI,IAAA,KAAS,MAAA,IAAa,IAAA,CAAK,MAAA,KAAW,CAAA,IAAK,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,KAAM,CAAA,KAAM,CAAC,CAAA,EAAG,OAAO,OAAA;AAClF,EAAA,MAAM,CAAA,GAAI,UAAU,KAAK,CAAA;AACzB,EAAA,KAAA,MAAW,KAAA,IAAS,CAAC,QAAA,EAAU,QAAQ,CAAA,EAAY;AACjD,IAAA,MAAM,MAAA,GAAS,mBAAmB,KAAK,CAAA;AAGvC,IAAA,IAAI,KAAK,MAAA,KAAW,MAAA,CAAO,UAAU,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA,EAAG,CAAA,KAAM,KAAK,GAAA,CAAI,IAAA,CAAK,CAAC,CAAA,GAAI,CAAA,GAAI,CAAC,CAAA,IAAK,CAAA,GAAI,IAAI,CAAA,EAAG;AAClG,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,OAAO,QAAA;AACT","file":"index.js","sourcesContent":["/**\n * FillStyle and Stroke types — the unified shape for \"what color or texture\n * paints these pixels,\" modeled on SVG's paint-server concept.\n *\n * - `FillStyle` is a tagged union: solid color, pattern, or gradient. Used\n * wherever a kit option previously took `fillStyle: string`.\n * - `Stroke` pairs a `FillStyle` with structural stroke parameters (width, dash,\n * line cap/join, alignment).\n * - These types are consumed by the GL renderer's DrawCommand path fills\n * and strokes.\n *\n * The 2D `applyPaint` / `applyStroke` / `renderFilledRegion` helpers that\n * formerly lived alongside these types were deleted with the 2D backend in\n * Step 10. `alignedStrokeRect` survives as a pure geometry helper used by\n * path tessellation and the selection overlay.\n */\n\nimport type { TextureHandle } from './texture';\n\n/**\n * Color/texture strategy for fills (and, via `Stroke.paint`, strokes).\n *\n * `fill` is optional and defaults to `'solid'` — `{ color: '#abc' }` is\n * equivalent to `{ fill: 'solid', color: '#abc' }`. Pattern paints must set\n * `fill: 'pattern'` explicitly.\n *\n * The `'pattern'` variant's payload is either a `TilePatternSpec` — plain\n * data naming one of the built-in tiles, which survives serialization — or a\n * `TextureHandle` for a tile the consumer built itself via\n * `createTilePattern()`. A handle is a session-scoped registry key, so a\n * paint carrying one cannot be persisted or exported; prefer the spec.\n *\n * `units` on a pattern names the space the tile's origin and scale live in,\n * not the space of any geometry (a pattern has none). Under `'bounds'` the\n * tile anchors to the painted node's box, so the pattern travels with the\n * node and a resize reveals more tiles rather than stretching them;\n * `fillInPoseFrame` resolves that to a `'local'` paint with an explicit\n * `origin` before the renderer sees it.\n */\nexport type FillStyle =\n | { fill?: 'solid'; color: string; opacity?: number }\n | { fill: 'pattern'; pattern: TextureHandle | TilePatternSpec; units?: GradientUnits; origin?: { x: number; y: number }; opacity?: number }\n | { fill: 'linear-gradient'; from: { x: number; y: number }; to: { x: number; y: number }; stops: GradStop[]; units?: GradientUnits; opacity?: number }\n | { fill: 'radial-gradient'; center: { x: number; y: number }; radius: number; stops: GradStop[]; units?: GradientUnits; opacity?: number }\n | { fill: 'conic-gradient'; center: { x: number; y: number }; angle: number; stops: GradStop[]; units?: GradientUnits; opacity?: number };\n\n/**\n * Which coordinate space a gradient's geometry (`from`/`to`, `center`,\n * `radius`) is expressed in. SVG's `gradientUnits`, plus an option for\n * paints that are themselves viewport furniture.\n *\n * - `'bounds'`: fractions of the painted node's bounding box, `0..1` on each\n * axis — SVG `objectBoundingBox`. Resolved by the node painter, before the\n * renderer sees it, so the paint follows the node through moves, resizes\n * and rotation. What a gradient on a scene node wants.\n * - `'local'`: the coordinate space the geometry was handed to the renderer\n * in — the enclosing group's frame. For draw commands a consumer builds\n * itself, where \"the coordinates I just wrote\" is the useful frame.\n * - `'world'`: scene coordinates. The paint stays put under pan and zoom\n * while the geometry moves through it. SVG `userSpaceOnUse`. Requires the\n * renderer to have been handed a view matrix; falls back to `'screen'`\n * when it has not.\n * - `'screen'`: CSS pixels of the drawing surface. For overlays and\n * viewport-fixed washes that should not move with the content at all.\n *\n * Defaults to `'screen'`, which is the behavior every gradient had before\n * this field existed.\n */\nexport type GradientUnits = 'bounds' | 'local' | 'world' | 'screen';\n\n/**\n * A built-in tile, described as plain data. The serializable half of the\n * pattern paint: `resolvePatternSpec()` turns one into a `TextureHandle`\n * at paint time, memoized so identical specs share a texture.\n *\n * `size` is the tile's edge length, and doubles as its extent in paint\n * space — a bigger `size` rasterizes a bigger tile rather than magnifying\n * a small one, which is why there is no separate scale field.\n */\nexport interface TilePatternSpec {\n tile: 'hatch' | 'crosshatch' | 'dots' | 'chunks';\n color: string;\n /** `chunks` only — omit for a transparent tile background. */\n bg?: string;\n size?: number;\n /** `hatch` / `crosshatch`. */\n lineWidth?: number;\n /** `dots`. */\n radius?: number;\n /** `chunks`. */\n density?: number;\n chunkSize?: number;\n seed?: number;\n}\n\n/** A single color stop within a gradient. `offset` is in 0..1. */\nexport interface GradStop {\n offset: number;\n color: string;\n}\n\n/** The gradient members of `FillStyle`, as one type. What a gradient editor\n * edits, and what the gradient-specific helpers accept. */\nexport type GradientFill = Extract<\n FillStyle,\n { fill: 'linear-gradient' | 'radial-gradient' | 'conic-gradient' }\n>;\n\n/** `GradientFill['fill']` — the three gradient discriminants on their own. */\nexport type GradientKind = GradientFill['fill'];\n\n/**\n * Where a stroke sits relative to the geometric edge it strokes.\n *\n * - `'center'` (default): canvas-native — half the stroke width sits inside\n * the geometry, half outside.\n * - `'inner'`: the entire stroke lies inside the geometry. The outer edge of\n * the stroke coincides with the geometric edge.\n * - `'outer'`: the entire stroke lies outside the geometry. The inner edge\n * of the stroke coincides with the geometric edge.\n *\n * Mirrors the (proposed) SVG `stroke-alignment` property. Honoring `inner`\n * or `outer` is the renderer's responsibility — for axis-aligned rects, the\n * kit shifts coordinates by `width / 2`. For arbitrary paths, renderers\n * typically use a stencil mask of the stroked path against the geometry.\n */\nexport type StrokeAlign = 'center' | 'inner' | 'outer';\n\n/** Stroke style: a FillStyle plus structural line parameters. */\nexport interface Stroke {\n paint: FillStyle;\n /** World units, or `{ px }` for screen pixels — resolved against the\n * accumulated transform scale at draw time, so it holds its on-screen\n * thickness as the view zooms. */\n width?: number | { px: number };\n /** Per `CanvasRenderingContext2D.setLineDash` — empty/omitted = solid. */\n dash?: number[];\n cap?: 'butt' | 'round' | 'square';\n join?: 'miter' | 'round' | 'bevel';\n /**\n * Miter join fallback threshold. When the miter length exceeds\n * `miterLimit * width / 2`, the join falls back to a bevel. Default 4,\n * matching SVG — which is also what the kit's own serializer implies when\n * it omits the attribute for an unset field. Canvas2D's 10 lets an acute\n * corner throw a spike four times the half-width.\n */\n miterLimit?: number;\n /** Where the stroke sits relative to the geometric edge. Default `'center'`. */\n align?: StrokeAlign;\n /**\n * Per-anchor RGBA, flat (length = 4 × countPathAnchors(path)). Each\n * value in 0..1. Arc-length interpolated across the tessellated ribbon\n * between consecutive anchors. When set, `paint` is still required —\n * its `opacity` (and color, as a placeholder) flow through the shader.\n */\n vertexColors?: number[];\n /**\n * Per-anchor stroke width (length = `countPathAnchors(path)`). When set,\n * the tessellator interpolates half-widths along each segment to produce\n * a tapered ribbon. `width` is used as the fallback for any anchor whose\n * entry is missing or non-finite. Pressure-driven pencil strokes use\n * this; pair with `pressureToWidth` to derive widths from stylus input.\n *\n * Joins between adjacent segments whose widths differ by more than\n * `varyingWidthJoinThreshold` (default 1.5×) are forced to bevel\n * regardless of the `join` setting — miter math is unstable when widths\n * vary across the corner; smooth round joins with mismatched widths\n * are a future enhancement.\n */\n vertexWidths?: number[];\n /**\n * Max width ratio (greater / lesser) at which a non-bevel join is\n * preserved when `vertexWidths` causes adjacent segments to differ.\n * Beyond this ratio the join falls back to bevel. Default 1.5. Ignored\n * when `vertexWidths` is absent.\n */\n varyingWidthJoinThreshold?: number;\n}\n\n/**\n * Inflate (positive) or deflate (negative) a rect to honor `align` when\n * stroking it. Returns the rect to pass to a stroked-rect renderer. `width`\n * is the stroke width (defaults to 1 to match canvas).\n *\n * Pure geometry helper — no rendering side effects. Used by path\n * tessellation and the selection overlay to produce a rect whose\n * center-aligned stroke visually coincides with the requested\n * inner/outer-aligned stroke of the original rect.\n */\nexport function alignedStrokeRect(\n rect: { x: number; y: number; width: number; height: number },\n align: StrokeAlign,\n width = 1,\n): { x: number; y: number; width: number; height: number } {\n if (align === 'center') return rect;\n // For 'inner', shift inward by width/2 so the stroke's outer edge coincides\n // with the geometric edge. For 'outer', shift outward.\n const sign = align === 'inner' ? -1 : 1;\n const d = (sign * width) / 2;\n return {\n x: rect.x - d,\n y: rect.y - d,\n width: rect.width + 2 * d,\n height: rect.height + 2 * d,\n };\n}\n\n/**\n * The named line styles a `Stroke.dash` array reads as.\n *\n * `custom` is what an imported array that matches no preset reads as — it is\n * reportable, not authorable: there is no array it maps back to.\n */\nexport type StrokeDashStyle = 'solid' | 'dashed' | 'dotted' | 'custom';\n\n/**\n * Dash and gap lengths of the presets, **as multiples of the stroke width**.\n *\n * SVG dash lengths are absolute, so a fixed pattern is a different style at\n * every width: `[6, 3]` is dots on a hairline and a railroad on a 20px\n * stroke. Scaling by the width is what makes \"dashed\" one style.\n */\nexport const STROKE_DASH_RATIOS = {\n dashed: [3, 2],\n dotted: [1, 2],\n} as const satisfies Record<'dashed' | 'dotted', readonly [number, number]>;\n\n/** `width` as a plain number — a `{ px }` width is read at scale 1, matching\n * what an unresolved stroke reaching the tessellator gets. */\nfunction dashWidth(width: number | { px: number } | undefined): number {\n const w = typeof width === 'object' ? width.px : (width ?? 1);\n return Number.isFinite(w) && w > 0 ? w : 1;\n}\n\n/**\n * The `Stroke.dash` array for a named style at `width`, or `undefined` for\n * `solid` — which is stored as no dash at all, not as an empty pattern.\n *\n * `custom` has no array of its own and returns `undefined`; a caller that\n * offers it as a choice should refuse the choice rather than call this.\n */\nexport function dashForStrokeStyle(\n style: StrokeDashStyle,\n width: number | { px: number } | undefined,\n): number[] | undefined {\n if (style !== 'dashed' && style !== 'dotted') return undefined;\n const w = dashWidth(width);\n return STROKE_DASH_RATIOS[style].map((r) => r * w);\n}\n\n/**\n * The style a stored `dash` reads as at `width`. Absent or empty is `solid`;\n * an array matching neither preset is `custom`.\n */\nexport function strokeDashStyleOf(\n dash: readonly number[] | undefined,\n width: number | { px: number } | undefined,\n): StrokeDashStyle {\n if (dash === undefined || dash.length === 0 || dash.every((v) => v === 0)) return 'solid';\n const w = dashWidth(width);\n for (const style of ['dashed', 'dotted'] as const) {\n const preset = STROKE_DASH_RATIOS[style];\n // Tolerance is relative to the width: the presets are multiples of it, and\n // a round-tripped array carries the serializer's decimal trimming.\n if (dash.length === preset.length && preset.every((r, i) => Math.abs(dash[i] - r * w) <= w * 1e-3)) {\n return style;\n }\n }\n return 'custom';\n}\n\n/** Region a fill is clipped to. */\nexport interface Region {\n x: number;\n y: number;\n w: number;\n h: number;\n shape: 'rectangle' | 'circle';\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weasel-js/paint",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Paint vocabulary for weasel: FillStyle, Stroke, gradients, dashes. Plain data, no renderer.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"build": "tsup"
|
|
21
|
+
},
|
|
22
|
+
"author": "orochi235",
|
|
23
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
27
|
+
"directory": "packages/paint"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true
|
|
43
|
+
}
|
|
44
|
+
}
|