Created
December 28, 2025 15:23
-
-
Save alex-quiterio/e281181895b3e6fea355bb0ed583e8ef to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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