@fulate/share 1.0.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/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # `@fulate/share`
2
+
3
+ > 文档角色:`GUIDE`。权威来源:share 源码导出与测试;本包只提供低层纯工具,不拥有场景、领域或 Editor 状态。
4
+
5
+ `@fulate/share` 是 Fulate 最底层的无场景工具包,提供点、矩形、矩阵、颜色、渐变和相交计算。
6
+
7
+ - 点:`PointView`、`MutablePointView`、`createPointView()`、`transformPoint()`、距离判断;
8
+ - 矩形:`Rect`、`BoundingBox`、可变 `Bound` 及 points/rects 边界计算;
9
+ - 矩阵和数学:`qrDecompose()`、角度/弧度换算、矩形边缘位置;
10
+ - 几何:`Intersection`、`clipPolygonToRect()`;
11
+ - 颜色和渐变:颜色解析/格式化、alpha 合成、混色和 Canvas gradient 创建。
12
+
13
+ ```ts
14
+ import {
15
+ Bound,
16
+ createPointView,
17
+ transformPoint,
18
+ colorWithAlpha
19
+ } from "@fulate/share";
20
+
21
+ const point = transformPoint(createPointView(10, 20), matrix);
22
+ const bounds = Bound.fromPoints([point]);
23
+ const overlay = colorWithAlpha("#6750a4", 0.12);
24
+ ```
25
+
26
+ 函数直接消费声明类型,不为 Core/UI 建立 DTO、registry 或缓存。返回可变 `Bound` 的方法按普通对象
27
+ 语义工作;调用方需要独立结果时使用明确的 `copy()` 或在自身 owner 内创建新值。
@@ -0,0 +1,32 @@
1
+ import type { PointView } from "./point";
2
+ import type { BoundingBox, Rect } from "./rect";
3
+ /** 将凸多边形裁剪到轴对齐矩形,并返回交集轮廓。 */
4
+ export declare function clipPolygonToRect(subject: readonly PointView[], rect: Readonly<Rect>): PointView[];
5
+ type IntersectionStatus = "Intersection" | "Coincident" | "Parallel";
6
+ export declare class Intersection {
7
+ private points;
8
+ status?: IntersectionStatus;
9
+ private constructor();
10
+ private includes;
11
+ private append;
12
+ private static isPointOnSegment;
13
+ /** Return whether a point lies inside or on the boundary of an AABB. */
14
+ static isPointInRect(point: PointView, rect: Readonly<Rect> | Readonly<BoundingBox>): boolean;
15
+ /** 判断两个 AABB 是否相交;边界接触也算相交。 */
16
+ static isAABBIntersect(first: Readonly<Rect> | Readonly<BoundingBox>, second: Readonly<Rect> | Readonly<BoundingBox>): boolean;
17
+ static isPointInPolygon(point: PointView, points: readonly PointView[]): boolean;
18
+ private static intersectSegments;
19
+ private static intersectSegmentPolygon;
20
+ static isContainedInRect(rect: {
21
+ left: number;
22
+ top: number;
23
+ width: number;
24
+ height: number;
25
+ }, topLeft: PointView, bottomRight: PointView): boolean;
26
+ private static intersectPolygonPolygon;
27
+ static intersectPolygonRectangle(points: readonly PointView[], r1: PointView, r2: PointView): Intersection;
28
+ static pointToLineSegmentDistance(point: PointView, start: PointView, end: PointView): number;
29
+ static isPointInCircle(point: PointView, center: PointView, radius: number): boolean;
30
+ static isPointInRoundRect(point: PointView, width: number, height: number, radius: number): boolean;
31
+ }
32
+ export {};
@@ -0,0 +1,13 @@
1
+ export declare function parseColor(val: string): import("colord").RgbaColor;
2
+ export declare function formatColor(rgba: {
3
+ r: number;
4
+ g: number;
5
+ b: number;
6
+ a: number;
7
+ }): string;
8
+ type RgbaColor = ReturnType<typeof parseColor>;
9
+ export declare function parsePremultipliedColor(val: string): RgbaColor;
10
+ export declare function formatPremultipliedColor({ r, g, b, a }: RgbaColor): string;
11
+ export declare function blendColor(base: string, overlay: string, ratio: number): string;
12
+ export declare function colorWithAlpha(color: string, alpha: number): string;
13
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { MutablePointView } from "./point";
2
+ export type GradientType = "linear" | "radial";
3
+ export interface GradientStop {
4
+ color: string;
5
+ position: number;
6
+ }
7
+ export interface GradientOption {
8
+ type: GradientType;
9
+ stops: GradientStop[];
10
+ from?: MutablePointView;
11
+ to?: MutablePointView;
12
+ angle?: number;
13
+ center?: MutablePointView;
14
+ radius?: number;
15
+ }
16
+ export type BackgroundColor = string | GradientOption;
17
+ export declare function isGradient(bg: BackgroundColor): bg is GradientOption;
18
+ export declare function createCanvasGradient(ctx: CanvasRenderingContext2D, gradient: GradientOption, width: number, height: number): CanvasGradient;
@@ -0,0 +1,11 @@
1
+ export { transformPoint, createPointView, isPointWithinDistance } from "./point";
2
+ export type { MutablePointView, PointView } from "./point";
3
+ export { Bound, makeBoundingBoxFromPoints, makeBoundsFromPoints, makeBoundingBoxFromRects } from "./rect";
4
+ export type { Rect, RectWithCenter, BoundingBox } from "./rect";
5
+ export { qrDecompose } from "./matrix";
6
+ export { degreesToRadians, radiansToDegrees, rectEdgePosition } from "./math";
7
+ export type { Edge } from "./math";
8
+ export { Intersection, clipPolygonToRect } from "./Intersection";
9
+ export { parseColor, formatColor, parsePremultipliedColor, formatPremultipliedColor, blendColor, colorWithAlpha } from "./color";
10
+ export { isGradient, createCanvasGradient } from "./gradient";
11
+ export type { GradientType, GradientStop, GradientOption, BackgroundColor } from "./gradient";
package/dist/index.js ADDED
@@ -0,0 +1,465 @@
1
+ import { colord } from "colord";
2
+ import { isString, isUndefined } from "lodash-es";
3
+ //#region packages/share/src/point.ts
4
+ function createPointView(x, y) {
5
+ return {
6
+ x,
7
+ y
8
+ };
9
+ }
10
+ function isPointWithinDistance(point, target, threshold) {
11
+ const dx = point.x - target.x;
12
+ const dy = point.y - target.y;
13
+ return dx * dx + dy * dy <= threshold ** 2;
14
+ }
15
+ function transformPoint(m, point, out) {
16
+ const x = m.a * point.x + m.c * point.y + m.e;
17
+ const y = m.b * point.x + m.d * point.y + m.f;
18
+ if (out) {
19
+ out.x = x;
20
+ out.y = y;
21
+ return out;
22
+ }
23
+ return createPointView(x, y);
24
+ }
25
+ //#endregion
26
+ //#region packages/share/src/rect.ts
27
+ function pointBounds(points) {
28
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
29
+ for (const { x, y } of points) {
30
+ if (x < minX) minX = x;
31
+ if (y < minY) minY = y;
32
+ if (x > maxX) maxX = x;
33
+ if (y > maxY) maxY = y;
34
+ }
35
+ return {
36
+ minX,
37
+ minY,
38
+ maxX,
39
+ maxY
40
+ };
41
+ }
42
+ function makeBoundingBoxFromPoints(points) {
43
+ const { minX, minY, maxX, maxY } = pointBounds(points);
44
+ return {
45
+ left: minX,
46
+ top: minY,
47
+ width: maxX - minX,
48
+ height: maxY - minY
49
+ };
50
+ }
51
+ var Bound = class Bound {
52
+ minX;
53
+ minY;
54
+ maxX;
55
+ maxY;
56
+ constructor(minX = 0, minY = 0, maxX = 0, maxY = 0) {
57
+ this.minX = minX;
58
+ this.minY = minY;
59
+ this.maxX = maxX;
60
+ this.maxY = maxY;
61
+ }
62
+ get left() {
63
+ return this.minX;
64
+ }
65
+ get top() {
66
+ return this.minY;
67
+ }
68
+ get width() {
69
+ return this.maxX - this.minX;
70
+ }
71
+ get height() {
72
+ return this.maxY - this.minY;
73
+ }
74
+ get centerX() {
75
+ return (this.minX + this.maxX) / 2;
76
+ }
77
+ get centerY() {
78
+ return (this.minY + this.maxY) / 2;
79
+ }
80
+ merge(other) {
81
+ this.minX = Math.min(this.minX, other.minX);
82
+ this.minY = Math.min(this.minY, other.minY);
83
+ this.maxX = Math.max(this.maxX, other.maxX);
84
+ this.maxY = Math.max(this.maxY, other.maxY);
85
+ return this;
86
+ }
87
+ copy() {
88
+ return new Bound(this.minX, this.minY, this.maxX, this.maxY);
89
+ }
90
+ expand(padding, minSize = 0) {
91
+ this.minX -= padding;
92
+ this.minY -= padding;
93
+ this.maxX += padding;
94
+ this.maxY += padding;
95
+ if (minSize > 0) {
96
+ if (this.maxX - this.minX < minSize) {
97
+ const cx = (this.minX + this.maxX) / 2;
98
+ this.minX = cx - minSize / 2;
99
+ this.maxX = cx + minSize / 2;
100
+ }
101
+ if (this.maxY - this.minY < minSize) {
102
+ const cy = (this.minY + this.maxY) / 2;
103
+ this.minY = cy - minSize / 2;
104
+ this.maxY = cy + minSize / 2;
105
+ }
106
+ }
107
+ return this;
108
+ }
109
+ includePoint(x, y, padding = 0) {
110
+ this.minX = Math.min(this.minX, x - padding);
111
+ this.minY = Math.min(this.minY, y - padding);
112
+ this.maxX = Math.max(this.maxX, x + padding);
113
+ this.maxY = Math.max(this.maxY, y + padding);
114
+ return this;
115
+ }
116
+ static fromPoints(points) {
117
+ const { minX, minY, maxX, maxY } = pointBounds(points);
118
+ return new Bound(minX, minY, maxX, maxY);
119
+ }
120
+ };
121
+ function makeBoundsFromPoints(points) {
122
+ return pointBounds(points);
123
+ }
124
+ function makeBoundingBoxFromRects(rects) {
125
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
126
+ for (let i = 0; i < rects.length; i++) {
127
+ const r = rects[i];
128
+ if (r.left < minX) minX = r.left;
129
+ if (r.top < minY) minY = r.top;
130
+ if (r.left + r.width > maxX) maxX = r.left + r.width;
131
+ if (r.top + r.height > maxY) maxY = r.top + r.height;
132
+ }
133
+ return {
134
+ left: minX,
135
+ top: minY,
136
+ width: maxX - minX,
137
+ height: maxY - minY
138
+ };
139
+ }
140
+ //#endregion
141
+ //#region packages/share/src/math.ts
142
+ var PiBy180 = Math.PI / 180;
143
+ var degreesToRadians = (degrees) => degrees * PiBy180;
144
+ var radiansToDegrees = (radians) => radians / PiBy180;
145
+ /**
146
+ * 矩形边上按 ratio 取点,同时返回朝外法线。
147
+ * ratio ∈ [0, 1],沿边的起点到终点方向插值。
148
+ */
149
+ function rectEdgePosition(w, h, edge, ratio) {
150
+ switch (edge) {
151
+ case "top": return {
152
+ x: w * ratio,
153
+ y: 0,
154
+ nx: 0,
155
+ ny: -1
156
+ };
157
+ case "right": return {
158
+ x: w,
159
+ y: h * ratio,
160
+ nx: 1,
161
+ ny: 0
162
+ };
163
+ case "bottom": return {
164
+ x: w * ratio,
165
+ y: h,
166
+ nx: 0,
167
+ ny: 1
168
+ };
169
+ case "left": return {
170
+ x: 0,
171
+ y: h * ratio,
172
+ nx: -1,
173
+ ny: 0
174
+ };
175
+ }
176
+ }
177
+ //#endregion
178
+ //#region packages/share/src/matrix.ts
179
+ function qrDecompose(matrix) {
180
+ const { a, b, c, d } = matrix;
181
+ const angleRad = Math.atan2(b, a);
182
+ const cos = Math.cos(-angleRad);
183
+ const sin = Math.sin(-angleRad);
184
+ const a1 = a * cos - b * sin;
185
+ const c1 = c * cos - d * sin;
186
+ const d1 = c * sin + d * cos;
187
+ return {
188
+ angle: radiansToDegrees(angleRad),
189
+ scaleX: a1,
190
+ scaleY: d1,
191
+ skewX: radiansToDegrees(Math.atan2(c1, d1))
192
+ };
193
+ }
194
+ //#endregion
195
+ //#region packages/share/src/Intersection.ts
196
+ /** 将凸多边形裁剪到轴对齐矩形,并返回交集轮廓。 */
197
+ function clipPolygonToRect(subject, rect) {
198
+ const boundaries = [
199
+ [
200
+ "x",
201
+ rect.left,
202
+ 1
203
+ ],
204
+ [
205
+ "x",
206
+ rect.left + rect.width,
207
+ -1
208
+ ],
209
+ [
210
+ "y",
211
+ rect.top,
212
+ 1
213
+ ],
214
+ [
215
+ "y",
216
+ rect.top + rect.height,
217
+ -1
218
+ ]
219
+ ];
220
+ let output = [...subject];
221
+ for (const [axis, edge, direction] of boundaries) {
222
+ const input = output;
223
+ output = [];
224
+ if (input.length === 0) break;
225
+ let previous = input[input.length - 1];
226
+ let previousDistance = direction * (previous[axis] - edge);
227
+ let previousInside = previousDistance >= 0;
228
+ for (const current of input) {
229
+ const currentDistance = direction * (current[axis] - edge);
230
+ const currentInside = currentDistance >= 0;
231
+ if (currentInside !== previousInside) {
232
+ const ratio = previousDistance / (previousDistance - currentDistance);
233
+ output.push({
234
+ x: previous.x + (current.x - previous.x) * ratio,
235
+ y: previous.y + (current.y - previous.y) * ratio
236
+ });
237
+ }
238
+ if (currentInside) output.push(current);
239
+ previous = current;
240
+ previousDistance = currentDistance;
241
+ previousInside = currentInside;
242
+ }
243
+ }
244
+ return output;
245
+ }
246
+ var Intersection = class Intersection {
247
+ points = [];
248
+ status;
249
+ constructor(status) {
250
+ this.status = status;
251
+ }
252
+ includes(point) {
253
+ return this.points.some((item) => item.x === point.x && item.y === point.y);
254
+ }
255
+ append(...points) {
256
+ this.points.push(...points.filter((point) => !this.includes(point)));
257
+ return this;
258
+ }
259
+ static isPointOnSegment(point, start, end, tolerance = 0) {
260
+ const dx = end.x - start.x;
261
+ const dy = end.y - start.y;
262
+ const cross = (point.x - start.x) * dy - (point.y - start.y) * dx;
263
+ if (Math.abs(cross) > tolerance) return false;
264
+ return point.x >= Math.min(start.x, end.x) - tolerance && point.x <= Math.max(start.x, end.x) + tolerance && point.y >= Math.min(start.y, end.y) - tolerance && point.y <= Math.max(start.y, end.y) + tolerance;
265
+ }
266
+ /** Return whether a point lies inside or on the boundary of an AABB. */
267
+ static isPointInRect(point, rect) {
268
+ const minX = "minX" in rect ? rect.minX : rect.left;
269
+ const minY = "minY" in rect ? rect.minY : rect.top;
270
+ const maxX = "maxX" in rect ? rect.maxX : rect.left + rect.width;
271
+ const maxY = "maxY" in rect ? rect.maxY : rect.top + rect.height;
272
+ return point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY;
273
+ }
274
+ /** 判断两个 AABB 是否相交;边界接触也算相交。 */
275
+ static isAABBIntersect(first, second) {
276
+ const firstMinX = "minX" in first ? first.minX : first.left;
277
+ const firstMinY = "minY" in first ? first.minY : first.top;
278
+ const firstMaxX = "maxX" in first ? first.maxX : first.left + first.width;
279
+ const firstMaxY = "maxY" in first ? first.maxY : first.top + first.height;
280
+ const secondMinX = "minX" in second ? second.minX : second.left;
281
+ const secondMinY = "minY" in second ? second.minY : second.top;
282
+ const secondMaxX = "maxX" in second ? second.maxX : second.left + second.width;
283
+ const secondMaxY = "maxY" in second ? second.maxY : second.top + second.height;
284
+ return firstMinX <= secondMaxX && firstMaxX >= secondMinX && firstMinY <= secondMaxY && firstMaxY >= secondMinY;
285
+ }
286
+ static isPointInPolygon(point, points) {
287
+ if (points.length < 3) return false;
288
+ let inside = false;
289
+ for (let index = 0, previous = points.length - 1; index < points.length; previous = index++) {
290
+ const current = points[index];
291
+ const prior = points[previous];
292
+ if (this.isPointOnSegment(point, prior, current, 1e-9)) return true;
293
+ if (current.y > point.y !== prior.y > point.y && point.x < (prior.x - current.x) * (point.y - current.y) / (prior.y - current.y) + current.x) inside = !inside;
294
+ }
295
+ return inside;
296
+ }
297
+ static intersectSegments(a1, a2, b1, b2) {
298
+ const a2xa1x = a2.x - a1.x;
299
+ const a2ya1y = a2.y - a1.y;
300
+ const b2xb1x = b2.x - b1.x;
301
+ const b2yb1y = b2.y - b1.y;
302
+ const a1xb1x = a1.x - b1.x;
303
+ const a1yb1y = a1.y - b1.y;
304
+ const uaT = b2xb1x * a1yb1y - b2yb1y * a1xb1x;
305
+ const ubT = a2xa1x * a1yb1y - a2ya1y * a1xb1x;
306
+ const uB = b2yb1y * a2xa1x - b2xb1x * a2ya1y;
307
+ if (uB !== 0) {
308
+ const ua = uaT / uB;
309
+ const ub = ubT / uB;
310
+ if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) return new Intersection("Intersection").append({
311
+ x: a1.x + ua * a2xa1x,
312
+ y: a1.y + ua * a2ya1y
313
+ });
314
+ return new Intersection();
315
+ }
316
+ if (uaT === 0 || ubT === 0) return new Intersection(this.isPointOnSegment(a1, b1, b2) || this.isPointOnSegment(a2, b1, b2) || this.isPointOnSegment(b1, a1, a2) || this.isPointOnSegment(b2, a1, a2) ? "Coincident" : void 0);
317
+ return new Intersection("Parallel");
318
+ }
319
+ static intersectSegmentPolygon(start, end, points) {
320
+ const result = new Intersection();
321
+ for (let index = 0; index < points.length; index++) {
322
+ const intersection = this.intersectSegments(start, end, points[index], points[(index + 1) % points.length]);
323
+ if (intersection.status === "Coincident") return intersection;
324
+ result.append(...intersection.points);
325
+ }
326
+ if (result.points.length) result.status = "Intersection";
327
+ return result;
328
+ }
329
+ static isContainedInRect(rect, topLeft, bottomRight) {
330
+ return rect.left >= topLeft.x && rect.left + rect.width <= bottomRight.x && rect.top >= topLeft.y && rect.top + rect.height <= bottomRight.y;
331
+ }
332
+ static intersectPolygonPolygon(points1, points2) {
333
+ const result = new Intersection();
334
+ let coincidentEdges = 0;
335
+ for (let index = 0; index < points1.length; index++) {
336
+ const start = points1[index];
337
+ const end = points1[(index + 1) % points1.length];
338
+ const intersection = this.intersectSegmentPolygon(start, end, points2);
339
+ if (intersection.status === "Coincident") {
340
+ coincidentEdges++;
341
+ result.append(start, end);
342
+ } else result.append(...intersection.points);
343
+ }
344
+ if (coincidentEdges && coincidentEdges === points1.length) return new Intersection("Coincident");
345
+ if (result.points.length) result.status = "Intersection";
346
+ return result;
347
+ }
348
+ static intersectPolygonRectangle(points, r1, r2) {
349
+ const minX = Math.min(r1.x, r2.x);
350
+ const minY = Math.min(r1.y, r2.y);
351
+ const maxX = Math.max(r1.x, r2.x);
352
+ const maxY = Math.max(r1.y, r2.y);
353
+ return this.intersectPolygonPolygon(points, [
354
+ {
355
+ x: minX,
356
+ y: minY
357
+ },
358
+ {
359
+ x: maxX,
360
+ y: minY
361
+ },
362
+ {
363
+ x: maxX,
364
+ y: maxY
365
+ },
366
+ {
367
+ x: minX,
368
+ y: maxY
369
+ }
370
+ ]);
371
+ }
372
+ static pointToLineSegmentDistance(point, start, end) {
373
+ const segmentX = end.x - start.x;
374
+ const segmentY = end.y - start.y;
375
+ const lengthSquared = segmentX * segmentX + segmentY * segmentY;
376
+ const ratio = lengthSquared ? ((point.x - start.x) * segmentX + (point.y - start.y) * segmentY) / lengthSquared : 0;
377
+ const nearestX = ratio <= 0 ? start.x : ratio >= 1 ? end.x : start.x + ratio * segmentX;
378
+ const nearestY = ratio <= 0 ? start.y : ratio >= 1 ? end.y : start.y + ratio * segmentY;
379
+ return Math.hypot(point.x - nearestX, point.y - nearestY);
380
+ }
381
+ static isPointInCircle(point, center, radius) {
382
+ const dx = point.x - center.x;
383
+ const dy = point.y - center.y;
384
+ return dx * dx + dy * dy <= radius * radius;
385
+ }
386
+ static isPointInRoundRect(point, width, height, radius) {
387
+ const x = Math.abs(point.x - width / 2);
388
+ const y = Math.abs(point.y - height / 2);
389
+ if (x > width / 2 || y > height / 2) return false;
390
+ if (x <= width / 2 - radius || y <= height / 2 - radius) return true;
391
+ const dx = x - (width / 2 - radius);
392
+ const dy = y - (height / 2 - radius);
393
+ return dx * dx + dy * dy <= radius * radius;
394
+ }
395
+ };
396
+ //#endregion
397
+ //#region packages/share/src/color.ts
398
+ function resolve(val) {
399
+ return !val || val === "transparent" ? "rgba(0,0,0,0)" : val;
400
+ }
401
+ function parseColor(val) {
402
+ return colord(resolve(val)).toRgb();
403
+ }
404
+ function formatColor(rgba) {
405
+ return colord(rgba).toRgbString();
406
+ }
407
+ function parsePremultipliedColor(val) {
408
+ const { r, g, b, a } = parseColor(val);
409
+ return {
410
+ r: r * a,
411
+ g: g * a,
412
+ b: b * a,
413
+ a
414
+ };
415
+ }
416
+ function formatPremultipliedColor({ r, g, b, a }) {
417
+ if (a === 0) return formatColor({
418
+ r: 0,
419
+ g: 0,
420
+ b: 0,
421
+ a
422
+ });
423
+ return formatColor({
424
+ r: r / a,
425
+ g: g / a,
426
+ b: b / a,
427
+ a
428
+ });
429
+ }
430
+ function blendColor(base, overlay, ratio) {
431
+ const b = colord(resolve(base)).toRgb();
432
+ const o = colord(resolve(overlay)).toRgb();
433
+ return colord({
434
+ r: b.r * (1 - ratio) + o.r * ratio,
435
+ g: b.g * (1 - ratio) + o.g * ratio,
436
+ b: b.b * (1 - ratio) + o.b * ratio,
437
+ a: b.a * (1 - ratio) + o.a * ratio
438
+ }).toRgbString();
439
+ }
440
+ function colorWithAlpha(color, alpha) {
441
+ return colord(resolve(color)).alpha(alpha).toRgbString();
442
+ }
443
+ //#endregion
444
+ //#region packages/share/src/gradient.ts
445
+ function isGradient(bg) {
446
+ return !isString(bg);
447
+ }
448
+ function createCanvasGradient(ctx, gradient, width, height) {
449
+ let result;
450
+ if (gradient.type === "radial") {
451
+ const x = (gradient.center?.x ?? .5) * width;
452
+ const y = (gradient.center?.y ?? .5) * height;
453
+ const radius = (gradient.radius ?? .5) * Math.max(width, height);
454
+ result = ctx.createRadialGradient(x, y, 0, x, y, radius);
455
+ } else if (!isUndefined(gradient.angle)) {
456
+ const angle = (gradient.angle - 90) * Math.PI / 180;
457
+ const x = Math.cos(angle) * .5;
458
+ const y = Math.sin(angle) * .5;
459
+ result = ctx.createLinearGradient(width * (.5 - x), height * (.5 - y), width * (.5 + x), height * (.5 + y));
460
+ } else result = ctx.createLinearGradient((gradient.from?.x ?? 0) * width, (gradient.from?.y ?? 0) * height, (gradient.to?.x ?? 1) * width, (gradient.to?.y ?? 0) * height);
461
+ for (const stop of gradient.stops) result.addColorStop(stop.position, stop.color);
462
+ return result;
463
+ }
464
+ //#endregion
465
+ export { Bound, Intersection, blendColor, clipPolygonToRect, colorWithAlpha, createCanvasGradient, createPointView, degreesToRadians, formatColor, formatPremultipliedColor, isGradient, isPointWithinDistance, makeBoundingBoxFromPoints, makeBoundingBoxFromRects, makeBoundsFromPoints, parseColor, parsePremultipliedColor, qrDecompose, radiansToDegrees, rectEdgePosition, transformPoint };
package/dist/math.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export declare const degreesToRadians: (degrees: number) => number;
2
+ export declare const radiansToDegrees: (radians: number) => number;
3
+ export type Edge = "top" | "right" | "bottom" | "left";
4
+ /**
5
+ * 矩形边上按 ratio 取点,同时返回朝外法线。
6
+ * ratio ∈ [0, 1],沿边的起点到终点方向插值。
7
+ */
8
+ export declare function rectEdgePosition(w: number, h: number, edge: Edge, ratio: number): {
9
+ x: number;
10
+ y: number;
11
+ nx: number;
12
+ ny: number;
13
+ };
@@ -0,0 +1,6 @@
1
+ export declare function qrDecompose(matrix: DOMMatrixReadOnly): {
2
+ angle: number;
3
+ scaleX: number;
4
+ scaleY: number;
5
+ skewX: number;
6
+ };
@@ -0,0 +1,11 @@
1
+ export type PointView = {
2
+ readonly x: number;
3
+ readonly y: number;
4
+ };
5
+ export type MutablePointView = {
6
+ -readonly [Key in keyof PointView]: PointView[Key];
7
+ };
8
+ export declare function createPointView(x: number, y: number): PointView;
9
+ export declare function isPointWithinDistance(point: PointView, target: PointView, threshold: number): boolean;
10
+ export declare function transformPoint(m: DOMMatrixReadOnly, point: PointView): PointView;
11
+ export declare function transformPoint<T extends MutablePointView>(m: DOMMatrixReadOnly, point: PointView, out: T): T;
package/dist/rect.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ import type { PointView } from "./point";
2
+ export interface Rect {
3
+ left: number;
4
+ top: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ export interface RectWithCenter extends Rect {
9
+ centerX?: number;
10
+ centerY?: number;
11
+ }
12
+ export interface BoundingBox {
13
+ minX: number;
14
+ minY: number;
15
+ maxX: number;
16
+ maxY: number;
17
+ }
18
+ export declare function makeBoundingBoxFromPoints(points: readonly PointView[]): Rect;
19
+ export declare class Bound implements RectWithCenter {
20
+ minX: number;
21
+ minY: number;
22
+ maxX: number;
23
+ maxY: number;
24
+ constructor(minX?: number, minY?: number, maxX?: number, maxY?: number);
25
+ get left(): number;
26
+ get top(): number;
27
+ get width(): number;
28
+ get height(): number;
29
+ get centerX(): number;
30
+ get centerY(): number;
31
+ merge(other: Bound): this;
32
+ copy(): Bound;
33
+ expand(padding: number, minSize?: number): this;
34
+ includePoint(x: number, y: number, padding?: number): this;
35
+ static fromPoints(points: readonly PointView[]): Bound;
36
+ }
37
+ export declare function makeBoundsFromPoints(points: readonly PointView[]): BoundingBox;
38
+ export declare function makeBoundingBoxFromRects(rects: readonly Rect[]): Rect;
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@fulate/share",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "license": "MIT",
17
+ "scripts": {
18
+ "test": "vitest run --config vitest.config.ts"
19
+ },
20
+ "dependencies": {
21
+ "colord": "^2.9.3",
22
+ "lodash-es": "^4.17.22"
23
+ }
24
+ }