HomeJournalThis post

Gabor Noise for Directional Canvas Texture

Create directional Gabor-noise Canvas texture with seeded sparse impulses, visible spectral controls, an approximate DFT, and verified PNG receipts.

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

Gabor noise gives Canvas texture explicit controls for direction, frequency, bandwidth, and sparse impulses. This tutorial builds a seeded finite renderer, inspects an approximate spectrum, and exports the generated image with a reproducible receipt.

Gabor noise begins with a spectral brief

Gabor noise gives a procedural texture explicit controls for orientation, frequency, bandwidth, and impulse density. Begin with those visual intentions rather than stacking opaque noise layers until something looks plausible. A brief might ask for soft diagonal fibers, a narrow frequency band, and sparse disruptions; each phrase maps to a parameter the artifact can publish.

The technique uses a Gaussian-windowed cosine kernel scattered at seeded impulse locations. Summing many local kernels creates oriented stochastic texture. The original sparse Gabor convolution paper provides the procedural-noise formulation; this browser studio implements a finite discrete teaching approximation on a small Canvas.

No photographed material or sampled artwork enters the teaching fixture. Every pixel comes from authored math and seeded generated impulses. That provenance makes the edition replayable and keeps the claim narrow: the result is generated art, not a scan or proof of natural texture fidelity. The seed does not make a composition artistically successful; it makes the inputs recoverable for critique, iteration, and export. Start with replayable seeded generative art, then use Gabor noise when the art direction needs a visible spectral vocabulary.

Anatomy of a discrete Gabor kernelA Gaussian envelope multiplies a rotated cosine carrier; orientation, frequency, sigma, phase, and finite support remain separately labeled.orientation θGaussian σ controls envelope · frequency controls carrier spacing · radius bounds work
Anatomy of a discrete Gabor kernel
A Gaussian envelope multiplies a rotated cosine carrier; orientation, frequency, sigma, phase, and finite support remain separately labeled.
Kernel controls
ParameterRoleVisible consequence
ThetaRotate sample coordinatesDirectional texture
FrequencyCosine cycles per pixelStripe spacing
SigmaGaussian envelope widthLocal bandwidth and support
Phase and amplitudeImpulse variationStochastic interference
Figure 1: The artistic controls correspond to distinct terms in the finite sampled kernel.

Build the Gaussian-envelope cosine kernel

A two-dimensional Gabor kernel multiplies a Gaussian envelope by a cosine carrier. Rotate the sample coordinates by the chosen angle, evaluate the envelope with its bandwidth or scale, and evaluate the carrier at the selected frequency and phase. Outside a finite radius, skip the contribution to keep the browser computation bounded.

The first figure separates envelope, carrier, angle, wavelength, and support radius. Its semantic table repeats the formula roles without relying on color. In the studio, sigma controls the envelope width, frequency controls cycles per pixel, theta controls orientation, and impulse amplitude determines each local contribution.

This discrete Gabor noise kernel is not a proof of the continuous stochastic process. Pixel sampling, finite support, normalization, and the number of impulses all shape the output. Publish those choices with the image. When a result changes, the receipt should show which artistic control moved instead of hiding the difference behind a new random seed. Bound the support radius relative to sigma and record the discarded tail policy. That decision trades computation for approximation and can visibly soften or truncate local marks, so it belongs beside the kernel parameters rather than in an implementation footnote.

Distribute seeded sparse impulses

Use a small deterministic generator to place impulses across an expanded canvas region. Each impulse has a position, amplitude, and phase drawn from the seed. Expanding beyond the visible bounds avoids a visibly quiet border where kernels would otherwise be clipped.

Impulse density controls granularity and cost. Too few impulses reveal isolated stamps; too many make the approximation expensive and can flatten contrast after normalization. The studio caps density and total operations, then reports the actual impulse count. Invalid seeds or parameters fail before rendering and disable exports.

Impulse placement is separate from blue-noise spacing. Blue-noise stippling aims to distribute points with characteristic spacing, while this Gabor noise fixture uses seeded uniform sparse locations to drive convolution. Keeping those mechanisms distinct gives creative coding a clearer palette: point distribution shapes where events occur, while the Gabor kernel shapes their directional frequency content. Save the impulse list or its generator contract in the receipt. A seed without the generator version, bounds, draw order, and parameter transforms cannot reproduce the same field after the implementation changes.

