Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ffd6faa
Add a JS API for using the icons on pins & such!
Mar 9, 2026
e13040e
Update README.md
dschep Mar 9, 2026
7b37141
Update README.md
dschep Mar 9, 2026
572b4a3
test covg!
Mar 10, 2026
ee48e33
Merge branch 'api' of github.com:dschep/pinhead into api
Mar 10, 2026
40e5429
doh, no lock file
Mar 10, 2026
9b00f12
Update README.md
dschep Mar 11, 2026
4b741cc
Update README.md
dschep Mar 11, 2026
47a905f
Update README.md
dschep Mar 11, 2026
5348013
import examples
Mar 11, 2026
75dbbfe
some reorg and format readme too mostly to ensure consistent js forma…
Mar 11, 2026
d98f821
format ci check
Mar 11, 2026
fee2e73
rename getSprite to getIcon
Mar 11, 2026
e7316f8
WIP migrateName. need to think through logic
Mar 11, 2026
5569857
oops
Mar 11, 2026
b4899c0
fix one bug
Mar 11, 2026
73e0a18
fix one bug
Mar 11, 2026
a32c563
fmt
Mar 11, 2026
0e9b286
Update check-format-js.yml
dschep Mar 11, 2026
638a3f7
revamp migrate
Mar 12, 2026
b47996a
Merge branch 'api' of github.com:dschep/pinhead into api
Mar 12, 2026
688f8cf
version
Mar 12, 2026
da5647a
Update README.md
dschep Mar 12, 2026
f004011
Merge remote-tracking branch 'upstream/main' into api
dschep Mar 19, 2026
7a0c523
bump
dschep Mar 19, 2026
b64b87a
Merge remote-tracking branch 'upstream/main' into api
dschep Mar 20, 2026
0348960
rotation
dschep Mar 23, 2026
a4e22ca
translucency!
dschep Mar 23, 2026
fc6cddb
flip
dschep Mar 24, 2026
155cebd
fix weird bug with hole in halo
dschep Mar 24, 2026
f59aaf1
doc
dschep Mar 24, 2026
48aba62
custom shapes!
dschep Mar 26, 2026
db3fb0d
reuse height&width
dschep Mar 26, 2026
29fd9ae
Merge remote-tracking branch 'upstream/main' into api
dschep Mar 26, 2026
c8304bd
fix bug with no shapes
dschep Mar 26, 2026
bf7852a
node25
dschep Mar 26, 2026
a6b43d0
nodeversion pkgjson and versionsync
dschep Mar 26, 2026
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
16 changes: 16 additions & 0 deletions .github/workflows/check-format-js.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Check JS formatting

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.x'
- run: npm --prefix js i
- run: npm --prefix js run check-format

16 changes: 16 additions & 0 deletions .github/workflows/test-js.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Run JS Tests

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '25.x'
- run: npm --prefix js i
- run: npm --prefix js test

5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ font/package.json
font/pinhead.css
font/pinhead.ttf
font/preview.html
qgis_resources_repo
qgis_resources_repo
node_modules/
package-lock.json
js/test/fixtures/*-diff.png
223 changes: 223 additions & 0 deletions js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Pinhead JS

**Pinhead JS** is a utility and library for composing [**Pinhead**](https://pinhead.ink) icons into
various shapes, including pins, markers, circles, and squares. It's designed for map developers who
need flexible, programmatically generated icons for MapLibre GL JS, Leaflet, or any other web-based
mapping platform.

![](./examples/cafe-black-stroke.svg)
![](./examples/bike-circle-green.svg)
![](./examples/jeep-map_pin-stroke-1.svg)
![](./examples/cargobike-square-blue.svg)
![](./examples/burger-marker.svg)
![](./examples/sun-square-yellow.svg)
![](./examples/plane-square-navy.svg)
![](./examples/ice_cream-circle-pink.svg)
![](./examples/beer-marker-amber.svg)
![](./examples/rocket-map_pin-purple.svg)
![](./examples/pizza-square-red.svg)
![](./examples/bus-circle-blue.svg)
![](./examples/camera-marker-darkgrey.svg)
![](./examples/tree-map_pin-green.svg)
![](./examples/tent-square-brown.svg)

## Features

- **Icon Composition:** Layer any Pinhead icon onto background shapes.
- **Smart Coloring:** Automatically chooses contrasting icon colors based on the background fill.
- **Multiple Shapes:** Supports `circle`, `square`, `map_pin`, and `marker`.
- **Basic transforms:** Rotate and flip icons.
- **CLI & API:** Use it as a command-line tool for batch processing or as a JavaScript library in your app.
- **Custom Icon SVGs:** Pass raw SVG strings as the icon name to use custom icons.
- **Custom Shapes:** Use custom SVG strings or PNG data URIs as background shapes.
- **Migration:** A function is provided to simplify the usage of Pinehead's `changelog.json`.

## Installation

```bash
npm install @waysidemapping/pinhead-js
```

## Options

These options are common across both the CLI and API.

| Option | Description | Default |
| :------------- | :--------------------------------------------------------------------------------------------- | :---------------------------------------------------- |
| `cornerRadius` | Corner radius (applies to `square` only) | `4` |
| `fill` | Sets the fill color of the icon | `black` or `white` (auto-calculated from `shapeFill`) |
| `padding` | Internal padding between icon and shape edge | Varies by shape |
| `scale` | Scale factor for the output SVG dimensions | `1` |
| `shape` | Background shape: `square`, `circle`, `map_pin`, `marker`, a raw SVG string, or a PNG data URI | `none` |
| `shapeFill` | Fill color of the background shape | `black` |
| `stroke` | Color of the stroke (applies to shape if present, otherwise icon) | Auto-calculated (contrasting or darkened/lightened) |
| `strokeWidth` | Width of the stroke | `1` for `marker`, else `0` |
| `flip` | Flip the icon: `horizontal` or `vertical` | `none` |
| `rotate` | Rotate the icon | `none` |

---

## Usage

### JavaScript API

#### Create Icons

Ideal for dynamic icon generation in the browser or on the server.

```javascript
import { getIcon } from "@waysidemapping/pinhead-js";

// Simple icon
const svg = getIcon("cargobike");

// Icon with background and custom colors
const marker = getIcon("jeep", {
shape: "map_pin",
shapeFill: "#6486f5",
strokeWidth: 1,
});
```

##### Examples

| Result | Code |
| :---------------------------------------- | :------------------------------------------------------------------------------------------------ |
| ![](./examples/cargobike.svg) | `getIcon("cargobike")` |
| ![](./examples/cafe-black-stroke.svg) | `getIcon("cup_and_saucer", { strokeWidth: 1 })` |
| ![](./examples/bike-circle-green.svg) | `getIcon("bicycle", { shape: "circle", shapeFill: "white", fill: "#6dad6f", stroke: "#6dad6f" })` |
| ![](./examples/burger-marker.svg) | `getIcon("burger", { shape: "marker", shapeFill: "#3FB1CE" })` |
| ![](./examples/ice_cream-circle-pink.svg) | `getIcon("ice_cream_on_cone", { shape: "circle", shapeFill: "pink" })` |
| ![](./examples/rocket-map_pin-purple.svg) | `getIcon("rocketship", { shape: "map_pin", shapeFill: "purple" })` |

#### Custom Icon SVGs

You can pass a raw SVG string as the icon name to use a custom icon.

```javascript
import { getIcon } from "@waysidemapping/pinhead-js";

const customIcon = getIcon(
'<svg viewBox="0 0 15 15"><path d="M7.5 10a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z" fill="red"/></svg>',
{
shape: "circle",
shapeFill: "white",
},
);
```

#### Custom Shapes

You can provide your own background shapes as SVG strings or PNG data URIs. Use the `padding` option as an array `[x, y]` to precisely position the 15x15 icon within your custom shape.

```javascript
import { getIcon } from "@waysidemapping/pinhead-js";

// Custom SVG shape
const customSvg = getIcon("bicycle", {
shape:
'<svg viewBox="0 0 25 25"><circle cx="12.5" cy="12.5" r="10" fill="green"/></svg>',
padding: [5, 5],
});

// Custom PNG shape (base64 data URI)
const customPng = getIcon("bus", {
shape:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAjCAYAAABhCKGo...",
padding: [5, 10],
});
```

#### Migrate an icon name

```javascript
import { migrateName } from "@waysidemapping/pinhead-js";

// Migrate a name previously used by Pinhead. If a name was used more than once, the more recent name is returned.
let name = migrateName("pedestrian"); // -> "person_walking"

// Migrate from a specific version (treasure_map was renamed, in v13, but a new treasure_map was introduced then too)
migrateName("treasure_map", "pinhead@10"); // -> "bifold_map_with_dotted_line_to_x"
migrateName("treasure_map", "pinhead@13"); // -> "treasure_map"

// Migrate from a seed source
migrateName("maps", "nps"); // -> "bifold_map_with_dotted_line_to_x"
```

### Command Line Interface (CLI)

#### 1. Generate a single icon

Outputs the SVG string directly to `stdout`.

```bash
npx pinhead get-icon cargobike --shape=square --shapeFill='#6486f5' > icon.svg
```

#### 2. Batch build from configuration

The `build-icons` command creates a collection of SVG files based on a JSON configuration file. By default, it looks for `pinhead.json` and writes results to a `./svgs/` directory.

```bash
npx pinhead build-icons --config my-icons.json --outdir ./assets/icons
```

**`pinhead.json` structure:**

```json
{
"groups": [
{
"icons": {
"bicycle": "bike-icon",
"bus": "bus-marker"
},
"options": {
"shape": "circle",
"shapeFill": "#6486f5"
}
}
]
}
```

---

## Custom Icon SVG requirements

To work with Pinhead JS as a custom icon (passed as the `name` argument), custom SVG strings must follow these constraints:

- Use only `<path>` elements.
- Path elements should only contain the `d` attribute.
- The `viewBox` should be `"0 0 15 15"`, or `height` and `width` should be set to `15`.

---

## Integrations

### MapLibre GL JS

To use Pinhead JS dynamically with MapLibre:

```javascript
const svg = getIcon("greek_cross", { shape: "circle", shapeFill: "red" });

const img = new Image();
const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
img.src = URL.createObjectURL(blob);
await img.decode();

map.addImage("hospital-icon", img);

URL.revokeObjectURL(url);
```

---

## Inspiration

Pinhead JS is inspired by the [Maki Icon Editor](https://labs.mapbox.com/maki-icons/editor/) and [makiwich](https://github.com/mapbox/makiwich)

## License

Pinhead JS is distributed under [CC0](/LICENSE).
82 changes: 82 additions & 0 deletions js/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env node

import fs from "fs";
import { parseArgs } from "util";
import { getIcon } from "./index.js";

const commands = {
"get-icon": {
config: {
options: {
fill: { type: "string" },
shape: { type: "string" },
shapeFill: { type: "string" },
stroke: { type: "string" },
strokeWidth: { type: "string" },
padding: { type: "string" },
cornerRadius: { type: "string" },
scale: { type: "string" },
},
allowPositionals: true,
},
run: ({ values, positionals }) => {
if (values.strokeWidth)
values.strokeWidth = parseFloat(values.strokeWidth);
if (values.padding) values.padding = parseFloat(values.padding);
if (values.cornerRadius)
values.cornerRadius = parseFloat(values.cornerRadius);
if (values.scale) values.scale = parseFloat(values.scale);
// validate arg
switch (positionals.length) {
case 0:
console.error("No icon name specified!");
return 1;
case 1:
break;
default:
console.error("More than one icon name specified!");
return 1;
}
console.log(getIcon(positionals[0], values));
return 0;
},
},
"build-icons": {
config: {
options: {
config: { type: "string", default: "pinhead.json" },
outdir: { type: "string", default: "./svgs" },
},
},
run: ({ values }) => {
const config = JSON.parse(fs.readFileSync(values.config));
fs.mkdirSync(values.outdir, { recursive: true });
for (const { icons, options } of config.groups) {
for (const [icon, name] of Object.entries(icons)) {
fs.writeFileSync(
`${values.outdir}/${name}.svg`,
getIcon(icon, options),
);
}
}
return 0;
},
},
};

if (process.argv.length < 3) {
console.log(`Supported subcommands: ${Object.keys(commands).join(", ")}`);
process.exit(0);
}
const subcommand = process.argv[2];
const args = process.argv.slice(3);
const command = commands[subcommand];
if (!command) {
if (subcommand.startsWith("-")) {
console.error(`No subcommand specified`);
} else {
console.error(`Unknown subcommand: ${subcommand}`);
}
process.exit(1);
}
process.exit(command.run(parseArgs({ ...command.config, args })));
1 change: 1 addition & 0 deletions js/examples/beer-marker-amber.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions js/examples/bike-circle-green.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions js/examples/burger-marker.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions js/examples/bus-circle-blue.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions js/examples/cafe-black-stroke.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading