@nightshadeui/util 2.11.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/package.json +12 -0
- package/src/DomEventProxy.ts +27 -0
- package/src/box.ts +69 -0
- package/src/css.ts +26 -0
- package/src/grid.ts +76 -0
- package/src/index.ts +6 -0
- package/src/math.ts +3 -0
- package/src/point.ts +44 -0
- package/tsconfig.json +15 -0
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nightshadeui/util",
|
|
3
|
+
"version": "2.11.0",
|
|
4
|
+
"description": "Shared utility helpers for Nightshade packages",
|
|
5
|
+
"author": "Boris Okunskiy",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./src": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type Listener = EventListenerOrEventListenerObject;
|
|
2
|
+
|
|
3
|
+
interface Entry {
|
|
4
|
+
type: string;
|
|
5
|
+
listener: Listener;
|
|
6
|
+
options?: AddEventListenerOptions | boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class DomEventProxy {
|
|
10
|
+
|
|
11
|
+
private entries: Entry[] = [];
|
|
12
|
+
|
|
13
|
+
constructor(private target: EventTarget) {}
|
|
14
|
+
|
|
15
|
+
add(type: string, listener: Listener, options?: AddEventListenerOptions | boolean) {
|
|
16
|
+
this.target.addEventListener(type, listener, options);
|
|
17
|
+
this.entries.push({ type, listener, options });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
removeAll() {
|
|
21
|
+
for (const entry of this.entries) {
|
|
22
|
+
this.target.removeEventListener(entry.type, entry.listener, entry.options);
|
|
23
|
+
}
|
|
24
|
+
this.entries = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
}
|
package/src/box.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { clamp } from './math.js';
|
|
2
|
+
import { type Point, pointsEqual, scalePoint } from './point.js';
|
|
3
|
+
|
|
4
|
+
export type Box = [Point, Point];
|
|
5
|
+
|
|
6
|
+
export function boxFromPoints(a: Point, b: Point): Box {
|
|
7
|
+
const min = { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y) };
|
|
8
|
+
const max = { x: Math.max(a.x, b.x), y: Math.max(a.y, b.y) };
|
|
9
|
+
return [min, max];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function boxOverlap(a: Box, b: Box): boolean {
|
|
13
|
+
// https://stackoverflow.com/questions/20925818/algorithm-to-check-if-two-boxes-overlap
|
|
14
|
+
const xOverlap = a[1].x >= b[0].x && b[1].x >= a[0].x;
|
|
15
|
+
const yOverlap = a[1].y >= b[0].y && b[1].y >= a[0].y;
|
|
16
|
+
return xOverlap && yOverlap;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function boxContains(box: Box, point: Point): boolean {
|
|
20
|
+
return boxOverlap(box, [point, point]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function boxCovers(boundingBox: Box, box: Box): boolean {
|
|
24
|
+
const [a, b] = boundingBox;
|
|
25
|
+
const [c, d] = box;
|
|
26
|
+
return a.x <= c.x && a.y <= c.y && b.x >= d.x && b.y >= d.y;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function boxEquals(a: Box, b: Box) {
|
|
30
|
+
return pointsEqual(a[0], b[0]) && pointsEqual(a[1], b[1]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function boxScale(box: Box, scale: number) {
|
|
34
|
+
return boxFromPoints(scalePoint(box[0], scale), scalePoint(box[1], scale));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function boxCenter(box: Box): Point {
|
|
38
|
+
const [min, max] = box;
|
|
39
|
+
return {
|
|
40
|
+
x: (max.x - min.x) * .5 + min.x,
|
|
41
|
+
y: (max.y - min.y) * .5 + min.y,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function boundingBox(a: Box, b: Box): Box {
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
x: Math.min(a[0].x, b[0].x),
|
|
49
|
+
y: Math.min(a[0].y, b[0].y),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
x: Math.max(a[1].x, b[1].x),
|
|
53
|
+
y: Math.max(a[1].y, b[1].y),
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function boxArea(box: Box) {
|
|
59
|
+
const a = Math.abs(box[0].x - box[1].x);
|
|
60
|
+
const b = Math.abs(box[0].y - box[1].y);
|
|
61
|
+
return a * b;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function clampPointToBox(point: Point, box: Box): Point {
|
|
65
|
+
return {
|
|
66
|
+
x: clamp(point.x, box[0].x, box[1].x),
|
|
67
|
+
y: clamp(point.y, box[0].y, box[1].y),
|
|
68
|
+
};
|
|
69
|
+
}
|
package/src/css.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function getCssVar(el: HTMLElement | null | undefined, name: string, fallback: string) {
|
|
2
|
+
const root = el ?? document.documentElement;
|
|
3
|
+
const value = getComputedStyle(root).getPropertyValue(name).trim();
|
|
4
|
+
return value || fallback;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getCssVarNumber(el: HTMLElement | null | undefined, name: string, fallback: number) {
|
|
8
|
+
const raw = getCssVar(el, name, String(fallback));
|
|
9
|
+
const parsed = Number(raw.replace(/px|em|%|vh|vw/g, ''));
|
|
10
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getCssColorVar(el: HTMLElement | null | undefined, name: string, fallback: string) {
|
|
14
|
+
const root = el ?? document.documentElement;
|
|
15
|
+
const value = getCssVar(el, name, fallback);
|
|
16
|
+
const probe = document.createElement('span');
|
|
17
|
+
probe.style.position = 'absolute';
|
|
18
|
+
probe.style.visibility = 'hidden';
|
|
19
|
+
probe.style.pointerEvents = 'none';
|
|
20
|
+
probe.style.color = fallback;
|
|
21
|
+
probe.style.color = value;
|
|
22
|
+
root.appendChild(probe);
|
|
23
|
+
const resolved = getComputedStyle(probe).color.trim();
|
|
24
|
+
probe.remove();
|
|
25
|
+
return resolved || fallback;
|
|
26
|
+
}
|
package/src/grid.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function createGridLinesSvg(
|
|
2
|
+
cellSize: number,
|
|
3
|
+
subdivisions: number,
|
|
4
|
+
majorColor: string,
|
|
5
|
+
minorColor: string,
|
|
6
|
+
) {
|
|
7
|
+
const subdivCoords = getSubdivCoords(cellSize, subdivisions)
|
|
8
|
+
.map(_ => _ + 1);
|
|
9
|
+
const lines = [
|
|
10
|
+
`<svg xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
width="${cellSize}"
|
|
12
|
+
height="${cellSize}"
|
|
13
|
+
viewBox="0 0 ${cellSize} ${cellSize}">`
|
|
14
|
+
];
|
|
15
|
+
// Minor lines
|
|
16
|
+
for (const c of subdivCoords) {
|
|
17
|
+
lines.push(
|
|
18
|
+
`<path d="M0,${c} H ${cellSize}" stroke="${minorColor}" stroke-width="1"/>`
|
|
19
|
+
);
|
|
20
|
+
lines.push(
|
|
21
|
+
`<path d="M${c},0 V ${cellSize}" stroke="${minorColor}" stroke-width="1"/>`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
// Major lines
|
|
25
|
+
lines.push(`<path d="M0,1 H ${cellSize}" stroke="${majorColor}" stroke-width="1"/>`);
|
|
26
|
+
lines.push(`<path d="M1,0 V ${cellSize}" stroke="${majorColor}" stroke-width="1"/>`);
|
|
27
|
+
lines.push('</svg>');
|
|
28
|
+
return lines.join('');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createGridDotsSvg(
|
|
32
|
+
cellSize: number,
|
|
33
|
+
subdivisions: number,
|
|
34
|
+
majorColor: string,
|
|
35
|
+
majorRadius = 2,
|
|
36
|
+
minorColor: string,
|
|
37
|
+
minorRadius = 1,
|
|
38
|
+
) {
|
|
39
|
+
const minorCoords = [0, ...getSubdivCoords(cellSize, subdivisions), cellSize];
|
|
40
|
+
const majorCoords = [0, cellSize];
|
|
41
|
+
const lines = [
|
|
42
|
+
`<svg xmlns="http://www.w3.org/2000/svg"
|
|
43
|
+
width="${cellSize}"
|
|
44
|
+
height="${cellSize}"
|
|
45
|
+
viewBox="0 0 ${cellSize} ${cellSize}">`,
|
|
46
|
+
];
|
|
47
|
+
if (minorRadius) {
|
|
48
|
+
for (const x of minorCoords) {
|
|
49
|
+
for (const y of minorCoords) {
|
|
50
|
+
lines.push(
|
|
51
|
+
`<circle cx="${x}" cy="${y}" r="${minorRadius}" fill="${minorColor}"/>`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (majorRadius) {
|
|
57
|
+
for (const x of majorCoords) {
|
|
58
|
+
for (const y of majorCoords) {
|
|
59
|
+
lines.push(
|
|
60
|
+
`<circle cx="${x}" cy="${y}" r="${majorRadius}" fill="${majorColor}"/>`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
lines.push('</svg>');
|
|
66
|
+
return lines.join('');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getSubdivCoords(cellSize: number, subdivisions: number) {
|
|
70
|
+
const subdivCoords: number[] = [];
|
|
71
|
+
const k = Math.round(cellSize / subdivisions);
|
|
72
|
+
for (let i = 1; i < subdivisions; i++) {
|
|
73
|
+
subdivCoords.push(k * i);
|
|
74
|
+
}
|
|
75
|
+
return subdivCoords;
|
|
76
|
+
}
|
package/src/index.ts
ADDED
package/src/math.ts
ADDED
package/src/point.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface Point {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function pointsEqual(a: Point, b: Point, threshold = 0) {
|
|
7
|
+
return Math.abs(a.x - b.x) <= threshold && Math.abs(a.y - b.y) <= threshold;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function clonePoint(point: Point): Point {
|
|
11
|
+
return { x: point.x, y: point.y };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function addPoints(a: Point, b: Point): Point {
|
|
15
|
+
return {
|
|
16
|
+
x: a.x + b.x,
|
|
17
|
+
y: a.y + b.y,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function subtractPoints(a: Point, b: Point): Point {
|
|
22
|
+
return {
|
|
23
|
+
x: a.x - b.x,
|
|
24
|
+
y: a.y - b.y,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function scalePoint(p: Point, fac: number): Point {
|
|
29
|
+
return {
|
|
30
|
+
x: p.x * fac,
|
|
31
|
+
y: p.y * fac,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function normalizePoint(p: Point): Point {
|
|
36
|
+
const l = Math.hypot(p.x, p.y);
|
|
37
|
+
if (l === 0) {
|
|
38
|
+
return { x: 0, y: 0 };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
x: p.x / l,
|
|
42
|
+
y: p.y / l,
|
|
43
|
+
};
|
|
44
|
+
}
|