HomeJournalThis post

Coons Patch Mesh Gradients From Four Curves

Derive a Coons patch from four authored curves, inspect the bilinear correction, sample it into SVG cells, and export a reproducible gamut-aware gradient.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

A Coons patch mesh gradient answers how four authored boundary curves can become one smooth, editable color field in SVG. This tutorial derives the patch, exposes its correction term, samples mesh-like cells, and preserves color and parameter provenance.

Coons patch mesh gradient begins at compatible corners

Name four parametric boundary curves: top and bottom vary with u, while left and right vary with v, each parameter running from zero to one. Their endpoints must agree at the four corners. If authored paths disagree, choose and record a reconciliation rule before interpolation; otherwise the surface begins with cracks disguised by sampling.

The original Coons technical report provides the historical mathematical basis. A Coons patch mesh gradient uses that surface geometry as an art tool, then adds a separately authored color field. It should be described as a mesh-like sampled SVG construction rather than implying universal native SVG mesh-gradient support.

The committed HTML fixture uses normalized cubic boundary curves with four draggable midpoint controls and deterministic sampling. Its default points, colors, and exported receipt are local synthetic inputs, not measurements from an image or commercial design. Every visible cell can be regenerated from those parameters.

Show boundary direction arrows while editing a Coons patch mesh gradient. A reversed curve can share the same corners yet twist correspondence across the browser patch preview and produce an unexpected interior.

Four-curve Coons latticeFour irregular boundary curves surround a warped lattice with editable midpoint controls.top curvebottom curveleftright
  • Each edge is an authored parametric boundary.
  • Interior lattice points come from transfinite interpolation.
  • Moving one midpoint changes the surface while corner compatibility stays explicit.
Four-curve Coons lattice reading key
SignalInterpretation
Four-curve Coons latticeFour irregular boundary curves surround a warped lattice with editable midpoint controls.
Figure 1: Four curves define the patch; the lattice reveals the interpolation between them.

Blend opposite boundaries into ruled surfaces

Interpolate between left and right at each u and v to create one ruled surface. Separately interpolate between top and bottom to create another. Each surface respects one pair of boundaries exactly but includes a simple corner contribution also present in the other surface. Adding them directly therefore counts corner behavior twice.

Transfinite interpolation solves that overlap by subtracting the bilinear surface made from the four corner points. The resulting patch honors all four boundary curves. Write the three terms separately in code and visualization so sign errors are visible. The decomposition figure uses overlap and a dashed subtraction plane rather than hiding the equation behind a utility.

A Coons patch mesh gradient can fold if boundaries or controls are extreme; interpolation does not guarantee a pleasant or one-to-one parameterization. Show the lattice and detect inverted sample cells. Artistic freedom improves when invalid geometry becomes a visible diagnostic instead of a mysterious rendering seam.

Test each ruled term by temporarily hiding the others. That diagnostic turns a compact equation into three visible drawing operations an artist can reason about.

Ruled surfaces minus corner blendTwo translucent ruled layers overlap above a subtractive bilinear corner plane.left/right ruled blendtop/bottom ruled blendsubtract bilinear corners
  1. Interpolate between left and right boundaries.
  2. Interpolate between top and bottom boundaries.
  3. Subtract the bilinear corner contribution counted twice.
Ruled surfaces minus corner blend reading key
SignalInterpretation
Ruled surfaces minus corner blendTwo translucent ruled layers overlap above a subtractive bilinear corner plane.
Figure 2: The decomposition explains the correction term instead of presenting the patch as a black box.

Sample the parametric surface into cells

Choose u and v sample counts, evaluate the patch at grid intersections, and join adjacent points into quadrilateral cells. SVG paths can represent each cell, while shared coordinates keep neighboring edges identical. The SVG 2 paths specification defines the exported geometry, including the path commands a consumer must understand.

Uniform sampling is simple but spends equal cells on flat and highly curved regions. Start with it for a deterministic edition, then add adaptive subdivision only if a committed error metric justifies complexity. Bound maximum depth and cell count. A Coons patch mesh gradient should remain inspectable at export rather than creating millions of tiny paths without a receipt.

