Delaunay Triangulation for Low-Poly Portraits
Build a deterministic low-poly portrait from generated pixels, edge-aware samples, verified triangles, and reproducible exports.
A Delaunay triangulation portrait becomes expressive through sampling and color choices; triangulation supplies the mesh, not the art direction. This tutorial builds a generated portrait-like raster, mixes uniform and edge-aware points, checks the topology, and exports a replayable SVG and recipe.
A Delaunay triangulation portrait starts from generated pixels
A Delaunay triangulation portrait does not require a photograph. The studio in this tutorial generates a face-like raster from simple gradients, ellipses, curves, and seeded texture. It contains no upload control, biometric analysis, external image, or claimed likeness. That safe source keeps the creative pipeline reproducible while still providing cheeks, eyes, hair, shadow, and silhouette edges for sampling.
Freeze the source dimensions, seed, palette, and drawing order. Export a pixel digest before sampling so a later mesh can be traced to exact generated bytes. The source is art direction, not ground truth: changing the eye spacing or light direction creates a different edition and therefore a different receipt.
The WHATWG Canvas pixel-manipulation specification defines ImageData access used by the artifact. The lab reads generated RGBA pixels, never a user's portrait. Its outcome is stylized triangle mesh art, not identity inference. A Delaunay triangulation portrait can then be discussed through geometry and color without importing consent or licensing ambiguity from a scraped face dataset.
- Generated raster to verified triangle mesh
- One seed passes through generated portrait pixels, edge energy, mixed point sampling, and a colored mesh with stage counts and digests.
- Generate a synthetic portrait-like raster.
- Measure local luminance change without recognition.
- Mix boundary, uniform, and edge-aware points.
- Triangulate, color, verify, and export.
Measure edge energy without recognizing a face
Convert neighboring pixel differences into an edge-energy field. A simple luminance gradient is enough for this teaching studio: estimate horizontal and vertical change, combine their magnitude, and normalize the result. High energy appears around generated boundaries such as the silhouette, eye shapes, nose shadow, and hair mass. The algorithm does not label those structures or decide that they belong to a face.
The distinction matters. Edge-aware point sampling responds to local contrast, whether it comes from an eye, a leaf, typography, or noise. It is a visual sampling strategy, not a recognition system. Publish the energy formula, border handling, normalization, and digest so the resulting point density can be reproduced.
Art direction begins here. Strong texture can steal samples from important contours, so blur or cap the energy field when necessary. Very smooth generated regions may receive too few points and create large triangles; a uniform floor prevents that collapse. A Delaunay triangulation portrait gains character from this uneven allocation, but every bias should be visible in the recipe. The point distribution, more than a raw triangle-count target, determines which structures survive abstraction.
Mix uniform and edge-aware sample points
Start with the four corners and evenly spaced boundary points so the mesh covers the canvas. Add a uniform seeded set to guarantee coverage, then accept edge-aware candidates with probability derived from normalized energy. Deduplicate coordinates before triangulation and cap attempts so a hostile density setting cannot loop forever. The mixed strategy balances calm facets in broad regions with smaller facets around generated detail.
Compare three recipes using the same total point budget: uniform, edge-aware, and mixed. Uniform sampling produces an even crystalline rhythm but may blur the silhouette. Pure edge-aware sampling can crowd high-contrast features and starve the background. Mixed sampling keeps structure while preserving enough large shapes to look intentional. This is a creative choice, not a universally optimized error metric.
The relation to adaptive rectangular subdivision in quadtree mosaics is instructive. Both strategies allocate detail where an image changes, but the resulting geometry feels different: quadtrees reveal hierarchical rectangles, while a Delaunay triangulation portrait creates irregular connected facets. Save point coordinates and generator state in the receipt; a seed alone is insufficient if the sampling algorithm or draw order changes.
Build the triangulation and explain the dual
Delaunay triangulation connects points so that, under nondegenerate conditions, no sample lies inside the circumcircle of a triangle. The geometry is dual to a Voronoi diagram: Delaunay edges connect sites whose Voronoi cells share a boundary. That connection makes moving from Voronoi cells to their Delaunay dual a useful conceptual bridge from stippled points to polygonal facets.
The studio uses an embedded finite Bowyer–Watson implementation for its modest point count, avoiding a runtime dependency or network fetch. For production-scale work, Delaunator provides a documented triangulation API and half-edge representation. This article does not vendor it or claim comparative performance.
Bowyer–Watson begins with a supertriangle, inserts points, removes triangles whose circumcircles contain the new point, finds the polygonal cavity boundary, and connects that boundary to the point. Final triangles touching the supertriangle are removed. A Delaunay triangulation portrait then has a complete list of sample indices and oriented facets. The algorithm is understandable, but ordinary floating-point predicates can become unreliable around collinear or cocircular cases; that limitation belongs in the receipt rather than behind the finished image.
Estimate triangle color and art-direct the mesh
The fastest color estimate samples the generated raster at a triangle centroid. A richer estimate samples the centroid plus three edge midpoints and averages in a declared color space. The studio uses the second method in sRGB byte space for a textured but stable result. It labels that choice because averaging encoded sRGB is not the same as averaging linear light.
Art direction can retain a thin stroke, quantize colors, shift the palette, or modulate lightness by triangle area. Keep those gestures deterministic and separate from topology. If a large triangle crosses a strong boundary, increasing point density near that edge is usually more faithful than hiding the error with a heavy outline. The lesson from using image energy as an art-direction signal applies here too: numeric energy proposes where detail matters, while the artist decides what the image should emphasize.
A Delaunay triangulation portrait is successful when the mesh structure supports the intended expression. Mean pixel residual can compare recipes, but it cannot score taste. The contact sheet therefore publishes triangle count and a sampled residual proxy beside a curator note. Numeric evidence narrows technical defects; it does not crown the most beautiful sampling strategy.
Check orientation, incidence, and circumcircles
Visual inspection can miss duplicate points, zero-area triangles, inverted winding, holes, and nonmanifold edges. Before rendering, reject duplicate coordinates and facets whose signed area is within a declared epsilon. Normalize every triangle to counterclockwise orientation. Build an undirected edge map: boundary edges should have one incident triangle and interior edges should have two. Any greater count fails the topology receipt.
Sample the empty-circumcircle property by checking nonvertex points against each triangle's circumcircle with a documented tolerance. This ordinary floating-point check is diagnostic, not a proof. Jonathan Shewchuk's primary-author resource on adaptive precision arithmetic and robust predicates explains why orientation and incircle tests require special care near degeneracy. This live CMU page replaces the frozen PDF after that URL began redirecting to a 404. Production-grade robust geometric predicates therefore remain a separate upgrade. The studio clearly labels its approximate arithmetic and rejects deliberately collinear and duplicate fixtures.
These checks distinguish a Delaunay triangulation portrait from “a picture that looks triangular.” The receipt lists point count, triangle count, winding failures, incidence failures, degeneracies, and sampled circumcircle violations. If exact or adaptive predicates are later integrated, record the implementation, source, version, license, and digest before strengthening the claim.
- Circumcircle and half-edge inspector
- One triangle shows counterclockwise orientation, incident-edge counts, duplicate rejection, and a sampled empty-circumcircle check with approximate labels.
| Gate | Check | Boundary |
|---|---|---|
| Orientation | Positive signed area | Ordinary doubles |
| Incidence | Boundary 1; interior 2 | Combinatorial |
| Degeneracy | Area exceeds epsilon | Declared tolerance |
| Circumcircle | Sampled empty test | Diagnostic, not proof |
Export SVG, PNG, and a reproducible recipe
The browser studio renders the generated source and selected mesh to Canvas. It serializes every triangle as an SVG polygon with a computed fill, validates the SVG text, and offers it as a download. It also exports a PNG from Canvas and a JSON recipe containing dimensions, seed, generator version, point strategy, points, triangles, palette controls, topology checks, source pixel digest, and mesh digest.
Replay uses the full recipe, not just the seed. The artifact reruns generation and confirms the source digest, point digest, triangle indices, and SVG digest match. Changing any control clears stale download links before recomputation. A Delaunay triangulation portrait receipt is valid only when all outputs belong to the currently visible edition.
The studio contains synthetic generated data only. It does not recognize faces, reproduce a photographic likeness, benchmark triangulators, or prove robust geometry. Its circumcircle and orientation checks use ordinary double-precision arithmetic and say so in the export. Run the duplicate and collinear hostile fixtures before enabling downloads. Then preserve structural edges in another geometric rendering system if you want to compare triangular color masses with detected-line abstraction.
Runnable artifact — Generated art, not a photograph, biometric system, likeness claim, robust-predicate implementation, performance benchmark, or proof that one sampling method is universally superior. Vendored code must include exact source, version, license, and SHA.
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Delaunay portrait studio</title><style>:root{color-scheme:dark}*{box-sizing:border-box}html{overflow-wrap:anywhere}body{margin:auto;max-width:1080px;padding:24px;background:#10151f;color:#f7f2e8;font:16px/1.5 system-ui}h1,h2{line-height:1.12}button,a,input,select{font:inherit;min-height:44px}input,select{width:100%;min-width:0}button{border:0;border-radius:10px;padding:10px 16px;background:#ff9a72;color:#261007;font-weight:800}.panel{margin:16px 0;padding:16px;border:1px solid #7d8ca6;border-radius:14px}.controls{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(180px,100%),1fr));gap:12px}.controls label{display:grid;gap:4px;min-width:0}table{width:100%;border-collapse:collapse}th,td{padding:8px;border-bottom:1px solid #58657a;text-align:left;vertical-align:top}.scroll{max-width:100%;overflow:auto}textarea{display:block;width:100%;min-width:0;min-height:280px;background:#080c13;color:#fff}.downloads{display:flex;flex-wrap:wrap;gap:12px}.downloads a{display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;padding:9px 12px;border:1px solid currentColor;border-radius:9px;color:#9dd6ff}.downloads a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #ffe270;outline-offset:3px}[aria-disabled="true"]{opacity:.55;pointer-events:none}canvas,svg{display:block;max-width:100%;height:auto}@media(max-width:620px){body{padding:14px}.panel{padding:12px}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;animation:none!important}}@media(forced-colors:active){button,.panel,.downloads a{border:2px solid ButtonText}.downloads a{color:LinkText}}</style><main><h1>Delaunay portrait studio</h1><p>Create three matched-budget stylized editions from an original generated raster. This studio does not upload, recognize, or reproduce a photographic face.</p><div class="panel controls"><label>Seed<input id="seed" type="number" value="907" min="1" max="999999"></label><label>Displayed strategy<select id="strategy"><option value="mixed">Mixed</option><option value="uniform">Uniform</option><option value="edge">Edge-aware</option></select></label><label>Point budget per strategy<input id="count" type="number" value="88" min="24" max="140"></label><button id="run">Generate and verify</button></div><p id="status" class="panel" aria-live="polite">Not run</p><canvas id="canvas" width="480" height="360">Generated low-poly portrait; the receipt lists its source, point, triangle, residual, palette, and replay equivalents.</canvas><textarea id="receipt" readonly aria-label="Delaunay portrait receipt"></textarea><p class="downloads"><a id="svg" download="delaunay-portrait.svg" aria-disabled="true">Download SVG</a><a id="png" download="delaunay-portrait.png" aria-disabled="true">Download PNG</a><a id="json" download="delaunay-portrait-recipe.json" aria-disabled="true">Download recipe</a></p></main><script>
const q=s=>document.querySelector(s),canvas=q("#canvas"),ctx=canvas.getContext("2d",{willReadFrequently:true}),W=canvas.width,H=canvas.height,links=[q("#svg"),q("#png"),q("#json")];let urls=[];
function rng(seed){let state=seed>>>0;return()=>{state^=state<<13;state^=state>>>17;state^=state<<5;return(state>>>0)/4294967296}}
const hash=async bytes=>[...new Uint8Array(await crypto.subtle.digest("SHA-256",bytes instanceof ArrayBuffer?bytes:new TextEncoder().encode(bytes)))].map(v=>v.toString(16).padStart(2,"0")).join("");
function input(){const seed=Number(q("#seed").value),count=Number(q("#count").value),strategy=q("#strategy").value;if(!Number.isInteger(seed)||seed<1||seed>999999)throw new Error("seed-out-of-range");if(!Number.isInteger(count)||count<24||count>140)throw new Error("point-count-out-of-range");return{seed,count,strategy}}
function source(seed){const random=rng(seed);const gradient=ctx.createLinearGradient(0,0,W,H);gradient.addColorStop(0,"#17253d");gradient.addColorStop(1,"#d46d5d");ctx.globalAlpha=1;ctx.fillStyle=gradient;ctx.fillRect(0,0,W,H);ctx.fillStyle="#d9a47e";ctx.beginPath();ctx.ellipse(W*.5,H*.52,W*.27,H*.38,0,0,Math.PI*2);ctx.fill();ctx.fillStyle="#30243b";ctx.beginPath();ctx.ellipse(W*.5,H*.3,W*.3,H*.2,0,Math.PI,Math.PI*2);ctx.fill();ctx.fillStyle="#241d29";for(const x of [W*.41,W*.59]){ctx.beginPath();ctx.arc(x,H*.48,9,0,Math.PI*2);ctx.fill()}ctx.strokeStyle="#8b3d48";ctx.lineWidth=7;ctx.beginPath();ctx.arc(W*.5,H*.62,52,.25,Math.PI-.25);ctx.stroke();ctx.globalAlpha=.12;for(let i=0;i<160;i++){ctx.fillStyle=i%2?"#fff":"#000";ctx.fillRect(random()*W,random()*H,2,2)}ctx.globalAlpha=1;return ctx.getImageData(0,0,W,H)}
const luminance=(data,x,y)=>{const i=(Math.max(0,Math.min(H-1,y))*W+Math.max(0,Math.min(W-1,x)))*4;return .2126*data[i]+.7152*data[i+1]+.0722*data[i+2]};
function energy(image,x,y){return Math.min(1,(Math.abs(luminance(image.data,x+1,y)-luminance(image.data,x-1,y))+Math.abs(luminance(image.data,x,y+1)-luminance(image.data,x,y-1)))/180)}
function points(image,{seed,count,strategy}){const strategySalt={uniform:101,edge:211,mixed:307}[strategy],random=rng(seed^0x9e3779b9^strategySalt),out=[[0,0],[W-1,0],[W-1,H-1],[0,H-1]];let attempts=0;while(out.length<count&&attempts<count*120){attempts++;const x=Math.round(random()*(W-1)),y=Math.round(random()*(H-1)),accept=strategy==="uniform"||strategy==="mixed"&&out.length<count*.45||random()<.08+.92*energy(image,x,y);if(accept&&!out.some(p=>p[0]===x&&p[1]===y))out.push([x,y])}if(out.length!==count)throw new Error("sampling-attempt-limit");return out}
const orient=(a,b,c)=>(b[0]-a[0])*(c[1]-a[1])-(b[1]-a[1])*(c[0]-a[0]);
function validatePoints(pts){if(new Set(pts.map(p=>p.join(","))).size!==pts.length)throw new Error("duplicate-points");let noncollinear=false;for(let i=2;i<pts.length;i++)if(Math.abs(orient(pts[0],pts[1],pts[i]))>1e-7){noncollinear=true;break}if(!noncollinear)throw new Error("collinear-points")}
function circle(a,b,c){const d=2*(a[0]*(b[1]-c[1])+b[0]*(c[1]-a[1])+c[0]*(a[1]-b[1]));if(Math.abs(d)<1e-9)return null;const aa=a[0]*a[0]+a[1]*a[1],bb=b[0]*b[0]+b[1]*b[1],cc=c[0]*c[0]+c[1]*c[1],x=(aa*(b[1]-c[1])+bb*(c[1]-a[1])+cc*(a[1]-b[1]))/d,y=(aa*(c[0]-b[0])+bb*(a[0]-c[0])+cc*(b[0]-a[0]))/d;return{x,y,r2:(x-a[0])**2+(y-a[1])**2}}
function triangulate(inputPoints){const pts=[...inputPoints,[W*-10,H*-8],[W*11,H*-8],[W*.5,H*12]],n=inputPoints.length;let tris=[[n,n+1,n+2]];for(let p=0;p<n;p++){const bad=tris.filter(t=>{const c=circle(pts[t[0]],pts[t[1]],pts[t[2]]);return c&&(pts[p][0]-c.x)**2+(pts[p][1]-c.y)**2<c.r2+1e-7}),edges=[];for(const t of bad)for(const e of [[t[0],t[1]],[t[1],t[2]],[t[2],t[0]]]){const reverse=edges.findIndex(x=>x[0]===e[1]&&x[1]===e[0]);if(reverse>=0)edges.splice(reverse,1);else edges.push(e)}tris=tris.filter(t=>!bad.includes(t));for(const e of edges){const t=[e[0],e[1],p];tris.push(orient(pts[t[0]],pts[t[1]],pts[t[2]])>0?t:[t[1],t[0],t[2]])}}return tris.filter(t=>t.every(i=>i<n)&&Math.abs(orient(pts[t[0]],pts[t[1]],pts[t[2]]))>1e-7)}
function triangulateChecked(pts){validatePoints(pts);return triangulate(pts)}
function rgb(image,pts,t){const samples=[[...pts[t[0]]],[...pts[t[1]]],[...pts[t[2]]],[Math.round((pts[t[0]][0]+pts[t[1]][0]+pts[t[2]][0])/3),Math.round((pts[t[0]][1]+pts[t[1]][1]+pts[t[2]][1])/3)]];return[0,1,2].map(channel=>Math.round(samples.reduce((sum,p)=>sum+image.data[(p[1]*W+p[0])*4+channel],0)/samples.length))}
function verify(pts,tris){const orientationFailures=tris.filter(t=>orient(pts[t[0]],pts[t[1]],pts[t[2]])<=1e-7).length,edges=new Map();for(const t of tris)for(const [a,b] of [[t[0],t[1]],[t[1],t[2]],[t[2],t[0]]]){const key=a<b?a+":"+b:b+":"+a;edges.set(key,(edges.get(key)||0)+1)}const incidenceFailures=[...edges.values()].filter(n=>n<1||n>2).length,circumcircleViolations=tris.reduce((sum,t)=>{const c=circle(pts[t[0]],pts[t[1]],pts[t[2]]);return sum+(c?pts.some((p,i)=>!t.includes(i)&&(p[0]-c.x)**2+(p[1]-c.y)**2<c.r2-1e-5):true)},0);return{orientationFailures,incidenceFailures,circumcircleViolations,predicateBoundary:"ordinary floating-point diagnostics; not robust-predicate proof",pass:!orientationFailures&&!incidenceFailures&&!circumcircleViolations}}
function inside(p,a,b,c){const s1=orient(a,b,p),s2=orient(b,c,p),s3=orient(c,a,p);return s1>=-1e-6&&s2>=-1e-6&&s3>=-1e-6}
function residual(image,pts,tris,colors){let total=0,samples=0;for(let y=6;y<H;y+=12)for(let x=6;x<W;x+=12){const index=tris.findIndex(t=>inside([x,y],pts[t[0]],pts[t[1]],pts[t[2]]));if(index<0)continue;const pixel=(y*W+x)*4,color=colors[index];total+=(Math.abs(image.data[pixel]-color[0])+Math.abs(image.data[pixel+1]-color[1])+Math.abs(image.data[pixel+2]-color[2]))/3;samples++}return{meanAbsoluteRgb:Number((total/samples).toFixed(3)),samples,stride:12,formula:"mean absolute RGB channel error over a 12-pixel deterministic grid"}}
async function edition(image,config,strategy){const pts=points(image,{...config,strategy}),tris=triangulateChecked(pts),topology=verify(pts,tris);if(!topology.pass)throw new Error("topology-check-failed:"+strategy);const colors=tris.map(t=>rgb(image,pts,t)),fills=colors.map(value=>"rgb("+value.join(",")+")"),polygons=tris.map((t,index)=>"<polygon points=\""+t.map(i=>pts[i].join(",")).join(" ")+"\" fill=\""+fills[index]+"\"/>").join(""),svg="<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 "+W+" "+H+"\">"+polygons+"</svg>",counts=new Map();for(const fill of fills)counts.set(fill,(counts.get(fill)||0)+1);const palette=[...counts.entries()].sort((a,b)=>b[1]-a[1]||a[0].localeCompare(b[0])).slice(0,5).map(([color,count])=>({color,count}));return{strategy,pointCount:pts.length,triangleCount:tris.length,pointHash:await hash(JSON.stringify(pts)),triangleHash:await hash(JSON.stringify(tris)),svgHash:await hash(svg),residual:residual(image,pts,tris,colors),palette,points:pts,triangles:tris,topology,svg,colors}}
async function pipeline(config){const image=source(config.seed),sourceSha256=await hash(image.data.buffer),editions=[];for(const strategy of ["uniform","edge","mixed"])editions.push(await edition(image,config,strategy));return{image,sourceSha256,editions}}
function render(item){ctx.clearRect(0,0,W,H);for(let index=0;index<item.triangles.length;index++){const t=item.triangles[index];ctx.beginPath();ctx.moveTo(...item.points[t[0]]);ctx.lineTo(...item.points[t[1]]);ctx.lineTo(...item.points[t[2]]);ctx.closePath();ctx.fillStyle="rgb("+item.colors[index].join(",")+")";ctx.fill();ctx.strokeStyle="rgba(20,20,30,.18)";ctx.stroke()}}
function clear(){urls.forEach(URL.revokeObjectURL);urls=[];links.forEach(link=>{link.removeAttribute("href");link.setAttribute("aria-disabled","true")});q("#receipt").value="";ctx.clearRect(0,0,W,H)}
async function hostileChecks(valid){const result={duplicate:{fixture:"valid points plus exact duplicate",rejected:false,error:""},collinear:{fixture:[[0,0],[1,1],[2,2],[3,3]],rejected:false,error:""}};try{triangulateChecked([...valid,[...valid[0]]])}catch(error){result.duplicate.rejected=true;result.duplicate.error=error.message}try{triangulateChecked(result.collinear.fixture)}catch(error){result.collinear.rejected=true;result.collinear.error=error.message}return result}
async function run(){clear();try{const config=input(),first=await pipeline(config),second=await pipeline(config),firstReplay=first.editions.map(item=>({strategy:item.strategy,pointHash:item.pointHash,triangleHash:item.triangleHash,svgHash:item.svgHash})),secondReplay=second.editions.map(item=>({strategy:item.strategy,pointHash:item.pointHash,triangleHash:item.triangleHash,svgHash:item.svgHash})),replay={source:first.sourceSha256===second.sourceSha256,firstSourceSha256:first.sourceSha256,secondSourceSha256:second.sourceSha256,editions:JSON.stringify(firstReplay)===JSON.stringify(secondReplay),first:firstReplay,second:secondReplay};if(!replay.source||!replay.editions)throw new Error("same-input-replay-mismatch");const selected=first.editions.find(item=>item.strategy===config.strategy),hostile=await hostileChecks(selected.points);if(!hostile.duplicate.rejected||hostile.duplicate.error!=="duplicate-points"||!hostile.collinear.rejected||hostile.collinear.error!=="collinear-points")throw new Error("hostile-validation-failed");render(selected);const png=await new Promise(resolve=>canvas.toBlob(resolve,"image/png"));if(!png)throw new Error("png-export-failed");const compact=item=>({strategy:item.strategy,pointCount:item.pointCount,triangleCount:item.triangleCount,pointHash:item.pointHash,triangleHash:item.triangleHash,svgHash:item.svgHash,residual:item.residual,palette:item.palette,points:item.points,triangles:item.triangles,topology:item.topology,svg:item.svg}),core={schema:"delaunay-portrait-recipe-v1",config,dimensions:{width:W,height:H},generator:"procedural-face-like-raster-v1",sourceSha256:first.sourceSha256,matchedBudget:{pointCount:config.count,strategies:["uniform","edge","mixed"],equal:first.editions.every(item=>item.pointCount===config.count)},contactSheet:first.editions.map(item=>({strategy:item.strategy,pointCount:item.pointCount,triangleCount:item.triangleCount,residual:item.residual,palette:item.palette,pointHash:item.pointHash,triangleHash:item.triangleHash,svgHash:item.svgHash})),selection:{strategy:config.strategy,reason:"User-selected rendering; residual values are descriptive receipts, not a universal quality ranking."},selected:compact(selected),replay,hostile,provenance:"Original procedural face-like raster and seeded sampling; all metrics are generated/synthetic. No uploaded photograph, biometric data, external image, or unlicensed asset.",claimBoundary:"Three matched-budget strategy receipts with ordinary floating-point checks; no recognition, likeness, triangulator benchmark, robust-geometry proof, or universal winner."},receipt={...core,recipeHash:await hash(JSON.stringify(core))},serialized=JSON.stringify(receipt,null,2),blobs=[new Blob([selected.svg],{type:"image/svg+xml"}),png,new Blob([serialized],{type:"application/json"})];urls=blobs.map(URL.createObjectURL);links.forEach((link,index)=>{link.href=urls[index];link.removeAttribute("aria-disabled")});q("#receipt").value=serialized;q("#status").textContent="PASS: 3 matched "+config.count+"-point editions; selected "+selected.strategy+" residual "+selected.residual.meanAbsoluteRgb+"; replay verified"}catch(error){q("#status").textContent="FAIL: "+error.message}}
q("#run").addEventListener("click",run);run();</script><script>document.querySelectorAll(".controls input,.controls select").forEach(control=>control.addEventListener("input",()=>{document.querySelectorAll(".downloads a").forEach(link=>{link.removeAttribute("href");link.setAttribute("aria-disabled","true")});document.querySelectorAll("#rows,#cards,#highlight").forEach(node=>node.textContent="");const canvas=document.querySelector("#canvas");if(canvas)canvas.getContext("2d").clearRect(0,0,canvas.width,canvas.height);const receipt=document.querySelector("#receipt");if(receipt)receipt.value="";const status=document.querySelector("#status");if(status)status.textContent="Inputs changed; run again."}))</script></html>
Compare strategies and publish numeric limits
Make three editions with the same generated source and point budget. Uniform sampling usually creates steady facet size. Edge-aware sampling concentrates geometry around contrast. Mixed sampling blends both rhythms. Publish a contact sheet with strategy, point and triangle counts, sampled residual proxy, palette, digest prefix, and curator note. A Delaunay triangulation portrait remains an artwork even when its pipeline is measurable.
Avoid optimizing only residual. More small triangles can reduce numeric error while making the composition noisy. A palette reduction can increase error while creating stronger visual unity. Choose the edition through a stated aesthetic brief—soft planes, crystalline detail, or graphic silhouette—then use receipts to ensure the chosen pixels and topology can be reproduced.
Revisit the pipeline when the triangulator, predicate strategy, Canvas behavior, or export encoding changes. Keep exact-predicate claims out until exact or adaptive code is genuinely present. Keep photographed inputs out unless consent, provenance, retention, and likeness risks are explicitly redesigned. The bounded studio is valuable because its claim matches its evidence: it turns a generated raster into a deterministic low-poly mesh, rejects basic topology defects, and exports enough state for another browser to inspect. Export the seed and topology receipt with every low-poly portrait.
- Three sampling strategies, one point budget
- Uniform, edge-aware, and mixed meshes form a contact sheet with triangle count, residual proxy, palette, hash, and curator decision.
| Strategy | Technical tendency | Curator note |
|---|---|---|
| Uniform | Even facet rhythm | Calm but soft edges |
| Edge-aware | Detail concentrated at contrast | Busy focal features |
| Mixed | Coverage plus structural detail | Selected for balanced planes |