Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions examples/layers/outlined-path-layer/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
// Ported from https://github.com/ubilabs/outlined-path-layer example

import React, {useMemo, useState} from 'react';
import DeckGL from '@deck.gl/react';
import type {Color, MapViewState, Unit} from '@deck.gl/core';
import {OutlinedPathLayer} from '@deck.gl-community/layers';

// --- Data types ---

type Route = {
name: string;
path: [number, number][];
color: Color;
};

// --- Helper: generate a sinusoidal path that weaves across the view ---

const CENTER_LNG = -122.42;
const CENTER_LAT = 37.785;
const STEPS = 40;

function sinePath(
amplitude: number,
frequency: number,
phase: number,
angle: number
): [number, number][] {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const points: [number, number][] = [];
for (let i = 0; i <= STEPS; i++) {
const t = (i / STEPS - 0.5) * 2;
const along = t * 0.015;
const across = Math.sin(t * Math.PI * frequency + phase) * amplitude * 0.005;
const dx = along * cos - across * sin;
const dy = along * sin + across * cos;
points.push([CENTER_LNG + dx, CENTER_LAT + dy]);
}
return points;
}

// --- Sample data: weaving routes that cross each other ---

const ROUTES: Route[] = [
{name: 'Blue Weave', path: sinePath(1.2, 3, 0, 0), color: [65, 140, 255]},
{name: 'Red Weave', path: sinePath(1.2, 3, Math.PI, 0), color: [240, 80, 80]},
{name: 'Green Diagonal', path: sinePath(1.0, 2.5, 0, Math.PI / 4), color: [2, 200, 120]},
{name: 'Orange Diagonal', path: sinePath(1.0, 2.5, Math.PI, -Math.PI / 4), color: [255, 160, 40]},
{name: 'Purple Cross', path: sinePath(1.4, 4, Math.PI / 2, Math.PI / 2), color: [180, 100, 255]}
];

const INITIAL_VIEW_STATE: MapViewState = {
longitude: CENTER_LNG,
latitude: CENTER_LAT,
zoom: 14,
pitch: 0,
bearing: 0
};

// --- UI helpers ---

function hexToRgb(hex: string): Color {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
: [0, 0, 0];
}

const row: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: 8,
height: 24
};
const labelStyle: React.CSSProperties = {flex: '0 0 140px', textAlign: 'right'};
const valueStyle: React.CSSProperties = {flex: '0 0 28px', textAlign: 'right', fontVariantNumeric: 'tabular-nums'};

function Slider({
label, value, min, max, step, onChange
}: {
label: string; value: number; min: number; max: number; step: number;
onChange: (v: number) => void;
}) {
return (
<div style={row}>
<span style={labelStyle}>{label}</span>
<input type="range" min={min} max={max} step={step} value={value}
style={{flex: 1}} onChange={(e) => onChange(Number(e.target.value))} />
<span style={valueStyle}>{value}</span>
</div>
);
}

function Dropdown({
label, value, options, onChange
}: {
label: string; value: string; options: string[];
onChange: (v: string) => void;
}) {
return (
<div style={row}>
<span style={labelStyle}>{label}</span>
<select value={value} onChange={(e) => onChange(e.target.value)}
style={{flex: 1}}>
{options.map((o) => <option key={o} value={o}>{o}</option>)}
</select>
</div>
);
}

function Checkbox({
label, value, onChange
}: {
label: string; value: boolean; onChange: (v: boolean) => void;
}) {
return (
<div style={row}>
<span style={labelStyle}>{label}</span>
<input type="checkbox" checked={value} onChange={(e) => onChange(e.target.checked)} />
</div>
);
}

function ColorPicker({
label, value, onChange
}: {
label: string; value: string; onChange: (v: string) => void;
}) {
return (
<div style={row}>
<span style={labelStyle}>{label}</span>
<input type="color" value={value} onChange={(e) => onChange(e.target.value)} />
</div>
);
}

// --- App ---

