stipple-maplibre 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,10 @@ It supports geometric patterns, repeated text, and SVG symbols. Patterns can
8
8
  be configured in JavaScript or prepared in the playground and exported as
9
9
  MapLibre code.
10
10
 
11
- <p align="center"><strong><a href="https://stipple.pages.dev/">Run the playground</a></strong></p>
11
+ <img width="1456" alt="Stipple playground showing pattern customization on a MapLibre map" src="./assets/readme/playground.png" />
12
+
13
+ <p align="center"><strong><a href="https://stipple.pages.dev/">➞ Run the playground</a></strong></p>
12
14
 
13
- <img width="1456" height="858" alt="Stipple playground showing a patterned polygon on a MapLibre map" src="https://github.com/user-attachments/assets/f7d28a0f-267a-4bd3-b7a2-30b49eabf709" />
14
15
 
15
16
  ## What problem does it solve?
16
17
 
@@ -20,8 +21,7 @@ map, and kept at an appropriate resolution when the display or style changes.
20
21
 
21
22
  Stipple generates this image from a small set of pattern parameters. It draws
22
23
  the tile in the browser, installs it with MapLibre's public `addImage` API,
23
- and provides the corresponding `fill-pattern` value. It does not patch or
24
- fork MapLibre.
24
+ and provides the corresponding `fill-pattern` value.
25
25
 
26
26
  ## What can I make with it?
27
27
 
@@ -30,6 +30,47 @@ fork MapLibre.
30
30
  - SVG fills: repeat one of the bundled symbols or bring your own SVG.
31
31
  - A background colour and polygon outline to go with the pattern.
32
32
 
33
+ ### Examples
34
+
35
+ <table>
36
+ <tr>
37
+ <td align="center" width="25%">
38
+ <img src="./assets/readme/example-solid.png" alt="Polygon with a solid fill" width="100%"><br>
39
+ <sub><em>Solid</em></sub>
40
+ </td>
41
+ <td align="center" width="25%">
42
+ <img src="./assets/readme/example-stipple.png" alt="Polygon filled with a stipple pattern" width="100%"><br>
43
+ <sub><em>Stipple</em></sub>
44
+ </td>
45
+ <td align="center" width="25%">
46
+ <img src="./assets/readme/example-hatches.png" alt="Polygon filled with diagonal hatches" width="100%"><br>
47
+ <sub><em>Hatches</em></sub>
48
+ </td>
49
+ <td align="center" width="25%">
50
+ <img src="./assets/readme/example-dots.png" alt="Polygon filled with a regular dot pattern" width="100%"><br>
51
+ <sub><em>Dots</em></sub>
52
+ </td>
53
+ </tr>
54
+ <tr>
55
+ <td align="center" width="25%">
56
+ <img src="./assets/readme/example-grid.png" alt="Polygon filled with a grid pattern" width="100%"><br>
57
+ <sub><em>Grid</em></sub>
58
+ </td>
59
+ <td align="center" width="25%">
60
+ <img src="./assets/readme/example-font-fill.png" alt="Polygon filled with repeated text" width="100%"><br>
61
+ <sub><em>Font fill</em></sub>
62
+ </td>
63
+ <td align="center" width="25%">
64
+ <img src="./assets/readme/example-svg-symbol.png" alt="Polygon filled with a bundled SVG motif" width="100%"><br>
65
+ <sub><em>SVG fill</em></sub>
66
+ </td>
67
+ <td align="center" width="25%">
68
+ <img src="./assets/readme/example-custom-svg.png" alt="Polygon filled with a custom SVG motif" width="100%"><br>
69
+ <sub><em>Custom</em></sub>
70
+ </td>
71
+ </tr>
72
+ </table>
73
+
33
74
  You can change the colour, opacity, spacing, weight, angle, scale, and layout.
34
75
  Font fills also let you choose the typeface, style, and letter spacing. SVG
35
76
  fills can use regular rows, offset rows, or a more natural-looking seeded
@@ -39,7 +80,6 @@ The playground includes 34 SVG motifs covering vegetation, trees,
39
80
  agriculture, water, terrain, land use, and simple shapes. The same seed always
40
81
  produces the same arrangement.
41
82
 
42
- <!-- Add a small gallery here: geometric, font, SVG, and custom SVG. -->
43
83
 
44
84
  ## But can’t I just ask AI to do this?
45
85
 
@@ -64,16 +104,17 @@ locally in the browser and are not uploaded to a server.
64
104
 
