Skip to content

Commit 5cadf7f

Browse files
authored
Add Polygon and Ellipse primitives (#533)
* feat: Add Polygon primative to draw number of point polygons such as triangles, hexagon, and octagons including stars. Includes rounding corners and adjustable insetting. * Simplify roundedPolygonPath * Cleanup * Support rotation and simplify * Simplify roundedPolygonPath if no radius * Update docs * feat: Add Ellipse primitive * feat(Polygon): Suppoprt scaleX / scaleY * feat(Polygon): Support `skewX` / `skewY` to create rhombus / parallelogram * feat(Polygon): Support `tiltX` / `tiltY` to create trapezoid * docs(Polygon): Cleanup example * refactor(Polygon): Rename `curveRadius` to `cornerRadius` (alogn with `Arc`) * docs(BarChart): Use Polygon for indicator example * docs(BarChart): Fix indicator example when using canvas (add padding to fix <canvas> clipping) * fix(Polygon): Apply same default classes for canvas as svg * refactor(Polygon): Change default of `inset` to `0` (no inset instead of `1`) and positve values insets inward, negative outward * refactor(Polygon): Change default of `tiltX` and `tiltY` to `0` and adjust logic to support adjusting each side (top/bottom, left/rigth) based on positive/negative)
1 parent 340b97b commit 5cadf7f

File tree

15 files changed

+1021
-12
lines changed

15 files changed

+1021
-12
lines changed

.changeset/nine-pens-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'layerchart': patch
3+
---
4+
5+
feat: Add Polygon primitive

.changeset/wide-berries-invite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'layerchart': patch
3+
---
4+
5+
feat: Add Ellipse primitive
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<script lang="ts" module>
2+
import type { CommonStyleProps, Without } from '$lib/utils/types.js';
3+
4+
export type EllipsePropsWithoutHTML = {
5+
/**
6+
* The center x position of the ellipse.
7+
*
8+
* @default 0
9+
*/
10+
cx?: number;
11+
12+
/**
13+
* The initial center x position of the ellipse.
14+
*
15+
* @default cx
16+
*/
17+
initialCx?: number;
18+
19+
/**
20+
* The center y position of the ellipse.
21+
*
22+
* @default 0
23+
*/
24+
cy?: number;
25+
26+
/**
27+
* The initial center y position of the ellipse.
28+
*
29+
* @default cy
30+
*/
31+
initialCy?: number;
32+
33+
/**
34+
* The radius of the ellipse on the x-axis.
35+
*
36+
* @default 1
37+
*/
38+
rx?: number;
39+
40+
/**
41+
* The initial radius of the ellipse on the x-axis.
42+
*
43+
* @default rx
44+
*/
45+
initialRx?: number;
46+
47+
/**
48+
* The radius of the ellipse on the y-axis.
49+
*
50+
* @default 1
51+
*/
52+
ry?: number;
53+
54+
/**
55+
* The initial radius of the ellipse on the y-axis.
56+
*
57+
* @default ry
58+
*/
59+
initialRy?: number;
60+
61+
/**
62+
* A bindable reference to the `<ellipse>` element
63+
*
64+
* @bindable
65+
*/
66+
ref?: SVGEllipseElement;
67+
68+
motion?: MotionProp;
69+
} & CommonStyleProps;
70+
71+
export type EllipseProps = EllipsePropsWithoutHTML &
72+
Without<SVGAttributes<Element>, EllipsePropsWithoutHTML>;
73+
</script>
74+
75+
<script lang="ts">
76+
import { cls } from '@layerstack/tailwind';
77+
import { merge } from 'lodash-es';
78+
79+
import { getRenderContext } from './Chart.svelte';
80+
import { createMotion, type MotionProp } from '$lib/utils/motion.svelte.js';
81+
import { registerCanvasComponent } from './layout/Canvas.svelte';
82+
import { renderEllipse, type ComputedStylesOptions } from '$lib/utils/canvas.js';
83+
import type { SVGAttributes } from 'svelte/elements';
84+
import { createKey } from '$lib/utils/key.svelte.js';
85+
import { layerClass } from '$lib/utils/attributes.js';
86+
87+
let {
88+
cx = 0,
89+
initialCx: initialCxProp,
90+
cy = 0,
91+
initialCy: initialCyProp,
92+
rx = 1,
93+
initialRx: initialRxProp,
94+
ry = 1,
95+
initialRy: initialRyProp,
96+
motion,
97+
fill,
98+
fillOpacity,
99+
stroke,
100+
strokeWidth,
101+
opacity,
102+
class: className,
103+
ref: refProp = $bindable(),
104+
...restProps
105+
}: EllipseProps = $props();
106+
107+
let ref = $state<SVGEllipseElement>();
108+
109+
$effect.pre(() => {
110+
refProp = ref;
111+
});
112+
113+
const initialCx = initialCxProp ?? cx;
114+
const initialCy = initialCyProp ?? cy;
115+
const initialRx = initialRxProp ?? rx;
116+
const initialRy = initialRyProp ?? ry;
117+
118+
const renderCtx = getRenderContext();
119+
120+
const motionCx = createMotion(initialCx, () => cx, motion);
121+
const motionCy = createMotion(initialCy, () => cy, motion);
122+
const motionRx = createMotion(initialRx, () => rx, motion);
123+
const motionRy = createMotion(initialRy, () => ry, motion);
124+
125+
function render(
126+
ctx: CanvasRenderingContext2D,
127+
styleOverrides: ComputedStylesOptions | undefined
128+
) {
129+
renderEllipse(
130+
ctx,
131+
{ cx: motionCx.current, cy: motionCy.current, rx: motionRx.current, ry: motionRy.current },
132+
styleOverrides
133+
? merge({ styles: { strokeWidth } }, styleOverrides)
134+
: {
135+
styles: { fill, fillOpacity, stroke, strokeWidth, opacity },
136+
classes: className,
137+
}
138+
);
139+
}
140+
141+
// TODO: Use objectId to work around Svelte 4 reactivity issue (even when memoizing gradients)
142+
const fillKey = createKey(() => fill);
143+
const strokeKey = createKey(() => stroke);
144+
145+
if (renderCtx === 'canvas') {
146+
registerCanvasComponent({
147+
name: 'Ellipse',
148+
render,
149+
events: {
150+
click: restProps.onclick,
151+
pointerdown: restProps.onpointerdown,
152+
pointerenter: restProps.onpointerenter,
153+
pointermove: restProps.onpointermove,
154+
pointerleave: restProps.onpointerleave,
155+
},
156+
deps: () => [
157+
motionCx.current,
158+
motionCy.current,
159+
motionRx.current,
160+
motionRy.current,
161+
fillKey.current,
162+
fillOpacity,
163+
strokeKey.current,
164+
strokeWidth,
165+
opacity,
166+
className,
167+
],
168+
});
169+
}
170+
</script>
171+
172+
{#if renderCtx === 'svg'}
173+
<ellipse
174+
bind:this={ref}
175+
cx={motionCx.current}
176+
cy={motionCy.current}
177+
rx={motionRx.current}
178+
ry={motionRy.current}
179+
{fill}
180+
fill-opacity={fillOpacity}
181+
{stroke}
182+
stroke-width={strokeWidth}
183+
{opacity}
184+
class={cls(layerClass('ellipse'), fill == null && 'fill-surface-content', className)}
185+
{...restProps}
186+
/>
187+
{/if}

0 commit comments

Comments
 (0)