Orientation-frequency control mapA polar direction ray and spectral ellipse connect requested orientation, carrier frequency, and bandwidth to named texture outcomes.polar orientation targetspectral concentration35°narrower bandwidth
Orientation-frequency control map
A polar direction ray and spectral ellipse connect requested orientation, carrier frequency, and bandwidth to named texture outcomes.
  1. Polar ray names the requested orientation: 35 degrees.
  2. Radius represents the requested frequency band.
  3. Ellipse orientation mirrors the directional spectrum.
  4. Ellipse thickness represents bandwidth, not confidence.
  5. The rendered spectrum is measured separately in the artifact.
Figure 2: The diagram is an art-direction map; the studio's sampled DFT is the measured diagnostic.

Control orientation, frequency, and bandwidth

Orientation rotates the carrier and the envelope coordinate system. Frequency changes stripe spacing. Bandwidth changes how concentrated the response is around the chosen frequency and how local each impulse appears. Adjust one at a time, then render a small contact sheet so the visual consequence remains attributable.

The control map pairs a polar direction marker with an ellipse representing spectral concentration. It is a teaching diagram, not a measured Fourier transform. The generated approximate spectrum in the studio is a separate computation taken from the rendered pixels.

Compare frequency direction with coordinate warping: domain warping bends where another function is sampled, while Gabor noise directly builds oriented local oscillations. Both can create flowing texture, but their parameters and failure modes differ. A good spectral art direction note names which mechanism supplies curvature, direction, scale, and interruption rather than grouping every effect under “organic noise.” Freeze two contrast cases around the selected edition: one lower frequency and one rotated orientation. Those neighbors make the chosen parameter story visible without presenting taste as a numeric optimum.

Normalize finite output without hiding clipping

The finite sum can extend beyond displayable luminance. First verify every sample is finite, then find the generated minimum and maximum. Map that observed interval into a conservative display range and count any values clipped by the final conversion. Store the raw range, normalization rule, and clipped-pixel count in the receipt.

Per-image min–max normalization improves visible contrast but makes absolute brightness incomparable across editions. The studio labels that limitation. A series that needs comparable energy should freeze a global mapping derived from a declared calibration set instead. Gabor noise art direction is stronger when normalization is an explicit aesthetic decision rather than an invisible rescue step.

The Canvas path uses ImageData and a standard PNG serialization boundary. The WHATWG Canvas specification defines the pixel surface, fallback content, ImageData, and serialization behavior. The studio verifies decoded PNG dimensions after creating the blob; it does not treat a download URL as proof that valid image bytes exist.

Inspect a small approximate spectrum

A direct discrete Fourier summary on a small downsampled grid is enough to reveal whether energy concentrates near the requested orientation and frequency. Compute selected frequency bins, store their magnitudes, and find the strongest non-DC bin. Compare its angle and radius with the artistic target using a declared tolerance.

This is an approximate diagnostic, not a full spectral proof. Downsampling, finite windows, phase, and sparse impulses spread energy. The Gabor Noise by Example project explores example-driven parameterization and anisotropic spectra at research depth; the studio here exposes a smaller, auditable bridge between controls and pixels.

The contact sheet pairs each Gabor noise edition with the approximate spectrum, seed, parameters, and a curator note. If the strongest bin disagrees with the target, keep the image if it is artistically useful but mark the spectral check as failed. Artistic selection can override a preference, not rewrite a measurement. Preserve the downsample size, bin coordinates, window policy, DC exclusion, angle convention, and magnitude normalization with that check. Otherwise two implementations can display similar heat maps while computing different summaries.

Seed-sweep schematic keyed to live receiptsFour schematic swatches—not generated pixels—index the real seed-only 905–908 sweep. Every edition uses 35 degrees, frequency 0.085, sigma 9, and 72 impulses; measured pixels remain in the runnable contact sheet.SCHEMATIC SWATCHES — NOT GENERATED PIXELSseed 905 · 35° · f .085 · σ 9 · n 72spectral PASS · SHA-256 receiptSELECT: balanced diagonal weaveseed 906 · 35° · f .085 · σ 9 · n 72spectral PASS · SHA-256 receiptREJECT: uneven crossingsseed 907 · 35° · f .085 · σ 9 · n 72spectral PASS · SHA-256 receiptHOLD: softer secondary fieldseed 908 · 35° · f .085 · σ 9 · n 72spectral PASS · SHA-256 receiptREJECT: dense centerReal pixels, digests, DFT bins, and tolerances live in the runnable artifact receipt.
Seed-sweep schematic keyed to live receipts
Four schematic swatches—not generated pixels—index the real seed-only 905–908 sweep. Every edition uses 35 degrees, frequency 0.085, sigma 9, and 72 impulses; measured pixels remain in the runnable contact sheet.
Live artifact edition records; the visible swatches are schematic
SeedAngle (degrees)FrequencySigmaImpulsesEvidence statusCurator decision
905350.085972spectral PASSSELECT: balanced diagonal weave
906350.085972spectral PASSREJECT: uneven crossings
907350.085972spectral PASSHOLD: softer secondary field
908350.085972spectral PASSREJECT: dense center
Figure 3: These schematic swatches index the seed-only sweep; generated pixels and measurements live in the runnable contact-sheet receipt.

Generate an edition and export its receipt

Run the default seed twice. The studio hashes all RGBA bytes and requires equality. Then run seed plus one and require a different hash. It checks finite samples, bounded pixels, decoded PNG dimensions, parameter limits, and the approximate spectral summary before enabling two downloads.

The PNG contains the generated pixels. The JSON contains schema version, seed, dimensions, impulse count, kernel parameters, raw range, clipping count, pixel hash, selected spectral bins, dominant-bin summary, provenance, and claim boundary. Both downloads are created from the same completed run; any invalid input clears the canvas status and disables stale links.

This is the minimum useful Gabor noise edition record. A larger production renderer would add color-space choice, higher-resolution tiling tests, performance profiling, and perhaps multiple kernel families. Those are separate claims. The browser artifact proves only its finite CPU approximation and exported bytes. Seeded Canvas noise also needs an invalid-state rule: any nonfinite sample, exceeded operation budget, or failed PNG decode clears prior exports so a stale successful edition cannot masquerade as the current run.

Runnable artifact — A finite discrete teaching approximation and generated artwork. It is not a production performance benchmark, continuous-process proof, material scan, or claim of natural texture fidelity.

