Karta
95 | 96 | 106 | 107 |Examples
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | ├── .gitignore ├── noise └── noise.go ├── cell.go ├── go.mod ├── cmd ├── karta-server │ ├── http.go │ └── main.go ├── karta │ └── main.go └── karta-voxel │ └── main.go ├── diagram └── diagram.go ├── palette └── palette.go ├── karta.go ├── go.sum ├── generate.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | _progress 2 | *.png 3 | *.log 4 | -------------------------------------------------------------------------------- /noise/noise.go: -------------------------------------------------------------------------------- 1 | package noise 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | // Noise wraps simplexnoise.SimplexNoise 6 | type Noise struct { 7 | *gfx.SimplexNoise 8 | } 9 | 10 | // New generates a new Noise instance 11 | func New(seed int64) *Noise { 12 | return &Noise{gfx.NewSimplexNoise(seed)} 13 | } 14 | -------------------------------------------------------------------------------- /cell.go: -------------------------------------------------------------------------------- 1 | package karta 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/pzsz/voronoi" 7 | ) 8 | 9 | // Cell is the smalles unit on the map 10 | type Cell struct { 11 | Index int `json:"index"` 12 | CenterDistance float64 `json:"center_distance"` 13 | NoiseLevel float64 `json:"noise_level"` 14 | Elevation float64 `json:"elevation"` 15 | Land bool `json:"land"` 16 | Site voronoi.Vertex `json:"site"` 17 | FillColor color.RGBA `json:"fill_color"` 18 | StrokeColor color.RGBA `json:"stroke_color"` 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/peterhellberg/karta 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/fogleman/fauxgl v0.0.0-20200818143847-27cddc103802 7 | github.com/llgcode/draw2d v0.0.0-20210904075650-80aa0a2a901d 8 | github.com/peterhellberg/gfx v0.0.0-20210905153911-4a6ef1535e02 9 | github.com/pzsz/voronoi v0.0.0-20130609164533-4314be88c79f 10 | ) 11 | 12 | require ( 13 | github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046 // indirect 14 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 15 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /cmd/karta-server/http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | type context struct { 9 | Logger *log.Logger 10 | } 11 | 12 | func (c *context) log(a ...interface{}) { 13 | c.Logger.Println(a...) 14 | } 15 | 16 | func (c *context) logf(f string, a ...interface{}) { 17 | c.Logger.Printf(f, a...) 18 | } 19 | 20 | type handler func(*context, *http.Request, http.ResponseWriter) error 21 | 22 | func baseHandler(ctx *context, fn handler) http.Handler { 23 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 | err := fn(ctx, r, w) 25 | if err != nil { 26 | ctx.Logger.Printf("baseHandler: uri=%s err=%s", r.RequestURI, err) 27 | w.WriteHeader(500) 28 | } 29 | }) 30 | } 31 | 32 | // ListenAndServe creates a context, registers all handlers 33 | // using the provided routes function, and starts listening on the port 34 | func ListenAndServe(l *log.Logger, port string, routes func(*context)) error { 35 | routes(&context{Logger: l}) 36 | 37 | l.Printf("Listening on http://0.0.0.0:%s\n", port) 38 | 39 | return http.ListenAndServe(":"+port, nil) 40 | } 41 | -------------------------------------------------------------------------------- /diagram/diagram.go: -------------------------------------------------------------------------------- 1 | package diagram 2 | 3 | import ( 4 | "github.com/pzsz/voronoi" 5 | "github.com/pzsz/voronoi/utils" 6 | ) 7 | 8 | // Diagram wraps a Voronoi diagram and contains 9 | // a vertex pointing to the center of the map 10 | type Diagram struct { 11 | *voronoi.Diagram 12 | Center voronoi.Vertex 13 | } 14 | 15 | // New generates a Voronoi diagram, relaxed by Lloyd's algorithm 16 | func New(w, h float64, c, r int) *Diagram { 17 | bbox := voronoi.NewBBox(0, w, 0, h) 18 | sites := utils.RandomSites(bbox, c) 19 | 20 | // Compute voronoi diagram. 21 | d := voronoi.ComputeDiagram(sites, bbox, true) 22 | 23 | // Max number of iterations is 16 24 | if r > 16 { 25 | r = 16 26 | } 27 | 28 | // Relax using Lloyd's algorithm 29 | for i := 0; i < r; i++ { 30 | sites = utils.LloydRelaxation(d.Cells) 31 | d = voronoi.ComputeDiagram(sites, bbox, true) 32 | } 33 | 34 | center := voronoi.Vertex{ 35 | X: float64(w / 2), 36 | Y: float64(h / 2), 37 | } 38 | 39 | return &Diagram{d, center} 40 | } 41 | 42 | // Distance returns the distance between two vertices 43 | func Distance(a, b voronoi.Vertex) float64 { 44 | return utils.Distance(a, b) 45 | } 46 | -------------------------------------------------------------------------------- /cmd/karta/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "image/png" 6 | "log" 7 | "math/rand" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | 12 | "github.com/peterhellberg/karta" 13 | ) 14 | 15 | var ( 16 | output = flag.String("output", "karta.png", "Output filename") 17 | count = flag.Int("count", 2048, "The number of sites in the voronoi diagram") 18 | width = flag.Int("width", 512, "The width of the map in pixels") 19 | height = flag.Int("height", 512, "The height of the map in pixels") 20 | relax = flag.Int("iterations", 1, "The number of iterations of Lloyd's algorithm to run (max 16)") 21 | seed = flag.Int64("seed", 3, "The starting seed for the map generator") 22 | show = flag.Bool("show", false, "Show generated map using Preview.app") 23 | ) 24 | 25 | func main() { 26 | // Parse the command line flags 27 | flag.Parse() 28 | 29 | // Seed the random number generator 30 | rand.Seed(*seed) 31 | 32 | if *count < 3 { 33 | log.Fatalf("count must be at least 3") 34 | } 35 | 36 | // Create a new karta 37 | k := karta.New(*width, *height, *count, *relax) 38 | 39 | if k.Generate() == nil { 40 | file, err := os.Create(*output) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | defer file.Close() 45 | 46 | if strings.HasSuffix(*output, ".json") { 47 | if j, err := k.MarshalJSON(); err == nil { 48 | file.Write(j) 49 | } 50 | } else { 51 | err := png.Encode(file, k.Image) 52 | 53 | if err != nil { 54 | log.Fatal(err) 55 | } else if *show { 56 | previewImage(*output) 57 | } 58 | } 59 | } 60 | } 61 | 62 | func previewImage(name string) { 63 | cmd := exec.Command("open", "-a", "/Applications/Preview.app", name) 64 | 65 | if err := cmd.Run(); err != nil { 66 | log.Fatal(err) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /palette/palette.go: -------------------------------------------------------------------------------- 1 | package palette 2 | 3 | import "image/color" 4 | 5 | var ( 6 | Black = color.RGBA{0x00, 0x00, 0x00, 0xFF} 7 | White = color.RGBA{0xFF, 0xFF, 0xFF, 0xFF} 8 | Pink = color.RGBA{0xF2, 0x07, 0x65, 0xFF} 9 | Purple = color.RGBA{0x85, 0x08, 0x41, 0xFF} 10 | Orange = color.RGBA{0xFF, 0x66, 0x00, 0xFF} 11 | Blue0 = color.RGBA{0x1e, 0x8b, 0xb8, 0xff} 12 | Blue1 = color.RGBA{0x17, 0x6c, 0x8f, 0xff} 13 | Blue2 = color.RGBA{0x14, 0x5d, 0x7b, 0xff} 14 | Blue3 = color.RGBA{0x11, 0x4d, 0x66, 0xff} 15 | Blue4 = color.RGBA{0x0d, 0x3e, 0x52, 0xff} 16 | Blue5 = color.RGBA{0x0a, 0x2e, 0x3d, 0xff} 17 | Blue6 = color.RGBA{0x06, 0x1f, 0x29, 0xff} 18 | Blue7 = color.RGBA{0x03, 0x0f, 0x14, 0xff} 19 | Green1 = color.RGBA{0xbc, 0xcf, 0x48, 0xff} 20 | Green2 = color.RGBA{0xa7, 0xb8, 0x40, 0xff} 21 | Green3 = color.RGBA{0x92, 0xa1, 0x38, 0xff} 22 | Green4 = color.RGBA{0x7d, 0x8a, 0x30, 0xff} 23 | Green5 = color.RGBA{0x68, 0x73, 0x28, 0xff} 24 | Green6 = color.RGBA{0x53, 0x5c, 0x20, 0xff} 25 | Green7 = color.RGBA{0x3e, 0x45, 0x18, 0xff} 26 | Green8 = color.RGBA{0x29, 0x2e, 0x10, 0xff} 27 | Yellow1 = color.RGBA{0xf1, 0xea, 0x9b, 0xff} 28 | Yellow2 = color.RGBA{0xD6, 0xC5, 0x84, 0xff} 29 | ) 30 | 31 | // Inspiration 32 | // 33 | // Gray http://www.colourlovers.com/color/B8B6B6/boring 34 | // Black http://www.colourlovers.com/color/000000/Black 35 | // White http://www.colourlovers.com/color/FFFFFF/white 36 | // Blue http://www.colourlovers.com/color/4DBCE9/electric_blue 37 | // Darkblue http://www.colourlovers.com/color/26ADE4/blube 38 | // Orange http://www.colourlovers.com/color/FF6600/Rounge 39 | // Pink http://www.colourlovers.com/color/F20765/Dont_Stop_Loving_Me 40 | // Purple http://www.colourlovers.com/color/850841/complaining_violet 41 | // Green http://www.colourlovers.com/color/D1E751/limey 42 | -------------------------------------------------------------------------------- /karta.go: -------------------------------------------------------------------------------- 1 | package karta 2 | 3 | import ( 4 | "encoding/json" 5 | "image" 6 | "math" 7 | "math/rand" 8 | 9 | "github.com/peterhellberg/karta/diagram" 10 | "github.com/peterhellberg/karta/noise" 11 | "github.com/pzsz/voronoi" 12 | ) 13 | 14 | // Cells represents a list of cells 15 | type Cells []*Cell 16 | 17 | // Karta represents the entire map 18 | type Karta struct { 19 | Width int 20 | Height int 21 | Unit float64 22 | Cells Cells 23 | Diagram *diagram.Diagram 24 | Noise *noise.Noise 25 | Image image.Image 26 | } 27 | 28 | // New instantiates a new Karta 29 | func New(w, h, c, r int) *Karta { 30 | return &Karta{ 31 | Width: w, 32 | Height: h, 33 | Unit: float64(math.Min(float64(w), float64(h)) / 20), 34 | Cells: Cells{}, 35 | Diagram: diagram.New(float64(w), float64(h), c, r), 36 | Noise: noise.New(rand.Int63n(int64(w * h))), 37 | } 38 | } 39 | 40 | // Image creates a new Karta, then generates an Image 41 | func Image(w, h, c, r int) image.Image { 42 | return New(w, h, c, r).GenerateImage() 43 | } 44 | 45 | // Edge represents two vertexes 46 | type Edge struct { 47 | VaVertex voronoi.Vertex 48 | VbVertex voronoi.Vertex 49 | } 50 | 51 | // MarshalJSON marshals the map as JSON 52 | func (k *Karta) MarshalJSON() ([]byte, error) { 53 | edges := []*Edge{} 54 | 55 | for _, ev := range k.Diagram.Edges { 56 | edges = append(edges, &Edge{ 57 | VaVertex: ev.Va.Vertex, 58 | VbVertex: ev.Vb.Vertex, 59 | }) 60 | } 61 | 62 | return json.MarshalIndent(struct { 63 | Width int `json:"width"` 64 | Height int `json:"height"` 65 | Unit float64 `json:"unit"` 66 | Edges []*Edge `json:"edges"` 67 | Cells Cells `json:"cells"` 68 | }{ 69 | Width: k.Width, 70 | Height: k.Height, 71 | Unit: k.Unit, 72 | Edges: edges, 73 | Cells: k.Cells, 74 | }, "", " ") 75 | } 76 | 77 | // GenerateImage generates an image 78 | func (k *Karta) GenerateImage() image.Image { 79 | if k.Generate() == nil { 80 | return k.Image 81 | } 82 | 83 | return image.NewRGBA(image.Rect(0, 0, k.Width, k.Height)) 84 | } 85 | -------------------------------------------------------------------------------- /cmd/karta-voxel/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "math" 6 | "math/rand" 7 | 8 | "github.com/peterhellberg/karta" 9 | 10 | . "github.com/fogleman/fauxgl" 11 | ) 12 | 13 | const ( 14 | fovy = 10 15 | near = 1 16 | far = 100 17 | relax = 4 18 | count = 2048 19 | ) 20 | 21 | var ( 22 | eye = V(8, 5, 5) 23 | center = V(0.04, 0, 0) 24 | up = V(0, 0, 1) 25 | 26 | coal = HexColor("191616") 27 | ) 28 | 29 | func main() { 30 | var seed int64 31 | 32 | flag.Int64Var(&seed, "seed", 4, "The starting seed for the map generator") 33 | 34 | flag.Parse() 35 | 36 | rand.Seed(seed) 37 | 38 | k := karta.New(256, 256, count, relax) 39 | 40 | if k.Generate() == nil { 41 | voxels := []Voxel{} 42 | 43 | m := k.Image 44 | 45 | w, h := m.Bounds().Max.X, m.Bounds().Max.Y 46 | 47 | for x := 0; x < w; x++ { 48 | for y := 0; y < h; y++ { 49 | z := getZ(k.Cells, x, y) 50 | 51 | for i := 0; i < z; i++ { 52 | voxels = append(voxels, Voxel{ 53 | X: x, 54 | Y: y, 55 | Z: i + 1, 56 | Color: MakeColor(m.At(x, y)), 57 | }) 58 | } 59 | } 60 | } 61 | 62 | mesh := NewVoxelMesh(voxels) 63 | 64 | mesh.BiUnitCube() 65 | 66 | context := NewContext(1024, 768) 67 | context.ClearColor = coal 68 | context.ClearColorBuffer() 69 | 70 | aspect := float64(1024) / float64(768) 71 | matrix := LookAt(eye, center, up).Perspective(fovy, aspect, near, far) 72 | light := V(1, 0.6, 1.9).Normalize() 73 | 74 | shader := NewPhongShader(matrix, light, eye) 75 | 76 | shader.DiffuseColor = Gray(1.2) 77 | 78 | context.Shader = shader 79 | context.DrawMesh(mesh) 80 | 81 | SavePNG("karta.png", context.Image()) 82 | } 83 | } 84 | 85 | func getZ(cells karta.Cells, x, y int) int { 86 | sd := math.Inf(1) 87 | 88 | var cc *karta.Cell 89 | 90 | for _, c := range cells { 91 | if d := distance(float64(x), float64(y), c.Site.X, c.Site.Y); d < sd { 92 | sd = d 93 | cc = c 94 | } 95 | } 96 | 97 | if cc != nil { 98 | return 2 + int(cc.Elevation) 99 | } 100 | 101 | return 2 102 | } 103 | 104 | func distance(x1, y1, x2, y2 float64) float64 { 105 | return math.Sqrt(math.Pow(x2-x1, 2) + math.Pow(y2-y1, 2)) 106 | } 107 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fogleman/fauxgl v0.0.0-20200818143847-27cddc103802 h1:5vdq0jOnV15v1NdZbAcU+dIJ22rFgwaieiFewPvnKCA= 2 | github.com/fogleman/fauxgl v0.0.0-20200818143847-27cddc103802/go.mod h1:7f7F8EvO8MWvDx9sIoloOfZBCKzlWuZV/h3TjpXOO3k= 3 | github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046 h1:n3RPbpwXSFT0G8FYslzMUBDO09Ix8/dlqzvUkcJm4Jk= 4 | github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046/go.mod h1:KDwyDqFmVUxUmo7tmqXtyaaJMdGon06y8BD2jmh84CQ= 5 | github.com/go-gl/gl v0.0.0-20180407155706-68e253793080/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= 6 | github.com/go-gl/glfw v0.0.0-20180426074136-46a8d530c326/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 7 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 8 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 9 | github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 10 | github.com/llgcode/draw2d v0.0.0-20210904075650-80aa0a2a901d h1:4/ycg+VrwjGurTqiHv2xM/h6Qm81qSra+KbfT4FH2FA= 11 | github.com/llgcode/draw2d v0.0.0-20210904075650-80aa0a2a901d/go.mod h1:mVa0dA29Db2S4LVqDYLlsePDzRJLDfdhVZiI15uY0FA= 12 | github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb h1:61ndUreYSlWFeCY44JxDDkngVoI7/1MVhEl98Nm0KOk= 13 | github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY= 14 | github.com/peterhellberg/gfx v0.0.0-20210905153911-4a6ef1535e02 h1:2wZ5I/apBKogpjp3n8rpK12i1kkizoNdpmnY+OjvsXI= 15 | github.com/peterhellberg/gfx v0.0.0-20210905153911-4a6ef1535e02/go.mod h1:ipo3f7y1+RHNR32fj+4TETou6OF4VXvJBhb9rwncKuw= 16 | github.com/pzsz/voronoi v0.0.0-20130609164533-4314be88c79f h1:YeUeWAPTrycaikVXiVoAu1dxIR4+tSsn9IbTkNfRsss= 17 | github.com/pzsz/voronoi v0.0.0-20130609164533-4314be88c79f/go.mod h1:T6EcHUIQ7r0Xr1jjOJ7hmpUaqVyqb9WimeNzS9nz4Zo= 18 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 19 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= 20 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 21 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 22 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | package karta 2 | 3 | import ( 4 | "image" 5 | "math/rand" 6 | 7 | "github.com/llgcode/draw2d/draw2dimg" 8 | "github.com/peterhellberg/karta/diagram" 9 | "github.com/peterhellberg/karta/palette" 10 | ) 11 | 12 | // Generate generates a map 13 | func (k *Karta) Generate() error { 14 | k.generateTopography() 15 | k.drawImage() 16 | 17 | return nil 18 | } 19 | 20 | func (k *Karta) generateTopography() { 21 | u := k.Unit 22 | 23 | for i, cell := range k.Diagram.Cells { 24 | d := diagram.Distance(cell.Site, k.Diagram.Center) 25 | n := k.Noise.Noise2D( 26 | cell.Site.X/(float64(k.Width)/4), 27 | cell.Site.Y/(float64(k.Height)/4)) 28 | 29 | e := elevation(k, d, n) 30 | c := &Cell{ 31 | Index: i, 32 | CenterDistance: d, 33 | NoiseLevel: n, 34 | Elevation: e, 35 | Land: e >= 0, 36 | Site: cell.Site, 37 | } 38 | 39 | if c.Land { 40 | // Make sure edges of the map are water 41 | if (cell.Site.X < u*0.5 || cell.Site.X > float64(k.Width)-u*0.5) || 42 | (cell.Site.Y < u/1.5 || cell.Site.Y > float64(k.Height)-u/1.5) || 43 | (cell.Site.Y < u/3 || cell.Site.Y > float64(k.Height)-u/3) { 44 | c.Land = false 45 | c.Elevation = -1.5 * c.NoiseLevel 46 | } 47 | } 48 | 49 | if c.Land { 50 | if d < u*3.3 { 51 | c.Elevation += 0.3 52 | } 53 | 54 | if d < u*2.3 { 55 | c.Elevation += 0.6 56 | } 57 | 58 | if d < u*1.3 { 59 | c.Elevation += 0.9 60 | } 61 | 62 | // Add some lakes 63 | if c.NoiseLevel < -0.3 { 64 | c.Elevation = c.NoiseLevel 65 | } 66 | 67 | switch { 68 | case c.Elevation > 7: 69 | c.FillColor = palette.Green7 70 | c.StrokeColor = palette.Green8 71 | case c.Elevation > 6.1: 72 | c.FillColor = palette.Green6 73 | c.StrokeColor = palette.Green7 74 | case c.Elevation > 4.8: 75 | c.FillColor = palette.Green5 76 | c.StrokeColor = palette.Green6 77 | case c.Elevation > 3.1: 78 | c.FillColor = palette.Green4 79 | c.StrokeColor = palette.Green5 80 | case c.Elevation > 2.4: 81 | c.FillColor = palette.Green3 82 | c.StrokeColor = palette.Green4 83 | case c.Elevation > 1.5: 84 | c.FillColor = palette.Green2 85 | c.StrokeColor = palette.Green3 86 | case c.Elevation < -0.6: 87 | c.FillColor = palette.Blue1 88 | c.StrokeColor = palette.Blue2 89 | case c.Elevation < -0.4: 90 | c.FillColor = palette.Blue0 91 | c.StrokeColor = palette.Blue1 92 | case c.Elevation < 0: 93 | c.FillColor = palette.Yellow1 94 | c.StrokeColor = palette.Yellow2 95 | default: 96 | c.FillColor = palette.Green1 97 | c.StrokeColor = palette.Green2 98 | } 99 | } else { 100 | switch { 101 | case c.Elevation < -6: 102 | c.FillColor = palette.Blue7 103 | c.StrokeColor = palette.Blue7 104 | case c.Elevation < -5: 105 | c.FillColor = palette.Blue6 106 | c.StrokeColor = palette.Blue7 107 | case c.Elevation < -4: 108 | c.FillColor = palette.Blue5 109 | c.StrokeColor = palette.Blue6 110 | case c.Elevation < -3: 111 | c.FillColor = palette.Blue4 112 | c.StrokeColor = palette.Blue5 113 | case c.Elevation < -2: 114 | c.FillColor = palette.Blue3 115 | c.StrokeColor = palette.Blue4 116 | case c.Elevation < -1: 117 | c.FillColor = palette.Blue2 118 | c.StrokeColor = palette.Blue3 119 | default: 120 | c.FillColor = palette.Blue1 121 | c.StrokeColor = palette.Blue2 122 | } 123 | } 124 | 125 | k.Cells = append(k.Cells, c) 126 | } 127 | } 128 | 129 | func (k *Karta) drawImage() { 130 | img := image.NewRGBA(image.Rect(0, 0, k.Width, k.Height)) 131 | 132 | l := draw2dimg.NewGraphicContext(img) 133 | 134 | l.SetLineWidth(1.2) 135 | 136 | // Iterate over cells 137 | for i, cell := range k.Diagram.Cells { 138 | l.SetFillColor(k.Cells[i].FillColor) 139 | l.SetStrokeColor(k.Cells[i].StrokeColor) 140 | 141 | for _, hedge := range cell.Halfedges { 142 | a := hedge.GetStartpoint() 143 | b := hedge.GetEndpoint() 144 | 145 | l.MoveTo(a.X, a.Y) 146 | l.LineTo(b.X, b.Y) 147 | } 148 | 149 | l.FillStroke() 150 | } 151 | 152 | l.Close() 153 | 154 | k.Image = img 155 | } 156 | 157 | func elevation(k *Karta, d, n float64) (e float64) { 158 | e = 1.8 + n 159 | 160 | e -= (d / k.Unit) / 3.75 161 | 162 | if e > 0 { 163 | e += 1 + float64(rand.Int63n(2)) 164 | 165 | if e > 1.5 && rand.Intn(3) < 2 { 166 | e += 0.5 + rand.Float64() 167 | } 168 | 169 | if e > 3 { 170 | e += 1.5 + rand.Float64() 171 | } 172 | } 173 | 174 | return 175 | } 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Karta 2 | 3 | Just playing around with [Voronoi diagrams](http://en.wikipedia.org/wiki/Voronoi_diagram). 4 | 5 | [](https://godoc.org/github.com/peterhellberg/karta) 6 | 7 |  8 | 9 | The goal is to create something similar to what is described in the article 10 | [Polygonal Map Generation for Games](http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/) 11 | 12 | ## Installation 13 | 14 | ```bash 15 | # Basic command line tool 16 | go install github.com/peterhellberg/karta/cmd/karta@latest 17 | 18 | # A web server generating maps based on query parameters 19 | go install github.com/peterhellberg/karta/cmd/karta-server@latest 20 | ``` 21 | 22 | ## Development 23 | 24 | I spend most of my time in [iTerm 2](http://iterm2.com/) and this project is no different. 25 | 26 | This is how I preview the map in a split pane: 27 | 28 | ```bash 29 | rerun -p "**/*.go" -c -x -b -- \ 30 | "go run cmd/karta/main.go -width 55 -height 55 && aimg -w 55 karta.png" 31 | ``` 32 | 33 |  34 | 35 | ## Usage 36 | 37 | The project includes two binaries `karta` and `karta-server`, the latter serving both PNG and JPEG. 38 | 39 | ### Command line arguments 40 | 41 | ``` 42 | Usage of karta: 43 | -count=2048: The number of sites in the voronoi diagram 44 | -height=512: The height of the map in pixels 45 | -iterations=1: The number of iterations of Lloyd's algorithm to run (max 16) 46 | -output="karta.png": Output filename 47 | -seed=3: The starting seed for the map generator 48 | -show=false: Show generated map using Preview.app 49 | -width=512: The width of the map in pixels 50 | ``` 51 | 52 | ### Query string parameters 53 | 54 | - **s** - size 55 | - **w** - width 56 | - **h** - height 57 | - **c** - count 58 | - **i** - iterations 59 | 60 | ## Progress 61 | 62 | First i just plotted out random dots: 63 | 64 |  65 | 66 | Then drew a voroni diagram: 67 | 68 |  69 | 70 | Found the centroids: 71 | 72 |  73 | 74 | Ran the diagram through [Lloyd's algorithm](http://en.wikipedia.org/wiki/Lloyd%27s_algorithm): 75 | 76 |  77 | 78 | A few iterations later: 79 | 80 |  81 | 82 | Animating 0-16 iterations: 83 | 84 |  85 | 86 | Started coloring the map according to the distance from the center: 87 | 88 |  89 | 90 | Added some randomness: 91 | 92 |  93 | 94 | Added a different types of elevation: 95 | 96 |  97 | 98 | Removed the centroid markers: 99 | 100 |  101 | 102 | Started working on using [Simplex noise](http://en.wikipedia.org/wiki/Simplex_noise) for 103 | island shape and elevation: 104 | 105 |  106 | 107 | Elevation based on noise + distance from center of map: 108 | 109 |  110 | 111 | Tweaked noise and yellow beaches: 112 | 113 |  114 | 115 | ## License 116 | 117 | **The MIT License (MIT)** 118 | 119 | Copyright (C) 2014 [Peter Hellberg](https://c7.se/) 120 | 121 | > Permission is hereby granted, free of charge, to any person obtaining 122 | > a copy of this software and associated documentation files (the "Software"), 123 | > to deal in the Software without restriction, including without limitation 124 | > the rights to use, copy, modify, merge, publish, distribute, sublicense, 125 | > and/or sell copies of the Software, and to permit persons to whom the 126 | > Software is furnished to do so, subject to the following conditions: 127 | > 128 | > The above copyright notice and this permission notice shall be included 129 | > in all copies or substantial portions of the Software. 130 | > 131 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 132 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 133 | > OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 134 | > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 135 | > DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 136 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 137 | > OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 138 | -------------------------------------------------------------------------------- /cmd/karta-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "html/template" 7 | "image/jpeg" 8 | "image/png" 9 | "log" 10 | "math" 11 | "math/rand" 12 | "net/http" 13 | "os" 14 | "strconv" 15 | 16 | "github.com/peterhellberg/karta" 17 | ) 18 | 19 | const defaultPort = "8100" 20 | 21 | func main() { 22 | // Parse the command line flags 23 | flag.Parse() 24 | 25 | // Setup the logger 26 | l := log.New(os.Stdout, "", 0) 27 | 28 | // Get the port to bind the server to 29 | p := getEnv("PORT", defaultPort) 30 | 31 | // Start the server 32 | err := ListenAndServe(l, p, func(ctx *context) { 33 | http.Handle("/", baseHandler(ctx, kartaIndexHandler)) 34 | http.Handle("/map.jpg", baseHandler(ctx, kartaJPEGHandler)) 35 | http.Handle("/map.png", baseHandler(ctx, kartaPNGHandler)) 36 | }) 37 | 38 | if err != nil { 39 | panic(err) 40 | } 41 | } 42 | 43 | func getEnv(key, fallback string) string { 44 | v := os.Getenv(key) 45 | if v != "" { 46 | return v 47 | } 48 | return fallback 49 | } 50 | 51 | func getInt(r *http.Request, s string, def, min, max int) int { 52 | val, err := strconv.Atoi(r.URL.Query().Get(s)) 53 | if err != nil || val < min || val > max { 54 | return def 55 | } 56 | 57 | return val 58 | } 59 | 60 | var index = template.Must(template.New("index").Parse(` 61 | 62 |
63 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |