Skip to content

Commit 04bd220

Browse files
feat(layers): add clipping to TextLayer (#10118)
1 parent b2c8c40 commit 04bd220

13 files changed

Lines changed: 585 additions & 9 deletions

File tree

docs/api-reference/layers/text-layer.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ If `true`, the text always faces camera. Otherwise the text faces up (z).
198198

199199
Whether to render background for the text blocks.
200200

201+
If a valid content box is defined by [getContentBox](#getcontentbox), the background will fill this box. Otherwise, background is generated around the natural bounding box of the text.
202+
201203
#### `backgroundBorderRadius` (number | number[4], optional) {#backgroundborderradius}
202204

203205
- Default `0`
@@ -275,6 +277,50 @@ A unitless number that will be multiplied with the current text size to set the
275277

276278
For example, `maxWidth: 10.0` used with `getSize: 12` is roughly the equivalent of `max-width: 120px` in CSS.
277279

280+
281+
#### `contentCutoffPixels` (number[2], optional) {#contentcutoffpixels}
282+
283+
* Default: `[0, 0]`
284+
285+
Minimum visible region of the content box, as `[width, height]` in screen pixels. If the visible width or height is smaller than the specified cutoff, the corresponding text is hidden completely.
286+
This prop can be used to set the minimum length of clipped texts to improve readability.
287+
288+
Only effective with a valid content box returned by [getContentBox](#getcontentbox).
289+
290+
#### `contentAlignHorizontal` (string, optional) {#contentalignhorizontal}
291+
292+
* Default: `'none'`
293+
294+
Align the text horizontally to the visible region of the content box.
295+
This prop can be used to keep the text visible while zooming and panning, similar to the CSS `position: 'sticky'` behavior.
296+
297+
Only effective with a valid content box returned by [getContentBox](#getcontentbox). Usually used with a matching [getTextAnchor](#gettextanchor) prop.
298+
299+
Supported values:
300+
301+
- `'none'`
302+
- `'start'`
303+
- `'center'`
304+
- `'end'`
305+
306+
307+
#### `contentAlignVertical` (string, optional) {#contentalignvertical}
308+
309+
* Default: `'none'`
310+
311+
Align the text vertically to the visible region of the content box.
312+
This prop can be used to keep the text visible while zooming and panning, similar to the CSS `position: 'sticky'` behavior.
313+
314+
Only effective with a valid content box returned by [getContentBox](#getcontentbox). Usually used with a matching [getAlignmentBaseline](#getalignmentbaseline) prop.
315+
316+
Supported values:
317+
318+
- `'none'`
319+
- `'start'`
320+
- `'center'`
321+
- `'end'`
322+
323+
278324
#### `outlineWidth` (number, optional) {#outlinewidth}
279325

280326
* Default: `0`
@@ -363,6 +409,18 @@ Screen space offset relative to the `coordinates` in pixel unit.
363409
* If a function is provided, it is called on each object to retrieve its offset.
364410

365411

412+
#### `getContentBox` ([Accessor<number[4]>](../../developer-guide/using-layers.md#accessors), optional) {#getcontentbox}
413+
414+
* Default: `[0, 0, -1, -1]`
415+
416+
Called to retrieve the context box that contains the text. Characters that overflow the area are not displayed. Returns `[x, y, width, height]`, where all values are world space (meter) offsets from the text anchor position.
417+
418+
- `x`, `y` define the rectangle's origin relative to the text anchor.
419+
- `width`, `height` define the rectangle size.
420+
- A negative `width` disables clipping on the X axis.
421+
- A negative `height` disables clipping on the Y axis.
422+
423+
366424
#### `getBackgroundColor` ([Accessor<Color>](../../developer-guide/using-layers.md#accessors), optional) ![transition-enabled](https://img.shields.io/badge/transition-enabled-green.svg?style=flat-square") {#getbackgroundcolor}
367425

368426
* Default: `[255, 255, 255, 255]`

examples/website/treemap/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
This is a standalone version of the ClippingTextExtension example on [deck.gl](http://deck.gl) website.
2+
3+
### Usage
4+
5+
Copy the content of this folder to your project.
6+
7+
```bash
8+
# install dependencies
9+
npm install
10+
# or
11+
yarn
12+
# bundle and serve the app with vite
13+
npm start
14+
```
15+
16+
### Data format
17+
18+
Sample data is from [D3 Treemap demo](https://observablehq.com/@d3/treemap/2).

examples/website/treemap/app.tsx

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
import React, {useMemo} from 'react';
6+
import {createRoot} from 'react-dom/client';
7+
import {DeckGL} from '@deck.gl/react';
8+
import {OrthographicView} from '@deck.gl/core';
9+
import type {PickingInfo, OrthographicViewState, Color} from '@deck.gl/core';
10+
import {TextLayer} from '@deck.gl/layers';
11+
import {scaleOrdinal, hierarchy, treemap, treemapSquarify, format} from 'd3';
12+
import type {HierarchyNode, HierarchyRectangularNode} from 'd3';
13+
14+
// Sample data
15+
const DATA_URL =
16+
'https://raw.githubusercontent.com/d3/d3-hierarchy/refs/heads/main/test/data/flare.json';
17+
18+
const ColorScale = scaleOrdinal<string, Color>().range([
19+
[78, 121, 167],
20+
[242, 142, 44],
21+
[225, 87, 89],
22+
[118, 183, 178],
23+
[89, 161, 79],
24+
[237, 201, 73],
25+
[175, 122, 161],
26+
[255, 157, 167],
27+
[156, 117, 95],
28+
[186, 176, 171]
29+
]);
30+
31+
type Datum = {
32+
name: string;
33+
value: number;
34+
};
35+
36+
const formatValue = format(',d');
37+
38+
const WIDTH = 1000;
39+
const HEIGHT = 1000;
40+
const INITIAL_VIEW_STATE: OrthographicViewState = {
41+
target: [WIDTH / 2, HEIGHT / 2, 0],
42+
zoom: -1
43+
};
44+
45+
export default function App({data}: {data?: HierarchyNode<Datum>}) {
46+
const layoutRoot = useMemo(() => {
47+
if (!data) return null;
48+
data.sort((a, b) => b.value! - a.value!);
49+
return treemap<Datum>().tile(treemapSquarify).size([WIDTH, HEIGHT]).padding(1)(data);
50+
}, [data]);
51+
52+
const leaves = useMemo(() => layoutRoot?.leaves(), [layoutRoot]);
53+
54+
const layers = [
55+
new TextLayer<HierarchyRectangularNode<Datum>>({
56+
id: 'labels-name',
57+
data: leaves,
58+
getPosition: d => [d.x0, d.y1],
59+
getText: d => d.data.name,
60+
getPixelOffset: [4, 0],
61+
getSize: 12,
62+
getColor: [255, 255, 255],
63+
getTextAnchor: 'start',
64+
getAlignmentBaseline: 'center',
65+
background: true,
66+
getBackgroundColor: d => {
67+
while (d.depth > 1) d = d.parent!;
68+
return ColorScale(d.data.name);
69+
},
70+
getContentBox: d => [0, d.y0 - d.y1, d.x1 - d.x0, d.y1 - d.y0],
71+
contentAlignHorizontal: 'start',
72+
contentAlignVertical: 'center',
73+
contentCutoffPixels: [60, 30]
74+
}),
75+
new TextLayer<HierarchyRectangularNode<Datum>>({
76+
id: 'labels-value',
77+
data: leaves,
78+
getPosition: d => [d.x0, d.y0],
79+
getText: d => formatValue(d.data.value),
80+
getPixelOffset: [4, 12],
81+
getSize: 10,
82+
getColor: [255, 255, 255, 200],
83+
getTextAnchor: 'start',
84+
getAlignmentBaseline: 'center',
85+
getContentBox: d => [0, 0, d.x1 - d.x0, d.y1 - d.y0],
86+
contentAlignHorizontal: 'start',
87+
contentAlignVertical: 'center',
88+
contentCutoffPixels: [60, 60]
89+
})
90+
];
91+
92+
return (
93+
<DeckGL
94+
layers={layers}
95+
views={new OrthographicView()}
96+
initialViewState={INITIAL_VIEW_STATE}
97+
controller
98+
getTooltip={getTooltip}
99+
/>
100+
);
101+
}
102+
103+
function getTooltip({object}: PickingInfo<HierarchyRectangularNode<Datum>>) {
104+
return object
105+
? `\
106+
name: ${object
107+
.ancestors()
108+
.map(d => d.data.name)
109+
.reverse()
110+
.join('.')}
111+
value: ${formatValue(object.data.value)}
112+
`
113+
: null;
114+
}
115+
116+
export async function renderToDOM(container: HTMLDivElement) {
117+
const root = createRoot(container);
118+
root.render(<App />);
119+
120+
const resp = await fetch(DATA_URL);
121+
const json = await resp.json();
122+
const data = hierarchy(json).sum(d => d.value);
123+
root.render(<App data={data} />);
124+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>deck.gl Example</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<link href='https://unpkg.com/maplibre-gl@3.6.0/dist/maplibre-gl.css' rel='stylesheet' />
8+
<style>
9+
body {margin: 0; font-family: sans-serif; width: 100vw; height: 100vh; overflow: hidden;}
10+
.deck-tooltip {font-size: 13px; margin: 8px; min-width: 160px;}
11+
</style>
12+
</head>
13+
<body>
14+
<div id="app"></div>
15+
</body>
16+
<script type="module">
17+
import {renderToDOM} from './app.tsx';
18+
renderToDOM(document.getElementById('app'));
19+
</script>
20+
</html>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "deckgl-examples-treemap",
3+
"version": "0.0.0",
4+
"private": true,
5+
"license": "MIT",
6+
"scripts": {
7+
"start": "vite --open",
8+
"start-local": "vite --config ../../vite.config.local.mjs",
9+
"build": "vite build"
10+
},
11+
"dependencies": {
12+
"@types/d3": "^7.4.3",
13+
"@types/react": "^18.0.0",
14+
"@types/react-dom": "^18.0.0",
15+
"d3": "^7.9.0",
16+
"deck.gl": "^9.0.0",
17+
"react": "^18.0.0",
18+
"react-dom": "^18.0.0",
19+
"react-map-gl": "^8.0.0"
20+
},
21+
"devDependencies": {
22+
"typescript": "^4.6.0",
23+
"vite": "^4.0.0"
24+
}
25+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2020",
4+
"jsx": "react",
5+
"moduleResolution": "node",
6+
"allowSyntheticDefaultImports": true
7+
}
8+
}

modules/core/src/shaderlib/project/project.glsl.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ vec2 project_pixel_size_to_clipspace(vec2 pixels) {
265265
float project_size_to_pixel(float meters) {
266266
return project_size(meters) * project.scale;
267267
}
268+
vec2 project_size_to_pixel(vec2 meters) {
269+
return project_size(meters) * project.scale;
270+
}
268271
float project_size_to_pixel(float size, int unit) {
269272
if (unit == UNIT_METERS) return project_size_to_pixel(size);
270273
if (unit == UNIT_COMMON) return size * project.scale;

0 commit comments

Comments
 (0)