<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Seeded Gabor noise studio</title><style>
:root{color-scheme:dark}*{box-sizing:border-box}body{margin:auto;max-width:1060px;padding:24px;background:#10121f;color:#fbf7ec;font:16px/1.5 system-ui}button,a,input{min-height:44px;font:inherit}button{padding:10px 16px;background:#ff8b68;color:#2b0900;border:0;border-radius:8px;font-weight:800}.controls{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.controls label{display:grid;gap:4px}.panel{padding:16px;margin:16px 0;border:1px solid #8d91b3;border-radius:14px}canvas{display:block;width:min(100%,768px);height:auto;image-rendering:auto;border:1px solid #c6c9e8;background:#111}.edition-table{overflow:auto}.edition-table table{border-collapse:collapse;min-width:760px;width:100%}th,td{padding:8px;text-align:left;border-bottom:1px solid #555a7a}textarea{width:100%;min-height:300px;background:#080914;color:#fff}.downloads a{margin-right:18px}@media(max-width:620px){body{padding:14px}.controls{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}
</style><main><h1>Gabor noise studio</h1><p>Four real editions use seeds 905–908 by default. Each stores collision-resistant pixel digests, a bounded operation count, and a measured target-versus-observed spectral verdict.</p><div class="panel controls"><label>Seed<input id="seed" type="number" value="905" step="1"></label><label>Angle degrees<input id="angle" type="number" value="35" min="0" max="180"></label><label>Frequency<input id="frequency" type="number" value="0.085" min="0.02" max="0.2" step="0.005"></label><label>Sigma<input id="sigma" type="number" value="9" min="3" max="18" step="1"></label><label>Impulses<input id="impulses" type="number" value="72" min="8" max="160" step="1"></label><button id="run" type="button">Generate four editions</button></div><p id="status" class="panel" aria-live="polite">Not run</p><canvas id="canvas" width="192" height="128">Selected generated Gabor texture; use the receipt for a numeric equivalent.</canvas><h2>Four-edition contact sheet</h2><canvas id="contact" width="768" height="128">Generated editions for four consecutive seeds.</canvas><div class="edition-table"><table><caption>Edition receipts and curator decisions</caption><thead><tr><th>Seed</th><th>SHA-256</th><th>Angle</th><th>Radius</th><th>Spectral verdict</th><th>Curator note</th></tr></thead><tbody id="editions"></tbody></table></div><textarea id="receipt" readonly aria-label="Gabor noise receipt"></textarea><p class="downloads"><a id="png" download="gabor-noise-edition.png" aria-disabled="true">Download selected PNG</a><a id="json" download="gabor-noise-receipt.json" aria-disabled="true">Download JSON</a></p></main><script>
const W=192,H=128,SPECTRUM_N=24,BIN_LIMIT=4,MAX_OPERATIONS=2000000,q=selector=>document.querySelector(selector),canvas=q('#canvas'),ctx=canvas.getContext('2d'),contact=q('#contact'),contactCtx=contact.getContext('2d'),links=[q('#png'),q('#json')],notes=['SELECT: balanced diagonal weave','REJECT: uneven crossings','HOLD: softer secondary field','REJECT: dense center'];let urls=[];
const toHex=bytes=>[...new Uint8Array(bytes)].map(value=>value.toString(16).padStart(2,'0')).join(''),sha256=async bytes=>toHex(await crypto.subtle.digest('SHA-256',bytes));
function rng(seed){let state=seed>>>0;return()=>{state^=state<<13;state^=state>>>17;state^=state<<5;return(state>>>0)/4294967296}}
function strict(raw,name,{integer=false,min=-Infinity,max=Infinity}={}){if(String(raw).trim()==='')throw new Error(name+'-blank');const value=Number(raw);if(!Number.isFinite(value)||integer&&!Number.isInteger(value)||value<min||value>max)throw new Error(name+'-out-of-contract');return value}
function parameters(){const seed=strict(q('#seed').value,'seed',{integer:true,min:1,max:4294967292}),angle=strict(q('#angle').value,'angle',{min:0,max:180}),frequency=strict(q('#frequency').value,'frequency',{min:.02,max:.2}),sigma=strict(q('#sigma').value,'sigma',{min:3,max:18}),impulses=strict(q('#impulses').value,'impulses',{integer:true,min:8,max:160});return{seed,angle,frequency,sigma,impulses}}
function budget(p,renderCount){const radius=Math.ceil(p.sigma*3),perEditionUpperBound=p.impulses*(radius*2+1)**2,upperBound=perEditionUpperBound*renderCount;return{formula:'impulses * (2 * ceil(3*sigma) + 1)^2 * renderCount',radius,renderCount,perEditionUpperBound,upperBound,limit:MAX_OPERATIONS,pass:upperBound<=MAX_OPERATIONS}}
function render(p){const random=rng(p.seed),margin=Math.ceil(p.sigma*3),points=Array.from({length:p.impulses},()=>({x:random()*(W+margin*2)-margin,y:random()*(H+margin*2)-margin,a:random()*2-1,phase:random()*Math.PI*2})),raw=new Float64Array(W*H);let lo=Infinity,hi=-Infinity,operations=0;const theta=p.angle*Math.PI/180,c=Math.cos(theta),s=Math.sin(theta),radius=Math.ceil(p.sigma*3);for(const point of points){const x0=Math.max(0,Math.floor(point.x-radius)),x1=Math.min(W-1,Math.ceil(point.x+radius)),y0=Math.max(0,Math.floor(point.y-radius)),y1=Math.min(H-1,Math.ceil(point.y+radius));for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++){operations++;const dx=x-point.x,dy=y-point.y,u=dx*c+dy*s,v=-dx*s+dy*c,envelope=Math.exp(-(u*u+v*v)/(2*p.sigma*p.sigma));raw[y*W+x]+=point.a*envelope*Math.cos(2*Math.PI*p.frequency*u+point.phase)}}for(const value of raw){if(!Number.isFinite(value))throw new Error('non-finite-sample');lo=Math.min(lo,value);hi=Math.max(hi,value)}const image=new ImageData(W,H);let clipped=0;for(let i=0;i<raw.length;i++){const normalized=(raw[i]-lo)/(hi-lo||1),value=Math.round(22+normalized*218);if(value<0||value>255)clipped++;image.data[i*4]=Math.round(value*.58);image.data[i*4+1]=Math.round(value*.76);image.data[i*4+2]=value;image.data[i*4+3]=255}return{image,rawRange:[lo,hi],clipped,points,operations}}
const angleDelta=(a,b)=>{const raw=Math.abs(a-b)%180;return Math.min(raw,180-raw)};
function spectrum(image,p){const bins=[],startX=Math.floor((W-SPECTRUM_N)/2),startY=Math.floor((H-SPECTRUM_N)/2);for(let ky=-BIN_LIMIT;ky<=BIN_LIMIT;ky++)for(let kx=-BIN_LIMIT;kx<=BIN_LIMIT;kx++){if(kx===0&&ky===0)continue;let re=0,im=0;for(let y=0;y<SPECTRUM_N;y++)for(let x=0;x<SPECTRUM_N;x++){const value=image.data[((startY+y)*W+startX+x)*4+2]/255-.5,phase=-2*Math.PI*(kx*x/SPECTRUM_N+ky*y/SPECTRUM_N);re+=value*Math.cos(phase);im+=value*Math.sin(phase)}bins.push({kx,ky,magnitude:Number(Math.hypot(re,im).toFixed(6))})}bins.sort((a,b)=>b.magnitude-a.magnitude);const dominant=bins[0],observedAngleDegrees=Number(((Math.atan2(dominant.ky,dominant.kx)*180/Math.PI+180)%180).toFixed(3)),observedRadiusBins=Number(Math.hypot(dominant.kx,dominant.ky).toFixed(3)),targetAngleDegrees=Number((p.angle%180).toFixed(3)),targetRadiusBins=Number((p.frequency*SPECTRUM_N).toFixed(3)),angleDeltaDegrees=Number(angleDelta(observedAngleDegrees,targetAngleDegrees).toFixed(3)),radiusDeltaBins=Number(Math.abs(observedRadiusBins-targetRadiusBins).toFixed(3)),tolerances={angleDegrees:35,radiusBins:2.25},pass=angleDeltaDegrees<=tolerances.angleDegrees&&radiusDeltaBins<=tolerances.radiusBins;return{method:'centered 24x24 blue-channel DFT; non-DC bins -4..4',sample:{size:SPECTRUM_N,startX,startY},dominant,top:bins.slice(0,10),target:{angleDegrees:targetAngleDegrees,radiusBins:targetRadiusBins},observed:{angleDegrees:observedAngleDegrees,radiusBins:observedRadiusBins},delta:{angleDegrees:angleDeltaDegrees,radiusBins:radiusDeltaBins},tolerances,pass}}
function clearState(message){urls.forEach(URL.revokeObjectURL);urls=[];links.forEach(link=>{link.removeAttribute('href');link.setAttribute('aria-disabled','true')});ctx.clearRect(0,0,W,H);contactCtx.clearRect(0,0,contact.width,contact.height);q('#editions').innerHTML='';q('#receipt').value='';q('#status').textContent=message}
async function edition(p,index){const rendered=render(p),pixelSha256=await sha256(rendered.image.data),spectral=spectrum(rendered.image,p);return{seed:p.seed,parameters:p,pixelSha256,spectral,rawRange:rendered.rawRange,clippedPixels:rendered.clipped,impulseCount:rendered.points.length,operationCount:rendered.operations,curatorDecision:notes[index],image:rendered.image}}
async function run(){clearState('RUNNING: validating parameters and operation budget');try{const p=parameters(),operationBudget=budget(p,5);if(!operationBudget.pass)throw new Error('operation-budget-exceeded:'+operationBudget.upperBound+'>'+operationBudget.limit);const editions=[];for(let index=0;index<4;index++)editions.push(await edition({...p,seed:p.seed+index},index));const replay=await edition(p,0);if(editions[0].pixelSha256!==replay.pixelSha256)throw new Error('same-seed-digest-mismatch');if(editions[0].pixelSha256===editions[1].pixelSha256)throw new Error('different-seed-digest-collision');const actualOperations=editions.reduce((sum,item)=>sum+item.operationCount,0)+replay.operationCount;if(actualOperations>operationBudget.limit)throw new Error('actual-operation-budget-exceeded');ctx.putImageData(editions[0].image,0,0);editions.forEach((item,index)=>contactCtx.putImageData(item.image,index*W,0));const pngBlob=await new Promise(resolve=>canvas.toBlob(resolve,'image/png'));if(!pngBlob||pngBlob.type!=='image/png')throw new Error('png-serialization');const bitmap=await createImageBitmap(pngBlob);if(bitmap.width!==W||bitmap.height!==H)throw new Error('png-dimensions');bitmap.close();const publicEditions=editions.map(({image,...item})=>item),receipt={schema:'gabor-noise-edition-v2',dimensions:{width:W,height:H},contactSheet:{columns:4,seeds:publicEditions.map(item=>item.seed),width:W*4,height:H},generator:{version:'xorshift32-gabor-v2',drawOrder:'x,y,amplitude,phase per impulse'},provenance:'Seeded generated sparse impulses and procedural pixels only; no photographed material or sampled art.',claimBoundary:'Finite discrete CPU teaching approximation and approximate sampled spectrum; not performance, continuous-process, or natural-material evidence.',normalization:'per-edition min-max mapped to blue luminance 22..240',operationBudget:{...operationBudget,actualOperations},selectedSeed:p.seed,selectedPixelSha256:publicEditions[0].pixelSha256,sameSeedPixelSha256:replay.pixelSha256,differentSeedPixelSha256:publicEditions[1].pixelSha256,editions:publicEditions,png:{type:pngBlob.type,bytes:pngBlob.size,width:W,height:H}};const serialized=JSON.stringify(receipt,null,2);JSON.parse(serialized);const jsonBlob=new Blob([serialized],{type:'application/json'});urls=[URL.createObjectURL(pngBlob),URL.createObjectURL(jsonBlob)];q('#png').href=urls[0];q('#json').href=urls[1];links.forEach(link=>link.removeAttribute('aria-disabled'));q('#receipt').value=serialized;q('#editions').innerHTML=publicEditions.map(item=>'<tr><th>'+item.seed+'</th><td>'+item.pixelSha256.slice(0,16)+'…</td><td>'+item.spectral.observed.angleDegrees+'°</td><td>'+item.spectral.observed.radiusBins+'</td><td>'+(item.spectral.pass?'PASS':'FAIL')+'</td><td>'+item.curatorDecision+'</td></tr>').join('');q('#status').textContent='PASS: four editions '+publicEditions.map(item=>item.seed).join('–')+'; SHA-256 replay verified; '+actualOperations+' operations'}catch(error){clearState('FAIL: '+error.message)}}
q('#run').addEventListener('click',run);run();
</script></html>

Curate without claiming natural-material truth

Generated texture becomes design when someone selects, crops, combines, or rejects editions for a visual purpose. Record that curator decision beside the reproducible parameters. A rejected edition is valuable evidence: it shows which measurable configuration failed the intended mood without pretending the math determines taste. Write the note in visual language—dense, directional, brittle, soft, quiet—then connect each observation to the parameter or composition change proposed for the next edition. That creates an art-direction trail without claiming personal preference is an objective metric.

Flow-aligned texture may need line integral convolution instead, especially when a spatial vector field should guide strokes. Gabor noise is strongest when local oscillation and spectral direction are the core controls. Choose the mechanism whose parameter language matches the art direction.

Revisit this studio if Canvas color handling, spectral analysis, kernel families, or export formats change. The image remains original generated work with no material scan or borrowed texture. Archive the selected edition with at least one rejected neighbor, because a lone winner hides how the visual brief shaped the choice. Keep the normalized PNG and the complete parameter receipt together as one reviewable evidence pair, and reject any review copy whose pixel digest no longer matches the declared edition. Generate one seeded edition and publish its spectrum and parameter receipt beside the image.