The fixture compares each cell midpoint with the bilinear interpolation of its corners to estimate geometric deviation. That local value is derived from the same patch function. It is a teaching diagnostic, not proof of perceptual smoothness or printer quality.

Share cell vertices as stored values rather than reevaluating both neighbors independently. Identical arithmetic paths reduce tiny cracks after serialization and rasterization.

Author color independently from geometry

Assign corner colors, boundary color curves, or a full parametric color function. Interpolate in a declared color space and keep alpha handling explicit. Mixing encoded sRGB components is easy but can produce dull or uneven transitions; OKLCH or a linear-light workflow may better match the intended gesture, subject to gamut mapping and contrast requirements.

The Display P3 CSS and relative color syntax articles cover browser color systems. A parametric surface color should retain its source values and working space in the edition receipt. Geometry controls where a cell is; color controls what that cell communicates.

For the teaching mosaic, four bounded OKLCH-like parameter values are converted through a small display mapping. The implementation labels that mapping illustrative rather than colorimetrically complete. A production color pipeline should use a reviewed conversion library and test target displays and exports.

Keep lightness and chroma controls separate in the editor. A dramatic hue path can remain intentional while contrast around overlaid text follows an independent rule.

Control continuity across adjacent patches

A single patch is bounded by its four curves. A larger composition often joins several patches. Positional continuity requires shared boundary points. Tangent continuity requires compatible cross-boundary derivatives or aligned control decisions. A visible seam may come from geometry, color, alpha, antialiasing, or tiny coordinate differences, so test each layer separately.

Share one boundary function between neighboring patches rather than duplicating similar numbers. Generate both sides from the same samples and serialize coordinates consistently. A Coons patch mesh gradient can use deliberate discontinuity as composition, but an accidental crack should not be defended as texture.

The constraint-based layout approach is relevant to control-point relationships. Constraints can keep corners joined and tangents aligned while leaving interior handles expressive. Store which constraints were active so later editing does not mistake a derived point for a free one.

When several patches meet, visualize tangent handles on both sides of the seam. Positional agreement alone can hide a sharp derivative break until color bands emphasize it.

The cubic patch playground couples four boundary controls to OKLCH mapping, inversion and gamut diagnostics, plus a namespaced computed SVG export.

Runnable artifact — coons-mesh-gradient.html

