@samirdamle/calipers 0.0.0-stage → 0.1.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 +73 -2
- package/dist/align.d.ts +76 -0
- package/dist/align.js +321 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +524 -0
- package/dist/measure.d.ts +70 -0
- package/dist/measure.js +274 -0
- package/dist/place.d.ts +45 -0
- package/dist/place.js +274 -0
- package/dist/rect.d.ts +27 -0
- package/dist/rect.js +147 -0
- package/dist/types-PJD4F9tg.d.ts +52 -0
- package/package.json +84 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Samir Damle
|
|
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
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# calipers
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A tiny, dependency-free JavaScript geometry toolkit for the web — measure, align, place, and design with HTML and SVG elements.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { measure, place } from '@samirdamle/calipers';
|
|
7
|
+
|
|
8
|
+
// How far is the card from the hero?
|
|
9
|
+
measure('.card', '.hero', { anchor: 'tl', ofAnchor: 'bl', fields: ['dy', 'gapY'] });
|
|
10
|
+
// → { pointA: {…}, pointB: {…}, dy: -24, gapY: 24 }
|
|
11
|
+
|
|
12
|
+
// Park the tooltip's top-center 8px below the button's bottom-center.
|
|
13
|
+
place('.tooltip', {
|
|
14
|
+
anchor: 'tc',
|
|
15
|
+
at: { anchor: 'bc', of: '.button' },
|
|
16
|
+
offset: { y: 8 },
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm i @samirdamle/calipers
|
|
24
|
+
# pnpm add @samirdamle/calipers
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Import only what you use — every module is a separate, tree-shakeable entry point:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { rectOf } from '@samirdamle/calipers/rect';
|
|
31
|
+
import { measure, calipers, anchorPoint } from '@samirdamle/calipers/measure';
|
|
32
|
+
import { place } from '@samirdamle/calipers/place';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Modules
|
|
36
|
+
|
|
37
|
+
| Import | Does | Size (min+br) |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `@samirdamle/calipers/rect` | `rectOf` — resolve anything measurable into a rect, with an explicit box model and coordinate space | 863 B |
|
|
40
|
+
| `@samirdamle/calipers/measure` | `measure` / `calipers` — pairwise geometry, plus a DOM-free pure core | 1723 B |
|
|
41
|
+
| `@samirdamle/calipers/place` | `place` — position an element relative to a point or another element | 1444 B |
|
|
42
|
+
| `@samirdamle/calipers/align` | `align` / `distribute` / `stack` — design-tool alignment, even spacing, and stacking | 1890 B |
|
|
43
|
+
|
|
44
|
+
Sizes are measured from the actual build (see `size-budgets.json`); a stale figure is a bug.
|
|
45
|
+
|
|
46
|
+
`fit` and the dev-only `debug` overlay are on the roadmap (plan §9–§10).
|
|
47
|
+
|
|
48
|
+
## Anchors
|
|
49
|
+
|
|
50
|
+
Every anchor resolves to exactly one point. The 13 codes are sugar over fractional points — `{ x: 0.25, y: 0 }` and `(rect) => point` work too.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
tl ─── tc ─── tr
|
|
54
|
+
│ │
|
|
55
|
+
cl ─── cc ─── cr
|
|
56
|
+
│ │
|
|
57
|
+
bl ─── bc ─── br
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`t`/`b`/`l`/`r` are the edge midpoints (`tc`/`bc`/`cl`/`cr`).
|
|
61
|
+
|
|
62
|
+
## The one rule
|
|
63
|
+
|
|
64
|
+
Every number names its **box** (`content` | `border` | `margin` | `visual` | `bbox`) and its **coordinate space** (`viewport` | `document`). No silent defaults that change meaning on scroll or under transforms.
|
|
65
|
+
|
|
66
|
+
## Docs
|
|
67
|
+
|
|
68
|
+
- [API reference](docs/API.md) — every function, field, and option
|
|
69
|
+
- [Playground demo](https://samirdamle.github.io/calipers/demo/) — drag boxes, watch measurements update (or `pnpm demo` locally)
|
|
70
|
+
- [Library plan](docs/plans/calipers-plan.md) and [goal](docs/meta/goal.md)
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
package/dist/align.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { M as Measurable, B as Box, G as Gap } from './types-PJD4F9tg.js';
|
|
2
|
+
|
|
3
|
+
type AlignEdge = 'left' | 'right' | 'top' | 'bottom' | 'center-x' | 'center-y';
|
|
4
|
+
interface AlignOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Reference to align to. Omit to align to the selection's extreme
|
|
7
|
+
* (edges) or mean (centers), design-tool style.
|
|
8
|
+
*/
|
|
9
|
+
to?: Measurable;
|
|
10
|
+
/** Box model for measurement. @default 'border' */
|
|
11
|
+
box?: Box;
|
|
12
|
+
}
|
|
13
|
+
type DistributeAxis = 'x' | 'y';
|
|
14
|
+
interface DistributeOptions {
|
|
15
|
+
/** Fixed gap in px. Omit for even spacing across the selection span. */
|
|
16
|
+
gap?: number;
|
|
17
|
+
/** Box model for measurement. @default 'border' */
|
|
18
|
+
box?: Box;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Align elements along one edge or centerline, moving each via the CSS
|
|
22
|
+
* `translate` property (accumulates across calls, composes with `transform`).
|
|
23
|
+
* Measurement includes each element's existing translate, so repeating the
|
|
24
|
+
* same call is a no-op instead of re-applying the delta.
|
|
25
|
+
*/
|
|
26
|
+
declare function align(els: ArrayLike<Element>, edge: AlignEdge, options?: AlignOptions): void;
|
|
27
|
+
/**
|
|
28
|
+
* Spread elements evenly along an axis, in positional order, moving each via
|
|
29
|
+
* the CSS `translate` property. No-op for fewer than two elements.
|
|
30
|
+
* Measurement includes each element's existing translate, so repeating the
|
|
31
|
+
* same call is a no-op instead of re-applying the deltas.
|
|
32
|
+
*/
|
|
33
|
+
declare function distribute(els: ArrayLike<Element>, axis: DistributeAxis, options?: DistributeOptions): void;
|
|
34
|
+
/** Which way a stack grows. */
|
|
35
|
+
type StackDirection = 't' | 'b' | 'l' | 'r';
|
|
36
|
+
/**
|
|
37
|
+
* Lateral alignment of stacked boxes, relative to the anchor box. Only the
|
|
38
|
+
* axis perpendicular to `direction` applies; anything else centers.
|
|
39
|
+
*/
|
|
40
|
+
type StackAlign = 't' | 'b' | 'l' | 'r' | 'c';
|
|
41
|
+
interface StackOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Reference to stack from (the source). Omit to stack from the selection's
|
|
44
|
+
* trailing extreme in the stacking direction — that element stays put and
|
|
45
|
+
* the rest pile after it.
|
|
46
|
+
*/
|
|
47
|
+
to?: Measurable;
|
|
48
|
+
/** Which way the stack grows. @default 'b' */
|
|
49
|
+
direction?: StackDirection;
|
|
50
|
+
/**
|
|
51
|
+
* Gap between consecutive boxes. A single number is the axial separation
|
|
52
|
+
* (as before); `[x, y]` / `{ x, y }` keeps the axial meaning on the
|
|
53
|
+
* stacking axis and adds a per-step lateral cascade on the other —
|
|
54
|
+
* e.g. `gap: { x: 12, y: 10 }` with `direction: 'b'` stacks boxes 10px
|
|
55
|
+
* apart, each 12px further right: a staircase. @default 0
|
|
56
|
+
*/
|
|
57
|
+
gap?: Gap;
|
|
58
|
+
/**
|
|
59
|
+
* Lateral alignment relative to the anchor box. @default 'c'
|
|
60
|
+
*/
|
|
61
|
+
align?: StackAlign;
|
|
62
|
+
/** Box model for measurement. @default 'border' */
|
|
63
|
+
box?: Box;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Stack elements one after another from a reference, moving each via the CSS
|
|
67
|
+
* `translate` property (accumulates across calls, composes with `transform`).
|
|
68
|
+
* Targets are ordered nearest-the-anchor first along the stacking axis, so
|
|
69
|
+
* e.g. `stack(targets, { to: source, direction: 't', gap: 10, align: 'c' })`
|
|
70
|
+
* piles them upward from the source with 10px between consecutive boxes,
|
|
71
|
+
* center-aligned. Measurement includes each element's existing translate, so
|
|
72
|
+
* repeating the same call is a no-op.
|
|
73
|
+
*/
|
|
74
|
+
declare function stack(els: ArrayLike<Element>, options?: StackOptions): void;
|
|
75
|
+
|
|
76
|
+
export { type AlignEdge, type AlignOptions, type DistributeAxis, type DistributeOptions, type StackAlign, type StackDirection, type StackOptions, align, distribute, stack };
|
package/dist/align.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// src/internal/dom.ts
|
|
2
|
+
function isElement(v) {
|
|
3
|
+
return typeof Element !== "undefined" && v instanceof Element;
|
|
4
|
+
}
|
|
5
|
+
function px(value) {
|
|
6
|
+
const n = parseFloat(value);
|
|
7
|
+
return Number.isFinite(n) ? n : 0;
|
|
8
|
+
}
|
|
9
|
+
function parseTranslate(value) {
|
|
10
|
+
if (!value || value === "none") return { x: 0, y: 0 };
|
|
11
|
+
const [x = "0", y = "0"] = value.trim().split(/\s+/);
|
|
12
|
+
return { x: px(x), y: px(y) };
|
|
13
|
+
}
|
|
14
|
+
function getTranslate(el) {
|
|
15
|
+
return parseTranslate(getComputedStyle(el).translate);
|
|
16
|
+
}
|
|
17
|
+
function shiftTranslate(el, prev, dx, dy) {
|
|
18
|
+
if (dx === 0 && dy === 0) return;
|
|
19
|
+
el.style.setProperty("translate", `${prev.x + dx}px ${prev.y + dy}px`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/internal/gap.ts
|
|
23
|
+
function normalizeGap(gap) {
|
|
24
|
+
if (gap === void 0) return { x: 0, y: 0 };
|
|
25
|
+
if (typeof gap === "number") return { x: gap, y: gap };
|
|
26
|
+
if ("x" in gap) return { x: gap.x, y: gap.y };
|
|
27
|
+
return { x: gap[0], y: gap[1] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/rect/index.ts
|
|
31
|
+
function rectOf(target, options = {}) {
|
|
32
|
+
const { box = "border", space = "viewport" } = options;
|
|
33
|
+
const t = classify(target);
|
|
34
|
+
switch (t.kind) {
|
|
35
|
+
case "rect":
|
|
36
|
+
return { ...t.rect };
|
|
37
|
+
case "viewport":
|
|
38
|
+
return moveSpace(
|
|
39
|
+
{
|
|
40
|
+
x: 0,
|
|
41
|
+
y: 0,
|
|
42
|
+
width: window.innerWidth,
|
|
43
|
+
height: window.innerHeight
|
|
44
|
+
},
|
|
45
|
+
"viewport",
|
|
46
|
+
space
|
|
47
|
+
);
|
|
48
|
+
case "element":
|
|
49
|
+
return elementRect(t.el, box, space);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function classify(target) {
|
|
53
|
+
if (target === "viewport" || target === window) return { kind: "viewport" };
|
|
54
|
+
if (typeof target === "string") {
|
|
55
|
+
const el = document.querySelector(target);
|
|
56
|
+
if (!el)
|
|
57
|
+
throw new Error(`calipers: no element matches selector "${target}"`);
|
|
58
|
+
return { kind: "element", el };
|
|
59
|
+
}
|
|
60
|
+
if (isElement(target)) return { kind: "element", el: target };
|
|
61
|
+
if (isRectLike(target)) return { kind: "rect", rect: target };
|
|
62
|
+
if (isPointLike(target))
|
|
63
|
+
return {
|
|
64
|
+
kind: "rect",
|
|
65
|
+
rect: { x: target.x, y: target.y, width: 0, height: 0 }
|
|
66
|
+
};
|
|
67
|
+
const first = target[0];
|
|
68
|
+
if (first == null)
|
|
69
|
+
throw new Error("calipers: empty list has no measurable to resolve");
|
|
70
|
+
return classify(first);
|
|
71
|
+
}
|
|
72
|
+
function isPointLike(v) {
|
|
73
|
+
return typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number" && typeof v.width !== "number";
|
|
74
|
+
}
|
|
75
|
+
function isRectLike(v) {
|
|
76
|
+
return typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number" && typeof v.width === "number" && typeof v.height === "number";
|
|
77
|
+
}
|
|
78
|
+
function isSVGGraphics(el) {
|
|
79
|
+
return typeof SVGGraphicsElement !== "undefined" && el instanceof SVGGraphicsElement;
|
|
80
|
+
}
|
|
81
|
+
function elementRect(el, box, space) {
|
|
82
|
+
return isSVGGraphics(el) ? svgRect(el, box, space) : htmlRect(el, box, space);
|
|
83
|
+
}
|
|
84
|
+
function htmlRect(el, box, space) {
|
|
85
|
+
if (box === "visual") {
|
|
86
|
+
const r = el.getBoundingClientRect();
|
|
87
|
+
return moveSpace(
|
|
88
|
+
{ x: r.x, y: r.y, width: r.width, height: r.height },
|
|
89
|
+
"viewport",
|
|
90
|
+
space
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const border = layoutBorderBox(el);
|
|
94
|
+
const cs = getComputedStyle(el);
|
|
95
|
+
let rect;
|
|
96
|
+
if (box === "content") {
|
|
97
|
+
const bl = px(cs.borderLeftWidth);
|
|
98
|
+
const br = px(cs.borderRightWidth);
|
|
99
|
+
const bt = px(cs.borderTopWidth);
|
|
100
|
+
const bb = px(cs.borderBottomWidth);
|
|
101
|
+
const pl = px(cs.paddingLeft);
|
|
102
|
+
const pr = px(cs.paddingRight);
|
|
103
|
+
const pt = px(cs.paddingTop);
|
|
104
|
+
const pb = px(cs.paddingBottom);
|
|
105
|
+
rect = {
|
|
106
|
+
x: border.x + bl + pl,
|
|
107
|
+
y: border.y + bt + pt,
|
|
108
|
+
width: Math.max(0, border.width - bl - br - pl - pr),
|
|
109
|
+
height: Math.max(0, border.height - bt - bb - pt - pb)
|
|
110
|
+
};
|
|
111
|
+
} else if (box === "margin") {
|
|
112
|
+
const ml = px(cs.marginLeft);
|
|
113
|
+
const mr = px(cs.marginRight);
|
|
114
|
+
const mt = px(cs.marginTop);
|
|
115
|
+
const mb = px(cs.marginBottom);
|
|
116
|
+
rect = {
|
|
117
|
+
x: border.x - ml,
|
|
118
|
+
y: border.y - mt,
|
|
119
|
+
width: border.width + ml + mr,
|
|
120
|
+
height: border.height + mt + mb
|
|
121
|
+
};
|
|
122
|
+
} else {
|
|
123
|
+
rect = border;
|
|
124
|
+
}
|
|
125
|
+
return moveSpace(rect, "document", space);
|
|
126
|
+
}
|
|
127
|
+
function svgRect(el, box, space) {
|
|
128
|
+
if (box === "bbox") {
|
|
129
|
+
const b = el.getBBox();
|
|
130
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
131
|
+
}
|
|
132
|
+
const r = el.getBoundingClientRect();
|
|
133
|
+
return moveSpace(
|
|
134
|
+
{ x: r.x, y: r.y, width: r.width, height: r.height },
|
|
135
|
+
"viewport",
|
|
136
|
+
space
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function layoutBorderBox(el) {
|
|
140
|
+
let x = 0;
|
|
141
|
+
let y = 0;
|
|
142
|
+
let node = el;
|
|
143
|
+
const fixed = getComputedStyle(el).position === "fixed";
|
|
144
|
+
while (node) {
|
|
145
|
+
x += node.offsetLeft;
|
|
146
|
+
y += node.offsetTop;
|
|
147
|
+
node = node.offsetParent;
|
|
148
|
+
}
|
|
149
|
+
if (fixed) {
|
|
150
|
+
x += window.scrollX;
|
|
151
|
+
y += window.scrollY;
|
|
152
|
+
}
|
|
153
|
+
return { x, y, width: el.offsetWidth, height: el.offsetHeight };
|
|
154
|
+
}
|
|
155
|
+
function moveSpace(rect, from, to) {
|
|
156
|
+
if (from === to) return rect;
|
|
157
|
+
const dx = from === "viewport" ? window.scrollX : -window.scrollX;
|
|
158
|
+
const dy = from === "viewport" ? window.scrollY : -window.scrollY;
|
|
159
|
+
return {
|
|
160
|
+
x: rect.x + dx,
|
|
161
|
+
y: rect.y + dy,
|
|
162
|
+
width: rect.width,
|
|
163
|
+
height: rect.height
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/internal/rendered.ts
|
|
168
|
+
function rendered(el, box) {
|
|
169
|
+
const rect = rectOf(el, { box, space: "viewport" });
|
|
170
|
+
const translate = getTranslate(el);
|
|
171
|
+
if (box === "visual" || translate.x === 0 && translate.y === 0) {
|
|
172
|
+
return { rect, translate };
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
rect: { ...rect, x: rect.x + translate.x, y: rect.y + translate.y },
|
|
176
|
+
translate
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/align/index.ts
|
|
181
|
+
var pos = (r, ax) => ax === "x" ? r.x : r.y;
|
|
182
|
+
var size = (r, ax) => ax === "x" ? r.width : r.height;
|
|
183
|
+
var other = (ax) => ax === "x" ? "y" : "x";
|
|
184
|
+
function toRect(to, box) {
|
|
185
|
+
return isElement(to) ? rendered(to, box).rect : rectOf(to, { box, space: "viewport" });
|
|
186
|
+
}
|
|
187
|
+
function mean(vals) {
|
|
188
|
+
return vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
189
|
+
}
|
|
190
|
+
function align(els, edge, options = {}) {
|
|
191
|
+
const list = Array.from(els);
|
|
192
|
+
if (list.length === 0) return;
|
|
193
|
+
const box = options.box ?? "border";
|
|
194
|
+
const ax = edge === "left" || edge === "right" || edge === "center-x" ? "x" : "y";
|
|
195
|
+
const items = list.map((el) => ({ el, ...rendered(el, box) }));
|
|
196
|
+
const ref = options.to === void 0 ? null : toRect(options.to, box);
|
|
197
|
+
const line = ref !== null ? edgePos(edge, ref) : aggregate(
|
|
198
|
+
edge,
|
|
199
|
+
items.map((it) => edgePos(edge, it.rect))
|
|
200
|
+
);
|
|
201
|
+
for (const { el, rect, translate } of items) {
|
|
202
|
+
const delta = line - edgePos(edge, rect);
|
|
203
|
+
shiftTranslate(
|
|
204
|
+
el,
|
|
205
|
+
translate,
|
|
206
|
+
ax === "x" ? delta : 0,
|
|
207
|
+
ax === "y" ? delta : 0
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function edgePos(edge, r) {
|
|
212
|
+
switch (edge) {
|
|
213
|
+
case "left":
|
|
214
|
+
return r.x;
|
|
215
|
+
case "right":
|
|
216
|
+
return r.x + r.width;
|
|
217
|
+
case "center-x":
|
|
218
|
+
return r.x + r.width / 2;
|
|
219
|
+
case "top":
|
|
220
|
+
return r.y;
|
|
221
|
+
case "bottom":
|
|
222
|
+
return r.y + r.height;
|
|
223
|
+
case "center-y":
|
|
224
|
+
return r.y + r.height / 2;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function aggregate(edge, vals) {
|
|
228
|
+
if (edge === "center-x" || edge === "center-y") return mean(vals);
|
|
229
|
+
return edge === "left" || edge === "top" ? Math.min(...vals) : Math.max(...vals);
|
|
230
|
+
}
|
|
231
|
+
function distribute(els, axis, options = {}) {
|
|
232
|
+
const list = Array.from(els);
|
|
233
|
+
if (list.length < 2) return;
|
|
234
|
+
const box = options.box ?? "border";
|
|
235
|
+
const ax = axis;
|
|
236
|
+
const items = list.map((el) => ({ el, ...rendered(el, box) })).sort((a, b) => pos(a.rect, ax) - pos(b.rect, ax));
|
|
237
|
+
const first = items[0].rect;
|
|
238
|
+
const last = items[items.length - 1].rect;
|
|
239
|
+
const span = pos(last, ax) + size(last, ax) - pos(first, ax);
|
|
240
|
+
const sizes = items.reduce((sum, it) => sum + size(it.rect, ax), 0);
|
|
241
|
+
const gap = options.gap ?? (span - sizes) / (items.length - 1);
|
|
242
|
+
let cursor = pos(first, ax);
|
|
243
|
+
for (const { el, rect, translate } of items) {
|
|
244
|
+
const delta = cursor - pos(rect, ax);
|
|
245
|
+
shiftTranslate(
|
|
246
|
+
el,
|
|
247
|
+
translate,
|
|
248
|
+
ax === "x" ? delta : 0,
|
|
249
|
+
ax === "y" ? delta : 0
|
|
250
|
+
);
|
|
251
|
+
cursor += size(rect, ax) + gap;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function stack(els, options = {}) {
|
|
255
|
+
const list = Array.from(els);
|
|
256
|
+
if (list.length === 0) return;
|
|
257
|
+
const box = options.box ?? "border";
|
|
258
|
+
const direction = options.direction ?? "b";
|
|
259
|
+
const lateral = options.align ?? "c";
|
|
260
|
+
const ax = direction === "t" || direction === "b" ? "y" : "x";
|
|
261
|
+
const forward = direction === "b" || direction === "r";
|
|
262
|
+
const gap = normalizeGap(options.gap);
|
|
263
|
+
const gapAx = ax === "x" ? gap.x : gap.y;
|
|
264
|
+
const gapLat = typeof options.gap === "number" || options.gap === void 0 ? 0 : ax === "x" ? gap.y : gap.x;
|
|
265
|
+
const items = list.map((el) => ({ el, ...rendered(el, box) }));
|
|
266
|
+
let anchorEl;
|
|
267
|
+
let anchorRect;
|
|
268
|
+
if (options.to !== void 0) {
|
|
269
|
+
const to = options.to;
|
|
270
|
+
anchorEl = isElement(to) ? to : null;
|
|
271
|
+
anchorRect = toRect(to, box);
|
|
272
|
+
} else {
|
|
273
|
+
anchorEl = items[0].el;
|
|
274
|
+
anchorRect = items[0].rect;
|
|
275
|
+
for (const it of items) {
|
|
276
|
+
if (moreTrailing(it.rect, anchorRect, ax, forward)) {
|
|
277
|
+
anchorEl = it.el;
|
|
278
|
+
anchorRect = it.rect;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const targets = items.filter((it) => it.el !== anchorEl).sort(
|
|
283
|
+
(a, b) => forward ? pos(a.rect, ax) - pos(b.rect, ax) : pos(b.rect, ax) + size(b.rect, ax) - (pos(a.rect, ax) + size(a.rect, ax))
|
|
284
|
+
);
|
|
285
|
+
if (targets.length === 0) return;
|
|
286
|
+
let cursor = forward ? pos(anchorRect, ax) + size(anchorRect, ax) + gapAx : pos(anchorRect, ax) - gapAx;
|
|
287
|
+
let step = 0;
|
|
288
|
+
for (const { el, rect, translate } of targets) {
|
|
289
|
+
step += 1;
|
|
290
|
+
const s = size(rect, ax);
|
|
291
|
+
const slot = forward ? cursor : cursor - s;
|
|
292
|
+
const delta = slot - pos(rect, ax);
|
|
293
|
+
const lat = lateralOffset(lateral, anchorRect, rect, ax) + step * gapLat;
|
|
294
|
+
shiftTranslate(
|
|
295
|
+
el,
|
|
296
|
+
translate,
|
|
297
|
+
ax === "x" ? delta : lat,
|
|
298
|
+
ax === "y" ? delta : lat
|
|
299
|
+
);
|
|
300
|
+
cursor = forward ? slot + s + gapAx : slot - gapAx;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function moreTrailing(r, best, ax, forward) {
|
|
304
|
+
const e = forward ? pos(r, ax) : pos(r, ax) + size(r, ax);
|
|
305
|
+
const b = forward ? pos(best, ax) : pos(best, ax) + size(best, ax);
|
|
306
|
+
return forward ? e < b : e > b;
|
|
307
|
+
}
|
|
308
|
+
function lateralOffset(lateral, anchor, r, ax) {
|
|
309
|
+
const p = other(ax);
|
|
310
|
+
const leading = p === "x" ? "l" : "t";
|
|
311
|
+
const trailing = p === "x" ? "r" : "b";
|
|
312
|
+
const a0 = pos(anchor, p);
|
|
313
|
+
const a1 = a0 + size(anchor, p);
|
|
314
|
+
const r0 = pos(r, p);
|
|
315
|
+
const r1 = r0 + size(r, p);
|
|
316
|
+
if (lateral === leading) return a0 - r0;
|
|
317
|
+
if (lateral === trailing) return a1 - r1;
|
|
318
|
+
return (a0 + a1) / 2 - (r0 + r1) / 2;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export { align, distribute, stack };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AlignEdge, AlignOptions, DistributeAxis, DistributeOptions, StackAlign, StackDirection, StackOptions, align, distribute, stack } from './align.js';
|
|
2
|
+
export { Field, MeasureOptions, MeasureResult, anchorPoint, angleBetween, calipers, containsRect, distanceBetween, gapBetween, measure, overlapArea, overlaps } from './measure.js';
|
|
3
|
+
export { PlaceAt, PlaceOptions, place } from './place.js';
|
|
4
|
+
export { RectOptions, rectOf } from './rect.js';
|
|
5
|
+
export { A as Anchor, a as AnchorCode, B as Box, G as Gap, M as Measurable, P as Point, R as Rect, S as Space } from './types-PJD4F9tg.js';
|