Setup for being a library

This commit is contained in:
2026-02-26 22:18:08 +02:00
parent be6a0f0790
commit 7d7bf6ae05
3 changed files with 7 additions and 3 deletions

222
examples/web/main.go Normal file
View File

@@ -0,0 +1,222 @@
package main
import (
"embed"
"fmt"
"html/template"
"image/color"
"image/png"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
maze "tea.chunkbyte.com/kato/go-maze/maze"
)
const (
defaultWidth = 41
defaultHeight = 41
defaultScale = 8
)
type mazeParams struct {
Width int
Height int
Scale int
Seed uint64
Highlight bool
WallR uint8
WallG uint8
WallB uint8
PathR uint8
PathG uint8
PathB uint8
SolveR uint8
SolveG uint8
SolveB uint8
}
//go:embed templates/index.html
var templatesFS embed.FS
var pageTmpl = template.Must(template.ParseFS(templatesFS, "templates/index.html"))
type pageData struct {
mazeParams
MazeURL string
Query string
}
func main() {
http.HandleFunc("/", handleIndex)
http.HandleFunc("/maze.png", handleMazePNG)
addr := ":8080"
log.Printf("listening on http://localhost%s", addr)
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatal(err)
}
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
params := parseParams(r.URL.Query())
query := params.encode().Encode()
data := pageData{
mazeParams: params,
MazeURL: "/maze.png?" + query,
Query: query,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := pageTmpl.Execute(w, data); err != nil {
http.Error(w, "template rendering failed", http.StatusInternalServerError)
}
}
func handleMazePNG(w http.ResponseWriter, r *http.Request) {
params := parseParams(r.URL.Query())
grid, err := maze.GenerateWithSeed(params.Width, params.Height, params.Seed)
if err != nil {
http.Error(w, fmt.Sprintf("generate maze: %v", err), http.StatusBadRequest)
return
}
options := maze.DefaultRenderOptions()
options.Scale = params.Scale
options.HighlightPath = params.Highlight
options.WallColor = color.RGBA{R: params.WallR, G: params.WallG, B: params.WallB, A: 255}
options.PathColor = color.RGBA{R: params.PathR, G: params.PathG, B: params.PathB, A: 255}
options.SolutionColor = color.RGBA{R: params.SolveR, G: params.SolveG, B: params.SolveB, A: 255}
img, err := maze.RenderImage(grid, options)
if err != nil {
http.Error(w, fmt.Sprintf("render maze: %v", err), http.StatusBadRequest)
return
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "image/png")
if err := png.Encode(w, img); err != nil {
http.Error(w, "encoding png failed", http.StatusInternalServerError)
}
}
func parseParams(q url.Values) mazeParams {
defaults := maze.DefaultRenderOptions()
defaultSeed := uint64(time.Now().UnixNano())
out := mazeParams{
Width: parseOddInt(q.Get("w"), defaultWidth, 3, 151),
Height: parseOddInt(q.Get("h"), defaultHeight, 3, 151),
Scale: parseInt(q.Get("scale"), defaultScale, 2, 24),
Seed: parseUint64(q.Get("seed"), defaultSeed),
Highlight: parseBool(q.Get("solve")),
WallR: defaults.WallColor.R,
WallG: defaults.WallColor.G,
WallB: defaults.WallColor.B,
PathR: defaults.PathColor.R,
PathG: defaults.PathColor.G,
PathB: defaults.PathColor.B,
SolveR: defaults.SolutionColor.R,
SolveG: defaults.SolutionColor.G,
SolveB: defaults.SolutionColor.B,
}
out.WallR = parseUint8(q.Get("wall_r"), out.WallR)
out.WallG = parseUint8(q.Get("wall_g"), out.WallG)
out.WallB = parseUint8(q.Get("wall_b"), out.WallB)
out.PathR = parseUint8(q.Get("path_r"), out.PathR)
out.PathG = parseUint8(q.Get("path_g"), out.PathG)
out.PathB = parseUint8(q.Get("path_b"), out.PathB)
out.SolveR = parseUint8(q.Get("solve_r"), out.SolveR)
out.SolveG = parseUint8(q.Get("solve_g"), out.SolveG)
out.SolveB = parseUint8(q.Get("solve_b"), out.SolveB)
return out
}
func (p mazeParams) encode() url.Values {
v := url.Values{}
v.Set("w", strconv.Itoa(p.Width))
v.Set("h", strconv.Itoa(p.Height))
v.Set("scale", strconv.Itoa(p.Scale))
v.Set("seed", strconv.FormatUint(p.Seed, 10))
if p.Highlight {
v.Set("solve", "1")
}
v.Set("wall_r", strconv.Itoa(int(p.WallR)))
v.Set("wall_g", strconv.Itoa(int(p.WallG)))
v.Set("wall_b", strconv.Itoa(int(p.WallB)))
v.Set("path_r", strconv.Itoa(int(p.PathR)))
v.Set("path_g", strconv.Itoa(int(p.PathG)))
v.Set("path_b", strconv.Itoa(int(p.PathB)))
v.Set("solve_r", strconv.Itoa(int(p.SolveR)))
v.Set("solve_g", strconv.Itoa(int(p.SolveG)))
v.Set("solve_b", strconv.Itoa(int(p.SolveB)))
return v
}
func parseInt(raw string, fallback, min, max int) int {
n, err := strconv.Atoi(raw)
if err != nil {
return fallback
}
if n < min {
return min
}
if n > max {
return max
}
return n
}
func parseOddInt(raw string, fallback, min, max int) int {
n := parseInt(raw, fallback, min, max)
if n%2 == 0 {
if n == max {
n--
} else {
n++
}
}
return n
}
func parseUint8(raw string, fallback uint8) uint8 {
n, err := strconv.Atoi(raw)
if err != nil {
return fallback
}
if n < 0 {
return 0
}
if n > 255 {
return 255
}
return uint8(n)
}
func parseUint64(raw string, fallback uint64) uint64 {
n, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
return fallback
}
return n
}
func parseBool(raw string) bool {
raw = strings.TrimSpace(strings.ToLower(raw))
switch raw {
case "1", "true", "t", "yes", "y", "on":
return true
default:
return false
}
}

