package play import ( "opslag.de/schobers/geom" ) // IsometricProjection represents an 2D area (view) that contains isometric tiles. type IsometricProjection struct { center geom.PointF32 // tile coordinate zoom float32 // factor a tile is blown up (negative is smaller, possitive is larger) zoomInverse float32 // 1/zoom; calculated tileSize geom.PointF32 // size of a single tile (maximum width & height difference of its corners) viewBounds geom.RectangleF32 // bounds of the view (screen coordinates) viewCenter geom.PointF32 // center of view; calculated tileSizeTransformed geom.PointF32 // calculated tileToViewTransformation geom.PointF32 // calculated viewToTileTransformation geom.PointF32 // calculated } // NewIsometricProjection creates a new isometric projection. By default the tile with the coordinate (0, 0) will be centered in the viewBounds. The tile size is represented with maximum width & height difference of its corners. func NewIsometricProjection(tileSize geom.PointF32, viewBounds geom.RectangleF32) *IsometricProjection { p := &IsometricProjection{zoom: 1, tileSize: tileSize, viewBounds: viewBounds} p.update() return p } func (p *IsometricProjection) update() { if p.zoom == 0 { p.zoom = 1 } p.zoomInverse = 1 / p.zoom p.viewCenter = p.viewBounds.Center() p.tileSizeTransformed = p.tileSize.Mul(p.zoom) p.tileToViewTransformation = p.tileSize.Mul(.5 * p.zoom) p.viewToTileTransformation = geom.PtF32(1/p.tileSizeTransformed.X, 1/p.tileSizeTransformed.Y) } // Center gives back the coordinate of the center tile func (p *IsometricProjection) Center() geom.PointF32 { return p.center } // Enumerate enumerates all tiles in the set view bounds and calls action for every tile. func (p *IsometricProjection) Enumerate(action func(tile geom.PointF32, view geom.PointF32)) { p.EnumerateInt(func(tile geom.Point, view geom.PointF32) { action(tile.ToF32(), view) }) } // EnumerateInt enumerates all tiles in the set view bounds and calls action for every tile. func (p *IsometricProjection) EnumerateInt(action func(tile geom.Point, view geom.PointF32)) { visible := p.viewBounds visible.Max.Y += p.tileSize.Y * p.zoom topLeft := p.ViewToTile(geom.PtF32(visible.Min.X, visible.Min.Y)) topRight := p.ViewToTile(geom.PtF32(visible.Max.X, visible.Min.Y)) bottomLeft := p.ViewToTile(geom.PtF32(visible.Min.X, visible.Max.Y)) bottomRight := p.ViewToTile(geom.PtF32(visible.Max.X, visible.Max.Y)) minY, maxY := int(geom.Floor32(topRight.Y)), int(geom.Ceil32(bottomLeft.Y)) minX, maxX := int(geom.Floor32(topLeft.X)), int(geom.Ceil32(bottomRight.X)) tileOffset := p.tileSizeTransformed.Mul(.5) for y := minY; y <= maxY; y++ { for x := minX; x <= maxX; x++ { tile := geom.Pt(x, y) view := p.TileToView(tile.ToF32()) if view.X+tileOffset.X < visible.Min.X || view.Y+tileOffset.Y < visible.Min.Y { continue } if view.X-tileOffset.X > visible.Max.X || view.Y-tileOffset.Y > visible.Max.Y { break } action(tile, view) } } } // MoveCenterTo moves the center of the projection to the given tile. func (p *IsometricProjection) MoveCenterTo(tile geom.PointF32) { p.center = tile p.update() } // Pan translates the center of the projection with the given delta in view coordinates. func (p *IsometricProjection) Pan(delta geom.PointF32) { p.MoveCenterTo(p.center.Add(delta.Mul(p.zoomInverse))) } // SetTileSize sets the size of a single tile (maximum width & height difference of its corners). func (p *IsometricProjection) SetTileSize(size geom.PointF32) { p.tileSize = size p.update() } // SetViewBounds sets the bounds of the view coordinates. Used to calculate the center with & for calculating the visible tiles. func (p *IsometricProjection) SetViewBounds(bounds geom.RectangleF32) { p.viewBounds = bounds p.update() } // SetZoom changes the zoom to and keeps the around (tile) coordinate on the same position. func (p *IsometricProjection) SetZoom(around geom.PointF32, zoom float32) { if p.zoom == zoom { return } p.center = around.Sub(around.Sub(p.center).Mul(p.zoom / zoom)) p.zoom = zoom p.update() } // TileInt gives the integer tile coordinate. func (p *IsometricProjection) TileInt(tile geom.PointF32) geom.Point { return geom.Pt(int(geom.Round32(tile.X)), int(geom.Round32(tile.Y))) } // TileToView transforms the tile coordinate to the corresponding view coordinate. func (p *IsometricProjection) TileToView(tile geom.PointF32) geom.PointF32 { translated := tile.Sub(p.center) return p.viewCenter.Add2D((translated.X-translated.Y)*p.tileToViewTransformation.X, (translated.X+translated.Y)*p.tileToViewTransformation.Y) } // ViewCenter returns the center of the view (calculated from the set view bounds). func (p *IsometricProjection) ViewCenter() geom.PointF32 { return p.viewCenter } // ViewToTile transforms the view coordinate to the corresponding tile coordinate. func (p *IsometricProjection) ViewToTile(view geom.PointF32) geom.PointF32 { return p.ViewToTileRelative(view.Sub(p.viewCenter)).Add(p.center) } // ViewToTileInt transforms the view coordinate to the corresponding integer tile coordinate. func (p *IsometricProjection) ViewToTileInt(view geom.PointF32) geom.Point { tile := p.ViewToTile(view) return p.TileInt(tile) } // ViewToTileRelative transforms the relative (to 0,0) view coordinate to the corresponding tile coordinate func (p *IsometricProjection) ViewToTileRelative(view geom.PointF32) geom.PointF32 { return geom.PtF32(view.X*p.viewToTileTransformation.X+view.Y*p.viewToTileTransformation.Y, -view.X*p.viewToTileTransformation.X+view.Y*p.viewToTileTransformation.Y) } // Zoom returns the current zoom. func (p *IsometricProjection) Zoom() float32 { return p.zoom } // ZoomIn zooms in around the given tile coordinate. func (p *IsometricProjection) ZoomIn(around geom.PointF32) { if p.zoom >= 2 { return } p.SetZoom(around, 2*p.zoom) } // ZoomOut zooms in around the given tile coordinate. func (p *IsometricProjection) ZoomOut(around geom.PointF32) { if p.zoom <= .25 { return } p.SetZoom(around, .5*p.zoom) }