export default function App(): React.ReactElement {
const [width, setWidth] = useState(6);
const [widthUnits, setWidthUnits] = useState<Unit>('pixels');
const [outlineWidth, setOutlineWidth] = useState(2);
const [outlineWidthUnits, setOutlineWidthUnits] = useState<Unit>('pixels');
const [outlineColor, setOutlineColor] = useState('#1e1e1e');
const [widthMinPixels, setWidthMinPixels] = useState(4);
const [widthMaxPixels, setWidthMaxPixels] = useState(20);
const [outlineMinPixels, setOutlineMinPixels] = useState(1);
const [outlineMaxPixels, setOutlineMaxPixels] = useState(10);
const [capRounded, setCapRounded] = useState(true);
const [jointRounded, setJointRounded] = useState(true);
const [miterLimit, setMiterLimit] = useState(4);

const layers = useMemo(
() => [
new OutlinedPathLayer<Route>({
id: 'outlined-paths',
data: ROUTES,
pickable: true,
autoHighlight: true,
highlightColor: [255, 255, 0, 128],
getPath: (d) => d.path,
getColor: (d) => d.color,
getWidth: width,
widthUnits,
widthMinPixels,
widthMaxPixels,
capRounded,
jointRounded,
miterLimit,
getOutlineColor: hexToRgb(outlineColor),
getOutlineWidth: outlineWidth,
outlineWidthUnits,
outlineMinPixels,
outlineMaxPixels,
parameters: {depthCompare: 'always'}
})
],
[
width, widthUnits, outlineWidth, outlineWidthUnits, outlineColor,
widthMinPixels, widthMaxPixels, outlineMinPixels, outlineMaxPixels,
capRounded, jointRounded, miterLimit
]
);

return (
<>
<DeckGL
layers={layers}
initialViewState={INITIAL_VIEW_STATE}
controller={true}
parameters={{clearColor: [0.12, 0.12, 0.14, 1] as any}}
style={{position: 'absolute', width: '100%', height: '100%'}}
/>
<div
style={{
position: 'absolute',
top: 16,
right: 16,
background: 'white',
padding: 10,
borderRadius: 5,
fontFamily: 'monospace',
fontSize: 13,
display: 'flex',
flexDirection: 'column',
gap: 6,
minWidth: 360,
zIndex: 1
}}
>
<Slider label="Width" value={width} min={0} max={20} step={1} onChange={setWidth} />
<Dropdown label="Width Units" value={widthUnits}
options={['pixels', 'meters']} onChange={(v) => setWidthUnits(v as Unit)} />
<Slider label="Outline Width" value={outlineWidth} min={0} max={10} step={1}
onChange={setOutlineWidth} />
<Dropdown label="Outline Width Units" value={outlineWidthUnits}
options={['pixels', 'meters']} onChange={(v) => setOutlineWidthUnits(v as Unit)} />
<ColorPicker label="Outline Color" value={outlineColor} onChange={setOutlineColor} />
<Slider label="Width Min Pixels" value={widthMinPixels} min={0} max={20} step={1}
onChange={setWidthMinPixels} />
<Slider label="Width Max Pixels" value={widthMaxPixels} min={0} max={50} step={1}
onChange={setWidthMaxPixels} />
<Slider label="Outline Min Pixels" value={outlineMinPixels} min={0} max={10} step={1}
onChange={setOutlineMinPixels} />
<Slider label="Outline Max Pixels" value={outlineMaxPixels} min={0} max={20} step={1}
onChange={setOutlineMaxPixels} />
<Checkbox label="Cap Rounded" value={capRounded} onChange={setCapRounded} />
<Checkbox label="Joint Rounded" value={jointRounded} onChange={setJointRounded} />
<Slider label="Miter Limit" value={miterLimit} min={1} max={10} step={1}
onChange={setMiterLimit} />
</div>
</>
);
}
11 changes: 11 additions & 0 deletions examples/layers/outlined-path-layer/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OutlinedPathLayer</title>
</head>
<body>
<script type="module" src="./index.tsx"></script>
</body>
</html>
14 changes: 14 additions & 0 deletions examples/layers/outlined-path-layer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import {createRoot} from 'react-dom/client';
import App from './app';

const container = document.body.appendChild(document.createElement('div'));
container.style.position = 'fixed';
container.style.top = '0';
container.style.left = '0';
container.style.width = '100vw';
container.style.height = '100vh';
container.style.margin = '0';

const root = createRoot(container);
root.render(<App />);
18 changes: 18 additions & 0 deletions examples/layers/outlined-path-layer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"license": "MIT",
"scripts": {
"start": "vite --open",
"start-local": "vite --config ../../vite.config.local.mjs"
},
"dependencies": {
"@deck.gl-community/layers": "workspace:*",
"@deck.gl/core": "~9.2.1",
"@deck.gl/layers": "~9.2.1",
"@deck.gl/react": "~9.2.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"vite": "7.1.1"
}
}
7 changes: 7 additions & 0 deletions examples/layers/outlined-path-layer/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx"
},
"include": ["./**/*"]
}
3 changes: 3 additions & 0 deletions modules/layers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
export type {PathOutlineLayerProps} from './path-outline-layer/path-outline-layer';
export {PathOutlineLayer} from './path-outline-layer/path-outline-layer';

export type {OutlinedPathLayerProps} from './outlined-path-layer/outlined-path-layer';
export {OutlinedPathLayer} from './outlined-path-layer/outlined-path-layer';

export type {PathMarkerLayerProps} from './path-marker-layer/path-marker-layer';
export {PathMarkerLayer} from './path-marker-layer/path-marker-layer';
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
// Ported from https://github.com/ubilabs/outlined-path-layer (MIT license)

export const fs = /* glsl */ `\
#version 300 es
#define SHADER_NAME outlined-path-layer-fragment-shader

precision highp float;

in vec4 vColorInner;
in vec4 vColorOutline;
in float vWidthRatio;
in vec2 vCornerOffset;
in float vMiterLength;
in vec2 vPathPosition;
in float vPathLength;
in float vJointType;
in float vIsCap;

out vec4 fragColor;

void main(void) {
geometry.uv = vPathPosition;

bool isCapOrJoint = vPathPosition.y < 0.0 || vPathPosition.y > vPathLength;
bool isRound = vJointType > 0.5;

if (isCapOrJoint) {
if (isRound) {
if (length(vCornerOffset) > 1.0) discard;
} else {
if (vMiterLength > path.miterLimit + 1.0) discard;
}
}

float dist = mix(abs(vPathPosition.x), length(vCornerOffset), float(isRound && isCapOrJoint));

vec4 baseColor = mix(vColorInner, vColorOutline, step(vWidthRatio, dist));

float isSquareCap = float(vIsCap > 0.5 && isCapOrJoint && !isRound);

fragColor = mix(baseColor, vColorOutline, isSquareCap);

DECKGL_FILTER_COLOR(fragColor, geometry);
}
`;
Loading