<!doctype html><meta charset="utf-8"><title>Interactive cubic Coons patch fixture</title><style>body{font:16px system-ui;max-width:760px;margin:1rem auto}svg{width:100%;border:1px solid #777}circle[data-control]{cursor:grab;fill:#fff;stroke:#111;stroke-width:2}label{display:grid;grid-template-columns:9rem 1fr;gap:1rem;margin:.4rem 0}output,pre{display:block;white-space:pre-wrap;overflow-wrap:anywhere}</style><svg id="mesh" viewBox="0 0 320 220" aria-labelledby="coons-a3-live-title coons-a3-live-desc"><title id="coons-a3-live-title">Editable cubic Coons patch</title><desc id="coons-a3-live-desc">Ninety-six sampled cells with four boundary controls.</desc><g id="coons-a3-live-cells"></g><g id="coons-a3-live-handles"></g></svg><fieldset><legend>Cubic boundary controls</legend><label>Top bend <input data-slider="top" type="range" min="-45" max="45" value="28"></label><label>Bottom bend <input data-slider="bottom" type="range" min="-45" max="45" value="-22"></label><label>Left bend <input data-slider="left" type="range" min="-45" max="45" value="18"></label><label>Right bend <input data-slider="right" type="range" min="-45" max="45" value="-14"></label></fieldset><button id="export" type="button">Export namespaced SVG and receipt</button><output id="diagnostics"></output><output id="receipt"></output><pre id="exported"></pre><pre id="exported-svg"></pre><script>
const EDITION="coons-a3",offsets={top:28,bottom:-22,left:18,right:-14},colorAnchors=[[.76,.22,25],[.82,.2,115],[.68,.24,255],[.78,.21,325]],cubic=(p0,p1,p2,p3,t)=>{const s=1-t;return[0,1].map(index=>s*s*s*p0[index]+3*s*s*t*p1[index]+3*s*t*t*p2[index]+t*t*t*p3[index])},topBoundary=u=>cubic([0,0],[.28,offsets.top/200],[.72,offsets.top/300],[1,0],u),bottomBoundary=u=>cubic([0,1],[.28,1+offsets.bottom/300],[.72,1+offsets.bottom/200],[1,1],u),leftBoundary=v=>cubic([0,0],[offsets.left/200,.28],[offsets.left/300,.72],[0,1],v),rightBoundary=v=>cubic([1,0],[1+offsets.right/300,.28],[1+offsets.right/200,.72],[1,1],v),corner=(u,v)=>[u,v],patch=(u,v)=>{const side=leftBoundary(v).map((value,index)=>value*(1-u)+rightBoundary(v)[index]*u),vertical=topBoundary(u).map((value,index)=>value*(1-v)+bottomBoundary(u)[index]*v),bilinear=corner(u,v);return side.map((value,index)=>value+vertical[index]-bilinear[index])},screen=point=>[20+280*point[0],20+180*point[1]];
const toLab=([L,C,h])=>[L,C*Math.cos(h*Math.PI/180),C*Math.sin(h*Math.PI/180)],mix=(a,b,t)=>a.map((value,index)=>value*(1-t)+b[index]*t),colorAt=(u,v)=>mix(mix(toLab(colorAnchors[0]),toLab(colorAnchors[1]),u),mix(toLab(colorAnchors[2]),toLab(colorAnchors[3]),u),v),linearRgb=([L,a,b])=>{const l=(L+.3963377774*a+.2158037573*b)**3,m=(L-.1055613458*a-.0638541728*b)**3,s=(L-.0894841775*a-1.291485548*b)**3;return[4.0767416621*l-3.3077115913*m+.2309699292*s,-1.2684380046*l+2.6097574011*m-.3413193965*s,-.0041960863*l-.7034186147*m+1.707614701*s]},gamma=value=>value<=.0031308?12.92*value:1.055*value**(1/2.4)-.055,hex=value=>Math.round(Math.max(0,Math.min(1,value))*255).toString(16).padStart(2,"0"),mapColor=lab=>{let mapped=[...lab],rgb=linearRgb(mapped),iterations=0;while(rgb.some(value=>value<0||value>1)&&iterations<32){mapped=[mapped[0],mapped[1]*.92,mapped[2]*.92];rgb=linearRgb(mapped);iterations+=1}return{css:"#"+rgb.map(value=>hex(gamma(value))).join(""),gamutMapped:iterations>0,iterations,source:lab,mapped}},area=points=>points.reduce((sum,point,index)=>{const next=points[(index+1)%points.length];return sum+point[0]*next[1]-next[0]*point[1]},0)/2;
const svg=document.querySelector("#mesh"),cells=document.querySelector("#coons-a3-live-cells"),handles=document.querySelector("#coons-a3-live-handles"),diagnostics=document.querySelector("#diagnostics"),receipt=document.querySelector("#receipt"),exported=document.querySelector("#exported"),exportedSvg=document.querySelector("#exported-svg"),controlPoint=name=>screen(name==="top"?topBoundary(.5):name==="bottom"?bottomBoundary(.5):name==="left"?leftBoundary(.5):rightBoundary(.5));let current={paths:"",maxMidpointError:0,invertedCells:0,gamutMappedCells:0};
const midpointError=(x,y)=>{const corners=[[x/12,y/8],[(x+1)/12,y/8],[(x+1)/12,(y+1)/8],[x/12,(y+1)/8]].map(pair=>screen(patch(...pair))),center=screen(patch((x+.5)/12,(y+.5)/8)),average=[0,1].map(index=>corners.reduce((sum,point)=>sum+point[index],0)/4);return Math.hypot(center[0]-average[0],center[1]-average[1])},serializeSvg=()=>'<svg xmlns="http://www.w3.org/2000/svg" id="'+EDITION+'-export" viewBox="0 0 320 220" data-color-space="OKLCH" data-gamut-policy="reduce-chroma"><title id="'+EDITION+'-title">Cubic Coons patch edition</title><desc id="'+EDITION+'-desc">Computed 12 by 8 cell mosaic; diagnostics remain in the paired receipt.</desc><metadata id="'+EDITION+'-metadata">colorSpace=OKLCH;gamutPolicy=reduce-chroma;boundaryModel=cubic-bezier</metadata><g id="'+EDITION+'-cells">'+current.paths+'</g></svg>',exportReceipt=()=>{const svgText=serializeSvg(),data={id:EDITION+"-export",svgId:EDITION+"-export",boundaryModel:"cubic-bezier",colorSpace:"OKLCH",gamutPolicy:"reduce-chroma",controls:{...offsets},cells:cells.querySelectorAll("path").length,maxMidpointError:Number(diagnostics.dataset.error),invertedCells:Number(diagnostics.dataset.inverted),gamutMappedCells:Number(diagnostics.dataset.gamut)};exported.textContent=JSON.stringify(data,null,2);exportedSvg.textContent=svgText;return{data,svgText}};
const render=()=>{let paths="",maxError=0,inverted=0,gamutMapped=0,index=0;for(let y=0;y<8;y++)for(let x=0;x<12;x++){const points=[[x/12,y/8],[(x+1)/12,y/8],[(x+1)/12,(y+1)/8],[x/12,(y+1)/8]].map(pair=>screen(patch(...pair))),color=mapColor(colorAt((x+.5)/12,(y+.5)/8));if(area(points)<=0)inverted+=1;if(color.gamutMapped)gamutMapped+=1;paths+='<path id="'+EDITION+'-cell-'+index+'" d="M'+points.map(point=>point.map(value=>Number(value.toFixed(4))).join(",")).join("L")+'Z" fill="'+color.css+'"/>';maxError=Math.max(maxError,midpointError(x,y));index+=1}current={paths,maxMidpointError:maxError,invertedCells:inverted,gamutMappedCells:gamutMapped};cells.innerHTML=paths;handles.innerHTML=Object.keys(offsets).map(name=>{const point=controlPoint(name);return '<circle data-control="'+name+'" role="slider" tabindex="0" aria-label="'+name+' cubic boundary control" aria-valuemin="-45" aria-valuemax="45" aria-valuenow="'+offsets[name]+'" cx="'+point[0]+'" cy="'+point[1]+'" r="7"/>'}).join("");diagnostics.dataset.error=maxError.toFixed(6);diagnostics.dataset.inverted=String(inverted);diagnostics.dataset.gamut=String(gamutMapped);diagnostics.value="96 cells · cubic boundaries · max midpoint error "+diagnostics.dataset.error+" px · inverted "+inverted+" · gamut mapped "+gamutMapped;bindHandles();const result=exportReceipt();receipt.value=result.data.cells===96&&Object.keys(result.data.controls).length===4&&result.data.boundaryModel==="cubic-bezier"&&result.data.invertedCells===0&&result.svgText.includes('id="coons-a3-export"')?"PASS: cubic patch exports namespaced SVG with diagnostics":"FAIL"};
const adjust=(name,delta)=>{offsets[name]=Math.max(-45,Math.min(45,offsets[name]+delta));const slider=document.querySelector('[data-slider="'+name+'"]');if(slider)slider.value=offsets[name];render()},bindHandles=()=>{for(const handle of handles.querySelectorAll("[data-control]")){handle.addEventListener("keydown",event=>{if(event.key==="ArrowUp"||event.key==="ArrowRight"){event.preventDefault();adjust(handle.dataset.control,1)}if(event.key==="ArrowDown"||event.key==="ArrowLeft"){event.preventDefault();adjust(handle.dataset.control,-1)}});handle.addEventListener("pointerdown",event=>handle.setPointerCapture(event.pointerId));handle.addEventListener("pointermove",event=>{if(!handle.hasPointerCapture(event.pointerId))return;const point=svg.createSVGPoint();point.x=event.clientX;point.y=event.clientY;const local=point.matrixTransform(svg.getScreenCTM().inverse()),name=handle.dataset.control;offsets[name]=Math.round(name==="top"?(local.y-20)/180*200:name==="bottom"?(local.y-200)/180*200:name==="left"?(local.x-20)/280*200:(local.x-300)/280*200);adjust(name,0)})}};for(const slider of document.querySelectorAll("[data-slider]"))slider.addEventListener("input",()=>{offsets[slider.dataset.slider]=Number(slider.value);render()});document.querySelector("#export").addEventListener("click",exportReceipt);render();
</script>

Run open coons-mesh-gradient.html. Expected receipt: PASS: cubic patch exports namespaced SVG with diagnostics.

Map gamut and inspect cell error

Evaluate every sampled color in the target output space. Mark values outside its gamut, choose clipping or chroma reduction deliberately, and preserve the original working value when possible. A gamut map should be a named export setting, not an invisible side effect of CSS, a rasterizer, or a design tool.

Overlay geometric deviation, color delta, inverted cells, and gamut status. Increase sample density where geometry or color changes too quickly, but avoid presenting a lower numerical error as automatic artistic improvement. A Coons patch mesh gradient needs enough cells to support the thesis and few enough to preserve an editable object.

The sampling map's cells are symbolic in the article figure. The runnable fixture computes its own diagnostic values from the current controls and includes them in the exported parameter receipt. Spectral color mixing offers a different model when subtractive material behavior is the actual subject.

Export diagnostics as optional layers, not baked marks. Record one parameter receipt so the clean edition and analytical overlay can be verified while serving different reading purposes.

Sampling and gamut mapA mosaic increases in resolution from left to right while marked cells identify clipped colors and geometric error.coarse samplefiner sample× gamut mapped cell
DiagnosticResponse
Geometry errorincrease samples or adapt locally
Out of gamutmap color deliberately
Visible seamsshare sampled edges exactly
Sampling and gamut map reading key
SignalInterpretation
Sampling and gamut mapA mosaic increases in resolution from left to right while marked cells identify clipped colors and geometric error.
Figure 3: Sampling density and color mapping remain authored export decisions.

Export a namespaced SVG edition

Generate unique IDs from an edition identifier for gradients, clip paths, masks, and metadata. Avoid generic names that collide when several exported studies appear on one page. Include viewBox, accessible title and description, stable cell order, declared color space, boundary control points, sample counts, mapping policy, and generator version.

The SVG 2 gradient specification remains useful for ordinary fills and fallback construction. The exported object may use solid-filled sample cells because that is broadly portable and honest. Call it a sampled mesh-like gradient, then provide a raster fallback where the cell count is unsuitable.

Test the SVG at thumbnail, article, print-preview, and zoomed inspection sizes. Ensure the visual does not carry critical meaning without its semantic caption and transcript. The edition receipt should make one exported image reproducible without requiring the article's runtime controls.

Compress path precision only after comparing the rendered result at target scales. Aggressive decimal trimming can reopen seams the sampling algorithm carefully closed in an exported SVG mesh gradient.

Curate the patch as a drawing instrument

Begin with the silhouette of the four boundaries, not with random handles. Use asymmetry, tension, and negative space to establish a gesture. Then move color landmarks in response to the geometry rather than asking a dense sample grid to create interest by itself. Save variations as parameter receipts so changes are comparable and reversible.

Test restrained and extreme controls, corner disagreement, inverted cells, very low sampling, high sampling, monochrome color, wide-gamut color, and namespaced multi-export pages. A Coons patch mesh gradient succeeds when the surface remains mathematically explainable and visually authored at the same time.

The useful output is one four-curve edition whose boundaries, correction, sampling, color mapping, and diagnostics can all be inspected. Export that before adding a second patch; a disciplined small surface teaches more than a large opaque mesh.

Curate controls with keyboard-accessible numeric inputs alongside dragging. Fine adjustments, reproducibility, and non-pointer use all benefit from the same explicit coordinates. Save focus and control labels in the playground test so artistic interaction remains operable at narrow width and high zoom.

Continue into curve-constrained painted fields

Compare the parametric lattice with diffusion curves for vector painting whose two-sided colors, residual, and solver receipt remain visible.