package main import ( "image/color" "math" "strconv" "strings" "unsafe" rg "github.com/gen2brain/raylib-go/raygui" rl "github.com/gen2brain/raylib-go/raylib" ) func canvasMouse() rl.Vector2 { return rl.GetMousePosition() } // Keeps GPU texture uploads in sync with mapImage edits. Full uploads are used after // path rebuilds; edits use bounding boxes via UpdateTextureRec. type texSync struct { needUpload bool uploadFull bool partialOk bool x0, y0 int x1, y1 int rectBuf []color.RGBA } func absInt(v int) int { if v < 0 { return -v } return v } func (t *texSync) markFull() { t.needUpload = true t.uploadFull = true } func (t *texSync) markRegion(minX, minY, maxX, maxY, gridW, gridH int) { if minX > maxX { minX, maxX = maxX, minX } if minY > maxY { minY, maxY = maxY, minY } if minX < 0 { minX = 0 } if minY < 0 { minY = 0 } if maxX >= gridW { maxX = gridW - 1 } if maxY >= gridH { maxY = gridH - 1 } if minX > maxX || minY > maxY { return } t.needUpload = true if t.uploadFull { return } if !t.partialOk { t.partialOk = true t.x0, t.y0 = minX, minY t.x1, t.y1 = maxX, maxY return } if minX < t.x0 { t.x0 = minX } if minY < t.y0 { t.y0 = minY } if maxX > t.x1 { t.x1 = maxX } if maxY > t.y1 { t.y1 = maxY } } func (t *texSync) flush(img *rl.Image, tex *rl.Texture2D, gridW, gridH int) { if !t.needUpload { return } if t.uploadFull { ptr := (*color.RGBA)(unsafe.Pointer(img.Data)) pixels := unsafe.Slice(ptr, gridW*gridH) rl.UpdateTexture(*tex, pixels) t.uploadFull = false t.partialOk = false } else if t.partialOk { w := t.x1 - t.x0 + 1 h := t.y1 - t.y0 + 1 n := w * h if cap(t.rectBuf) < n { t.rectBuf = make([]color.RGBA, n) } else { t.rectBuf = t.rectBuf[:n] } ptr := (*color.RGBA)(unsafe.Pointer(img.Data)) full := unsafe.Slice(ptr, gridW*gridH) for row := 0; row < h; row++ { src := (t.y0+row)*gridW + t.x0 copy(t.rectBuf[row*w:], full[src:src+w]) } rec := rl.NewRectangle(float32(t.x0), float32(t.y0), float32(w), float32(h)) rl.UpdateTextureRec(*tex, rec, t.rectBuf) t.partialOk = false } t.needUpload = false } // paintWallLine updates both mapImage and the A* grid for every grid cell crossed by the segment. func paintWallLine(a *AStar, mapImage *rl.Image, x0, y0, x1, y1, gridW, gridH int, col color.RGBA, tex *texSync) { minX := x0 if x1 < minX { minX = x1 } maxX := x0 if x1 > maxX { maxX = x1 } minY := y0 if y1 < minY { minY = y1 } maxY := y0 if y1 > maxY { maxY = y1 } tex.markRegion(minX, minY, maxX, maxY, gridW, gridH) dx := absInt(x1 - x0) dy := -absInt(y1 - y0) sx := 1 sy := 1 if x0 > x1 { sx = -1 } if y0 > y1 { sy = -1 } err := dx + dy for { if x0 >= 0 && x0 < gridW && y0 >= 0 && y0 < gridH { switch a.GetGridType(x0, y0) { case 2, 3: // start / end default: rl.ImageDrawPixel(mapImage, int32(x0), int32(y0), col) a.SetGridType(x0, y0, 1) } } if x0 == x1 && y0 == y1 { break } e2 := 2 * err if e2 >= dy { err += dy x0 += sx } if e2 <= dx { err += dx y0 += sy } } } // paintEraseLine clears wall cells along the segment (same grid traversal as paintWallLine). func paintEraseLine(a *AStar, mapImage *rl.Image, x0, y0, x1, y1, gridW, gridH int, empty color.RGBA, tex *texSync) { minX := x0 if x1 < minX { minX = x1 } maxX := x0 if x1 > maxX { maxX = x1 } minY := y0 if y1 < minY { minY = y1 } maxY := y0 if y1 > maxY { maxY = y1 } tex.markRegion(minX, minY, maxX, maxY, gridW, gridH) dx := absInt(x1 - x0) dy := -absInt(y1 - y0) sx := 1 sy := 1 if x0 > x1 { sx = -1 } if y0 > y1 { sy = -1 } err := dx + dy for { if x0 >= 0 && x0 < gridW && y0 >= 0 && y0 < gridH { switch a.GetGridType(x0, y0) { case 2, 3: // start / end default: rl.ImageDrawPixel(mapImage, int32(x0), int32(y0), empty) a.SetGridType(x0, y0, 0) } } if x0 == x1 && y0 == y1 { break } e2 := 2 * err if e2 >= dy { err += dy x0 += sx } if e2 <= dx { err += dx y0 += sy } } } func drawInfiniteGridLines(camera rl.Camera2D, canvasW float32, canvasH float32, cellSize float32, width int, height int) { // 1. Level of Detail (LOD) Check // If we are zoomed out too far, don't draw the gridlines at all if camera.Zoom < 0.4 { return } lineThickness := 1.0 / camera.Zoom lineColor := rl.NewColor(200, 200, 200, 255) // Light gray so it's not distracting // 2. Draw Vertical Lines // Start at the left edge of the screen, draw a line from top to bottom, // step right by cellSize, repeat until off the right edge. for x := float32(0.0); x <= cellSize*float32(width); x += cellSize { rl.DrawLineEx( rl.NewVector2(x, 0), rl.NewVector2(x, cellSize*float32(height)), lineThickness, lineColor, ) } // 3. Draw Horizontal Lines // Start at the top edge of the screen, draw a line from left to right, // step down by cellSize, repeat until off the bottom edge. for y := float32(0.0); y <= cellSize*float32(height); y += cellSize { rl.DrawLineEx( rl.NewVector2(0, y), rl.NewVector2(cellSize*float32(width), y), lineThickness, lineColor, ) } } func main() { rl.SetConfigFlags(rl.FlagWindowResizable) rl.InitWindow(int32(1200), int32(800), "A* Visualizer") defer rl.CloseWindow() scale := rl.GetWindowScaleDPI().X + 0.25 if scale == 0 { scale = 2.0 // Fallback value } rg.SetStyle(rg.DEFAULT, rg.TEXT_SIZE, rg.PropertyValue(int64(10*scale))) rl.SetTargetFPS(60) // Keep offset safely at 0,0. We will never modify this during runtime. camera := rl.Camera2D{ Offset: rl.NewVector2(0, 0), Target: rl.NewVector2(0, 0), Rotation: 0.0, Zoom: 1.0, } width := 10 height := 10 widthInputValue := "10" heightInputValue := "10" editModeWidth := false editModeHeight := false generateGridError := false posError := false toolOptions := []string{"Wall", "Start", "End", "Erase"} toolOptionsText := strings.Join(toolOptions, ";") activeTool := int32(0) toolDropdownOpen := false var tex texSync // GPU uploads: partial rects while painting; full after path resets heuristicOptions := []string{"Manhattan", "Euclidean", "Chebyshev", "Squared Euclidean"} heuristicOptionsText := strings.Join(heuristicOptions, ";") activeHeuristic := int32(0) heuristicDropdownOpen := false mazeOptions := []string{"Recursive Divison", "Iterative DFS", "Cellular Automata"} mazeOptionsText := strings.Join(mazeOptions, ";") activeMaze := int32(0) mazeDropdownOpen := false cellSize := float32(25) lastMousePos := rl.NewVector2(-1, -1) startPos := rl.NewVector2(-1, -1) endPos := rl.NewVector2(-1, -1) mapImage := rl.GenImageColor(width, height, rl.NewColor(240, 240, 240, 255)) mapTexture := rl.LoadTextureFromImage(mapImage) defer rl.UnloadTexture(mapTexture) defer rl.UnloadImage(mapImage) updateChan := make(chan int, 100000) autoCompute := false speed := 1000 speedInputValue := "100" editModeSpeed := false astar := AStar{} astar.Init(width, height) // Above the channel loop lastEvaluatedNode := -1 // Track the "tip of the spear" for !rl.WindowShouldClose() { screenWidth := float32(rl.GetScreenWidth()) screenHeight := float32(rl.GetScreenHeight()) sidebarWidth := float32(200) * scale if sidebarWidth > screenWidth { sidebarWidth = screenWidth } canvasWidth := screenWidth - sidebarWidth wheel := rl.GetMouseWheelMove() // --- THE BULLETPROOF ZOOM MATH --- if wheel != 0 { mousePos := canvasMouse() // 1. Where is the mouse in the world BEFORE zooming? worldPosBefore := rl.GetScreenToWorld2D(mousePos, camera) // 2. Apply proportional zoom so wheel steps get smaller as the view zooms out. camera.Zoom *= float32(math.Pow(1.1, float64(wheel))) if camera.Zoom < 0.01 && width > 2000 && height > 2000 { camera.Zoom = 0.01 } // 3. Where does that same screen pixel point to AFTER zooming? worldPosAfter := rl.GetScreenToWorld2D(mousePos, camera) // 4. Shift the camera target to compensate for the difference camera.Target.X += (worldPosBefore.X - worldPosAfter.X) camera.Target.Y += (worldPosBefore.Y - worldPosAfter.Y) } // Move the camera with the right mouse button if rl.IsMouseButtonDown(rl.MouseRightButton) { delta := rl.GetMouseDelta() camera.Target.X -= delta.X / camera.Zoom camera.Target.Y -= delta.Y / camera.Zoom } // Paint logic if rl.IsMouseButtonDown(rl.MouseLeftButton) { mousePos := rl.GetMousePosition() if int(mousePos.X) < int(canvasWidth) { worldPos := rl.GetScreenToWorld2D(rl.GetMousePosition(), camera) x := int(worldPos.X / cellSize) y := int(worldPos.Y / cellSize) if x >= 0 && x < width && y >= 0 && y < height { // mapImage is one pixel per cell; drawing uses cell indices, not raw world coords. switch activeTool { case 0: // Wall — Bresenham line into image + simulator grid (skips start/end cells) wallCol := color.RGBA{A: 255} if lastMousePos.X != -1 && lastMousePos.Y != -1 { prevX := int(lastMousePos.X / cellSize) prevY := int(lastMousePos.Y / cellSize) paintWallLine(&astar, mapImage, prevX, prevY, x, y, width, height, wallCol, &tex) } else { paintWallLine(&astar, mapImage, x, y, x, y, width, height, wallCol, &tex) } case 1: // Start — must run on first paint frame (lastMousePos may be -1 after release) if int(startPos.X) != x || int(startPos.Y) != y { if startPos.X >= 0 && startPos.Y >= 0 { ox, oy := int(startPos.X), int(startPos.Y) rl.ImageDrawPixel(mapImage, int32(startPos.X), int32(startPos.Y), rl.NewColor(240, 240, 240, 255)) astar.SetGridType(ox, oy, 0) tex.markRegion(ox, oy, ox, oy, width, height) } rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 255, 0, 255)) tex.markRegion(x, y, x, y, width, height) startPos = rl.NewVector2(float32(x), float32(y)) astar.SetGridType(x, y, 2) } case 2: // End if int(endPos.X) != x || int(endPos.Y) != y { if endPos.X >= 0 && endPos.Y >= 0 { ox, oy := int(endPos.X), int(endPos.Y) rl.ImageDrawPixel(mapImage, int32(endPos.X), int32(endPos.Y), rl.NewColor(240, 240, 240, 255)) astar.SetGridType(ox, oy, 0) tex.markRegion(ox, oy, ox, oy, width, height) } rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(255, 0, 0, 255)) tex.markRegion(x, y, x, y, width, height) endPos = rl.NewVector2(float32(x), float32(y)) astar.SetGridType(x, y, 3) } case 3: // Erase — same cell indices as walls; ImageDrawLine used world coords before (wrong space). emptyCol := color.RGBA{R: 240, G: 240, B: 240, A: 255} if lastMousePos.X != -1 && lastMousePos.Y != -1 { prevX := int(lastMousePos.X / cellSize) prevY := int(lastMousePos.Y / cellSize) paintEraseLine(&astar, mapImage, prevX, prevY, x, y, width, height, emptyCol, &tex) } else { paintEraseLine(&astar, mapImage, x, y, x, y, width, height, emptyCol, &tex) } } lastMousePos = worldPos } } } if rl.IsMouseButtonReleased(rl.MouseLeftButton) { if autoCompute && lastMousePos.X != -1 && lastMousePos.Y != -1 && int(startPos.X) >= 0 && int(startPos.X) < width && int(startPos.Y) >= 0 && int(startPos.Y) < height && int(endPos.X) >= 0 && int(endPos.X) < width && int(endPos.Y) >= 0 && int(endPos.Y) < height { astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating gridTypes := astar.GetGridTypes() for i, gridType := range gridTypes { // reset the map image switch gridType { case 0: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255)) case 1: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255)) case 2: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255)) case 3: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255)) } } tex.markFull() astar.SetHeuristic(activeHeuristic) path := astar.CalculatePath(int(startPos.X), int(startPos.Y), int(endPos.X), int(endPos.Y)) closedSet := astar.GetClosedSet() for i, closed := range closedSet { if closed { x := i % width y := i / width if x != int(startPos.X) || y != int(startPos.Y) { rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 0, 255, 255)) } } } for _, p := range path { if p[0] != int(startPos.X) || p[1] != int(startPos.Y) { // we want to keep the start position green rl.ImageDrawPixel(mapImage, int32(p[0]), int32(p[1]), rl.NewColor(255, 255, 0, 255)) } } tex.markFull() } lastMousePos = rl.NewVector2(-1, -1) } updatesProcessed := 0 DrainLoop: // Use a label so we can break out of the infinite 'for' loop for { select { case nodeIndex := <-updateChan: x := nodeIndex % width y := nodeIndex / width if x != int(startPos.X) || y != int(startPos.Y) { // Bake the blue pixel into the image rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 0, 255, 255)) tex.markRegion(x, y, x, y, width, height) } lastEvaluatedNode = nodeIndex // Save the absolute latest node updatesProcessed++ if updatesProcessed > 50000 { break DrainLoop } default: // Channel is empty break DrainLoop } } // --- DRAWING --- rl.BeginDrawing() rl.ClearBackground(rl.White) // 1. Draw Canvas rl.BeginScissorMode(0, 0, int32(canvasWidth), int32(screenHeight)) rl.BeginMode2D(camera) tex.flush(mapImage, &mapTexture, width, height) rl.DrawTextureEx( mapTexture, rl.NewVector2(0, 0), // Position 0.0, // Rotation cellSize, // Scale factor rl.White, // Tint (White means no tint) ) // 2. Trace parent chain and draw a yellow polyline (cell centers) each frame. if lastEvaluatedNode != -1 { startIndex := int(startPos.Y)*width + int(startPos.X) parents := astar.ParentsSnapshot() pathThickness := float32(16.0) if pathThickness < 1 { pathThickness = 1 } pathColor := rl.NewColor(255, 255, 0, 255) var centers []rl.Vector2 for idx := lastEvaluatedNode; idx >= 0 && idx < width*height && idx != startIndex; { cx := (float32(idx%width) + 0.5) * cellSize cy := (float32(idx/width) + 0.5) * cellSize centers = append(centers, rl.NewVector2(cx, cy)) if idx == startIndex { break } p := parents[idx] if p == parentNone { break } next := astar.ParentIndexToXYIndex(idx%width, idx/width, p) if next < 0 || next == idx { break } idx = next } for i := 0; i < len(centers)-1; i++ { rl.DrawLineEx(centers[i], centers[i+1], pathThickness, pathColor) } } drawInfiniteGridLines(camera, canvasWidth, screenHeight, cellSize, width, height) rl.EndMode2D() rl.EndScissorMode() // 2. Draw Sidebar UI sidebarX := canvasWidth rl.DrawRectangleRec(rl.NewRectangle(sidebarX, 0, sidebarWidth, screenHeight), rl.RayWhite) // Width Input if rg.TextBox(rl.NewRectangle(sidebarX+(10*scale), (10*scale), (80*scale), (20*scale)), &widthInputValue, 20, editModeWidth) { editModeWidth = !editModeWidth if editModeWidth { editModeHeight = false editModeSpeed = false } } rg.Label(rl.NewRectangle(sidebarX+(95*scale), (10*scale), (10*scale), (20*scale)), "x") // Height Input if rg.TextBox(rl.NewRectangle(sidebarX+(110*scale), (10*scale), (80*scale), (20*scale)), &heightInputValue, 20, editModeHeight) { editModeHeight = !editModeHeight if editModeHeight { editModeWidth = false editModeSpeed = false } } if generateGridError { result := rg.MessageBox(rl.NewRectangle(screenWidth/2-(100*scale), screenHeight/2-(40*scale), (200*scale), (120*scale)), "Error", "Width and/or height must be\nless than 16000", "OK") if result >= 0 { generateGridError = false } } if posError { result := rg.MessageBox(rl.NewRectangle(screenWidth/2-(100*scale), screenHeight/2-(40*scale), (200*scale), (120*scale)), "Error", "Start and end positions must be\nwithin the generated grid", "OK") if result >= 0 { posError = false } } // Generate Grid Button if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (40*scale), (180*scale), (30*scale)), "Generate Grid") { width, _ = strconv.Atoi(widthInputValue) height, _ = strconv.Atoi(heightInputValue) if width > 16000 || height > 16000 { generateGridError = true } else { rl.UnloadTexture(mapTexture) rl.UnloadImage(mapImage) mapImage = rl.GenImageColor(width, height, rl.NewColor(240, 240, 240, 255)) mapTexture = rl.LoadTextureFromImage(mapImage) astar.RebuildGrid(width, height) startPos = rl.NewVector2(-1, -1) endPos = rl.NewVector2(-1, -1) tex = texSync{rectBuf: tex.rectBuf} } } // While a dropdown list is open it overlaps controls below; those widgets are handled // earlier in the frame and would otherwise steal the release-click. GuiLock skips input // for other controls; DropdownBox still handles input when its editMode is true. if toolDropdownOpen || heuristicDropdownOpen || mazeDropdownOpen { rg.Lock() } // Generate Maze Button if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (260*scale), (180*scale), (30*scale)), "Generate Maze") { astar.GenerateMaze(activeMaze) astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating lastEvaluatedNode = -1 gridTypes := astar.GetGridTypes() for i, gridType := range gridTypes { // reset the map image switch gridType { case 0: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255)) case 1: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255)) case 2: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255)) case 3: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255)) } } tex.markFull() } // Reset Visualization Button if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(120*scale)), (180*scale), (30*scale)), "Reset Visualization") { astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating lastEvaluatedNode = -1 gridTypes := astar.GetGridTypes() for i, gridType := range gridTypes { // reset the map image switch gridType { case 0: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255)) case 1: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255)) case 2: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255)) case 3: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255)) } } tex.markFull() } // Speed Slider speedText := "" if speed == 1000 { speedText = "Uncapped" } else { speedText = strconv.Itoa(speed/10) + "%" } speedLabel := "Speed: " + speedText rg.Label(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(185*scale)), (180*scale), (30*scale)), speedLabel) newSpeed := int(rg.SliderBar(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(160*scale)), (140*scale), (30*scale)), "", "", float32(speed), 0, 1000)) if editModeSpeed { if newSpeed != speed { speed = newSpeed speedInputValue = strconv.Itoa(speed) editModeSpeed = false } } else { speed = newSpeed speedInputValue = strconv.Itoa(speed) } if rg.TextBox(rl.NewRectangle(sidebarX+(160*scale), (screenHeight-(160*scale)), (30*scale), (30*scale)), &speedInputValue, 20, editModeSpeed) { editModeSpeed = !editModeSpeed if editModeSpeed { speedInputValue = strconv.Itoa(speed) editModeWidth = false editModeHeight = false } else { v, err := strconv.Atoi(strings.TrimSpace(speedInputValue)) if err != nil { speedInputValue = strconv.Itoa(speed) } else { if v < 0 { v = 0 } else if v > 1000 { v = 1000 } speed = v speedInputValue = strconv.Itoa(speed) } } } // Calculate Path (Live) Button if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(80*scale)), (180*scale), (30*scale)), "Calculate Path (Live)") { if int(startPos.X) < 0 || int(startPos.X) >= width || int(startPos.Y) < 0 || int(startPos.Y) >= height || int(endPos.X) < 0 || int(endPos.X) >= width || int(endPos.Y) < 0 || int(endPos.Y) >= height { posError = true } else { astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating astar.SetHeuristic(activeHeuristic) lastEvaluatedNode = -1 gridTypes := astar.GetGridTypes() for i, gridType := range gridTypes { // reset the map image switch gridType { case 0: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255)) case 1: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255)) case 2: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255)) case 3: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255)) } } tex.markFull() go func() { astar.CalculatePathLive(int(startPos.X), int(startPos.Y), int(endPos.X), int(endPos.Y), updateChan, &speed) }() } } // AutoCompute autoCompute = rg.CheckBox(rl.NewRectangle(sidebarX+(160*scale), (screenHeight-(40*scale)), (30*scale), (30*scale)), "", autoCompute) // no title because it would overlap the button // Calculate Path Button if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(40*scale)), (140*scale), (30*scale)), "Calculate Path") { if int(startPos.X) < 0 || int(startPos.X) >= width || int(startPos.Y) < 0 || int(startPos.Y) >= height || int(endPos.X) < 0 || int(endPos.X) >= width || int(endPos.Y) < 0 || int(endPos.Y) >= height { posError = true } else { astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating astar.SetHeuristic(activeHeuristic) lastEvaluatedNode = -1 gridTypes := astar.GetGridTypes() for i, gridType := range gridTypes { // reset the map image switch gridType { case 0: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255)) case 1: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255)) case 2: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255)) case 3: rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255)) } } path := astar.CalculatePath(int(startPos.X), int(startPos.Y), int(endPos.X), int(endPos.Y)) closedSet := astar.GetClosedSet() for i, closed := range closedSet { if closed { x := i % width y := i / width if x != int(startPos.X) || y != int(startPos.Y) { rl.ImageDrawPixel(mapImage, int32(x), int32(y), rl.NewColor(0, 0, 255, 255)) } } } for _, p := range path { if p[0] != int(startPos.X) || p[1] != int(startPos.Y) { // we want to keep the start position green rl.ImageDrawPixel(mapImage, int32(p[0]), int32(p[1]), rl.NewColor(255, 255, 0, 255)) } } tex.markFull() } } // Status Label rg.Label(rl.NewRectangle((10*scale), (screenHeight-(30*scale)), (canvasWidth-(20*scale)), (30*scale)), "Evaluated "+strconv.Itoa(astar.GetEvaluatedCells())+" cells in "+astar.GetTimeTaken().String()) // FPS counter rg.Label(rl.NewRectangle((10*scale), (0*scale), (canvasWidth-(20*scale)), (30*scale)), "FPS: "+strconv.Itoa(int(rl.GetFPS()))) rg.Unlock() // Tool Selector (text must be "opt1;opt2;..." — raygui splits on ';' and needs 2+ items) rg.Label(rl.NewRectangle(sidebarX+(10*scale), (75*scale), (180*scale), (30*scale)), "Tool:") if rg.DropdownBox(rl.NewRectangle(sidebarX+(10*scale), (100*scale), (180*scale), (30*scale)), toolOptionsText, &activeTool, toolDropdownOpen) { toolDropdownOpen = !toolDropdownOpen } // Heuristic Selector if !toolDropdownOpen { rg.Label(rl.NewRectangle(sidebarX+(10*scale), (135*scale), (180*scale), (30*scale)), "Heuristic:") if rg.DropdownBox(rl.NewRectangle(sidebarX+(10*scale), (160*scale), (180*scale), (30*scale)), heuristicOptionsText, &activeHeuristic, heuristicDropdownOpen) { heuristicDropdownOpen = !heuristicDropdownOpen } } // Maze Selector if !toolDropdownOpen && !heuristicDropdownOpen { rg.Label(rl.NewRectangle(sidebarX+(10*scale), (195*scale), (180*scale), (30*scale)), "Maze:") if rg.DropdownBox(rl.NewRectangle(sidebarX+(10*scale), (220*scale), (180*scale), (30*scale)), mazeOptionsText, &activeMaze, mazeDropdownOpen) { mazeDropdownOpen = !mazeDropdownOpen } } rl.EndDrawing() } }