feat(edge): add quadratic edge element (#5402)

* feat(edge): add quadratic edge element

* test(edge): add quadratic unit tests

* fix(edge): fix code review

---------

Co-authored-by: yvonneyx <banxuan.zyx@antgroup.com>
This commit is contained in:
Yuxin 2024-01-31 20:34:48 +08:00 committed by GitHub
parent 5f41eaea15
commit 82d28f264c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1315 additions and 371 deletions

View File

@ -0,0 +1,34 @@
import { Quadratic } from '../../../src/elements/edges';
import type { AnimationTestCase } from '../types';
export const edgeQuadratic: AnimationTestCase = async (context) => {
const { canvas } = context;
const quadratic = new Quadratic({
style: {
sourcePoint: [100, 50],
targetPoint: [300, 50],
lineWidth: 2,
stroke: '#1890FF',
labelText: 'quadratic-edge',
labelFontSize: 12,
endArrow: true,
},
});
await canvas.init();
canvas.appendChild(quadratic);
const result = quadratic.animate(
[
{ sourcePoint: [100, 150], targetPoint: [300, 200], lineWidth: 2 },
{ sourcePoint: [100, 150], targetPoint: [450, 350], lineWidth: 10 },
],
{ duration: 1000, fill: 'both' },
);
return result;
};
edgeQuadratic.times = [0, 500, 1000];

View File

@ -5,4 +5,6 @@ export * from './animation-fade-in';
export * from './animation-translate';
export * from './animation-wave';
export * from './animation-zoom-in';
export * from './edge-line';
export * from './edge-quadratic';
export * from './shape-label';

View File

@ -0,0 +1,82 @@
import { Quadratic } from '../../../src/elements/edges';
import type { StaticTestCase } from '../types';
export const edgeQuadratic: StaticTestCase = async (context) => {
const { canvas } = context;
const quadratic1 = new Quadratic({
style: {
// key shape
sourcePoint: [100, 50],
targetPoint: [300, 50],
stroke: '#1890FF',
lineWidth: 2,
// halo
halo: true,
haloOpacity: 0.25,
haloLineWidth: 12,
// label
labelText: 'default quadratic',
labelFontSize: 12,
// end arrow
endArrow: true,
},
});
const quadratic2 = new Quadratic({
style: {
// key shape
sourcePoint: [100, 150],
targetPoint: [300, 150],
controlPoint: [200, 200],
stroke: '#1890FF',
lineWidth: 2,
// label
labelText: 'controlPoint=[200, 200]',
labelFontSize: 12,
// end arrow
endArrow: true,
},
});
const quadratic3 = new Quadratic({
style: {
// key shape
sourcePoint: [100, 250],
targetPoint: [300, 250],
curveOffset: 50,
curvePosition: 0.5,
stroke: '#1890FF',
lineWidth: 2,
// label
labelText: 'curveOffset=50, curvePosition:0.5',
labelFontSize: 12,
// end arrow
endArrow: true,
},
});
const quadratic4 = new Quadratic({
style: {
// key shape
sourcePoint: [100, 350],
targetPoint: [300, 350],
curveOffset: 50,
curvePosition: 0.25,
stroke: '#1890FF',
lineWidth: 2,
// label
labelText: 'curveOffset=50, curvePosition:0.25',
labelFontSize: 12,
// end arrow
endArrow: true,
},
});
await canvas.init();
canvas.appendChild(quadratic1);
canvas.appendChild(quadratic2);
canvas.appendChild(quadratic3);
canvas.appendChild(quadratic4);
};

View File

@ -1,4 +1,5 @@
export * from './edge-line';
export * from './edge-quadratic';
export * from './layered-canvas';
export * from './node-circle';
export * from './shape-badge';

View File

@ -1,135 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g
id="g-root"
fill="none"
stroke="none"
visibility="visible"
font-size="16px"
font-family="sans-serif"
font-style="normal"
font-weight="normal"
font-variant="normal"
text-anchor="left"
stroke-dashoffset="0px"
transform="matrix(1,0,0,1,0,0)"
>
<g
id="g-svg-5"
fill="none"
stroke="rgba(24,144,255,1)"
stroke-width="2px"
marker-start="true"
marker-end="true"
stroke-dasharray="10px,10px"
transform="matrix(1,0,0,1,0,0)"
>
<g
opacity="0"
pointer-events="none"
stroke-width="12px"
stroke-dasharray="0px,0px"
transform="matrix(1,0,0,1,100,150)"
>
<line
id="halo"
fill="none"
x1="0"
y1="0"
x2="200"
y2="50"
stroke="rgba(24,144,255,1)"
/>
</g>
<g
stroke-width="2px"
stroke-dasharray="10px,10px"
transform="matrix(1,0,0,1,100,150)"
>
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="200"
y2="50"
stroke="rgba(24,144,255,1)"
/>
<g
stroke-width="2px"
stroke-dasharray="0px,0px"
transform="matrix(0.970143,0.242536,-0.242536,0.970143,0,0)"
>
<path
id="g-svg-8"
fill="rgba(24,144,255,1)"
d="M 0,5 A 5 5 0 1 0 10 5 A 5 5 0 1 0 0 5 Z"
transform="translate(-5,-5)"
stroke="none"
width="10px"
height="10px"
/>
</g>
<g
stroke-width="2px"
stroke-dasharray="0px,0px"
transform="matrix(-0.970143,-0.242536,0.242536,-0.970143,200,50)"
>
<path
id="g-svg-10"
fill="rgba(255,0,0,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke="none"
width="10px"
height="10px"
/>
</g>
</g>
<g
id="label"
fill="rgba(0,0,0,1)"
stroke="none"
font-size="12px"
text-anchor="middle"
transform="matrix(0.970143,0.242536,-0.242536,0.970143,205.335785,170.149292)"
>
<g transform="matrix(1,0,0,1,-33.187069,-28.430592)">
<path
id="background"
fill="none"
d="M 0,0 l 68.37411720861238,0 l 0,50.86117678317855 l-68.37411720861238 0 z"
stroke="none"
width="68.37411720861238px"
height="50.86117678317855px"
/>
</g>
<g
font-size="12px"
text-anchor="middle"
transform="matrix(1,0,0,1,0,0)"
>
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="alphabetic"
paint-order="stroke"
dx="1"
stroke="none"
>
line-edge
</text>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,97 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-5"
fill="none"
marker-start="true"
marker-end="true"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,100,50)">
<line
id="halo"
fill="none"
x1="0"
y1="0"
x2="200"
y2="150"
stroke-width="12"
stroke-dasharray="0,0"
stroke="rgba(24,144,255,1)"
pointer-events="none"
opacity="0"
/>
</g>
<g transform="matrix(1,0,0,1,100,50)">
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="200"
y2="150"
stroke-width="2"
stroke-dasharray="10,10"
stroke="rgba(24,144,255,1)"
/>
<g transform="matrix(0.800000,0.600000,-0.600000,0.800000,0,0)">
<path
id="g-svg-8"
fill="rgba(24,144,255,1)"
d="M 0,5 A 5 5 0 1 0 10 5 A 5 5 0 1 0 0 5 Z"
transform="translate(-5,-5)"
stroke-width="2"
stroke-dasharray="0,0"
stroke="rgba(24,144,255,1)"
/>
</g>
<g transform="matrix(-0.800000,-0.600000,0.600000,-0.800000,200,150)">
<path
id="g-svg-10"
fill="rgba(255,0,0,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke-width="2"
stroke-dasharray="0,0"
stroke="rgba(24,144,255,1)"
/>
</g>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,204,44)">
<g transform="matrix(1,0,0,1,-27.540001,-23)">
<path
id="background"
fill="none"
d="M 0,0 l 56.080000000000005,0 l 0,23 l-56.080000000000005 0 z"
width="56.080000000000005"
height="23"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
dy="-11.5px"
text-anchor="middle"
font-size="12"
>
line-edge
</text>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,135 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g
id="g-root"
fill="none"
stroke="none"
visibility="visible"
font-size="16px"
font-family="sans-serif"
font-style="normal"
font-weight="normal"
font-variant="normal"
text-anchor="left"
stroke-dashoffset="0px"
transform="matrix(1,0,0,1,0,0)"
>
<g
id="g-svg-5"
fill="none"
stroke="rgba(24,144,255,1)"
stroke-width="2px"
marker-start="true"
marker-end="true"
stroke-dasharray="10px,10px"
transform="matrix(1,0,0,1,0,0)"
>
<g
opacity="0.25"
pointer-events="none"
stroke-width="12px"
stroke-dasharray="0px,0px"
transform="matrix(1,0,0,1,100,150)"
>
<line
id="halo"
fill="none"
x1="0"
y1="0"
x2="350"
y2="200"
stroke="rgba(24,144,255,1)"
/>
</g>
<g
stroke-width="2px"
stroke-dasharray="10px,10px"
transform="matrix(1,0,0,1,100,150)"
>
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="350"
y2="200"
stroke="rgba(24,144,255,1)"
/>
<g
stroke-width="2px"
stroke-dasharray="0px,0px"
transform="matrix(0.868243,0.496139,-0.496139,0.868243,0,0)"
>
<path
id="g-svg-8"
fill="rgba(24,144,255,1)"
d="M 0,5 A 5 5 0 1 0 10 5 A 5 5 0 1 0 0 5 Z"
transform="translate(-5,-5)"
stroke="none"
width="10px"
height="10px"
/>
</g>
<g
stroke-width="2px"
stroke-dasharray="0px,0px"
transform="matrix(-0.868243,-0.496139,0.496139,-0.868243,350,200)"
>
<path
id="g-svg-10"
fill="rgba(255,0,0,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke="none"
width="10px"
height="10px"
/>
</g>
</g>
<g
id="label"
fill="rgba(0,0,0,1)"
stroke="none"
font-size="12px"
text-anchor="middle"
transform="matrix(0.970143,0.242536,-0.242536,0.970143,205.335785,170.149292)"
>
<g transform="matrix(1,0,0,1,-33.187069,-28.430592)">
<path
id="background"
fill="none"
d="M 0,0 l 68.37411720861238,0 l 0,50.86117678317855 l-68.37411720861238 0 z"
stroke="none"
width="68.37411720861238px"
height="50.86117678317855px"
/>
</g>
<g
font-size="12px"
text-anchor="middle"
transform="matrix(1,0,0,1,0,0)"
>
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="alphabetic"
paint-order="stroke"
dx="1"
stroke="none"
>
line-edge
</text>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,97 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-5"
fill="none"
marker-start="true"
marker-end="true"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,100,50)">
<line
id="halo"
fill="none"
x1="0"
y1="0"
x2="350"
y2="300"
stroke-width="12"
stroke-dasharray="0,0"
stroke="rgba(24,144,255,1)"
pointer-events="none"
opacity="0.25"
/>
</g>
<g transform="matrix(1,0,0,1,100,50)">
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="350"
y2="300"
stroke-width="2"
stroke-dasharray="10,10"
stroke="rgba(24,144,255,1)"
/>
<g transform="matrix(0.759257,0.650791,-0.650791,0.759257,0,0)">
<path
id="g-svg-8"
fill="rgba(24,144,255,1)"
d="M 0,5 A 5 5 0 1 0 10 5 A 5 5 0 1 0 0 5 Z"
transform="translate(-5,-5)"
stroke-width="2"
stroke-dasharray="0,0"
stroke="rgba(24,144,255,1)"
/>
</g>
<g transform="matrix(-0.759257,-0.650791,0.650791,-0.759257,350,300)">
<path
id="g-svg-10"
fill="rgba(255,0,0,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke-width="2"
stroke-dasharray="0,0"
stroke="rgba(24,144,255,1)"
/>
</g>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,204,44)">
<g transform="matrix(1,0,0,1,-27.540001,-23)">
<path
id="background"
fill="none"
d="M 0,0 l 56.080000000000005,0 l 0,23 l-56.080000000000005 0 z"
width="56.080000000000005"
height="23"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
dy="-11.5px"
text-anchor="middle"
font-size="12"
>
line-edge
</text>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,97 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-5"
fill="none"
marker-start="true"
marker-end="true"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,100,50)">
<line
id="halo"
fill="none"
x1="0"
y1="0"
x2="275"
y2="225"
stroke-width="12"
stroke-dasharray="0,0"
stroke="rgba(24,144,255,1)"
pointer-events="none"
opacity="0.125"
/>
</g>
<g transform="matrix(1,0,0,1,100,50)">
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="275"
y2="225"
stroke-width="2"
stroke-dasharray="10,10"
stroke="rgba(24,144,255,1)"
/>
<g transform="matrix(0.773957,0.633238,-0.633238,0.773957,0,0)">
<path
id="g-svg-8"
fill="rgba(24,144,255,1)"
d="M 0,5 A 5 5 0 1 0 10 5 A 5 5 0 1 0 0 5 Z"
transform="translate(-5,-5)"
stroke-width="2"
stroke-dasharray="0,0"
stroke="rgba(24,144,255,1)"
/>
</g>
<g transform="matrix(-0.773957,-0.633238,0.633238,-0.773957,275,225)">
<path
id="g-svg-10"
fill="rgba(255,0,0,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke-width="2"
stroke-dasharray="0,0"
stroke="rgba(24,144,255,1)"
/>
</g>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,204,44)">
<g transform="matrix(1,0,0,1,-27.540001,-23)">
<path
id="background"
fill="none"
d="M 0,0 l 56.080000000000005,0 l 0,23 l-56.080000000000005 0 z"
width="56.080000000000005"
height="23"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
dy="-11.5px"
text-anchor="middle"
font-size="12"
>
line-edge
</text>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,71 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-5"
fill="none"
marker-start="false"
marker-end="true"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,100,149.710602)">
<path
id="key"
fill="none"
d="M 0,0.2893917355592919 Q 92.72393124891002 -3.8148832688006564,200 50.28939173555929"
stroke-width="2"
stroke="rgba(24,144,255,1)"
/>
<g transform="matrix(-0.892870,-0.450315,0.450315,-0.892870,200,50.289391)">
<path
id="g-svg-8"
fill="rgba(24,144,255,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke-width="2"
stroke="rgba(24,144,255,1)"
stroke-dasharray="0,0"
/>
</g>
</g>
<g
id="label"
fill="none"
transform="matrix(0.999550,0.029986,-0.029986,0.999550,204.178116,29.122644)"
>
<g transform="matrix(1,0,0,1,-50.449379,-30.713144)">
<path
id="background"
fill="none"
d="M 0,0 l 101.89874994810936,0 l 0,38.42628857535926 l-101.89874994810936 0 z"
width="101.89874994810936"
height="38.42628857535926"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
dy="-11.5px"
text-anchor="middle"
font-size="12"
>
quadratic-edge
</text>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,71 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-5"
fill="none"
marker-start="false"
marker-end="true"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,100,150)">
<path
id="key"
fill="none"
d="M 0,0 Q 160.11583184929498 73.95270573626621,350 200"
stroke-width="10"
stroke="rgba(24,144,255,1)"
/>
<g transform="matrix(-0.833147,-0.553052,0.553052,-0.833147,350,200)">
<path
id="g-svg-8"
fill="rgba(24,144,255,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke-width="2"
stroke="rgba(24,144,255,1)"
stroke-dasharray="0,0"
/>
</g>
</g>
<g
id="label"
fill="none"
transform="matrix(0.999550,0.029986,-0.029986,0.999550,204.178116,29.122644)"
>
<g transform="matrix(1,0,0,1,-50.449379,-30.713144)">
<path
id="background"
fill="none"
d="M 0,0 l 101.89874994810936,0 l 0,38.42628857535926 l-101.89874994810936 0 z"
width="101.89874994810936"
height="38.42628857535926"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
dy="-11.5px"
text-anchor="middle"
font-size="12"
>
quadratic-edge
</text>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,71 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-5"
fill="none"
marker-start="false"
marker-end="true"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,100,150)">
<path
id="key"
fill="none"
d="M 0,0 C 84.27992103273499 23.282810243968754,175.94658769940168 64.94947691063538,275 125"
stroke-width="6"
stroke="rgba(24,144,255,1)"
/>
<g transform="matrix(-0.855128,-0.518416,0.518416,-0.855128,275,125)">
<path
id="g-svg-8"
fill="rgba(24,144,255,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke-width="2"
stroke="rgba(24,144,255,1)"
stroke-dasharray="0,0"
/>
</g>
</g>
<g
id="label"
fill="none"
transform="matrix(0.999550,0.029986,-0.029986,0.999550,204.178116,29.122644)"
>
<g transform="matrix(1,0,0,1,-50.449379,-30.713144)">
<path
id="background"
fill="none"
d="M 0,0 l 101.89874994810936,0 l 0,38.42628857535926 l-101.89874994810936 0 z"
width="101.89874994810936"
height="38.42628857535926"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
dy="-11.5px"
text-anchor="middle"
font-size="12"
>
quadratic-edge
</text>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -51,9 +51,7 @@
fill="rgba(24,144,255,1)"
d="M 0,5 L 5,0 L 10,5 L 5,10 Z"
transform="translate(-5,-5)"
stroke="none"
width="10px"
height="10px"
stroke="rgba(24,144,255,1)"
/>
</g>
</g>
@ -144,9 +142,7 @@
fill="rgba(24,144,255,1)"
d="M 0,5 A 5 5 0 1 0 10 5 A 5 5 0 1 0 0 5 Z"
transform="translate(-5,-5)"
stroke="none"
width="10px"
height="10px"
stroke="rgba(24,144,255,1)"
/>
</g>
<g
@ -159,9 +155,7 @@
fill="rgba(255,0,0,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke="none"
width="10px"
height="10px"
stroke="rgba(24,144,255,1)"
/>
</g>
</g>
@ -254,10 +248,7 @@
x="0"
y="0"
href="https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ"
transform="translate(-25,-25)"
stroke="none"
width="50px"
height="50px"
stroke="rgba(24,144,255,1)"
/>
</g>
<g
@ -273,8 +264,6 @@
cy="25"
stroke="rgba(24,144,255,1)"
r="25px"
width="10px"
height="10px"
/>
</g>
</g>

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -0,0 +1,305 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g
id="g-root"
fill="none"
stroke="none"
visibility="visible"
font-size="16px"
font-family="sans-serif"
font-style="normal"
font-weight="normal"
font-variant="normal"
text-anchor="left"
stroke-dashoffset="0px"
transform="matrix(1,0,0,1,0,0)"
>
<g
id="g-svg-5"
fill="none"
stroke="rgba(24,144,255,1)"
stroke-width="2px"
marker-start="false"
marker-end="true"
transform="matrix(1,0,0,1,0,0)"
>
<g
opacity="0.25"
pointer-events="none"
stroke-width="12px"
stroke-dasharray="0px,0px"
transform="matrix(1,0,0,1,100,35)"
>
<path
id="halo"
fill="none"
d="M 0,15 Q 100 -15,200 15"
stroke="rgba(24,144,255,1)"
/>
</g>
<g stroke-width="2px" transform="matrix(1,0,0,1,100,35)">
<path
id="key"
fill="none"
d="M 0,15 Q 100 -15,200 15"
stroke="rgba(24,144,255,1)"
/>
<g
stroke-width="2px"
stroke-dasharray="0px,0px"
transform="matrix(-0.957826,-0.287348,0.287348,-0.957826,200,15)"
>
<path
id="g-svg-8"
fill="rgba(24,144,255,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke="rgba(24,144,255,1)"
/>
</g>
</g>
<g
id="label"
fill="none"
stroke="none"
font-size="12px"
text-anchor="middle"
transform="matrix(0.999550,0.029986,-0.029986,0.999550,204.178116,29.122644)"
>
<g transform="matrix(1,0,0,1,-55.159351,-23.023664)">
<path
id="background"
fill="none"
d="M 0,0 l 112.31869543715979,0 l 0,40.047326504443134 l-112.31869543715979 0 z"
stroke="none"
width="112.31869543715979px"
height="40.047326504443134px"
/>
</g>
<g
font-size="12px"
text-anchor="middle"
transform="matrix(1,0,0,1,0,0)"
>
<text
id="text"
fill="none"
dominant-baseline="alphabetic"
paint-order="stroke"
dx="1"
stroke="none"
>
default quadratic
</text>
</g>
</g>
</g>
<g
id="g-svg-13"
fill="none"
stroke="rgba(24,144,255,1)"
stroke-width="2px"
marker-start="false"
marker-end="true"
transform="matrix(1,0,0,1,0,0)"
>
<g stroke-width="2px" transform="matrix(1,0,0,1,100,150)">
<path
id="key"
fill="none"
d="M 0,0 Q 100 50,200 0"
stroke="rgba(24,144,255,1)"
/>
<g
stroke-width="2px"
stroke-dasharray="0px,0px"
transform="matrix(-0.894427,0.447214,-0.447214,-0.894427,200,0)"
>
<path
id="g-svg-16"
fill="rgba(24,144,255,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke="rgba(24,144,255,1)"
/>
</g>
</g>
<g
id="label"
fill="none"
stroke="none"
font-size="12px"
text-anchor="middle"
transform="matrix(0.998752,-0.049935,0.049935,0.998752,203.695404,168.807739)"
>
<g transform="matrix(1,0,0,1,-85.936951,-28.053503)">
<path
id="background"
fill="none"
d="M 0,0 l 173.87391428120713,0 l 0,50.1070253812409 l-173.87391428120713 0 z"
stroke="none"
width="173.87391428120713px"
height="50.1070253812409px"
/>
</g>
<g
font-size="12px"
text-anchor="middle"
transform="matrix(1,0,0,1,0,0)"
>
<text
id="text"
fill="none"
dominant-baseline="alphabetic"
paint-order="stroke"
dx="1"
stroke="none"
>
controlPoint=[200, 200]
</text>
</g>
</g>
</g>
<g
id="g-svg-20"
fill="none"
stroke="rgba(24,144,255,1)"
stroke-width="2px"
marker-start="false"
marker-end="true"
transform="matrix(1,0,0,1,0,0)"
>
<g stroke-width="2px" transform="matrix(1,0,0,1,100,225)">
<path
id="key"
fill="none"
d="M 0,25 Q 100 -25,200 25"
stroke="rgba(24,144,255,1)"
/>
<g
stroke-width="2px"
stroke-dasharray="0px,0px"
transform="matrix(-0.894427,-0.447214,0.447214,-0.894427,200,25)"
>
<path
id="g-svg-23"
fill="rgba(24,144,255,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke="rgba(24,144,255,1)"
/>
</g>
</g>
<g
id="label"
fill="none"
stroke="none"
font-size="12px"
text-anchor="middle"
transform="matrix(0.998752,0.049935,-0.049935,0.998752,204.294617,219.207230)"
>
<g transform="matrix(1,0,0,1,-108.496948,-30.303787)">
<path
id="background"
fill="none"
d="M 0,0 l 218.99391546491714,0 l 0,54.60757626551481 l-218.99391546491714 0 z"
stroke="none"
width="218.99391546491714px"
height="54.60757626551481px"
/>
</g>
<g
font-size="12px"
text-anchor="middle"
transform="matrix(1,0,0,1,0,0)"
>
<text
id="text"
fill="none"
dominant-baseline="alphabetic"
paint-order="stroke"
dx="1"
stroke="none"
>
curveOffset=50, curvePosition:0.5
</text>
</g>
</g>
</g>
<g
id="g-svg-27"
fill="none"
stroke="rgba(24,144,255,1)"
stroke-width="2px"
marker-start="false"
marker-end="true"
transform="matrix(1,0,0,1,0,0)"
>
<g stroke-width="2px" transform="matrix(1,0,0,1,100,325)">
<path
id="key"
fill="none"
d="M 0,25 Q 50 -25,200 25"
stroke="rgba(24,144,255,1)"
/>
<g
stroke-width="2px"
stroke-dasharray="0px,0px"
transform="matrix(-0.948683,-0.316228,0.316228,-0.948683,200,25)"
>
<path
id="g-svg-30"
fill="rgba(24,144,255,1)"
d="M 0,5 L 10,0 L 10,10 Z"
transform="translate(-5,-5)"
stroke="rgba(24,144,255,1)"
/>
</g>
</g>
<g
id="label"
fill="none"
stroke="none"
font-size="12px"
text-anchor="middle"
transform="matrix(0.991600,0.129341,-0.129341,0.991600,203.144791,320.881134)"
>
<g transform="matrix(1,0,0,1,-114.098114,-47.451683)">
<path
id="background"
fill="none"
d="M 0,0 l 230.1962301306265,0 l 0,88.90336656842001 l-230.1962301306265 0 z"
stroke="none"
width="230.1962301306265px"
height="88.90336656842001px"
/>
</g>
<g
font-size="12px"
text-anchor="middle"
transform="matrix(1,0,0,1,0,0)"
>
<text
id="text"
fill="none"
dominant-baseline="alphabetic"
paint-order="stroke"
dx="1"
stroke="none"
>
curveOffset=50, curvePosition:0.25
</text>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -1,81 +1,110 @@
import { Line } from '@antv/g';
import { getLabelPositionStyle } from '../../../src/utils/edge';
import { Point } from '../../../src/types';
import { getLabelPositionStyle, getQuadraticPath } from '../../../src/utils/edge';
describe('edge', () => {
// horizontal line
const line = new Line({
style: {
x1: 0,
y1: 100,
x2: 100,
y2: 100,
},
describe('getLabelPositionStyle', () => {
it('should return correctly label position style', () => {
// horizontal line
const line = new Line({
style: {
x1: 0,
y1: 100,
x2: 100,
y2: 100,
},
});
// with rotation angle below Math.PI
const line1 = new Line({
style: {
x1: 0,
y1: 100,
x2: 100,
y2: 200,
},
});
// with rotation angle over Math.PI
const line2 = new Line({
style: {
x1: 0,
y1: 200,
x2: 100,
y2: 100,
},
});
const labelPosition = getLabelPositionStyle(line, 'center', false);
expect(labelPosition.textAlign).toEqual('center');
expect(labelPosition.x).toEqual(50);
expect(labelPosition.y).toEqual(100);
const labelPosition2 = getLabelPositionStyle(line, 'center', true, 5, 5);
expect(labelPosition2.textAlign).toEqual('center');
expect(labelPosition2.x).toEqual(55);
expect(labelPosition2.y).toEqual(105);
const labelPosition3 = getLabelPositionStyle(line, 'start', true, 5, 5);
expect(labelPosition3.textAlign).toEqual('left');
expect(labelPosition3.x).toEqual(5);
expect(labelPosition3.y).toEqual(105);
const labelPosition4 = getLabelPositionStyle(line, 'end', true, 5, 5);
expect(labelPosition4.textAlign).toEqual('right');
expect(labelPosition4.x).toBeCloseTo(100 * 0.99 + 5);
expect(labelPosition4.y).toEqual(105);
const labelPosition5 = getLabelPositionStyle(line, 0.5, true, 5, 5);
expect(labelPosition5.textAlign).toEqual('center');
expect(labelPosition5.x).toEqual(55);
expect(labelPosition5.y).toEqual(105);
// with rotation angle below Math.PI
const labelPosition6 = getLabelPositionStyle(line1, 'center', true, 5, 5);
expect(labelPosition6.textAlign).toEqual('center');
expect(labelPosition6.transform).toEqual('rotate(45deg)');
expect(labelPosition6.x).toEqual(50 + 5 * Math.cos(Math.PI / 4) - 5 * Math.sin(Math.PI / 4));
expect(labelPosition6.y).toEqual(150 + 5 * Math.sin(Math.PI / 4) + 5 * Math.cos(Math.PI / 4));
const labelPosition7 = getLabelPositionStyle(line1, 'start', true, 5, 5);
expect(labelPosition7.textAlign).toEqual('left');
const labelPosition8 = getLabelPositionStyle(line1, 'end', true, 5, 5);
expect(labelPosition8.textAlign).toEqual('right');
// with rotation angle over Math.PI
const labelPosition9 = getLabelPositionStyle(line2, 'center', true, 5, 5);
expect(labelPosition9.textAlign).toEqual('center');
expect(labelPosition9.transform).toEqual('rotate(-45deg)');
expect(labelPosition9.x).toEqual(50 + 5 * Math.cos(-Math.PI / 4) - 5 * Math.sin(-Math.PI / 4));
expect(labelPosition9.y).toEqual(150 + 5 * Math.sin(-Math.PI / 4) + 5 * Math.cos(-Math.PI / 4));
});
});
// with rotation angle below Math.PI
const line1 = new Line({
style: {
x1: 0,
y1: 100,
x2: 100,
y2: 200,
},
});
describe('getQuadraticPath', () => {
const sourcePoint: Point = [0, 10];
const targetPoint: Point = [10, 10];
const curvePosition = 0.5;
const curveOffset = 5;
const controlPoint: Point = [100, 100];
// with rotation angle over Math.PI
const line2 = new Line({
style: {
x1: 0,
y1: 200,
x2: 100,
y2: 100,
},
});
it('should return the correct path for given source and target points with a control point', () => {
const path = getQuadraticPath(sourcePoint, targetPoint, curvePosition, curveOffset, controlPoint);
it('getLabelPositionStyle', () => {
const labelPosition = getLabelPositionStyle(line, 'center', false);
expect(labelPosition.textAlign).toEqual('center');
expect(labelPosition.x).toEqual(50);
expect(labelPosition.y).toEqual(100);
expect(path).toEqual([
['M', 0, 10],
['Q', 100, 100, 10, 10],
]);
});
const labelPosition2 = getLabelPositionStyle(line, 'center', true, 5, 5);
expect(labelPosition2.textAlign).toEqual('center');
expect(labelPosition2.x).toEqual(55);
expect(labelPosition2.y).toEqual(105);
it('should return the correct path for given source and target points without a control point', () => {
const path = getQuadraticPath(sourcePoint, targetPoint, curvePosition, curveOffset);
const labelPosition3 = getLabelPositionStyle(line, 'start', true, 5, 5);
expect(labelPosition3.textAlign).toEqual('left');
expect(labelPosition3.x).toEqual(5);
expect(labelPosition3.y).toEqual(105);
const labelPosition4 = getLabelPositionStyle(line, 'end', true, 5, 5);
expect(labelPosition4.textAlign).toEqual('right');
expect(labelPosition4.x).toBeCloseTo(100 * 0.99 + 5);
expect(labelPosition4.y).toEqual(105);
const labelPosition5 = getLabelPositionStyle(line, 0.5, true, 5, 5);
expect(labelPosition5.textAlign).toEqual('center');
expect(labelPosition5.x).toEqual(55);
expect(labelPosition5.y).toEqual(105);
// with rotation angle below Math.PI
const labelPosition6 = getLabelPositionStyle(line1, 'center', true, 5, 5);
expect(labelPosition6.textAlign).toEqual('center');
expect(labelPosition6.transform).toEqual('rotate(45deg)');
expect(labelPosition6.x).toEqual(50 + 5 * Math.cos(Math.PI / 4) - 5 * Math.sin(Math.PI / 4));
expect(labelPosition6.y).toEqual(150 + 5 * Math.sin(Math.PI / 4) + 5 * Math.cos(Math.PI / 4));
const labelPosition7 = getLabelPositionStyle(line1, 'start', true, 5, 5);
expect(labelPosition7.textAlign).toEqual('left');
const labelPosition8 = getLabelPositionStyle(line1, 'end', true, 5, 5);
expect(labelPosition8.textAlign).toEqual('right');
// with rotation angle over Math.PI
const labelPosition9 = getLabelPositionStyle(line2, 'center', true, 5, 5);
expect(labelPosition9.textAlign).toEqual('center');
expect(labelPosition9.transform).toEqual('rotate(-45deg)');
expect(labelPosition9.x).toEqual(50 + 5 * Math.cos(-Math.PI / 4) - 5 * Math.sin(-Math.PI / 4));
expect(labelPosition9.y).toEqual(150 + 5 * Math.sin(-Math.PI / 4) + 5 * Math.cos(-Math.PI / 4));
expect(path).toEqual([
['M', 0, 10],
['Q', 5, 5, 10, 10],
]);
});
});
});

View File

@ -0,0 +1,31 @@
import { add, distance, divide, multiply, normalize, perpendicular, subtract } from '../../../src/utils/vector';
describe('Vector Functions', () => {
it('add', () => {
expect(add([0, 1], [2, 3])).toEqual([2, 4]);
});
it('subtract', () => {
expect(subtract([0, 1], [2, 3])).toEqual([-2, -2]);
});
it('multiply', () => {
expect(multiply([0, 1], [2, 3])).toEqual([0, 3]);
});
it('divide', () => {
expect(divide([0, 1], [2, 3])).toEqual([0, 1 / 3]);
});
it('distance', () => {
expect(distance([0, 0], [3, 4])).toEqual(5);
});
it('normalize', () => {
expect(normalize([3, 4])).toEqual([0.6, 0.8]);
});
it('perpendicular', () => {
expect(perpendicular([1, 0])).toEqual([-0, -1]);
});
});

View File

@ -8,7 +8,7 @@ import type {
} from '@antv/g';
import { Path } from '@antv/g';
import { deepMix, isFunction } from '@antv/util';
import type { Point, PrefixObject } from '../../types';
import type { PrefixObject } from '../../types';
import type { EdgeKey, EdgeLabelStyleProps } from '../../types/edge';
import { getLabelPositionStyle } from '../../utils/edge';
import { omitStyleProps, subStyleProps } from '../../utils/prefix';
@ -31,8 +31,6 @@ type EdgeArrowStyleProps = {
export type BaseEdgeStyleProps<KT extends object> = BaseShapeStyleProps &
KT & {
sourcePoint?: Point;
targetPoint?: Point;
label?: boolean;
halo?: boolean;
startArrow?: boolean;
@ -128,7 +126,7 @@ export abstract class BaseEdge<KT extends object, KS extends DisplayObject> exte
}
private getArrowStyle(attributes: ParsedBaseEdgeStyleProps<KT>, isStart: boolean) {
const { stroke, ...keyStyle } = this.getKeyStyle(attributes) as BaseStyleProps;
const keyStyle = this.getKeyStyle(attributes) as BaseStyleProps;
const arrowType = isStart ? 'startArrow' : 'endArrow';
const { width, height, type, ctor, ...arrowStyle } = subStyleProps<Required<EdgeArrowStyleProps>>(
this.getGraphicStyle(attributes),
@ -140,12 +138,9 @@ export abstract class BaseEdge<KT extends object, KS extends DisplayObject> exte
const arrowFn = isFunction(type) ? type : Symbol[type] || Symbol.triangle;
path = arrowFn(width, height);
}
return {
...keyStyle,
...(path && { path, fill: type === 'simple' ? '' : stroke }),
width,
height,
...(path && { path, fill: type === 'simple' ? '' : keyStyle.stroke }),
...arrowStyle,
};
}
@ -182,6 +177,8 @@ export abstract class BaseEdge<KT extends object, KS extends DisplayObject> exte
result.onframe = () => {
this.drawLabelShape(this.parsedAttributes, this);
// this.drawArrow(this.parsedAttributes, true);
// this.drawArrow(this.parsedAttributes, false);
};
return result;

View File

@ -1,3 +1,5 @@
export { Line } from './line';
export { Quadratic } from './quadratic';
export type { LineStyleProps } from './line';
export type { QuadraticStyleProps } from './quadratic';

View File

@ -1,12 +1,24 @@
import type { DisplayObjectConfig, LineStyleProps as GLineStyleProps, Group } from '@antv/g';
import { Line as GLine } from '@antv/g';
import { deepMix } from '@antv/util';
import type { Point } from '../../types';
import type { BaseEdgeStyleProps } from './base-edge';
import { BaseEdge } from './base-edge';
export type LineStyleProps = BaseEdgeStyleProps<LineKeyStyleProps>;
type LineKeyStyleProps = Partial<GLineStyleProps> & {
/**
* <zh/>
* <en/> The source point. Represents the start of the edge
*/
sourcePoint: Point;
/**
* <zh/>
* <en/> The target point. Represents the end of the edge
*/
targetPoint: Point;
};
type LineKeyStyleProps = Omit<GLineStyleProps, 'x1' | 'y1' | 'x2' | 'y2'>;
export type LineStyleProps = BaseEdgeStyleProps<LineKeyStyleProps>;
type ParsedLineStyleProps = Required<LineStyleProps>;
@ -15,9 +27,11 @@ type LineOptions = DisplayObjectConfig<LineStyleProps>;
/**
* Draw line based on BaseEdge, override drawKeyShape
*/
export class Line extends BaseEdge<LineKeyStyleProps, GLine> {
export class Line extends BaseEdge<GLineStyleProps, GLine> {
static defaultStyleProps: Partial<LineStyleProps> = {};
constructor(options: LineOptions) {
super(options);
super(deepMix({}, { style: Line.defaultStyleProps }, options));
}
protected drawKeyShape(attributes: ParsedLineStyleProps, container: Group): GLine | undefined {
@ -25,10 +39,8 @@ export class Line extends BaseEdge<LineKeyStyleProps, GLine> {
}
protected getKeyStyle(attributes: ParsedLineStyleProps): GLineStyleProps {
const { sourcePoint, targetPoint, ...keyShape } = super.getKeyStyle(attributes) as unknown as GLineStyleProps & {
sourcePoint: Point;
targetPoint: Point;
};
const { sourcePoint, targetPoint, ...keyShape } = super.getKeyStyle(attributes) as LineKeyStyleProps;
return { ...keyShape, x1: sourcePoint[0], y1: sourcePoint[1], x2: targetPoint[0], y2: targetPoint[1] };
}
}

View File

@ -0,0 +1,68 @@
import type { DisplayObjectConfig, Group, PathStyleProps } from '@antv/g';
import { Path } from '@antv/g';
import { deepMix } from '@antv/util';
import type { Point } from '../../types';
import { getQuadraticPath } from '../../utils/edge';
import type { BaseEdgeStyleProps } from './base-edge';
import { BaseEdge } from './base-edge';
type QuadraticKeyStyleProps = PathStyleProps & {
/**
* <zh/>
* <en/> The source point. Represents the start of the edge
*/
sourcePoint: Point;
/**
* <zh/>
* <en/> The target point. Represents the end of the edge
*/
targetPoint: Point;
/**
* <zh/> 线`curveOffset``curvePosition`
* <en/> Control point. Used to define the shape of the curve. If not specified, it will be calculated using `curveOffset` and `curvePosition`.
*/
controlPoint?: Point;
/**
* <zh/> 线`0-1`
* <en/> The relative position of the control point on the line, ranging from `0-1`
*/
curvePosition?: number;
/**
* <zh/> 线
* <en/> The distance of the control point from the line
*/
curveOffset?: number;
};
export type QuadraticStyleProps = BaseEdgeStyleProps<QuadraticKeyStyleProps>;
type ParsedQuadraticStyleProps = Required<QuadraticStyleProps>;
type QuadraticOptions = DisplayObjectConfig<QuadraticStyleProps>;
export class Quadratic extends BaseEdge<PathStyleProps, Path> {
static defaultStyleProps: Partial<QuadraticStyleProps> = {
curvePosition: 0.5,
curveOffset: 30,
isBillboard: true,
};
constructor(options: QuadraticOptions) {
super(deepMix({}, { style: Quadratic.defaultStyleProps }, options));
}
protected drawKeyShape(attributes: ParsedQuadraticStyleProps, container: Group): Path | undefined {
return this.upsert('key', Path, this.getKeyStyle(attributes), container);
}
protected getKeyStyle(attributes: ParsedQuadraticStyleProps): PathStyleProps {
const { sourcePoint, targetPoint, controlPoint, curvePosition, curveOffset, ...keyStyle } = super.getKeyStyle(
attributes,
) as Required<QuadraticKeyStyleProps>;
return {
...keyStyle,
path: getQuadraticPath(sourcePoint, targetPoint, curvePosition, curveOffset, controlPoint),
};
}
}

View File

@ -1,6 +1,9 @@
import type { PathArray } from '@antv/util';
import { pick } from '@antv/util';
import type { Point, Vector2 } from '../types';
import type { EdgeKey, EdgeLabelPosition, EdgeLabelStyleProps } from '../types/edge';
import { isHorizontal } from './point';
import { normalize, perpendicular, subtract } from './vector';
/**
* <zh/>
@ -92,3 +95,59 @@ function applyAutoRotation(key: EdgeKey, positionStyle: Partial<EdgeLabelStylePr
adjustLabelPosition(key, positionStyle, ratio, angle);
positionStyle.transform = `rotate(${(angle / Math.PI) * 180}deg)`;
}
/** ==================== Quadratic Edge =========================== */
/**
* <zh/> 线
*
* <en/> Calculate the path for drawing a quadratic Bessel curve
* @param sourcePoint - <zh/> | <en/> Source point
* @param targetPoint - <zh/> | <en/> Target point
* @param curvePosition - <zh/> 线 0-1 | <en/> The relative position of the control point on the line (value range from 0 to 1)
* @param curveOffset - <zh/> 线 | <en/> The distance between the control point and the line
* @param controlPoint - <zh/> | <en/> Control point
* @returns <zh/> 线 | <en/> Returns curve path
*/
export function getQuadraticPath(
sourcePoint: Point,
targetPoint: Point,
curvePosition: number,
curveOffset: number,
controlPoint?: Point,
): PathArray {
const actualControlPoint =
controlPoint || calculateControlPoint(sourcePoint, targetPoint, curvePosition, curveOffset);
return [
['M', sourcePoint[0], sourcePoint[1]],
['Q', actualControlPoint[0], actualControlPoint[1], targetPoint[0], targetPoint[1]],
];
}
/**
* <zh/> 线
*
* <en/> Calculate the control point of Quadratic Bessel curve
* @param sourcePoint - <zh/> | <en/> Source point
* @param targetPoint - <zh/> | <en/> Target point
* @param curvePosition - <zh/> 线 0-1 | <en/> The relative position of the control point on the line (value range from 0 to 1)
* @param curveOffset - <zh/> 线 | <en/> The distance between the control point and the line
* @returns <zh/> | <en/> Control points
*/
function calculateControlPoint(
sourcePoint: Point,
targetPoint: Point,
curvePosition: number,
curveOffset: number,
): Point {
const lineVector = subtract(targetPoint as Vector2, sourcePoint as Vector2);
const controlPoint: Point = [
sourcePoint[0] + curvePosition * lineVector[0],
sourcePoint[1] + curvePosition * lineVector[1],
];
const perpVector = normalize(perpendicular(lineVector));
controlPoint[0] += curveOffset * perpVector[0];
controlPoint[1] += curveOffset * perpVector[1];
return controlPoint;
}

View File

@ -0,0 +1,99 @@
import type { Vector2, Vector3 } from '../types';
export function add(a: Vector2, b: Vector2): Vector2;
export function add(a: Vector3, b: Vector3): Vector3;
/**
* <zh/>
*
* <en/> Adds two vectors
* @param a - <zh/> | <en/> The first vector
* @param b - <zh/> | <en/> The second vector
* @returns <zh/> | <en/> The sum of the two vectors
*/
export function add(a: Vector2 | Vector3, b: Vector2 | Vector3): Vector2 | Vector3 {
return a.map((v, i) => v + b[i]) as Vector2 | Vector3;
}
export function subtract(a: Vector2, b: Vector2): Vector2;
export function subtract(a: Vector3, b: Vector3): Vector3;
/**
* <zh/>
*
* <en/> Subtracts two vectors
* @param a - <zh/> | <en/> The first vector
* @param b - <zh/> | <en/> The second vector
* @returns <zh/> | <en/> The difference of the two vectors
*/
export function subtract(a: Vector2 | Vector3, b: Vector2 | Vector3): Vector2 | Vector3 {
return a.map((v, i) => v - b[i]) as Vector2 | Vector3;
}
export function multiply(a: Vector2, b: Vector2): Vector2;
export function multiply(a: Vector3, b: Vector3): Vector3;
/**
* <zh/>
*
* <en/> Multiplies two vectors
* @param a - <zh/> | <en/> The first vector
* @param b - <zh/> | <en/> The second vector
* @returns <zh/> | <en/> The product of the two vectors
*/
export function multiply(a: Vector2 | Vector3, b: Vector2 | Vector3): Vector2 | Vector3 {
return a.map((v, i) => v * b[i]) as Vector2 | Vector3;
}
export function divide(a: Vector2, b: Vector2): Vector2;
export function divide(a: Vector3, b: Vector3): Vector3;
/**
* <zh/>
*
* <en/> Divides two vectors
* @param a - <zh/> | <en/> The first vector
* @param b - <zh/> | <en/> The second vector
* @returns <zh/> | <en/> The quotient of the two vectors
*/
export function divide(a: Vector2 | Vector3, b: Vector2 | Vector3): Vector2 | Vector3 {
return a.map((v, i) => v / b[i]) as Vector2 | Vector3;
}
export function distance(a: Vector2, b: Vector2): number;
export function distance(a: Vector3, b: Vector3): number;
/**
* <zh/>
*
* <en/> Calculates the Euclidean distance between two vectors
* @param a - <zh/> | <en/> The first vector
* @param b - <zh/> | <en/> The second vector
* @returns <zh/> | <en/> The distance between the two vectors
*/
export function distance(a: Vector2 | Vector3, b: Vector2 | Vector3): number {
let sum = 0;
a.forEach((v, i) => (sum += (v - b[i]) ** 2));
return Math.sqrt(sum);
}
export function normalize(a: Vector2): Vector2;
export function normalize(a: Vector3): Vector3;
/**
* <zh/> 使 1
*
* <en/> Normalizes a vector (making its length 1)
* @param a - <zh/> | <en/> The vector to normalize
* @returns <zh/> | <en/> The normalized vector
*/
export function normalize(a: Vector2 | Vector3): Vector2 | Vector3 {
let length = 0;
a.forEach((v) => (length += v ** 2));
return a.map((v) => v / Math.sqrt(length)) as Vector2 | Vector3;
}
/**
* <zh/>
*
* <en/> Calculates the perpendicular vector to a given vector
* @param a - <zh/> | <en/> The original vector
* @returns <zh/> | <en/> The perpendicular vector to the original vector
*/
export function perpendicular(a: Vector2): Vector2 {
return [-a[1], -a[0]];
}