JavaScript API · @shapeaddress/core 0.3.0
One function, a few readers, a registry
The SVG string is the product. Everything else is how to make one or read one back. Every call is pure and deterministic: the same input and options always give the same string.
- Loading
- shapeaddress()
- Inputs
- Options
- toDataUri
- encode
- decode
- legend
- cells
- defaults
- Dictionaries
- Constants
- Errors
Loading
// ESM (bundlers, modern Node)
import shapeaddress from '@shapeaddress/core';
// CommonJS
const shapeaddress = require('@shapeaddress/core');
// script tag: a global named shapeaddress
<script src="https://cdn.jsdelivr.net/npm/@shapeaddress/core@0.3.0/dist/shapeaddress.min.js"></script>Types ship with the package (index.d.ts). The package has no dependencies and no side effects beyond registering the default dictionary.
shapeaddress(input, options?)
shapeaddress(input: Input, options?: Options): string
Returns the SVG markup for the input. The root element carries width and height in px, a viewBox, data-dict (the dictionary id) and data-sa (format, dictionary, layout and hex, informative only). Each cell is a <g> with data-i, data-shape and data-color; those three attributes are what decode reads.
shapeaddress('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045');
// '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 532 424" width="64" height="51" aria-hidden="true" data-dict="sa330" data-sa="2:sa330:grid:d8da…">…</svg>'
shapeaddress('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', { layout: 'coin', size: 96, title: 'vitalik.eth' });
shapeaddress({ text: 'entity.id' }, { style: 'glyph' });
shapeaddress([0, 31, 32, 255], { labels: true, gap: 0, cell: 'square' });With no title the SVG is aria-hidden="true", which is right for a decorative avatar next to the address text. With a title it becomes role="img" with a <title> child.
Inputs
Every function that takes an input accepts the same three forms. The output is always one cell per byte.
| Form | Example | Rules |
|---|---|---|
| Hex string | '0xd8dA…6045', 'd8da…' | 0x optional, any case, whitespace trimmed, even number of digits. Checksum casing never changes the picture. |
| Text | { text: 'entity.id' } | UTF-8 bytes of the string. One cell per byte, so non-Latin text uses more cells. |
| Bytes | [216, 218, 107], new Uint8Array(…) | Any array-like of integers 0 to 255. |
A wallet address is 20 bytes and renders as 20 cells: four rows of five in the grid, a ring of twenty around a checksum in the coin.
Options
All optional. Unknown keys throw, so a typo never silently renders the default.
| Option | Type | Default | What it does |
|---|---|---|---|
dictionary | string or dictionary object | 'sa330' | Which colour and glyph tables to draw with. An id must be registered; a dictionary object is registered on first use. Unknown id throws. Changes the data mapping, so decode must use the same dictionary (it reads the id from the SVG). |
layout | 'grid' or 'coin' | 'grid' | Grid: 5 columns in reading order. Coin: ring clockwise from the notch at 12 o'clock, with the XOR of all bytes as a checksum cell in the centre. |
size | number, 1 to 4096 | 64 | Rendered width in px. Height follows the layout's aspect: the grid is 532 x 424 units for 20 cells, the coin is square. |
style | 'tile' or 'glyph' | 'tile' | Tile: white glyph on a coloured tile. Glyph: coloured glyph on the page ground, no tile. |
cell | 'rounded', 'circle', 'square' | 'rounded' | Tile shape in the grid. The coin always draws circles. |
gap | number, 0 to 40 | 8 | Space between grid cells in viewBox units; a cell is 100 units. Grid only. |
radius | number, 0 to 50 | 14 | Corner radius of rounded cells, in cell units. |
background | CSS colour string | 'transparent' | Adds a full-size rect behind the cells when not transparent. |
palette | 'default' or 'cvd' | 'default' | cvd adds a distinct hatch pattern per colour for colour-blind readers. Pattern ids are sa-h0 to sa-h7; identical across icons, so many icons on one page share the defs without clashing. |
labels | boolean | false | Prints each byte's hex on its cell. Useful from 128 px up. |
title | string | undefined | Sets role="img" and a <title>. Omitted: aria-hidden="true". |
highlight | number[] | [] | Cell indices to outline with a dashed ring, for showing which cells differ between two addresses. |
The same address under different options. Only dictionary changes the data; everything else changes the drawing.
shapeaddress.toDataUri(input, options?)
toDataUri(input: Input, options?: Options): string
The same SVG as a data:image/svg+xml;utf8,… URI for <img src> or CSS url(). Use it in long lists: the browser rasterises an image without parsing SVG into the DOM for each row.
img.src = shapeaddress.toDataUri('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', { size: 32 });shapeaddress.encode(input)
encode(input: Input): number[]
Normalises any input form to its bytes. This is the first step of every other call, exposed so you can validate input or compare two addresses byte by byte.
shapeaddress.encode('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'); // [216, 218, 107, 242, …]
shapeaddress.encode({ text: 'hi' }); // [104, 105]
shapeaddress.encode('0xzz'); // throws: input is not hexshapeaddress.decode(svg, options?)
decode(svg: string, options?: { dictionary?: string | Dictionary }): { dictionary, layout, bytes, hex, checksumOk? }
Reads a ShapeAddress SVG back to its bytes. It reads the cells' data-shape and data-color names, not the informative data-sa hex, and looks the names up in the dictionary named by data-dict on the root. Pass dictionary to override that, for example for an SVG that was stripped of its root attributes. For the coin layout, checksumOk says whether the centre cell matches the XOR of the ring.
const svg = shapeaddress('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', { layout: 'coin' });
shapeaddress.decode(svg);
// { dictionary: 'sa330', layout: 'coin', bytes: [216, …], hex: 'd8da6bf26964af9d7eed9e03e53415d37aa96045', checksumOk: true }
// from the DOM
shapeaddress.decode(document.querySelector('#avatar').innerHTML).hex;Decoding throws when the SVG names a dictionary that is not registered (the message names the id), when a cell's names are not in that dictionary, or when the string holds no cells at all. It never returns a guess.
shapeaddress.legend(input, options?)
legend(input: Input, options?: { dictionary? }): string[]
The spoken form: one string per cell, colour name plus glyph name, in reading order. The names are the dictionary's, so the same byte reads differently under a different dictionary.
shapeaddress.legend('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045');
// ['purple flower', 'purple three-dots', 'green moon', 'pink bar', …]
shapeaddress.legend('0x00'); // ['red circle']
shapeaddress.legend('0xff'); // ['pink lens']
shapeaddress.legend('0x00', { dictionary: 'fa330' }); // ['purple square']shapeaddress.fromLegend(text, options?)
fromLegend(text: string | string[], options?: { dictionary? }): { dictionary, bytes, hex }
The reverse of legend: the words a person said or typed, back to bytes. Pass an array of cells or one string with cells separated by commas, semicolons or newlines. Any canonical name, language label or alias of the dictionary works, in either order inside a cell, ignoring case, accents and Arabic diacritics. A bare hex byte such as d8 is accepted as a cell. The first cell that cannot be read throws an error that names it.
shapeaddress.fromLegend('purple flower, purple three-dots, green moon').hex // 'd8da6b'
shapeaddress.fromLegend(['flower purple', 'THREE-DOTS purple']).hex // 'd8da'
shapeaddress.legend('0x00', { lang: 'ar' }) // a dictionary with Arabic labels answers in Arabiclegend(input, { lang }) picks each entry's label in that language and falls back to the canonical name, so a dictionary can carry English, Arabic and Portuguese words for the same cell and decode from any of them.
shapeaddress.layouts
layouts.register(id, fn) · layouts.has(id) · layouts.list()
Layouts are pluggable. A layout is a function (bytes, options, helpers) => svg that uses helpers.open(w, h, options, id, bytes) for the root and helpers.tile(byte, index, options, round) for each cell, so every layout stays decodable. The two built-in layouts are grid and coin; the id you register becomes a valid layout option and is written on the SVG root.
shapeaddress.layouts.register('row', (bytes, o, h) => {
let s = h.open(bytes.length * 100, 100, o, 'row', bytes);
bytes.forEach((b, i) => { s += '<g transform="translate(' + i * 100 + ' 0)">' + h.tile(b, i, o, false) + '</g>'; });
return s + '</svg>';
});
shapeaddress(address, { layout: 'row' });shapeaddress.cells(input, options?)
cells(input: Input, options?: { dictionary?, layout? }): { dictionary, layout, hex, cells: CellInfo[], checksum: CellInfo }
The data behind the picture, for JSON consumers and other renderers: per cell the index, the byte, the colour and glyph indices, their names and the colour's hex. checksum is the cell that the coin layout draws in the centre.
shapeaddress.cells('0xd8da');
// {
// dictionary: 'sa330', layout: 'grid', hex: 'd8da',
// cells: [
// { i: 0, byte: 216, shape: 24, color: 6, shapeName: 'flower', colorName: 'purple', hex: '#7048e8' },
// { i: 1, byte: 218, shape: 26, color: 6, shapeName: 'three-dots', colorName: 'purple', hex: '#7048e8' },
// ],
// checksum: { byte: 2, shape: 2, color: 0, shapeName: 'triangle', colorName: 'red', hex: '#c92a2a' },
// }shapeaddress.defaults
A frozen object with every option's default. Compare against it to print only what differs, as the playground does.
shapeaddress.defaults;
// { dictionary: 'sa330', layout: 'grid', size: 64, style: 'tile', cell: 'rounded', gap: 8, radius: 14,
// background: 'transparent', palette: 'default', labels: false, title: undefined, highlight: [] }shapeaddress.dictionaries
The registry of colour and glyph tables. sa330 is registered at load. Others are registered by you, once, before use. The dictionaries page shows every cell of each registered dictionary.
| Call | Returns | Notes |
|---|---|---|
register(dict) | the registered, frozen dictionary | Validates and normalises. Throws on a bad product, bad names, duplicates, or an id already taken by a different object. Registering the same object twice is a no-op. |
get(id) | the registered dictionary | Throws unknown dictionary "id" when not registered. |
has(id) | boolean | Guard before registering a dictionary shipped as a global. |
list() | [{ id, name, colors, glyphs, license }] | Counts, not tables. Use get(id) for the tables. |
The dictionary object
Every colour and glyph has a canonical name (lowercase ASCII, written into the SVG), optional labels per language ({ ar: "دائرة", pt: "círculo" }) and optional aliases (other words a reader may say). All of them decode through fromLegend; a word that names two different cells is rejected at registration.
From the format spec: a dictionary is an ordered list of n colours and an ordered list of m glyphs with n x m = 256, under a lowercase id. Byte b maps to glyph b mod m and colour (b - shape) / m. An id names its exact tables forever; a changed glyph or colour is a new dictionary with a new id. Ids starting with sa are reserved for ShapeAddress.
{
id: 'sa330', // [a-z0-9][a-z0-9-]*, written into every SVG as data-dict
version: 1, // for display; a real change is a new id
name: 'ShapeAddress 330',
author: 'Husam Abboud',
license: 'MIT',
colors: [{ name: 'red', hex: '#c92a2a' }, …], // n entries
glyphs: [
{ name: 'circle', svg: '<circle cx="50" cy="50" r="34"/>' }, // markup in a 100 x 100 box
{ name: 'bolt', d: 'M349.4 44.6c5.9…', box: [448, 512] }, // or one path in its own box, scaled to fit
… // m entries, n x m = 256
],
}Names are lowercase [a-z0-9-] and unique within their list. The registered form is frozen and adds byColor and byGlyph (name to index) for decoders. The dictionaries page has a full worked example.
Constants
| Property | Value |
|---|---|
shapeaddress.VERSION | '0.3.0', the package version |
shapeaddress.FORMAT | 2, the format version written into data-sa |
shapeaddress.COLORS | The default dictionary's colours, [{ name, hex }]. Kept for v0.2 callers; prefer dictionaries.get('sa330').colors. |
shapeaddress.GLYPHS | The default dictionary's glyphs as [name, svg] pairs. Kept for v0.2 callers; prefer dictionaries.get('sa330').glyphs. |
Errors
Every failure throws a plain Error whose message starts with shapeaddress: . Nothing returns null or a fallback picture; a wrong input never renders as a plausible address.
| Message | When |
|---|---|
empty input | Empty string, empty text, empty byte array. |
input is not hex | A character outside 0-9 a-f after the optional 0x. |
hex must have an even number of digits | Odd length. |
bytes must be integers 0..255 | A byte array with a non-integer or an out-of-range value. |
input must be a hex string, {text} or a byte array | Any other type. |
unknown option "x" | A key that is not in defaults. |
layout must be one of grid, coin and the same for style, cell, palette | A value outside the enumerated set. |
size must be a number between 1 and 4096, gap … 0 and 40, radius … 0 and 50 | Out of range or not a number. |
background must be a CSS colour string, labels must be true or false, title must be a string, highlight must be an array of cell indices | Wrong type. |
unknown dictionary "id" | Rendering, decoding or get with an id that was never registered. |
dictionary id must be lowercase [a-z0-9-], dictionary x needs colors[] and glyphs[], dictionary x: colors x glyphs must equal 256, got n x m, … duplicate colour, … duplicate glyph, … glyph "y" needs svg or d+box | register with an invalid dictionary. |
dictionary id "x" is already registered | register with a different object under a taken id. |
svg names no dictionary; pass {dictionary} | decode on markup with no data-dict and no override. |
cell "colour glyph" is not in dictionary x | decode found a cell whose names the dictionary does not have, for example an SVG drawn with another dictionary and a forced override. |
no ShapeAddress cells found | decode on markup without cells. |