Wang Tiles for Seamless Generative Textures
Build seamless generative textures from edge-matched Wang Tiles with seeded constraint solving, seam oracles, and repetition diagnostics.
Wang Tiles let a finite set of edge-matched motifs grow into seamless generative textures without hiding the construction rules. This tutorial builds a seeded solver, validates every shared edge, measures repetition, and exports a reproducible recipe.
Wang Tiles turn seams into edge constraints
Wang Tiles are square motifs whose north, east, south, and west edges carry codes. Two neighboring tiles may touch only when their shared codes agree. That tiny grammar is enough to assemble a large texture from a finite authored set without blending every boundary at runtime. The design task is to make every matching code share the same boundary pixels while letting each tile interior vary.
Begin with the seam, not the ornament. For each edge code, freeze a strip of pixels, curve endpoints, or vector landmarks that every tile using that code must reproduce. Corners need a convention too: either edge strips exclude corners, or corner pixels are derived from an ordered pair of adjacent codes. The first figure names this edge grammar with both letters and line patterns, so matching does not depend on color alone.
The downloadable generator uses a small synthetic tile catalog made from original SVG-like instructions rendered as text cells. It verifies adjacency and measures repeated tile identifiers. The texture is intentionally illustrative: it proves edge matching, seeded choice, bounded backtracking, and a portable receipt. It does not claim perceptual nonrepetition, reproduce a licensed texture source, or benchmark a Canvas implementation.
Author an edge-matched tile catalog
A practical catalog stores an identifier, four edge codes, and interior drawing parameters. Add rotations only if rotating both the art and ordered edge tuple preserves the intended style. Mirror transforms are more dangerous because they can reverse directional marks or typography. Wang Tiles work best when transformed variants are materialized and reviewed as ordinary tiles instead of generated invisibly during placement.
Cohen, Shade, Hiller, and Deussen’s paper on Wang Tiles for image and texture generation describes stochastic tiling with edge-matched tiles and discusses avoiding obvious repetition. The technique does not guarantee aperiodicity merely because tiles carry colored edges. A weak catalog or deterministic solver can still repeat. Use “nonperiodic tiling” as a measured goal, not a magical property of any set.
For each tile, archive the authored edge tuple, asset digest, interior family, and provenance. Reject duplicate identifiers, missing edge codes, and catalogs where a required west or north code has no candidate. This resembles Truchet tile composition in its combinatorial rhythm, but the matching contract differs: Truchet arcs compose through orientation, while this catalog exposes explicit boundary labels that a solver can validate.
Semantic equivalent: the edge tuples are listed below.
- Tile 1
- A-B-C-A clockwise.
- Tile 2
- C-A-B-B; its west B matches tile 1 east B.
- Tile 3
- North B and west A.
Place Wang Tiles with seeded constraint solving
Raster-scan placement is the simplest solver. At each cell, filter the catalog by the south code of the tile above and east code of the tile to the left. Shuffle candidates with a seeded generator, choose one, and backtrack when a future cell has no candidate. Wang Tiles remain deterministic when catalog order, candidate ordering, random generator, and traversal order are all part of the recipe.
Greedy placement without backtracking can paint itself into a corner even when the catalog has a valid solution. Unlimited backtracking can freeze a browser on a hostile or incomplete set. Set a maximum attempt count proportional to grid size, clear partial output on failure, and emit the cell and required codes that caused the dead end. The artifact caps search and rejects impossible catalogs before export.
The second figure shows a constructed four-by-three contact sheet. Arrows reveal traversal order, edge marks show constraints, and tile IDs make repeated choices visible. It is an explanatory fixture rather than output captured from the downloadable program. The public artifact independently produces its own six-by-six receipt. This separation prevents a static diagram from masquerading as runtime proof while still teaching how north and west neighbors constrain each new choice.
Render a Canvas texture generator safely
A Canvas texture generator can draw the chosen tile bitmap into each cell, but placement truth should stay independent from pixels. Keep a grid of tile IDs and edge tuples as the canonical output; rendering consumes that grid. This makes seam validation, replay, and export possible without reading pixels back from the canvas. It also lets a server or SVG renderer use the same recipe.
The HTML Standard defines the canvas element and its bitmap rendering model. Canvas itself does not provide a scene graph or accessible description of the drawn pattern. Pair the bitmap with a textual receipt: dimensions, tile size, seed, catalog digest, grid IDs, adjacency result, and limitation. If the texture is meaningful content, provide an equivalent image description rather than leaving fallback text generic.
For high-density displays, separate CSS size from backing-store size and scale the context once. For export, await a real blob and disable stale links whenever seed, dimensions, or catalog changes. The Node artifact here exports JSON rather than pixels, so it does not claim browser rendering parity. Use the recipe with image quilting techniques when boundaries must be selected from source-image overlap rather than a hand-authored code vocabulary.
- Traverse left to right, then top to bottom.
- At each cell, require the north and west edge codes already exposed.
- Seeded shuffle orders valid tile candidates.
- Backtrack within a fixed attempt cap when no candidate remains.
The contact sheet is illustrative, separate from the runnable recipe.
Measure seams before judging style
Edge-code equality is a structural test; pixel continuity is a rendering test. Validate every horizontal pair as left.east equals right.west and every vertical pair as top.south equals bottom.north. If tiles are rasters, compare the exact boundary strips after color conversion and scaling. Wang Tiles can pass their symbolic grammar while still showing a one-pixel seam caused by interpolation, antialiasing, or inconsistent corner ownership.
Record the coordinate and both codes for every mismatch. A zero-mismatch grid proves only the declared adjacency contract. It does not prove that the interior composition is attractive, that repeating motifs are hidden, or that exported filtering will remain seam-free at arbitrary transforms. Those require visual inspection at native scale, zoom, and the final renderer.
The third figure separates two maps. A seam map marks every adjacency as a line with a check or cross. A repetition heatmap counts tile-ID reuse by region and uses numerals plus intensity, never color alone. This is a more honest creative review than one “quality score.” Structural continuity can be binary while repetition, directional bias, motif clustering, and visual balance remain independent art-direction questions.
Reduce repetition without erasing authorship
A larger catalog is not automatically a better texture. Ten tiles that share one loud central motif can repeat more visibly than four restrained variants. Track immediate same-ID neighbors, short row cycles, regional frequency, and repeated interior-family signatures. Then alter catalog art or candidate weights while keeping the edge grammar valid. Wang Tiles provide a constraint scaffold for seamless procedural textures; the visual voice still comes from authored marks.
Use seeded randomness to make critique reproducible. A designer should be able to say “seed 908 has a diagonal clump in rows three through five,” edit the catalog, and rerun the same recipe. The guide to seeded randomness in generative art explains why seed alone is insufficient if traversal order or generator version changes. Archive all of them.
Weights should be conditional when necessary. A tile that is rare globally may still cluster because only it satisfies one edge pair. Report candidate counts per cell and code-pair coverage. If a pair has exactly one candidate, the catalog is forcing that motif. This diagnostic turns visible repetition into a catalog-design problem instead of asking the random function for luckier output.
Run the Wang Tiles recipe generator
The public Node program defines an original twelve-tile catalog, a seeded xorshift generator, raster traversal, bounded recursive backtracking, and a validator. It creates a six-by-six grid of Wang Tiles, verifies every shared edge, counts identifiers, scans short horizontal repeats, and emits the entire recipe with a SHA-256 digest. No network, image, or external asset is used.
The independent test executes the artifact twice and requires identical JSON. It changes the seed and requires a different grid while preserving zero seam errors. A one-by-one boundary succeeds without neighbors. Hostile fixtures reject a duplicate tile ID, an edge tuple with missing fields, and an impossible catalog before a receipt can be exported. Tests recompute each adjacency from the raw grid and catalog rather than trusting the artifact’s mismatch count.
The claim boundary is narrow: a valid symbolic tiling from this catalog. The fixture does not establish pixel-perfect raster seams, universal aperiodicity, or artistic quality. To experiment with spatial density rather than edge grammar, compare the approach with Poisson-disk layouts. Keep those questions separate so each algorithm earns its place in the composition.
| Region | Most-used tile count | Cue |
|---|---|---|
| Northwest | 2 | Open circle |
| Northeast | 4 | Half circle |
| Southwest | 7 | Square |
| Southeast | 3 | Quarter circle |
Every shown seam has a check mark; counts are illustrative.
Runnable artifact — Seeded symbolic edge matching and repetition diagnostics; not pixel seam or aesthetic proof.
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
const sha=value=>createHash("sha256").update(JSON.stringify(value)).digest("hex");
const tiles=["ABCA","CABB","BACB","BBAC","ACBC","CBBA","AABC","BCAB","CAAC","BCCA","ABBC","CABC"].map((edge,id)=>({id:"T"+(id+1),n:edge[0],e:edge[1],s:edge[2],w:edge[3]}));
function validateCatalog(list){if(new Set(list.map(x=>x.id)).size!==list.length)throw new Error("duplicate-tile-id");for(const tile of list)if(!tile.id||![tile.n,tile.e,tile.s,tile.w].every(x=>/^[ABC]$/.test(x)))throw new Error("invalid-edge-tuple")}
function rng(seed){let state=seed>>>0;return()=>{state^=state<<13;state^=state>>>17;state^=state<<5;return(state>>>0)/4294967296}}
function place(width,height,seed,catalog=tiles){validateCatalog(catalog);if(!Number.isInteger(width)||!Number.isInteger(height)||width<1||height<1||width*height>400)throw new Error("invalid-grid");const random=rng(seed),grid=Array(width*height).fill(null);let attempts=0;const solve=index=>{if(index===grid.length)return true;if(++attempts>grid.length*catalog.length*50)throw new Error("attempt-cap");const x=index%width,y=Math.floor(index/width),north=y?grid[index-width].s:null,west=x?grid[index-1].e:null,candidates=catalog.filter(t=>(!north||t.n===north)&&(!west||t.w===west)).map(t=>({t,k:random()})).sort((a,b)=>a.k-b.k||a.t.id.localeCompare(b.t.id)).map(x=>x.t);for(const tile of candidates){grid[index]=tile;if(solve(index+1))return true}grid[index]=null;return false};if(!solve(0))throw new Error("impossible-catalog");return{width,height,seed,attempts,ids:grid.map(t=>t.id),grid}}
function seams(result){const errors=[];for(let y=0;y<result.height;y++)for(let x=0;x<result.width;x++){const i=y*result.width+x,t=result.grid[i];if(x&&result.grid[i-1].e!==t.w)errors.push({x,y,axis:"horizontal"});if(y&&result.grid[i-result.width].s!==t.n)errors.push({x,y,axis:"vertical"})}return errors}
const seedIndex=process.argv.indexOf("--seed"),seed=seedIndex>=0?Number(process.argv[seedIndex+1]??909):908,result=place(6,6,seed),errors=seams(result),frequency=Object.fromEntries([...new Set(result.ids)].sort().map(id=>[id,result.ids.filter(x=>x===id).length]));assert.equal(errors.length,0);
const hostile={duplicate:"",tuple:"",impossible:""};try{validateCatalog([tiles[0],tiles[0]])}catch(error){hostile.duplicate=error.message}try{validateCatalog([{id:"bad",n:"A"}])}catch(error){hostile.tuple=error.message}try{place(2,1,seed,[{id:"only",n:"A",e:"A",s:"A",w:"B"}])}catch(error){hostile.impossible=error.message}assert.match(hostile.impossible,/impossible/);
const core={schema:"wang-tiles-recipe-v1",fixture:"original synthetic edge-code catalog",generator:"xorshift32",catalogDigest:sha(tiles),grid:{width:result.width,height:result.height,ids:result.ids},seed,attempts:result.attempts,seamErrors:errors,frequency,boundary:place(1,1,seed).ids,hostile,claimBoundary:"Symbolic edge matching only; not pixel seam proof, guaranteed aperiodicity, or an aesthetic ranking."};console.log(JSON.stringify({...core,receiptHash:sha(core)},null,2));console.log("PASS: seeded Wang tile solver, seam oracle, changed-seed path, hostile catalog checks, and digest verified");
Publish seamless procedural textures as recipes
A useful export includes grid width and height, tile dimensions, seed, generator version, catalog digest, ordered tile catalog, placed IDs, seam report, repetition diagnostics, and renderer assumptions. The image is one rendition of that recipe. Wang Tiles become maintainable design-system material when another renderer can reconstruct the same topology and identify which authored asset produced every cell.
Review in three passes. First, reject structural mismatches. Second, inspect pixels at native size, scale boundaries, and wrap edges if the whole sheet must repeat. Third, critique rhythm: motif clusters, direction, contrast, and negative space. A texture can be seamless but dull, or expressive but structurally broken. Keeping those judgments separate preserves both engineering clarity and artistic control.
The final action is concrete: run the downloadable generator, change the seed, and inspect the candidate and repetition receipt before drawing a larger catalog. If every visible flaw is attributed to randomness, the catalog is not yet observable enough. If every edge and motif family is traceable, iteration becomes an authored conversation with constraints rather than a sequence of lucky screenshots.