65
105
  ### Run it locally
66
106
 
67
- To work from the source repository, install the dependencies and build the
68
- library:
107
+ From a clone of this repository, install the development dependencies and
108
+ build Stipple:
69
109
 
70
110
  ```sh
71
111
  npm install
72
112
  npm run build
73
113
  ```
74
114
 
75
- Then open the local [`demo/index.html`](./demo/index.html) file in your
76
- browser.
115
+ The repository itself is the `stipple-maplibre` package, so you do not need to
116
+ install it separately. Then open the local
117
+ [`demo/index.html`](./demo/index.html) file in your browser.
77
118
 
78
119
  ## Supported file formats
79
120
 
@@ -117,7 +158,7 @@ map.on("load", () => {
117
158
  syncPatternTexture(map, {
118
159
  imageId: "my-hatches",
119
160
  pattern: "hachures",
120
- size: 16,
161
+ size: 33,
121
162
  color: "#2c6a5b",
122
163
  weight: 2,
123
164
  angle: 45,
@@ -186,12 +227,30 @@ The playground can export:
186
227
 
187
228
  - ready-to-use MapLibre code;
188
229
  - a reusable Stipple configuration;
189
- - a MapLibre style document;
190
- - a bundle containing the generated pattern images.
230
+ - a static bundle containing style layers, generated pattern images, and an
231
+ integration helper.
232
+
233
+ The copied code uses `addPatternFill`, which waits for the map style, installs
234
+ the generated textures, adapts the exported layers to an existing source, and
235
+ adds them in order:
236
+
237
+ ```js
238
+ import { addPatternFill } from "stipple-maplibre";
239
+
240
+ await addPatternFill(map, {
241
+ sourceId: "my-polygons",
242
+ sourceLayer: null, // Use the source-layer name for vector tiles.
243
+ pattern: exportedPattern,
244
+ });
245
+ ```
246
+
247
+ The source must already exist in the map. An optional `beforeId` places the
248
+ pattern layers below an existing label or symbol layer. The returned
249
+ `layerIds` list contains every layer added to the map.
191
250
 
192
- For code-driven styles, `buildStyleFragment` creates the background, pattern,
193
- and outline layers together. The resulting style metadata carries the pattern
194
- recipe. After the style loads, `installPatternFills` reads those recipes and
251
+ For lower-level, code-driven styles, `buildStyleFragment` creates the
252
+ background, pattern, and outline layers together. The resulting style metadata
253
+ carries the pattern recipe. `installPatternFills` reads those recipes and
195
254
  installs every texture the map needs:
196
255
 
197
256
  ```js
@@ -280,7 +339,7 @@ in Node.
280
339
 
281
340
  ## Project status
282
341
 
283
- Version `0.1.1` is available on
342
+ Version `0.3.0` is available on
284
343
  [npm](https://www.npmjs.com/package/stipple-maplibre). The whole-symbol
285
344
  scatter API is the only part currently marked experimental.
286
345
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Map } from 'maplibre-gl';
1
+ import { Map, LayerSpecification } from 'maplibre-gl';
2
2
 
3
3
  type PatternType = "solid" | "stipple" | "hachures" | "cross" | "grid" | "dots";
4
4
  /** Tile edge length in px. MapLibre does not require power-of-two fill-pattern images. Larger tiles read as a lower-density pattern. */
@@ -396,6 +396,37 @@ interface StyleLike {
396
396
  */
397
397
  declare function installPatternFills(map: Map, style: StyleLike): Promise<void>;
398
398
 
399
+ interface PatternFillLayerTemplate {
400
+ id: string;
401
+ type: LayerSpecification["type"];
402
+ metadata?: Record<string, unknown>;
403
+ [key: string]: unknown;
404
+ }
405
+ interface PatternFillFragment {
406
+ layers: PatternFillLayerTemplate[];
407
+ }
408
+ interface AddPatternFillOptions {
409
+ /** Id of a source that already exists in the map. */
410
+ sourceId: string;
411
+ /** Omit or use null for GeoJSON. Required for vector tile sources. */
412
+ sourceLayer?: string | null;
413
+ /** Insert every generated layer immediately before this existing layer. */
414
+ beforeId?: string;
415
+ /** Source-neutral layer templates exported by Stipple. */
416
+ pattern: PatternFillFragment;
417
+ }
418
+ interface AddedPatternFill {
419
+ layerIds: string[];
420
+ }
421
+ /**
422
+ * Installs a Stipple pattern export on an existing MapLibre source.
423
+ *
424
+ * The helper waits for the style, resolves the source placeholders, installs
425
+ * every generated texture, then adds the exported background, fill, and
426
+ * outline layers in order.
427
+ */
428
+ declare function addPatternFill(map: Map, options: AddPatternFillOptions): Promise<AddedPatternFill>;
429
+
399
430
  interface ObservePatternFillsOptions {
400
431
  /** Defaults to `map.getStyle()`. Useful when metadata is stored separately. */
401
432
  getStyle?: () => StyleLike;
@@ -602,4 +633,4 @@ interface InstallSvgIconScatterOptions {
602
633
  */
603
634
  declare function installSvgIconScatter(map: Map, options: InstallSvgIconScatterOptions): Promise<void>;
604
635
 
605
- export { type BackgroundFillConfig, type BuildStyleFragmentOptions, type FontPatternDefinition, type FontPatternOptions, type GeoJsonPolygonImport, type GeometricPatternDefinition, type GeometricPatternType, type HachureAngle, type IconScaleMode, type ImportedPolygonFeature, type InstallSvgIconScatterOptions, type MakeTileOptions, type ObservePatternFillsOptions, type OutlineConfig, PATTERN_METADATA_KEY, type PatternDefinition, type PatternFillConfig, type PatternFillObserver, type PatternMetadataV1, type PatternScaleMode, type PatternScaleOptions, type PatternScaleResult, type PatternType, type PatternVariant, type PointFeature, type PointFeatureCollection, type PolygonGeometry, type Ring, type ScatterIconPointsOptions, type ScatterPointsOptions, type ScatteredPoint, type ScreenPatternPhase, type StyleLike, type SvgDistributionMode, type SvgPatternDefinition, type SvgPatternOptions, type SvgScatterLayoutOptions, type SvgStampPlacement, type SyncPatternTextureOptions, type TileContext, type TileImage, type TileSize, buildStyleFragment, createFontPatternDefinition, createFontPatternTile, createMiniContext, createSvgPatternDefinition, createSvgScatterLayout, createSvgScatterTile, hashStringToSeed, importGeoJsonPolygons, installFontPatternFill, installPatternFills, installSvgIconScatter, installSvgPatternFill, makeTile, mulberry32, observePatternFills, parsePatternDefinition, parsePatternMetadata, patternDefinitionId, scalePatternForZoom, scatterIconPoints, scatterPointsInPolygon, screenPatternPhase, serializePatternDefinition, syncPatternTexture };
636
+ export { type AddPatternFillOptions, type AddedPatternFill, type BackgroundFillConfig, type BuildStyleFragmentOptions, type FontPatternDefinition, type FontPatternOptions, type GeoJsonPolygonImport, type GeometricPatternDefinition, type GeometricPatternType, type HachureAngle, type IconScaleMode, type ImportedPolygonFeature, type InstallSvgIconScatterOptions, type MakeTileOptions, type ObservePatternFillsOptions, type OutlineConfig, PATTERN_METADATA_KEY, type PatternDefinition, type PatternFillConfig, type PatternFillFragment, type PatternFillLayerTemplate, type PatternFillObserver, type PatternMetadataV1, type PatternScaleMode, type PatternScaleOptions, type PatternScaleResult, type PatternType, type PatternVariant, type PointFeature, type PointFeatureCollection, type PolygonGeometry, type Ring, type ScatterIconPointsOptions, type ScatterPointsOptions, type ScatteredPoint, type ScreenPatternPhase, type StyleLike, type SvgDistributionMode, type SvgPatternDefinition, type SvgPatternOptions, type SvgScatterLayoutOptions, type SvgStampPlacement, type SyncPatternTextureOptions, type TileContext, type TileImage, type TileSize, addPatternFill, buildStyleFragment, createFontPatternDefinition, createFontPatternTile, createMiniContext, createSvgPatternDefinition, createSvgScatterLayout, createSvgScatterTile, hashStringToSeed, importGeoJsonPolygons, installFontPatternFill, installPatternFills, installSvgIconScatter, installSvgPatternFill, makeTile, mulberry32, observePatternFills, parsePatternDefinition, parsePatternMetadata, patternDefinitionId, scalePatternForZoom, scatterIconPoints, scatterPointsInPolygon, screenPatternPhase, serializePatternDefinition, syncPatternTexture };
@@ -22,6 +22,7 @@ var MaplibrePatternFills = (() => {
22
22
  var src_exports = {};
23
23
  __export(src_exports, {
24
24
  PATTERN_METADATA_KEY: () => PATTERN_METADATA_KEY,
25
+ addPatternFill: () => addPatternFill,
25
26
  buildStyleFragment: () => buildStyleFragment,
26
27
  createFontPatternDefinition: () => createFontPatternDefinition,
27
28
  createFontPatternTile: () => createFontPatternTile,
@@ -1480,6 +1481,83 @@ var MaplibrePatternFills = (() => {
1480
1481
  await Promise.all(pending);
1481
1482
  }
1482
1483
 
1484
+ // src/maplibre/addPatternFill.ts
1485
+ function nonEmptyString2(value, name) {
1486
+ if (typeof value !== "string" || value.trim().length === 0) {
1487
+ throw new TypeError(`${name} must be a non-empty string`);
1488
+ }
1489
+ }
1490
+ async function waitForStyle(map) {
1491
+ if (map.getStyle()) return;
1492
+ await new Promise((resolve) => {
1493
+ map.once("style.load", () => resolve());
1494
+ });
1495
+ }
1496
+ async function addPatternFill(map, options) {
1497
+ nonEmptyString2(options.sourceId, "sourceId");
1498
+ if (options.sourceLayer !== void 0 && options.sourceLayer !== null) {
1499
+ nonEmptyString2(options.sourceLayer, "sourceLayer");
1500
+ }
1501
+ if (options.beforeId !== void 0) {
1502
+ nonEmptyString2(options.beforeId, "beforeId");
1503
+ }
1504
+ if (!options.pattern || !Array.isArray(options.pattern.layers)) {
1505
+ throw new TypeError("pattern.layers must be an array");
1506
+ }
1507
+ if (options.pattern.layers.length === 0) {
1508
+ throw new RangeError("pattern.layers must contain at least one layer");
1509
+ }
1510
+ await waitForStyle(map);
1511
+ if (!map.getSource(options.sourceId)) {
1512
+ throw new Error(`MapLibre source "${options.sourceId}" was not found`);
1513
+ }
1514
+ if (options.beforeId && !map.getLayer(options.beforeId)) {
1515
+ throw new Error(`MapLibre layer "${options.beforeId}" was not found`);
1516
+ }
1517
+ const layers = options.pattern.layers.map((template, index) => {
1518
+ if (!template || typeof template !== "object") {
1519
+ throw new TypeError(`pattern.layers[${index}] must be an object`);
1520
+ }
1521
+ nonEmptyString2(template.id, `pattern.layers[${index}].id`);
1522
+ nonEmptyString2(template.type, `pattern.layers[${index}].type`);
1523
+ const layer = {
1524
+ ...template,
1525
+ id: template.id.split("<source>").join(options.sourceId),
1526
+ source: options.sourceId
1527
+ };
1528
+ if (options.sourceLayer === void 0 || options.sourceLayer === null) {
1529
+ delete layer["source-layer"];
1530
+ } else {
1531
+ layer["source-layer"] = options.sourceLayer;
1532
+ }
1533
+ return layer;
1534
+ });
1535
+ const layerIds = layers.map((layer) => layer.id);
1536
+ const uniqueIds = new Set(layerIds);
1537
+ if (uniqueIds.size !== layerIds.length) {
1538
+ throw new Error("Pattern fill contains duplicate layer ids");
1539
+ }
1540
+ for (const layerId of layerIds) {
1541
+ if (map.getLayer(layerId)) {
1542
+ throw new Error(`MapLibre layer "${layerId}" already exists`);
1543
+ }
1544
+ }
1545
+ await installPatternFills(map, { layers });
1546
+ const addedLayerIds = [];
1547
+ try {
1548
+ for (const layer of layers) {
1549
+ map.addLayer(layer, options.beforeId);
1550
+ addedLayerIds.push(layer.id);
1551
+ }
1552
+ } catch (error) {
1553
+ for (const layerId of addedLayerIds.reverse()) {
1554
+ if (map.getLayer(layerId)) map.removeLayer(layerId);
1555
+ }
1556
+ throw error;
1557
+ }
1558
+ return { layerIds };
1559
+ }
1560
+
1483
1561
  // src/maplibre/observePatternFills.ts
1484
1562
  function observePatternFills(map, options = {}) {
1485
1563
  const getStyle = options.getStyle ?? (() => map.getStyle());
package/dist/index.js CHANGED
@@ -1429,6 +1429,83 @@ async function installPatternFills(map, style) {
1429
1429
  await Promise.all(pending);
1430
1430
  }
1431
1431
 
1432
+ // src/maplibre/addPatternFill.ts
1433
+ function nonEmptyString2(value, name) {
1434
+ if (typeof value !== "string" || value.trim().length === 0) {
1435
+ throw new TypeError(`${name} must be a non-empty string`);
1436
+ }
1437
+ }
1438
+ async function waitForStyle(map) {
1439
+ if (map.getStyle()) return;
1440
+ await new Promise((resolve) => {
1441
+ map.once("style.load", () => resolve());
1442
+ });
1443
+ }
1444
+ async function addPatternFill(map, options) {
1445
+ nonEmptyString2(options.sourceId, "sourceId");
1446
+ if (options.sourceLayer !== void 0 && options.sourceLayer !== null) {
1447
+ nonEmptyString2(options.sourceLayer, "sourceLayer");
1448
+ }
1449
+ if (options.beforeId !== void 0) {
1450
+ nonEmptyString2(options.beforeId, "beforeId");
1451
+ }
1452
+ if (!options.pattern || !Array.isArray(options.pattern.layers)) {
1453
+ throw new TypeError("pattern.layers must be an array");
1454
+ }
1455
+ if (options.pattern.layers.length === 0) {
1456
+ throw new RangeError("pattern.layers must contain at least one layer");
1457
+ }
1458
+ await waitForStyle(map);
1459
+ if (!map.getSource(options.sourceId)) {
1460
+ throw new Error(`MapLibre source "${options.sourceId}" was not found`);
1461
+ }
1462
+ if (options.beforeId && !map.getLayer(options.beforeId)) {
1463
+ throw new Error(`MapLibre layer "${options.beforeId}" was not found`);
1464
+ }
1465
+ const layers = options.pattern.layers.map((template, index) => {
1466
+ if (!template || typeof template !== "object") {
1467
+ throw new TypeError(`pattern.layers[${index}] must be an object`);
1468
+ }
1469
+ nonEmptyString2(template.id, `pattern.layers[${index}].id`);
1470
+ nonEmptyString2(template.type, `pattern.layers[${index}].type`);
1471
+ const layer = {
1472
+ ...template,
1473
+ id: template.id.split("<source>").join(options.sourceId),
1474
+ source: options.sourceId
1475
+ };
1476
+ if (options.sourceLayer === void 0 || options.sourceLayer === null) {
1477
+ delete layer["source-layer"];
1478
+ } else {
1479
+ layer["source-layer"] = options.sourceLayer;
1480
+ }
1481
+ return layer;
1482
+ });
1483
+ const layerIds = layers.map((layer) => layer.id);
1484
+ const uniqueIds = new Set(layerIds);
1485
+ if (uniqueIds.size !== layerIds.length) {
1486
+ throw new Error("Pattern fill contains duplicate layer ids");
1487
+ }
1488
+ for (const layerId of layerIds) {
1489
+ if (map.getLayer(layerId)) {
1490
+ throw new Error(`MapLibre layer "${layerId}" already exists`);
1491
+ }
1492
+ }
1493
+ await installPatternFills(map, { layers });
1494
+ const addedLayerIds = [];
1495
+ try {
1496
+ for (const layer of layers) {
1497
+ map.addLayer(layer, options.beforeId);
1498
+ addedLayerIds.push(layer.id);
1499
+ }
1500
+ } catch (error) {
1501
+ for (const layerId of addedLayerIds.reverse()) {
1502
+ if (map.getLayer(layerId)) map.removeLayer(layerId);
1503
+ }
1504
+ throw error;
1505
+ }
1506
+ return { layerIds };
1507
+ }
1508
+
1432
1509
  // src/maplibre/observePatternFills.ts
1433
1510
  function observePatternFills(map, options = {}) {
1434
1511
  const getStyle = options.getStyle ?? (() => map.getStyle());
@@ -1734,6 +1811,7 @@ async function installSvgIconScatter(map, options) {
1734
1811
  }
1735
1812
  export {
1736
1813
  PATTERN_METADATA_KEY,
1814
+ addPatternFill,
1737
1815
  buildStyleFragment,
1738
1816
  createFontPatternDefinition,
1739
1817
  createFontPatternTile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stipple-maplibre",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Runtime-generated geometric and SVG fill patterns for MapLibre GL JS, with deterministic styling and a visual playground.",
5
5
  "license": "MIT",
6
6
  "type": "module",