From d3315656f6e07a1638bbe31d6bcc18357cb1aaf1 Mon Sep 17 00:00:00 2001 From: Grimsace Date: Thu, 19 Feb 2026 09:49:26 -0600 Subject: [PATCH] merged some of the code --- custom_widgets.go | 25 +++ rivers.go | 505 ---------------------------------------------- tappable_image.go | 31 --- water.go | 495 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 520 insertions(+), 536 deletions(-) delete mode 100644 rivers.go delete mode 100644 tappable_image.go diff --git a/custom_widgets.go b/custom_widgets.go index d90c4cc..866d9b3 100644 --- a/custom_widgets.go +++ b/custom_widgets.go @@ -12,6 +12,31 @@ import ( "fyne.io/fyne/v2/widget" ) +type tappableImage struct { + widget.BaseWidget + image *fyne.Container + onTapped func() +} + +func newTappableImage(img *fyne.Container, tapped func()) *tappableImage { + ti := &tappableImage{ + image: img, + onTapped: tapped, + } + ti.ExtendBaseWidget(ti) + return ti +} + +func (t *tappableImage) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(t.image) +} + +func (t *tappableImage) Tapped(*fyne.PointEvent) { + if t.onTapped != nil { + t.onTapped() + } +} + // numericInputSlider combines a slider and text entry for numeric input type numericInputSlider struct { widget.BaseWidget diff --git a/rivers.go b/rivers.go deleted file mode 100644 index 9bfa518..0000000 --- a/rivers.go +++ /dev/null @@ -1,505 +0,0 @@ -package main - -import ( - "image" - "image/color" - "math" - "math/rand" - "runtime" - "sync" - - "github.com/ojrac/opensimplex-go" -) - -type riverParams struct { - EdgeBandRatio float64 - RoughnessStrength float64 - IslandAttemptProb float64 - IslandSeedChance float64 - MinIslandSize int - MaxIslandSize int - WaterLevelBias float64 - NoiseFrequency float64 - KeepInnerFraction float64 - MaxWorkers int - MinWidthPx float64 - MaxWidthPx float64 -} - -// computeSinWaveEdgeOffset computes dual sine wave edge roughening for realistic river banks -func computeSinWaveEdgeOffset(absX, absY int, largeAmplitude, smallAmplitude float64) float64 { - positionPhase := float64(absX)*0.008 + float64(absY)*0.012 - largeWave := math.Sin(positionPhase) * largeAmplitude - smallWave := math.Sin(positionPhase*3.5) * smallAmplitude - return largeWave + smallWave -} - -// RasterizeAndRoughenRiver rasterizes a river path with natural edge roughening and optional islands -func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidthPx float64, heightmap image.Image, isWater map[image.Point]bool, seed int64, minWidthPx, maxWidthPx float64) []image.Point { - if canvas == nil || len(path) == 0 || riverWidthPx <= 0 { - return nil - } - - params := riverParams{ - EdgeBandRatio: 0.6, - RoughnessStrength: 0.65, - IslandAttemptProb: 0.07, - IslandSeedChance: 0.0025, - MinIslandSize: 8, - MaxIslandSize: 800, - WaterLevelBias: 0.02, - NoiseFrequency: 0.02, - KeepInnerFraction: 0.85, - MaxWorkers: 0, - MinWidthPx: minWidthPx, - MaxWidthPx: maxWidthPx, - } - - bounds := canvas.Bounds() - imgW, imgH := bounds.Dx(), bounds.Dy() - - heightGrid := precomputeHeightGrid(heightmap, imgW, imgH) - - radius := riverWidthPx / 2.0 - edgeBand := radius * params.EdgeBandRatio - expand := int(math.Ceil(radius + edgeBand + 2)) - - minX, minY := imgW, imgH - maxX, maxY := 0, 0 - for _, p := range path { - if p.X < minX { - minX = p.X - } - if p.Y < minY { - minY = p.Y - } - if p.X > maxX { - maxX = p.X - } - if p.Y > maxY { - maxY = p.Y - } - } - minX -= expand - minY -= expand - maxX += expand - maxY += expand - if minX < 0 { - minX = 0 - } - if minY < 0 { - minY = 0 - } - if maxX >= imgW { - maxX = imgW - 1 - } - if maxY >= imgH { - maxY = imgH - 1 - } - - bw := maxX - minX + 1 - bh := maxY - minY + 1 - if bw <= 0 || bh <= 0 { - return nil - } - - baseMask := make([]uint8, bw*bh) - - radiusSq := radius * radius - for _, c := range path { - cx := c.X - minX - cy := c.Y - minY - if cx < -int(radius) || cx > bw+int(radius) || cy < -int(radius) || cy > bh+int(radius) { - continue - } - minRx := int(math.Max(0, float64(cx)-radius)) - maxRx := int(math.Min(float64(bw-1), float64(cx)+radius)) - minRy := int(math.Max(0, float64(cy)-radius)) - maxRy := int(math.Min(float64(bh-1), float64(cy)+radius)) - for yy := minRy; yy <= maxRy; yy++ { - for xx := minRx; xx <= maxRx; xx++ { - dx := float64(xx - cx) - dy := float64(yy - cy) - if dx*dx+dy*dy <= radiusSq { - baseMask[yy*bw+xx] = 1 - } - } - } - } - - dist := chamferDistanceField(baseMask, bw, bh) - - noise := opensimplex.New(seed) - noiseFreq := params.NoiseFrequency - - innerKeepRadius := radius * params.KeepInnerFraction - - finalMask := make([]uint8, bw*bh) - - workers := params.MaxWorkers - if workers <= 0 { - workers = runtime.NumCPU() - } - var wg sync.WaitGroup - rowsPerWorker := (bh + workers - 1) / workers - randBase := rand.New(rand.NewSource(seed)) - - heightWeight := 2.0 * params.RoughnessStrength - distWeight := params.RoughnessStrength - noiseWeight := 0.5 * params.RoughnessStrength - - largeAmplitude := params.MaxWidthPx - params.MinWidthPx - smallAmplitude := largeAmplitude / 4.0 - - for wi := 0; wi < workers; wi++ { - startY := wi * rowsPerWorker - endY := startY + rowsPerWorker - if endY > bh { - endY = bh - } - if startY >= endY { - continue - } - wg.Add(1) - go func(startY, endY, workerID int) { - defer wg.Done() - localRand := rand.New(rand.NewSource(randBase.Int63() + int64(workerID)*7919)) - for y := startY; y < endY; y++ { - for x := 0; x < bw; x++ { - idx := y*bw + x - if baseMask[idx] == 1 { - d := dist[idx] - absX := x + minX - absY := y + minY - if d <= float32(innerKeepRadius) { - finalMask[idx] = 1 - continue - } - - sinWaveOffset := computeSinWaveEdgeOffset(absX, absY, largeAmplitude, smallAmplitude) - effectiveInnerRadius := innerKeepRadius + sinWaveOffset - - normDist := float64((float32(d) - float32(effectiveInnerRadius)) / float32(edgeBand)) - if normDist < 0 { - normDist = 0 - } - if normDist > 1 { - normDist = 1 - } - - heightVal := sampleHeightGrid(heightGrid, imgW, imgH, absX, absY) - heightAdj := float64(heightVal) - params.WaterLevelBias - - noiseVal := noise.Eval2(float64(absX)*noiseFreq, float64(absY)*noiseFreq) - noiseNorm := (noiseVal + 1.0) / 2.0 - - score := distWeight*normDist + heightWeight*heightAdj + noiseWeight*(noiseNorm-0.5) - - threshold := 0.35 + 0.5*params.RoughnessStrength - if localRand.Float64() < 0.0005 { - score += (localRand.Float64() - 0.5) * 0.2 - } - - if score < threshold { - finalMask[idx] = 1 - } else { - finalMask[idx] = 0 - } - } - } - } - }(startY, endY, wi) - } - wg.Wait() - - randForIsland := rand.New(rand.NewSource(seed + 1234567)) - tryIslands := randForIsland.Float64() < params.IslandAttemptProb - if tryIslands { - generateIslandsInMask(finalMask, bw, bh, minX, minY, heightGrid, imgW, imgH, ¶ms, seed+4242) - } - - var added []image.Point - for y := 0; y < bh; y++ { - absY := y + minY - for x := 0; x < bw; x++ { - absX := x + minX - pt := image.Point{X: absX, Y: absY} - if !pt.In(bounds) { - continue - } - idx := y*bw + x - if finalMask[idx] == 1 && !isWater[pt] { - canvas.Set(absX, absY, color.RGBA{R: 0, G: 0, B: 255, A: 255}) - isWater[pt] = true - added = append(added, pt) - } - } - } - - removeSpeckles(&finalMask, bw, bh, 2) - - return added -} - -// precomputeHeightGrid converts heightmap to normalized float32 grid -func precomputeHeightGrid(hmap image.Image, width, height int) []float32 { - out := make([]float32, width*height) - if hmap == nil { - for i := range out { - out[i] = 0.5 - } - return out - } - b := hmap.Bounds() - for y := 0; y < height; y++ { - for x := 0; x < width; x++ { - absX := x + b.Min.X - absY := y + b.Min.Y - r, _, _, _ := hmap.At(absX, absY).RGBA() - val := float32(r) / 65535.0 - out[y*width+x] = val - } - } - return out -} - -// sampleHeightGrid safely samples height at coordinates -func sampleHeightGrid(grid []float32, width, height, x, y int) float32 { - if x < 0 || x >= width || y < 0 || y >= height { - return 0.5 - } - return grid[y*width+x] -} - -// chamferDistanceField computes fast approximate distance from any pixel to centerline -func chamferDistanceField(baseMask []uint8, w, h int) []float32 { - const maxF = 1e6 - dist := make([]float32, w*h) - - for i := 0; i < w*h; i++ { - if baseMask[i] == 1 { - dist[i] = 0 - } else { - dist[i] = maxF - } - } - - // Forward pass - for y := 0; y < h; y++ { - for x := 0; x < w; x++ { - i := y*w + x - if dist[i] == 0 { - continue - } - if x > 0 { - v := dist[i-1] + 1.0 - if v < dist[i] { - dist[i] = v - } - } - if y > 0 { - v := dist[i-w] + 1.0 - if v < dist[i] { - dist[i] = v - } - } - if x > 0 && y > 0 { - v := dist[i-w-1] + 1.41421356 - if v < dist[i] { - dist[i] = v - } - } - if x < w-1 && y > 0 { - v := dist[i-w+1] + 1.41421356 - if v < dist[i] { - dist[i] = v - } - } - } - } - - // Backward pass - for y := h - 1; y >= 0; y-- { - for x := w - 1; x >= 0; x-- { - i := y*w + x - if x < w-1 { - v := dist[i+1] + 1.0 - if v < dist[i] { - dist[i] = v - } - } - if y < h-1 { - v := dist[i+w] + 1.0 - if v < dist[i] { - dist[i] = v - } - } - if x < w-1 && y < h-1 { - v := dist[i+w+1] + 1.41421356 - if v < dist[i] { - dist[i] = v - } - } - if x > 0 && y < h-1 { - v := dist[i+w-1] + 1.41421356 - if v < dist[i] { - dist[i] = v - } - } - } - } - - return dist -} - -// generateIslandsInMask creates small islands inside water areas -func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []float32, fullW, fullH int, params *riverParams, seed int64) { - r := rand.New(rand.NewSource(seed)) - type pt struct{ x, y int } - candidates := make([]pt, 0) - for y := 0; y < bh; y++ { - for x := 0; x < bw; x++ { - idx := y*bw + x - if mask[idx] != 1 { - continue - } - absX := x + minX - absY := y + minY - hv := sampleHeightGrid(heightGrid, fullW, fullH, absX, absY) - if float64(hv) > params.WaterLevelBias+0.03 { - if r.Float64() < params.IslandSeedChance { - candidates = append(candidates, pt{x, y}) - } - } - } - } - if len(candidates) == 0 { - return - } - - r.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] }) - - visited := make([]uint8, bw*bh) - - for _, c := range candidates { - ci := c.y*bw + c.x - if visited[ci] != 0 { - continue - } - maxSize := params.MaxIslandSize - minSize := params.MinIslandSize - targetSize := minSize + r.Intn(maxSize-minSize+1) - - queue := []pt{{c.x, c.y}} - visited[ci] = 1 - island := make([]pt, 0, targetSize) - touchesEdge := false - - for qi := 0; qi < len(queue) && len(island) < targetSize; qi++ { - p := queue[qi] - absX := p.x + minX - absY := p.y + minY - hv := sampleHeightGrid(heightGrid, fullW, fullH, absX, absY) - if float64(hv) < params.WaterLevelBias+0.01 { - continue - } - island = append(island, p) - for dy := -1; dy <= 1; dy++ { - for dx := -1; dx <= 1; dx++ { - nx, ny := p.x+dx, p.y+dy - if nx < 0 || nx >= bw || ny < 0 || ny >= bh { - touchesEdge = true - continue - } - nidx := ny*bw + nx - if visited[nidx] != 0 { - continue - } - if mask[nidx] != 1 { - continue - } - visited[nidx] = 1 - queue = append(queue, pt{nx, ny}) - } - } - } - - if touchesEdge { - continue - } - - if len(island) < minSize { - continue - } - - for _, p := range island { - mask[p.y*bw+p.x] = 0 - } - - if r.Float64() < 0.7 { - if r.Intn(3) == 0 { - break - } - } - } -} - -// removeSpeckles removes tiny isolated water pixels -func removeSpeckles(mask *[]uint8, bw, bh, minNeighbors int) { - arr := *mask - out := make([]uint8, len(arr)) - copy(out, arr) - for y := 0; y < bh; y++ { - for x := 0; x < bw; x++ { - idx := y*bw + x - if arr[idx] == 0 { - continue - } - count := 0 - for dy := -1; dy <= 1; dy++ { - for dx := -1; dx <= 1; dx++ { - if dx == 0 && dy == 0 { - continue - } - nx := x + dx - ny := y + dy - if nx < 0 || nx >= bw || ny < 0 || ny >= bh { - continue - } - if arr[ny*bw+nx] == 1 { - count++ - } - } - } - if count < minNeighbors { - out[idx] = 0 - } - } - } - copy(arr, out) - *mask = arr -} - -// maskToPoints converts mask to point slice for visualization -func maskToPoints(mask []uint8, bw, bh, minX, minY int) []image.Point { - var pts []image.Point - for y := 0; y < bh; y++ { - for x := 0; x < bw; x++ { - if mask[y*bw+x] == 1 { - pts = append(pts, image.Point{X: x + minX, Y: y + minY}) - } - } - } - return pts -} - -// clamp01 clamps value to 0..1 range -func clamp01(v float64) float64 { - if v < 0 { - return 0 - } - if v > 1 { - return 1 - } - return v -} diff --git a/tappable_image.go b/tappable_image.go deleted file mode 100644 index 56ea7a4..0000000 --- a/tappable_image.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/widget" -) - -type tappableImage struct { - widget.BaseWidget - image *fyne.Container - onTapped func() -} - -func newTappableImage(img *fyne.Container, tapped func()) *tappableImage { - ti := &tappableImage{ - image: img, - onTapped: tapped, - } - ti.ExtendBaseWidget(ti) - return ti -} - -func (t *tappableImage) CreateRenderer() fyne.WidgetRenderer { - return widget.NewSimpleRenderer(t.image) -} - -func (t *tappableImage) Tapped(*fyne.PointEvent) { - if t.onTapped != nil { - t.onTapped() - } -} diff --git a/water.go b/water.go index c069cd1..b26f61e 100644 --- a/water.go +++ b/water.go @@ -7,7 +7,9 @@ import ( "image/draw" "math" "math/rand" + "runtime" "sort" + "sync" "github.com/ojrac/opensimplex-go" ) @@ -43,6 +45,21 @@ func (pq *priorityQueue) Pop() any { return item } +type riverParams struct { + EdgeBandRatio float64 + RoughnessStrength float64 + IslandAttemptProb float64 + IslandSeedChance float64 + MinIslandSize int + MaxIslandSize int + WaterLevelBias float64 + NoiseFrequency float64 + KeepInnerFraction float64 + MaxWorkers int + MinWidthPx float64 + MaxWidthPx float64 +} + // GenerateLakes creates lakes on the map using a priority queue growth algorithm func GenerateLakes(width, height, numLakes int, lakeSizeLower, lakeSizeUpper float64, seed int64, lakeEdgeRoughness float64) (image.Image, [][]image.Point) { canvas := image.NewRGBA(image.Rect(0, 0, width, height)) @@ -189,6 +206,484 @@ type River struct { Points []image.Point } +// computeSinWaveEdgeOffset computes dual sine wave edge roughening for realistic river banks +func computeSinWaveEdgeOffset(absX, absY int, largeAmplitude, smallAmplitude float64) float64 { + positionPhase := float64(absX)*0.008 + float64(absY)*0.012 + largeWave := math.Sin(positionPhase) * largeAmplitude + smallWave := math.Sin(positionPhase*3.5) * smallAmplitude + return largeWave + smallWave +} + +// RasterizeAndRoughenRiver rasterizes a river path with natural edge roughening and optional islands +func RasterizeAndRoughenRiver(canvas *image.RGBA, path []image.Point, riverWidthPx float64, heightmap image.Image, isWater map[image.Point]bool, seed int64, minWidthPx, maxWidthPx float64) []image.Point { + if canvas == nil || len(path) == 0 || riverWidthPx <= 0 { + return nil + } + + params := riverParams{ + EdgeBandRatio: 0.6, + RoughnessStrength: 0.65, + IslandAttemptProb: 0.07, + IslandSeedChance: 0.0025, + MinIslandSize: 8, + MaxIslandSize: 800, + WaterLevelBias: 0.02, + NoiseFrequency: 0.02, + KeepInnerFraction: 0.85, + MaxWorkers: 0, + MinWidthPx: minWidthPx, + MaxWidthPx: maxWidthPx, + } + + bounds := canvas.Bounds() + imgW, imgH := bounds.Dx(), bounds.Dy() + + heightGrid := precomputeHeightGrid(heightmap, imgW, imgH) + + radius := riverWidthPx / 2.0 + edgeBand := radius * params.EdgeBandRatio + expand := int(math.Ceil(radius + edgeBand + 2)) + + minX, minY := imgW, imgH + maxX, maxY := 0, 0 + for _, p := range path { + if p.X < minX { + minX = p.X + } + if p.Y < minY { + minY = p.Y + } + if p.X > maxX { + maxX = p.X + } + if p.Y > maxY { + maxY = p.Y + } + } + minX -= expand + minY -= expand + maxX += expand + maxY += expand + if minX < 0 { + minX = 0 + } + if minY < 0 { + minY = 0 + } + if maxX >= imgW { + maxX = imgW - 1 + } + if maxY >= imgH { + maxY = imgH - 1 + } + + bw := maxX - minX + 1 + bh := maxY - minY + 1 + if bw <= 0 || bh <= 0 { + return nil + } + + baseMask := make([]uint8, bw*bh) + + radiusSq := radius * radius + for _, c := range path { + cx := c.X - minX + cy := c.Y - minY + if cx < -int(radius) || cx > bw+int(radius) || cy < -int(radius) || cy > bh+int(radius) { + continue + } + minRx := int(math.Max(0, float64(cx)-radius)) + maxRx := int(math.Min(float64(bw-1), float64(cx)+radius)) + minRy := int(math.Max(0, float64(cy)-radius)) + maxRy := int(math.Min(float64(bh-1), float64(cy)+radius)) + for yy := minRy; yy <= maxRy; yy++ { + for xx := minRx; xx <= maxRx; xx++ { + dx := float64(xx - cx) + dy := float64(yy - cy) + if dx*dx+dy*dy <= radiusSq { + baseMask[yy*bw+xx] = 1 + } + } + } + } + + dist := chamferDistanceField(baseMask, bw, bh) + + noise := opensimplex.New(seed) + noiseFreq := params.NoiseFrequency + + innerKeepRadius := radius * params.KeepInnerFraction + + finalMask := make([]uint8, bw*bh) + + workers := params.MaxWorkers + if workers <= 0 { + workers = runtime.NumCPU() + } + var wg sync.WaitGroup + rowsPerWorker := (bh + workers - 1) / workers + randBase := rand.New(rand.NewSource(seed)) + + heightWeight := 2.0 * params.RoughnessStrength + distWeight := params.RoughnessStrength + noiseWeight := 0.5 * params.RoughnessStrength + + largeAmplitude := params.MaxWidthPx - params.MinWidthPx + smallAmplitude := largeAmplitude / 4.0 + + for wi := 0; wi < workers; wi++ { + startY := wi * rowsPerWorker + endY := startY + rowsPerWorker + if endY > bh { + endY = bh + } + if startY >= endY { + continue + } + wg.Add(1) + go func(startY, endY, workerID int) { + defer wg.Done() + localRand := rand.New(rand.NewSource(randBase.Int63() + int64(workerID)*7919)) + for y := startY; y < endY; y++ { + for x := 0; x < bw; x++ { + idx := y*bw + x + if baseMask[idx] == 1 { + d := dist[idx] + absX := x + minX + absY := y + minY + if d <= float32(innerKeepRadius) { + finalMask[idx] = 1 + continue + } + + sinWaveOffset := computeSinWaveEdgeOffset(absX, absY, largeAmplitude, smallAmplitude) + effectiveInnerRadius := innerKeepRadius + sinWaveOffset + + normDist := float64((float32(d) - float32(effectiveInnerRadius)) / float32(edgeBand)) + if normDist < 0 { + normDist = 0 + } + if normDist > 1 { + normDist = 1 + } + + heightVal := sampleHeightGrid(heightGrid, imgW, imgH, absX, absY) + heightAdj := float64(heightVal) - params.WaterLevelBias + + noiseVal := noise.Eval2(float64(absX)*noiseFreq, float64(absY)*noiseFreq) + noiseNorm := (noiseVal + 1.0) / 2.0 + + score := distWeight*normDist + heightWeight*heightAdj + noiseWeight*(noiseNorm-0.5) + + threshold := 0.35 + 0.5*params.RoughnessStrength + if localRand.Float64() < 0.0005 { + score += (localRand.Float64() - 0.5) * 0.2 + } + + if score < threshold { + finalMask[idx] = 1 + } else { + finalMask[idx] = 0 + } + } + } + } + }(startY, endY, wi) + } + wg.Wait() + + randForIsland := rand.New(rand.NewSource(seed + 1234567)) + tryIslands := randForIsland.Float64() < params.IslandAttemptProb + if tryIslands { + generateIslandsInMask(finalMask, bw, bh, minX, minY, heightGrid, imgW, imgH, ¶ms, seed+4242) + } + + var added []image.Point + for y := 0; y < bh; y++ { + absY := y + minY + for x := 0; x < bw; x++ { + absX := x + minX + pt := image.Point{X: absX, Y: absY} + if !pt.In(bounds) { + continue + } + idx := y*bw + x + if finalMask[idx] == 1 && !isWater[pt] { + canvas.Set(absX, absY, color.RGBA{R: 0, G: 0, B: 255, A: 255}) + isWater[pt] = true + added = append(added, pt) + } + } + } + + removeSpeckles(&finalMask, bw, bh, 2) + + return added +} + +// precomputeHeightGrid converts heightmap to normalized float32 grid +func precomputeHeightGrid(hmap image.Image, width, height int) []float32 { + out := make([]float32, width*height) + if hmap == nil { + for i := range out { + out[i] = 0.5 + } + return out + } + b := hmap.Bounds() + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + absX := x + b.Min.X + absY := y + b.Min.Y + r, _, _, _ := hmap.At(absX, absY).RGBA() + val := float32(r) / 65535.0 + out[y*width+x] = val + } + } + return out +} + +// sampleHeightGrid safely samples height at coordinates +func sampleHeightGrid(grid []float32, width, height, x, y int) float32 { + if x < 0 || x >= width || y < 0 || y >= height { + return 0.5 + } + return grid[y*width+x] +} + +// chamferDistanceField computes fast approximate distance from any pixel to centerline +func chamferDistanceField(baseMask []uint8, w, h int) []float32 { + const maxF = 1e6 + dist := make([]float32, w*h) + + for i := 0; i < w*h; i++ { + if baseMask[i] == 1 { + dist[i] = 0 + } else { + dist[i] = maxF + } + } + + // Forward pass + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + i := y*w + x + if dist[i] == 0 { + continue + } + if x > 0 { + v := dist[i-1] + 1.0 + if v < dist[i] { + dist[i] = v + } + } + if y > 0 { + v := dist[i-w] + 1.0 + if v < dist[i] { + dist[i] = v + } + } + if x > 0 && y > 0 { + v := dist[i-w-1] + 1.41421356 + if v < dist[i] { + dist[i] = v + } + } + if x < w-1 && y > 0 { + v := dist[i-w+1] + 1.41421356 + if v < dist[i] { + dist[i] = v + } + } + } + } + + // Backward pass + for y := h - 1; y >= 0; y-- { + for x := w - 1; x >= 0; x-- { + i := y*w + x + if x < w-1 { + v := dist[i+1] + 1.0 + if v < dist[i] { + dist[i] = v + } + } + if y < h-1 { + v := dist[i+w] + 1.0 + if v < dist[i] { + dist[i] = v + } + } + if x < w-1 && y < h-1 { + v := dist[i+w+1] + 1.41421356 + if v < dist[i] { + dist[i] = v + } + } + if x > 0 && y < h-1 { + v := dist[i+w-1] + 1.41421356 + if v < dist[i] { + dist[i] = v + } + } + } + } + + return dist +} + +// generateIslandsInMask creates small islands inside water areas +func generateIslandsInMask(mask []uint8, bw, bh, minX, minY int, heightGrid []float32, fullW, fullH int, params *riverParams, seed int64) { + r := rand.New(rand.NewSource(seed)) + type pt struct{ x, y int } + candidates := make([]pt, 0) + for y := 0; y < bh; y++ { + for x := 0; x < bw; x++ { + idx := y*bw + x + if mask[idx] != 1 { + continue + } + absX := x + minX + absY := y + minY + hv := sampleHeightGrid(heightGrid, fullW, fullH, absX, absY) + if float64(hv) > params.WaterLevelBias+0.03 { + if r.Float64() < params.IslandSeedChance { + candidates = append(candidates, pt{x, y}) + } + } + } + } + if len(candidates) == 0 { + return + } + + r.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] }) + + visited := make([]uint8, bw*bh) + + for _, c := range candidates { + ci := c.y*bw + c.x + if visited[ci] != 0 { + continue + } + maxSize := params.MaxIslandSize + minSize := params.MinIslandSize + targetSize := minSize + r.Intn(maxSize-minSize+1) + + queue := []pt{{c.x, c.y}} + visited[ci] = 1 + island := make([]pt, 0, targetSize) + touchesEdge := false + + for qi := 0; qi < len(queue) && len(island) < targetSize; qi++ { + p := queue[qi] + absX := p.x + minX + absY := p.y + minY + hv := sampleHeightGrid(heightGrid, fullW, fullH, absX, absY) + if float64(hv) < params.WaterLevelBias+0.01 { + continue + } + island = append(island, p) + for dy := -1; dy <= 1; dy++ { + for dx := -1; dx <= 1; dx++ { + nx, ny := p.x+dx, p.y+dy + if nx < 0 || nx >= bw || ny < 0 || ny >= bh { + touchesEdge = true + continue + } + nidx := ny*bw + nx + if visited[nidx] != 0 { + continue + } + if mask[nidx] != 1 { + continue + } + visited[nidx] = 1 + queue = append(queue, pt{nx, ny}) + } + } + } + + if touchesEdge { + continue + } + + if len(island) < minSize { + continue + } + + for _, p := range island { + mask[p.y*bw+p.x] = 0 + } + + if r.Float64() < 0.7 { + if r.Intn(3) == 0 { + break + } + } + } +} + +// removeSpeckles removes tiny isolated water pixels +func removeSpeckles(mask *[]uint8, bw, bh, minNeighbors int) { + arr := *mask + out := make([]uint8, len(arr)) + copy(out, arr) + for y := 0; y < bh; y++ { + for x := 0; x < bw; x++ { + idx := y*bw + x + if arr[idx] == 0 { + continue + } + count := 0 + for dy := -1; dy <= 1; dy++ { + for dx := -1; dx <= 1; dx++ { + if dx == 0 && dy == 0 { + continue + } + nx := x + dx + ny := y + dy + if nx < 0 || nx >= bw || ny < 0 || ny >= bh { + continue + } + if arr[ny*bw+nx] == 1 { + count++ + } + } + } + if count < minNeighbors { + out[idx] = 0 + } + } + } + copy(arr, out) + *mask = arr +} + +// maskToPoints converts mask to point slice for visualization +func maskToPoints(mask []uint8, bw, bh, minX, minY int) []image.Point { + var pts []image.Point + for y := 0; y < bh; y++ { + for x := 0; x < bw; x++ { + if mask[y*bw+x] == 1 { + pts = append(pts, image.Point{X: x + minX, Y: y + minY}) + } + } + } + return pts +} + +// clamp01 clamps value to 0..1 range +func clamp01(v float64) float64 { + if v < 0 { + return 0 + } + if v > 1 { + return 1 + } + return v +} + // GenerateRivers creates rivers flowing across the map from edge to edge func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakes [][]image.Point, seed int64, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) (image.Image, []image.Point) { if numRivers == 0 {