View File

@@ -0,0 +1,123 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maze Generator</title>
<style>
body { font-family: "Segoe UI", Tahoma, sans-serif; margin: 24px; background: #f4f6f8; color: #222; }
.layout { display: grid; grid-template-columns: 320px 1fr; gap: 20px; align-items: start; }
.panel { background: #fff; border: 1px solid #d8dee5; border-radius: 10px; padding: 16px; }
label { display: block; margin: 10px 0 6px; font-size: 14px; }
input[type="range"] { width: 100%; }
.row { display: grid; grid-template-columns: 1fr 60px; align-items: center; gap: 8px; }
.rgb { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
.swatch { width: 100%; height: 34px; border-radius: 8px; border: 1px solid #ccc; }
.submit-btn { margin-top: 14px; width: 100%; padding: 10px; border-radius: 8px; border: 0; background: #1b5fc6; color: #fff; font-weight: 600; }
#shuffle_seed { margin: 0; width: 100%; padding: 8px; border-radius: 8px; border: 1px solid #c8d0da; background: #f7f9fb; color: #222; font-weight: 600; }
img { max-width: 100%; border: 1px solid #d8dee5; border-radius: 10px; background: #fff; }
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<h1>Maze Generator</h1>
<div class="layout">
<form class="panel" method="get" action="/">
<h3>Maze</h3>
<label>Width</label>
<div class="row"><input id="w" type="range" min="3" max="151" step="2" name="w" value="{{.Width}}"><output id="w_out">{{.Width}}</output></div>
<label>Height</label>
<div class="row"><input id="h" type="range" min="3" max="151" step="2" name="h" value="{{.Height}}"><output id="h_out">{{.Height}}</output></div>
<label>Scale (px per cell)</label>
<div class="row"><input id="scale" type="range" min="2" max="24" step="1" name="scale" value="{{.Scale}}"><output id="scale_out">{{.Scale}}</output></div>
<label>Seed</label>
<div class="row"><input id="seed" type="number" min="0" max="18446744073709551615" step="1" name="seed" value="{{.Seed}}"><button id="shuffle_seed" type="button">Random</button></div>
<label><input type="checkbox" name="solve" value="1" {{if .Highlight}}checked{{end}}> Highlight solution path</label>
<h3>Wall Color</h3>
<div class="rgb">
<div><label>R</label><input type="range" min="0" max="255" name="wall_r" value="{{.WallR}}"></div>
<div><label>G</label><input type="range" min="0" max="255" name="wall_g" value="{{.WallG}}"></div>
<div><label>B</label><input type="range" min="0" max="255" name="wall_b" value="{{.WallB}}"></div>
</div>
<div class="swatch" style="background: rgb({{.WallR}}, {{.WallG}}, {{.WallB}});"></div>
<h3>Path Color</h3>
<div class="rgb">
<div><label>R</label><input type="range" min="0" max="255" name="path_r" value="{{.PathR}}"></div>
<div><label>G</label><input type="range" min="0" max="255" name="path_g" value="{{.PathG}}"></div>
<div><label>B</label><input type="range" min="0" max="255" name="path_b" value="{{.PathB}}"></div>
</div>
<div class="swatch" style="background: rgb({{.PathR}}, {{.PathG}}, {{.PathB}});"></div>
<h3>Solution Color</h3>
<div class="rgb">
<div><label>R</label><input type="range" min="0" max="255" name="solve_r" value="{{.SolveR}}"></div>
<div><label>G</label><input type="range" min="0" max="255" name="solve_g" value="{{.SolveG}}"></div>
<div><label>B</label><input type="range" min="0" max="255" name="solve_b" value="{{.SolveB}}"></div>
</div>
<div class="swatch" style="background: rgb({{.SolveR}}, {{.SolveG}}, {{.SolveB}});"></div>
<button class="submit-btn" type="submit">Generate</button>
</form>
<div class="panel">
<img id="maze_img" src="{{.MazeURL}}" alt="Generated maze">
<p><code id="maze_get">GET /maze.png?{{.Query}}</code></p>
</div>
</div>
<script>
var form = document.querySelector("form");
var img = document.getElementById("maze_img");
var getCode = document.getElementById("maze_get");
var seedInput = document.getElementById("seed");
var randomSeedButton = document.getElementById("shuffle_seed");
[["w","w_out"],["h","h_out"],["scale","scale_out"]].forEach(function(pair) {
var input = document.getElementById(pair[0]);
var out = document.getElementById(pair[1]);
if (!input || !out) return;
input.addEventListener("input", function() { out.textContent = input.value; });
});
function buildQuery() {
return new URLSearchParams(new FormData(form)).toString();
}
function refreshMaze() {
var query = buildQuery();
img.src = "/maze.png?" + query;
getCode.textContent = "GET /maze.png?" + query;
history.replaceState(null, "", "/?" + query);
}
form.querySelectorAll("input").forEach(function(el) {
el.addEventListener("input", refreshMaze);
el.addEventListener("change", refreshMaze);
});
function randomUint64String() {
if (typeof BigInt !== "function") {
return String(Date.now());
}
if (window.crypto && window.crypto.getRandomValues) {
var values = new Uint32Array(2);
window.crypto.getRandomValues(values);
return ((BigInt(values[0]) << 32n) | BigInt(values[1])).toString();
}
var hi = Math.floor(Math.random() * 4294967296);
var lo = Math.floor(Math.random() * 4294967296);
return (BigInt(hi) << 32n | BigInt(lo)).toString();
}
randomSeedButton.addEventListener("click", function() {
seedInput.value = randomUint64String();
refreshMaze();
});
</script>
</body>
</html>