A local design canvas that bundles two views into one tabbed interface:
- Tools — a single canvas with 34 SVG generators in one sidebar:
- 7 Weave generators (Slash, Quartz, Dust, Tessera, Orbit, Scale, Prism)
- 8 Form generators (Burst, Halo, Vortex, Coil, Leaf, Splat, Mirror, Bloom)
- 6 Flow generators (Surf, Wave, Ripple, Drift, Whorl, Shine)
- 1 Field generator (Haze)
- 1 Glow generator (Flux)
- 11 Tile presets (Radius, Mixtape, Odessa, Veil, Blossom, Disque, Bloks, Terrain, Trigram, Ring, Symmetry) — pure SVG grid engine, no dependency.
- Textures — browser/downloader for a library of 360 textures, paginated grid (48 per page, auto-fit columns at 270 px minimum).
A shared Palette dropdown in the right-hand panel applies one of 100 hand-picked colour palettes (from nice-color-palettes) to every generator at once — switching from Slash to Halo keeps the same colours.
Everything is plain HTML, CSS and ES2017 JavaScript served by a tiny Node.js process. No build step, no framework.
Requires Node.js 16 or newer.
git clone https://github.com/<you>/martes.git
cd martes
npm start # or: node server.jsThen open http://localhost:8081.
On macOS you can also double-click start.command from Finder — it picks Node if available, otherwise falls back to a static Python server.
martes/
├── index.html # entry point, two-tab shell
├── server.js # minimal static file server
├── start.command # macOS double-click launcher
├── start.sh # POSIX launcher
├── package.json
│
├── assets/
│ ├── css/
│ │ └── style.css # all interface styles (light minimal theme,
│ │ # responsive layout, system font stacks)
│ │
│ └── js/
│ ├── core.js # utilities (svgEl, uid, rnd…), canvas state,
│ │ # render dispatcher, tab logic, sidebar toggle
│ ├── registry.js # TOOLS array, sidebar + panel builder,
│ │ # controls, undo/redo, keyboard shortcuts,
│ │ # RAF throttle, actions (save/copy/PNG)
│ ├── palettes.js # 100 nice-color-palettes + dropdown UI,
│ │ # applyPaletteGlobal() (paints every tool)
│ ├── textures.js # paginated auto-fit gallery, filter, shuffle
│ └── tools/
│ ├── _helpers.js # shared tile engine (tileGrid, clipPaths,
│ │ # filters, registerTilePreset)
│ ├── _voronoi.js # shared Voronoi engine (buildUniformVoronoi),
│ │ # SVG shape parser, path sanitiser
│ ├── slash.js # Weave: 10-Print diagonal lines
│ ├── quartz.js # Weave: crystalline grid
│ ├── dust.js # Weave: noise particles
│ ├── tessera.js # Weave: isometric 3D tiling
│ ├── orbit.js # Weave: orbital circles
│ ├── scale.js # Weave: fish scale pattern
│ ├── prism.js # Weave: rainbow Voronoi circles
│ ├── burst.js # Form: Voronoi burst, multi-shape
│ ├── halo.js # Form: halo rings
│ ├── vortex.js # Form: spiral vortex
│ ├── coil.js # Form: spring coil
│ ├── leaf.js # Form: organic leaf grid
│ ├── splat.js # Form: paint splatter
│ ├── mirror.js # Form: reflected geometry
│ ├── bloom.js # Form: bloom circles
│ ├── surf.js # Flow: wave surface
│ ├── wave.js # Flow: oscillating wave
│ ├── ripple.js # Flow: ripple rings
│ ├── drift.js # Flow: drifting scribble
│ ├── whorl.js # Flow: whorl spiral
│ ├── shine.js # Flow: twinkling stars
│ ├── haze.js # Field: blurry gradient
│ ├── flux.js # Field: flux distortion
│ └── tile/ # 11 grid presets
│ ├── radius.js, mixtape.js, odessa.js, veil.js,
│ ├── blossom.js, disque.js, bloks.js, terrain.js,
│ └── trigram.js, ring.js, symmetry.js
│
├── textures/
│ ├── thumb/ # 360 thumbnail JPGs
│ └── full/ # 360 full-resolution JPGs
└── LICENSE
The split keeps each concern in one file. Render functions are declared in tools/<slug>.js. Each tool self-registers via TOOLS.push(...) in registry.js. Shared utilities live in _helpers.js (tile engine) and _voronoi.js (Voronoi + SVG parser). There is no bundler.
Top tabbar offers Tools and Textures. Each tab corresponds to a .tab-panel. The Tools tab is rendered eagerly; Textures is initialised lazily on first open (see core.js tab handler).
Three-column layout: sidebar with all 34 generators (grouped by category), center canvas (one <svg> element), and right panel with controls.
On screens narrower than 1024px, the sidebar collapses into a hamburger drawer. Below 768px, the panel moves under the canvas.
Every tool renders into the same SVG, so Save SVG / Save PNG / Copy / Randomize work identically across the board:
function renderTool() {
if (!currentTool) return;
clearSVG();
currentTool.render(svg, canvasW, canvasH, getState());
}Each tools/<slug>.js declares its render function and self-registers:
function renderSlash(svg, W, H, s) { /* … */ }
TOOLS.push({
cat: 'Weave', slug: 'slash', name: 'Slash', icon: '…',
desc: '10 Print-inspired diagonal lines',
render: renderSlash,
controls: [
{ type:'color', id:'color', label:'Stroke color', default:'#e83e8c' },
{ type:'range', id:'blockSize', label:'Block size', default:54, min:10, max:200, step:2 },
// …
]
});The 11 presets are pure SVG generators. The file _helpers.js ships a grid engine — tileGrid(parent, W, H, gridStr, bg, paint) — that walks an M × N grid and calls paint() for each cell. Each preset calls registerTilePreset() to auto-build controls from palette + extras.
48 textures per page on an auto-fit grid — columns are 270 px minimum, so 2 cols on a phone, 3-4 on a laptop, 5-6 on wide displays. Layout in assets/css/style.css:
.tex-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
gap: 1rem;
}A minimal Node http server that:
- Serves files from the project root.
- Decodes percent-encoding and resolves every path inside
ROOT—..traversal and null-byte injection are blocked. - Rejects requests outside the project tree with
403.
The server intentionally has no proxy, no remote fetches, no third-party endpoints. Everything runs locally.
Two independent effect layers sit under every generator's panel.
FX — post-processing built from native SVG filter primitives: Grain, Warp, Halftone, Dither, Bloom, Chromatic, Posterize, Blur, Emboss, Duotone, Glitch, Vignette, Sharpen. These live inside the document, so they survive Copy, Save-SVG and PNG alike, and they work headlessly through the API.
Shader — 24 real WebGL shaders from
@paper-design/shaders, vendored
under assets/vendor/ and loaded lazily on first use.
The two are separate because of one hard constraint: a WebGL shader renders
to a <canvas>, and a canvas cannot be serialised into an SVG document.
So a shader is preview and PNG only — Save-SVG and Copy export the
artwork without it. The panel says so where you turn it on, and PNG export
composites artwork and shader together rather than rasterising the SVG alone.
Reach for FX when the result has to stay vector; reach for Shader when you
want the GPU look and a raster export.
npm install @legenki/martesMartes ships a headless entry point: every generator is a pure function from state to SVG, usable without the browser app.
import { tools, NICE_PALETTES, applyPaletteToTool, core } from '@legenki/martes';
// Any DOM implementation works — a real browser, or jsdom in Node:
// const { window } = new JSDOM('<!doctype html><html></html>');
// globalThis.document = window.document;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 600 900');
const tool = tools.radius; // one of 34
const state = Object.fromEntries(tool.controls.map(c => [c.id, c.default]));
core.setSeed('release-poster'); // reproducible output
tool.render(svg, 600, 900, state);
console.log(svg.outerHTML);Generators draw randomness through core.random(). Seeding makes a render
byte-for-byte identical — including generated clipPath/filter ids:
core.setSeed(42); // number or string
core.setSeed(null); // back to Math.random()NICE_PALETTES holds 104 five-colour palettes. applyPaletteToTool writes a
palette into a tool's colour slots, cycling it so every slot is filled:
const store = {};
applyPaletteToTool(tool, NICE_PALETTES[7], store);
Object.assign(state, store[tool.slug]);| Import | Contents |
|---|---|
@legenki/martes |
tools, core, helpers, voronoi, NICE_PALETTES, applyPaletteToTool |
@legenki/martes/core |
utilities — toHex, setSeed, random, svgEl, lerp, … |
@legenki/martes/palettes |
palette data only, no DOM dependency |
@legenki/martes/tools/* |
a single generator, e.g. @legenki/martes/tools/tile/radius.js |
Rendering needs a document; importing does not. In Node, pair it with
jsdom and assign globalThis.document.
| Key | Action |
|---|---|
R |
Randomize |
S |
Save as SVG |
P |
Save as PNG |
C |
Copy SVG to clipboard |
↑ / ↓ |
Previous / Next tool |
Ctrl+Z |
Undo |
Ctrl+Y or Ctrl+Shift+Z |
Redo |
Single-key shortcuts (R/S/P/C/↑/↓) work without modifiers and are disabled whenever focus is inside an <input>, <textarea>, or <select>. Inside the palette dropdown, ↑/↓ walk the filtered list, Home/End jump to the first/last match, Enter selects, Escape closes.
A single light minimal palette in :root at the top of assets/css/style.css:
| Token | Value |
|---|---|
--bg-main |
#ffffff |
--bg-soft |
#fafafa |
--bg-canvas |
#f4f4f5 |
--text-primary |
#18181b |
--text-muted |
rgba(24,24,27,0.55) |
--accent |
#18181b (monochrome) |
--border |
rgba(0,0,0,0.08) |
--font-sans |
system UI stack |
--font-serif |
ui-serif, Georgia, … |
--font-mono |
ui-monospace, SF Mono… |
All controls read from these tokens. To rebrand, edit :root and the rest follows. Fonts are system-only — no @font-face declarations, no woff2 downloads.
textures/full/ is currently ~280 MB (compressed JPGs, avg ~780 KB each). If you fork this repo:
- Recompress the JPGs (
cjpeg -quality 80or similar) before committing. - Or move
textures/full/to a release asset and load it on first run. - Or add
textures/full/to.gitignoreand ship onlytextures/thumb/.
Thumbnails alone (textures/thumb/, ~20 MB) are enough for the gallery; full files are only fetched on download.
- Create
tools/<slug>.jswith a render function:function renderMytool(svg, W, H, s) { svg.appendChild(svgEl('rect', { x:0, y:0, width:W, height:H, fill:s.bgColor })); // … }
- Self-register via
TOOLS.push(...):TOOLS.push({ cat:'Weave', slug:'mytool', name:'Mytool', icon:'…', desc:'…', render:renderMytool, controls:[ { type:'color', id:'bgColor', label:'Background', default:'#fff' }, { type:'range', id:'size', label:'Size', default:50, min:10, max:200, step:1 }, ] });
- Add
<script src="assets/js/tools/mytool.js"></script>toindex.html.
- Write a
renderTileFoo(svg, W, H, s)function intools/tile/foo.js:function renderTileFoo(svg, W, H, s) { const { defs, pal, grid, freq } = setupTile(svg, W, H, s); tileGrid(svg, W, H, grid, pal[0], (cell, x, y, w, h) => { if (!chance(freq)) return; cell.appendChild(svgEl('circle', { cx: x + w/2, cy: y + h/2, r: Math.min(w,h)/3, fill: pickFrom([pal[1], pal[2], pal[3]]) })); }); }
- Register via
registerTilePreset(...):registerTilePreset({ slug:'tile-foo', name:'Foo', render:renderTileFoo, palette:['#fff','#000','#3FFFB2','#3EECFF','#FF3D8B','#ECFF3D'], defaults:{ grid:'4x6', frequency:1 }, extras:['grid','frequency'] });
- Add the
<script>tag toindex.html.
Modern Chromium / Firefox / Safari. Uses native CSS Grid, aspect-ratio, backdrop-filter, :focus-visible, replaceChildren(), and ES2017 — no transpiler, no custom elements.
MIT.
