Skip to content

Commit d1b56c2

Browse files
authored
fix(ArrowSource): Improve Bezier->polyline algorithm (#228)
1 parent d3fc4f4 commit d1b56c2

File tree

4 files changed

+86
-29
lines changed

4 files changed

+86
-29
lines changed

components/src/maplibre/ArrowSource/ArrowSource.stories.svelte

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import AttributionControl from '../AttributionControl/AttributionControl.svelte';
77
88
import { SWRDataLabLight } from '../MapStyle';
9-
import { tokens } from '../../DesignTokens';
109
import DesignTokens from '../../DesignTokens/DesignTokens.svelte';
10+
import { tokens } from '../../DesignTokens';
1111
1212
const { Story } = defineMeta({
1313
title: 'Maplibre/Source/ArrowSource',
@@ -22,9 +22,9 @@
2222
showDebug={true}
2323
style={SWRDataLabLight()}
2424
initialLocation={{
25-
lng: -84.783,
26-
lat: 15.623,
27-
zoom: 3.39,
25+
lng: -78.404,
26+
lat: 13.411,
27+
zoom: 5.124,
2828
pitch: 0
2929
}}
3030
>
@@ -39,7 +39,7 @@
3939
c: [-81, 14.6]
4040
},
4141
{
42-
width: 15,
42+
width: 25,
4343
a: [-71.1, 11.3],
4444
b: [-75.783, 15.6],
4545
c: [-75, 12.6]
@@ -60,18 +60,19 @@
6060
0,
6161
'transparent',
6262
0.4,
63-
tokens.shades.red.base
63+
tokens.shades.violet.base
6464
],
6565
'line-width': ['get', 'width']
6666
}}
6767
/>
68+
6869
<VectorLayer
6970
sourceId="arrows"
7071
id="arrow-heads"
7172
filter={['==', 'kind', 'arrow-head']}
7273
type="fill"
7374
paint={{
74-
'fill-color': tokens.shades.red.base
75+
'fill-color': tokens.shades.violet.base
7576
}}
7677
/>
7778
</Map>

components/src/maplibre/ArrowSource/ArrowSource.svelte

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<script lang="ts">
22
import { type GeoJSONSourceSpecification } from 'maplibre-gl';
3+
import { onDestroy, onMount } from 'svelte';
34
45
import MapSource from '../Source';
56
import { getMapContext } from '../context.svelte.js';
67
import quadraticToPoints from './quadraticToPoints';
7-
import { onDestroy } from 'svelte';
88
9-
const { map } = $derived(getMapContext());
9+
import type { V2 } from '../types';
1010
11-
type V2 = [number, number];
11+
const { map } = $derived(getMapContext());
1212
1313
interface ArrowSourceProps {
1414
id: string;
@@ -35,7 +35,7 @@
3535
const ars: JsonArrow[] = arrows.map((a) => {
3636
return {
3737
width: a.width || 10,
38-
points: quadraticToPoints(a.a, a.b, a.c, 10),
38+
points: quadraticToPoints(a.a, a.b, a.c),
3939
headScale: a.headScale
4040
};
4141
});
@@ -93,7 +93,7 @@
9393
const onZoom = () => {
9494
sourceSpec = { ...sourceSpec, data: arrowsToJson(ars) };
9595
};
96-
$effect(() => {
96+
onMount(() => {
9797
map?.on('zoom', onZoom);
9898
});
9999
onDestroy(() => {
Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,80 @@
1+
import type { V2 } from '../types';
2+
3+
/**
4+
* Returns the euclidian distance between two points
5+
*/
6+
const distance = (a: V2, b: V2) => {
7+
return Math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2);
8+
};
9+
10+
/**
11+
* Given a sorted array arr and a value, returns the index i of arr
12+
* such that arr[i] < value && arr[i + 1] > value using binary search
13+
*/
14+
const getSortedIndex = (arr: number[], v: number) => {
15+
let low = 0,
16+
high = arr.length;
17+
while (low < high) {
18+
let mid = (low + high) >>> 1; // === Math.abs((low + high) * .5) but faster
19+
if (arr[mid] < v) low = mid + 1;
20+
else high = mid;
21+
}
22+
return low;
23+
};
24+
125
/**
2-
Given a quadratic bezier defined by points a, b and c,
3-
returns a series of n points on the curve
26+
* Given a polyline, returns n evenly-spaced points on the polyline
27+
*/
28+
const interpolatePolyline = (points: V2[], n: number) => {
29+
let res: V2[] = [],
30+
totalLength: number = 0,
31+
segments: number[] = [0];
32+
33+
for (let i = 0; i < points.length - 2; i++) {
34+
totalLength += distance(points[i], points[i + 1]);
35+
segments.push(totalLength);
36+
}
37+
38+
for (let i = 0; i < n; i++) {
39+
const d: number = totalLength * (i / n);
40+
const si: number = getSortedIndex(segments, d);
41+
const p0 = points[si];
42+
const p1 = points[si + 1];
43+
44+
const sn = [p1[0] - p0[0], p1[1] - p0[1]];
45+
const segmentFraction = (d - segments[si]) / distance(p0, p1);
46+
47+
res.push([p1[0] + sn[0] * segmentFraction, p1[1] + sn[1] * segmentFraction]);
48+
}
449

5-
The quadratic bezier is: B(t) = (1-t)[(1-t)P0 + tP1] + t[(1-t)P1+tP2]
6-
Source: https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B%C3%A9zier_curves
50+
return res;
51+
};
752

8-
Below is a naive implementation but good enough for now. For a better approach see:
9-
https://bit-101.com/blog/posts/2024-09-29/evenly-placed-points-on-bezier-curves
53+
/**
54+
* Given a quadratic Bezier defined by points a, b and c
55+
* returns a series of evenly-spaced points on the curve
56+
*
57+
* Sources:
58+
* - https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B%C3%A9zier_curves
59+
* - https://bit-101.com/blog/posts/2024-09-29/evenly-placed-points-on-bezier-curves
1060
*/
11-
const quadraticToPoints = (
12-
a: [number, number],
13-
b: [number, number],
14-
c: [number, number],
15-
n = 10
16-
) => {
17-
let points = [];
61+
62+
const quadraticToPoints = (a: V2, b: V2, c: V2, n = 25) => {
63+
let points: V2[] = [];
64+
65+
// 1. Interpolate the quadratic bezier function to obtain an uneven polyline
1866
for (let i = 0; i < n; i++) {
1967
const t = i / n;
20-
const x = (1 - t) * ((1 - t) * a[0] + t * c[0]) + t * ((1 - t) * c[0] + t * b[0]);
21-
const y = (1 - t) * ((1 - t) * a[1] + t * c[1]) + t * ((1 - t) * c[1] + t * b[1]);
22-
points.push([x, y]);
68+
points.push([
69+
(1 - t) * ((1 - t) * a[0] + t * c[0]) + t * ((1 - t) * c[0] + t * b[0]),
70+
(1 - t) * ((1 - t) * a[1] + t * c[1]) + t * ((1 - t) * c[1] + t * b[1])
71+
]);
2372
}
24-
return [...points, b] as [number, number][];
73+
74+
// 2. Interpolate the polyline from (1) to get evenly-spaced points
75+
points = interpolatePolyline([...points, b], n);
76+
77+
return points;
2578
};
2679

2780
export default quadraticToPoints;

components/src/maplibre/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ export type Layer =
88
| LineLayerSpecification
99
| FillLayerSpecification
1010
| FillExtrusionLayerSpecification;
11+
1112
export type GeocodingService = 'maptiler';
1213
export type GeocodingCountry = 'de' | 'at';
1314
export type GeocodingLanguage = 'de' | 'en';
1415

16+
export type V2 = [number, number];
17+
1518
export interface Location {
1619
lat: number;
1720
lng: number;

0 commit comments

Comments
 (0)