Skip to content

Instantly share code, notes, and snippets.

@alex-quiterio
Created December 28, 2025 15:23
Show Gist options
  • Select an option

  • Save alex-quiterio/e281181895b3e6fea355bb0ed583e8ef to your computer and use it in GitHub Desktop.

Select an option

Save alex-quiterio/e281181895b3e6fea355bb0ed583e8ef to your computer and use it in GitHub Desktop.
// voronoi-organic-generator.ts
interface Point {
x: number;
y: number;
}
interface VoronoiCell {
site: Point;
vertices: Point[];
textureType: 'scales' | 'concentric' | 'parallel' | 'stipple' | 'wavy' | 'empty';
textureAngle?: number;
textureScale?: number;
}
class OrganicPatternGenerator {
private width: number;
private height: number;
private cells: VoronoiCell[] = [];
private seed: number;
constructor(width: number, height: number, seed: number = Date.now()) {
this.width = width;
this.height = height;
this.seed = seed;
}
// Seeded random number generator
private random(): number {
this.seed = (this.seed * 9301 + 49297) % 233280;
return this.seed / 233280;
}
// Generate random points with some clustering for organic feel
private generateSites(count: number): Point[] {
const sites: Point[] = [];
const clusterCount = Math.floor(count / 5);
// Create cluster centers
const clusters: Point[] = [];
for (let i = 0; i < clusterCount; i++) {
clusters.push({
x: this.random() * this.width,
y: this.random() * this.height
});
}
// Generate points around clusters
for (let i = 0; i < count; i++) {
const cluster = clusters[Math.floor(this.random() * clusters.length)];
const angle = this.random() * Math.PI * 2;
const distance = (this.random() * 0.3 + 0.1) * Math.min(this.width, this.height);
sites.push({
x: Math.max(0, Math.min(this.width, cluster.x + Math.cos(angle) * distance)),
y: Math.max(0, Math.min(this.height, cluster.y + Math.sin(angle) * distance))
});
}
return sites;
}
// Simple Voronoi using nearest neighbor (suitable for artistic purposes)
private generateVoronoi(sites: Point[]): VoronoiCell[] {
const cells: VoronoiCell[] = [];
const resolution = 10; // Lower = more accurate but slower
// Create grid and assign to nearest site
const grid: number[][] = [];
for (let y = 0; y < this.height; y += resolution) {
grid[y] = [];
for (let x = 0; x < this.width; x += resolution) {
let minDist = Infinity;
let nearestSite = 0;
sites.forEach((site, idx) => {
const dist = Math.hypot(x - site.x, y - site.y);
if (dist < minDist) {
minDist = dist;
nearestSite = idx;
}
});
grid[y][x] = nearestSite;
}
}
// Extract cell boundaries
sites.forEach((site, idx) => {
const textureTypes: VoronoiCell['textureType'][] = ['scales', 'concentric', 'parallel', 'stipple', 'wavy', 'empty'];
const textureType = textureTypes[Math.floor(this.random() * textureTypes.length)];
cells.push({
site,
vertices: [], // Simplified - we'll use the site directly for texture generation
textureType,
textureAngle: this.random() * Math.PI * 2,
textureScale: 0.5 + this.random() * 1.5
});
});
return cells;
}
// Generate scale texture (like fish scales or feathers)
private generateScales(cell: VoronoiCell): string {
const paths: string[] = [];
const radius = 30 * (cell.textureScale || 1);
const rows = 6;
const cols = 6;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const offsetX = (row % 2) * radius * 0.5;
const x = cell.site.x - (cols / 2) * radius + col * radius + offsetX;
const y = cell.site.y - (rows / 2) * radius * 0.7 + row * radius * 0.7;
// Create arc scale shape
const path = `M ${x} ${y}
Q ${x - radius * 0.4} ${y - radius * 0.3}, ${x} ${y - radius * 0.5}
Q ${x + radius * 0.4} ${y - radius * 0.3}, ${x} ${y}`;
paths.push(`<path d="${path}" fill="none" stroke="black" stroke-width="1.5"/>`);
}
}
return paths.join('\n');
}
// Generate concentric rings
private generateConcentric(cell: VoronoiCell): string {
const paths: string[] = [];
const maxRadius = 80 * (cell.textureScale || 1);
const rings = 8;
for (let i = 1; i <= rings; i++) {
const radius = (maxRadius / rings) * i;
paths.push(`<circle cx="${cell.site.x}" cy="${cell.site.y}" r="${radius}"
fill="none" stroke="black" stroke-width="1.5"/>`);
}
return paths.join('\n');
}
// Generate parallel lines
private generateParallel(cell: VoronoiCell): string {
const paths: string[] = [];
const spacing = 8;
const length = 100;
const count = 15;
const angle = cell.textureAngle || 0;
for (let i = -count / 2; i < count / 2; i++) {
const offset = i * spacing;
const x1 = cell.site.x + Math.cos(angle + Math.PI / 2) * offset - Math.cos(angle) * length / 2;
const y1 = cell.site.y + Math.sin(angle + Math.PI / 2) * offset - Math.sin(angle) * length / 2;
const x2 = cell.site.x + Math.cos(angle + Math.PI / 2) * offset + Math.cos(angle) * length / 2;
const y2 = cell.site.y + Math.sin(angle + Math.PI / 2) * offset + Math.sin(angle) * length / 2;
paths.push(`<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
stroke="black" stroke-width="1.5"/>`);
}
return paths.join('\n');
}
// Generate stipple pattern
private generateStipple(cell: VoronoiCell): string {
const dots: string[] = [];
const count = 50;
const spread = 60;
for (let i = 0; i < count; i++) {
const angle = this.random() * Math.PI * 2;
const distance = this.random() * spread;
const x = cell.site.x + Math.cos(angle) * distance;
const y = cell.site.y + Math.sin(angle) * distance;
const size = 1 + this.random() * 2;
dots.push(`<circle cx="${x}" cy="${y}" r="${size}" fill="black"/>`);
}
return dots.join('\n');
}
// Generate wavy lines
private generateWavy(cell: VoronoiCell): string {
const paths: string[] = [];
const waves = 6;
const spread = 80;
for (let i = 0; i < waves; i++) {
const offsetY = (i - waves / 2) * 15;
let path = `M ${cell.site.x - spread} ${cell.site.y + offsetY}`;
for (let x = -spread; x <= spread; x += 10) {
const y = offsetY + Math.sin(x / 10 + i) * 5;
path += ` L ${cell.site.x + x} ${cell.site.y + y}`;
}
paths.push(`<path d="${path}" fill="none" stroke="black" stroke-width="1.5"/>`);
}
return paths.join('\n');
}
// Generate cell boundary with organic shape
private generateOrganicBoundary(cell: VoronoiCell, allSites: Point[]): string {
// Find neighboring sites and create boundary
const neighbors = allSites
.filter(s => s !== cell.site)
.sort((a, b) => {
const distA = Math.hypot(a.x - cell.site.x, a.y - cell.site.y);
const distB = Math.hypot(b.x - cell.site.x, b.y - cell.site.y);
return distA - distB;
})
.slice(0, 6); // Get 6 closest neighbors
// Create boundary points
const boundaryPoints: Point[] = [];
neighbors.forEach((neighbor) => {
const angle = Math.atan2(neighbor.y - cell.site.y, neighbor.x - cell.site.x);
const distance = Math.hypot(neighbor.x - cell.site.x, neighbor.y - cell.site.y) / 2;
boundaryPoints.push({
x: cell.site.x + Math.cos(angle) * distance,
y: cell.site.y + Math.sin(angle) * distance
});
});
// Sort boundary points by angle
boundaryPoints.sort((a, b) => {
const angleA = Math.atan2(a.y - cell.site.y, a.x - cell.site.x);
const angleB = Math.atan2(b.y - cell.site.y, b.x - cell.site.x);
return angleA - angleB;
});
// Create smooth path through boundary points
if (boundaryPoints.length < 3) return '';
let path = `M ${boundaryPoints[0].x} ${boundaryPoints[0].y}`;
for (let i = 1; i < boundaryPoints.length; i++) {
const curr = boundaryPoints[i];
const prev = boundaryPoints[i - 1];
const next = boundaryPoints[(i + 1) % boundaryPoints.length];
// Control point for smooth curve
const cp1x = prev.x + (curr.x - prev.x) * 0.5;
const cp1y = prev.y + (curr.y - prev.y) * 0.5;
path += ` Q ${cp1x} ${cp1y}, ${curr.x} ${curr.y}`;
}
path += ' Z';
return path;
}
// Generate complete SVG
public generate(siteCount: number = 25): string {
const sites = this.generateSites(siteCount);
this.cells = this.generateVoronoi(sites);
let svg = `<svg width="${this.width}" height="${this.height}" xmlns="http://www.w3.org/2000/svg">`;
svg += `<rect width="100%" height="100%" fill="white"/>`;
// Draw cell boundaries
svg += '<g id="boundaries">';
this.cells.forEach(cell => {
const boundary = this.generateOrganicBoundary(cell, sites);
if (boundary) {
svg += `<path d="${boundary}" fill="#e8e8e8" stroke="black" stroke-width="3"/>`;
}
});
svg += '</g>';
// Draw textures
svg += '<g id="textures">';
this.cells.forEach(cell => {
svg += '<g>';
switch (cell.textureType) {
case 'scales':
svg += this.generateScales(cell);
break;
case 'concentric':
svg += this.generateConcentric(cell);
break;
case 'parallel':
svg += this.generateParallel(cell);
break;
case 'stipple':
svg += this.generateStipple(cell);
break;
case 'wavy':
svg += this.generateWavy(cell);
break;
}
svg += '</g>';
});
svg += '</g>';
svg += '</svg>';
return svg;
}
}
// Usage
const generator = new OrganicPatternGenerator(800, 800, 12345);
const svg = generator.generate(30);
console.log(svg);
// To save to file (Node.js):
// import { writeFileSync } from 'fs';
// writeFileSync('organic-pattern.svg', svg);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment