Added IsometricProjection.

This commit is contained in:
Sander Schobers 2020-05-23 10:19:10 +02:00
parent 2238f8749a
commit 7793fe823f
2 changed files with 194 additions and 0 deletions

158
play/isometricprojection.go Normal file
View File

@ -0,0 +1,158 @@
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)
}

View File

@ -0,0 +1,36 @@
package play
import (
"testing"
"github.com/stretchr/testify/assert"
"opslag.de/schobers/geom"
)
func createIsometricProjection() *IsometricProjection {
return NewIsometricProjection(geom.PtF32(23, 11), geom.RectRelF32(0, 0, 160, 160))
}
func TestViewToTile(t *testing.T) {
p := createIsometricProjection()
assert.Equal(t, geom.PtF32(0, 0), p.ViewToTile(geom.PtF32(80, 80)))
assert.Equal(t, geom.PtF32(-1, 1), p.ViewToTile(geom.PtF32(57, 80)))
assert.Equal(t, geom.PtF32(2, 2), p.ViewToTile(geom.PtF32(80, 102)))
assert.Equal(t, geom.PtF32(-1, -3), p.ViewToTile(geom.PtF32(103, 58)))
}
func TestViewToTileInt(t *testing.T) {
p := createIsometricProjection()
assert.Equal(t, geom.Pt(0, 0), p.ViewToTileInt(geom.PtF32(80, 80)))
assert.Equal(t, geom.Pt(0, 0), p.ViewToTileInt(geom.PtF32(69, 80)))
assert.Equal(t, geom.Pt(-1, 1), p.ViewToTileInt(geom.PtF32(68, 80)))
}
func TestTileToView(t *testing.T) {
p := createIsometricProjection()
assert.Equal(t, geom.PtF32(80, 80), p.TileToView(geom.PtF32(0, 0)))
assert.Equal(t, geom.PtF32(57, 80), p.TileToView(geom.PtF32(-1, 1)))
assert.Equal(t, geom.PtF32(80, 102), p.TileToView(geom.PtF32(2, 2)))
assert.Equal(t, geom.PtF32(103, 58), p.TileToView(geom.PtF32(-1, -3)))
}