Skip to content

Commit

Permalink
feat(geom): rewrite roundedRect() to allow individual corner radii
Browse files Browse the repository at this point in the history
BREAKING CHANGE: update roundedRect() radius handling to allow individual corner radii

- update docs
- add tests
  • Loading branch information
postspectacular committed May 7, 2024
1 parent b2134c2 commit a4817aa
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 18 deletions.
87 changes: 69 additions & 18 deletions packages/geom/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,26 @@ import { map } from "@thi.ng/transducers/map";
import { mapcat } from "@thi.ng/transducers/mapcat";
import type { ReadonlyVec, Vec } from "@thi.ng/vectors";
import { equals2 } from "@thi.ng/vectors/equals";
import { maddN2 } from "@thi.ng/vectors/maddn";
import type { Cubic } from "./api/cubic.js";
import { Path } from "./api/path.js";
import { asCubic } from "./as-cubic.js";
import { PathBuilder } from "./path-builder.js";

/**
* Creates a new {@link Path} instance, optional with given `segments`,
* `subPaths` and `attribs`.
*
* @remarks
* Segments and sub-paths can also be later added via {@link Path.addSegments}
* or {@link Path.addSubPaths}.
*
* @param segments
* @param subPaths
* @param attribs
*/
export const path = (
segments: Iterable<PathSegment>,
subPaths: Iterable<PathSegment[]> = [],
segments?: Iterable<PathSegment>,
subPaths?: Iterable<PathSegment[]>,
attribs?: Attribs
) => new Path(segments, subPaths, attribs);

Expand All @@ -28,6 +39,8 @@ export const path = (
* not the same as the last point of the previous curve, a new sub path will be
* started.
*
* Also see {@link normalizedPath}.
*
* @param cubics
* @param attribs
*/
Expand All @@ -52,6 +65,15 @@ export const pathFromCubics = (cubics: Cubic[], attribs?: Attribs) => {
return path;
};

/**
* Converts given path into a new one with all segments converted to
* {@link Cubic} bezier segments.
*
* @remarks
* Also see {@link pathFromCubics}.
*
* @param path
*/
export const normalizedPath = (path: Path) => {
const $normalize = (segments: PathSegment[]) => [
...mapcat(
Expand All @@ -72,23 +94,52 @@ export const normalizedPath = (path: Path) => {
);
};

/**
* Creates a new rounded rect {@link Path}, using the given corner radius or
* radii.
*
* @remarks
* If multiple `radii` are given, the interpretation logic is the same as:
* https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/roundRect
*
* - number: all corners
* - `[top-left-and-bottom-right, top-right-and-bottom-left]`
* - `[top-left, top-right-and-bottom-left, bottom-right]`
* - `[top-left, top-right, bottom-right, bottom-left]`
*
* No arc segments will be generated for those corners where the radius <= 0
*
* @param pos
* @param size
* @param radii
* @param attribs
*/
export const roundedRect = (
pos: Vec,
size: Vec,
r: number | Vec,
[w, h]: Vec,
radii:
| number
| [number, number]
| [number, number, number]
| [number, number, number, number],
attribs?: Attribs
) => {
r = isNumber(r) ? [r, r] : r;
const [w, h] = maddN2([], r, -2, size);
return new PathBuilder(attribs)
.moveTo([pos[0] + r[0], pos[1]])
.hlineTo(w, true)
.arcTo(r, r, 0, false, true, true)
.vlineTo(h, true)
.arcTo([-r[0], r[1]], r, 0, false, true, true)
.hlineTo(-w, true)
.arcTo([-r[0], -r[1]], r, 0, false, true, true)
.vlineTo(-h, true)
.arcTo([r[0], -r[1]], r, 0, false, true, true)
.current();
const [tl, tr, br, bl] = isNumber(radii)
? [radii, radii, radii, radii]
: radii.length === 2
? [radii[0], radii[1], radii[0], radii[1]]
: radii.length === 3
? [radii[0], radii[1], radii[2], radii[1]]
: radii;
const b = new PathBuilder(attribs)
.moveTo([pos[0] + tl, pos[1]])
.hlineTo(w - tl - tr, true);
if (tr > 0) b.arcTo([tr, tr], [tr, tr], 0, false, true, true);
b.vlineTo(h - tr - br, true);
if (br > 0) b.arcTo([-br, br], [br, br], 0, false, true, true);
b.hlineTo(-(w - br - bl), true);
if (bl > 0) b.arcTo([-bl, -bl], [bl, bl], 0, false, true, true);
b.vlineTo(-(h - bl - tl), true);
if (tl > 0) b.arcTo([tl, -tl], [tl, tl], 0, false, true, true);
return b.current().close();
};
22 changes: 22 additions & 0 deletions packages/geom/test/path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
pathFromCubics,
pathFromSvg,
polyline,
roundedRect,
simplify,
vertices,
} from "../src/index.js";
Expand Down Expand Up @@ -122,4 +123,25 @@ describe("path", () => {
])
).toBe(true);
});

test("roundedRect", () => {
expect(asSvg(roundedRect([0, 0], [100, 100], 20))).toBe(
'<path d="M20,0H80A20,20,0,0,1,100,20V80A20,20,0,0,1,80,100H20A20,20,0,0,1,0,80V20A20,20,0,0,1,20.000,0z"/>'
);
expect(asSvg(roundedRect([0, 0], [100, 100], [10, 20]))).toBe(
'<path d="M10,0H80A20,20,0,0,1,100,20V90A10,10,0,0,1,90,100H20A20,20,0,0,1,0,80V10A10,10,0,0,1,10.000,0z"/>'
);
expect(asSvg(roundedRect([0, 0], [100, 100], [10, 20, 40]))).toBe(
'<path d="M10,0H80A20,20,0,0,1,100,20V60A40,40,0,0,1,60,100H20A20,20,0,0,1,0,80V10A10,10,0,0,1,10.000,0z"/>'
);
expect(asSvg(roundedRect([0, 0], [100, 100], [10, 20, 30, 40]))).toBe(
'<path d="M10,0H80A20,20,0,0,1,100,20V70A30,30,0,0,1,70,100H40A40,40,0,0,1,0,60.000V10A10,10,0,0,1,10.000,0z"/>'
);
expect(asSvg(roundedRect([0, 0], [100, 100], [0, 40]))).toBe(
'<path d="M0,0H60A40,40,0,0,1,100,40V100H40A40,40,0,0,1,0,60.000V0z"/>'
);
expect(asSvg(roundedRect([0, 0], [100, 100], 0))).toBe(
'<path d="M0,0H100V100H0V0z"/>'
);
});
});

0 comments on commit a4817aa

Please sign in to comment.