Compare commits

...

4 Commits

Author SHA1 Message Date
000348339d Added support for changing controls.
Changed info screen slightly.
2021-08-10 22:36:42 +02:00
165d1fcd26 Fixed extract command (and changed it to --extract). 2021-08-10 19:41:30 +02:00
f9da21b61c Added console build tag (which doesn't hide the console window). 2021-08-10 19:40:59 +02:00
7053e7b9f2 Reverted back to go:embed.
Parallelized generation of textures.
Refactored animation rendering.
2021-08-10 19:33:30 +02:00
29 changed files with 836 additions and 434 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
.vscode/launch.json .vscode/launch.json
scripts/build scripts/build
cmd/tins2021/rice-box.go

View File

@ -19,7 +19,6 @@
* [go-colurful](###go-colurful) * [go-colurful](###go-colurful)
* [resize](###resize) * [resize](###resize)
* [testify](###testify) * [testify](###testify)
* [go.rice](###rice)
## Introduction ## Introduction
@ -133,7 +132,7 @@ go install -tags static,allegro -ldflags "-s -w" opslag.de/schobers/tins2021/cmd
In this case the SDL 2.0 prerequisite is replaced by the Allegro 5.2 (development libraries must be available for your C compiler). In this case the SDL 2.0 prerequisite is replaced by the Allegro 5.2 (development libraries must be available for your C compiler).
## Command line interface ## Command line interface
You can extract all resources embedded in the executable by running it from the command line with the `--extract-resources` flag. The resources will be extract in the current working directory. Note that the game will use the resources on disk first if they are available. You can extract all resources embedded in the executable by running it from the command line with the `--extract` flag. The resources will be extract in the current working directory. Note that the game will use the resources on disk first if they are available.
## Sources ## Sources
Can be found at https://opslag.de/schobers/tins2021 (Git repository). Can be found at https://opslag.de/schobers/tins2021 (Git repository).
@ -388,28 +387,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
### rice
Copyright (c) 2013, Geert-Johan Riemer
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,8 +1,8 @@
- [X] Increase difficulty. - [X] Increase difficulty.
- [ ] Add music & sounds. - [ ] Add music & sounds.
- [ ] Keep score/difficulty level (resume & restart). - [ ] Keep score/difficulty level (resume & restart).
- [ ] Explain controls on info page. - [X] ~~Explain controls on info page~~ add settings for controls.
- [ ] Fix usage of go/embed (and remove rice again). - [X] Fix usage of go/embed (and remove rice again).
- [X] Add monster animations (~~jumping on tile &~~ towards new tile). - [X] Add monster animations (~~jumping on tile &~~ towards new tile).
- [ ] Scale icons (heart & star on right side) when playing. - [ ] Scale icons (heart & star on right side) when playing.
- [ ] Change layout when playing in portrait mode. - [ ] Change layout when playing in portrait mode.

54
animatedtexture.go Normal file
View File

@ -0,0 +1,54 @@
package tins2021
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
type AnimatedTexture struct {
texture NamedTexture
animation ui.Texture
frames []geom.RectangleF32
}
func newAnimatedTexture(texture NamedTexture, animation ui.Texture, n int) AnimatedTexture {
frames := make([]geom.RectangleF32, 0, n)
height := float32(animation.Height())
width := float32(animation.Width())
for i := 0; i < n; i++ {
left := width * float32(i) / float32(n)
right := width * float32(i+1) / float32(n)
frames = append(frames, geom.RectF32(left, 0, right, height))
}
return AnimatedTexture{texture: texture, animation: animation, frames: frames}
}
func NewAnimatedTexture(texture NamedTexture, n int) AnimatedTexture {
return newAnimatedTexture(texture, texture.Texture(), n)
}
func FitAnimatedTexture(texture NamedTexture, scale float32, n int) AnimatedTexture {
height := float32(texture.Texture().Height())
scale = ScaleRound(height, scale)
return newAnimatedTexture(texture, texture.Scaled(scale), n)
}
func (t AnimatedTexture) Draw(renderer ui.Renderer, pos geom.PointF32, frame int) {
renderer.DrawTexturePointOptions(t.animation, pos, ui.DrawOptions{Source: &t.frames[frame]})
}
func (t AnimatedTexture) FrameSize(i int) geom.PointF32 { return t.frames[i].Size() }
func (t AnimatedTexture) Frames() int { return len(t.frames) }
func (t AnimatedTexture) Scale(scale float32) AnimatedTexture {
return FitAnimatedTexture(t.texture, scale, t.Frames())
}
func FindScaleRound(length, desired float32) float32 {
return ScaleRound(length, desired/length)
}
func ScaleRound(length, scale float32) float32 {
return geom.Round32(length*scale) / length
}

View File

@ -5,47 +5,8 @@ import (
"time" "time"
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
) )
type AnimatedTexture struct {
Texture ui.Texture
Frames []geom.RectangleF32
}
func NewAnimatedTexture(texture ui.Texture, n int) AnimatedTexture {
frames := make([]geom.RectangleF32, 0, n)
height := float32(texture.Height())
width := float32(texture.Width())
for i := 0; i < n; i++ {
left := width * float32(i) / float32(n)
right := width * float32(i+1) / float32(n)
frames = append(frames, geom.RectF32(left, 0, right, height))
}
return AnimatedTexture{Texture: texture, Frames: frames}
}
func FitAnimatedTexture(textures *ui.Textures, name string, scale float32, n int) AnimatedTexture {
height := float32(textures.Texture(name).Height())
scale = geom.Round32(height*scale) / height // clip scale to integer width/height
return NewAnimatedTexture(textures.ScaledByName(name, scale), n)
}
func (t AnimatedTexture) Scale(scale float32) AnimatedTexture {
frames := make([]geom.RectangleF32, 0, len(t.Frames))
for _, frame := range t.Frames {
frames = append(frames, geom.RectangleF32{Min: frame.Min.Mul(scale), Max: frame.Max.Mul(scale)})
}
return AnimatedTexture{
Texture: t.Texture,
Frames: frames,
}
}
func (t AnimatedTexture) Draw(renderer ui.Renderer, pos geom.PointF32, frame int) {
renderer.DrawTexturePointOptions(t.Texture, pos, ui.DrawOptions{Source: &t.Frames[frame]})
}
type Animation struct { type Animation struct {
LastUpdate time.Time LastUpdate time.Time
Frame int Frame int

View File

@ -35,18 +35,18 @@ const fpsOverlayName = `fps`
func (a *app) Init(ctx ui.Context) error { func (a *app) Init(ctx ui.Context) error {
if err := a.loadFonts(ctx, if err := a.loadFonts(ctx,
fontDescriptor{"debug", "fonts/FiraMono-Regular.ttf", 12}, fontDescriptor{"debug", "resources/fonts/FiraMono-Regular.ttf", 12},
fontDescriptor{"default", "fonts/escheresk.ttf", 48}, fontDescriptor{"default", "resources/fonts/escheresk.ttf", 48},
fontDescriptor{"small", "fonts/escheresk.ttf", 16}, fontDescriptor{"small", "resources/fonts/escheresk.ttf", 16},
fontDescriptor{"score", "fonts/FiraMono-Regular.ttf", 24}, fontDescriptor{"score", "resources/fonts/FiraMono-Regular.ttf", 24},
fontDescriptor{"title", "fonts/escher.ttf", 80}, fontDescriptor{"title", "resources/fonts/escher.ttf", 80},
); err != nil { ); err != nil {
return err return err
} }
textureLoader := tins2021.NewResourceLoader() textureLoader := tins2021.NewResourceLoader()
textures := ctx.Textures() textures := ctx.Textures()
if err := textureLoader.LoadFromFile(ctx.Resources(), "textures.txt", func(name, content string) error { if err := textureLoader.LoadFromFile(ctx.Resources(), "resources/textures.txt", func(name, content string) error {
_, err := textures.CreateTexturePath(name, content, true) _, err := textures.CreateTexturePath(name, content, true)
return err return err
}); err != nil { }); err != nil {
@ -55,7 +55,7 @@ func (a *app) Init(ctx ui.Context) error {
ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{Align: ui.AlignRight}, false) ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{Align: ui.AlignRight}, false)
a.context = newAppContext(ctx, func(control ui.Control) { a.context = newAppContext(ctx, a.settings, func(control ui.Control) {
a.Content = control a.Content = control
}) })
a.context.ShowMainMenu(ctx) a.context.ShowMainMenu(ctx)

View File

@ -1,8 +1,6 @@
package main package main
import ( import (
"image"
"opslag.de/schobers/tins2021" "opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
) )
@ -10,36 +8,29 @@ import (
type appContext struct { type appContext struct {
setView func(ui.Control) setView func(ui.Control)
Debug bool Settings *tins2021.Settings
Debug bool
MonsterTextureNames map[tins2021.MonsterType]string StarTexture tins2021.AnimatedTexture
HeartTexture tins2021.AnimatedTexture
MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
} }
func newAppContext(ctx ui.Context, setView func(ui.Control)) *appContext { func newAppContext(ctx ui.Context, settings *tins2021.Settings, setView func(ui.Control)) *appContext {
newAnimatedTexture(ctx, "star", "images/star.png", defaultAnimationFrames, func() image.Image { textures := textureGenerator{}
return tins2021.AnimatePolygon(tins2021.CreateStar(5), tins2021.Yellow, tins2021.NewRotateAnimation(defaultAnimationFrames))
})
newAnimatedTexture(ctx, "heart", "images/heart.png", defaultAnimationFrames, func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHeart(), tins2021.Red, tins2021.NewRotateAnimation(defaultAnimationFrames))
})
app := &appContext{ app := &appContext{
setView: setView, setView: setView,
MonsterTextureNames: map[tins2021.MonsterType]string{ Settings: settings,
tins2021.MonsterTypeStraight: "straight-walking-monster", StarTexture: newAnimatedTexture(ctx, "star", defaultAnimationFrames, textures.Star),
tins2021.MonsterTypeRandom: "random-walking-monster", HeartTexture: newAnimatedTexture(ctx, "heart", defaultAnimationFrames, textures.Heart),
tins2021.MonsterTypeChaser: "chasing-monster", MonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{
tins2021.MonsterTypeStraight: newAnimatedTexture(ctx, "straight-walking-monster", defaultAnimationFrames, textures.StraightWalkingMonster),
tins2021.MonsterTypeRandom: newAnimatedTexture(ctx, "random-walking-monster", defaultAnimationFrames, textures.RandomWalkingMonster),
tins2021.MonsterTypeChaser: newAnimatedTexture(ctx, "chasing-monster", defaultAnimationFrames, textures.ChasingMonster),
}, },
} }
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeStraight], "images/monster-straight.png", defaultAnimationFrames, func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Green, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
})
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeRandom], "images/monster-random.png", defaultAnimationFrames, func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Blue, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
})
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeChaser], "images/monster-chaser.png", defaultAnimationFrames, func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Purple, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
})
return app return app
} }
@ -73,6 +64,10 @@ func (app *appContext) ShowCredits(ctx ui.Context) {
app.setView(newCredits(app, ctx)) app.setView(newCredits(app, ctx))
} }
func (app *appContext) ShowSettings(ctx ui.Context) {
app.setView(newSettings(app, ctx))
}
func (app *appContext) ShowInfo(ctx ui.Context) { func (app *appContext) ShowInfo(ctx ui.Context) {
app.setView(newInfo(app, ctx)) app.setView(newInfo(app, ctx))
} }

View File

@ -58,8 +58,6 @@ func newCredits(app *appContext, ctx ui.Context) *credits {
" - https://github.com/nfnt/resize", "", " - https://github.com/nfnt/resize", "",
"testify: a testing library for Go", "testify: a testing library for Go",
" - https://github.com/stretchr/testify", "", " - https://github.com/stretchr/testify", "",
"rice: a library for embedding files in Go",
" - https://github.com/GeertJohan/go.rice", "",
"", "",
"# THANKS", "# THANKS",
"", "",

View File

@ -1,12 +1,11 @@
package main package main
import ( import (
"embed"
"io" "io"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
rice "github.com/GeertJohan/go.rice"
) )
func copyFile(path string, file fs.File) error { func copyFile(path string, file fs.File) error {
@ -21,19 +20,19 @@ func copyFile(path string, file fs.File) error {
return err return err
} }
func copyBoxToDisk() error { func copyBoxToDisk(resources embed.FS) error {
box := rice.MustFindBox(`resources`) return fs.WalkDir(resources, ".", func(path string, entry fs.DirEntry, err error) error {
return box.Walk("", func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
if info.IsDir() { if entry.IsDir() {
return nil return nil
} }
src, err := box.Open(path) src, err := resources.Open(path)
if err != nil { if err != nil {
return err return err
} }
return copyFile(filepath.Join(box.Name(), path), src) defer src.Close()
return copyFile(path, src)
}) })
} }

View File

@ -6,14 +6,19 @@ import (
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
) )
const infoLegendIconSize = .36
const infoLegendIconMargin = 4.
const infoLegendSeparatorMargin = 24
const infoText = "Qbitter is a game loosly based on the work \"LW305 Kringloop\" of M.C. Escher where a gnome runs down a stairs and morphs into its 2D abstract shape. The game also lends ideas from Q*Bert, a game from the eighties that is based on the works of M.C. Escher. \n\nIn the game you (represented as a gnome) have to collect stars while trying to avoid enemies (hexagons). Every level has increasing difficulty."
var infoLegendIconRectangle = geom.RectRelF32(0, 0, infoLegendIconSize*tins2021.TextureSize, infoLegendIconSize*tins2021.TextureSize)
type info struct { type info struct {
ui.StackPanel ui.StackPanel
app *appContext app *appContext
} }
const infoText = "Qbitter is a game loosly based on the work \"LW305 Kringloop\" of M.C. Escher where a gnome runs down a stairs and morphs into its 2D abstract shape. The game also lends ideas from Q*Bert, a game from the eighties that is based on the works of M.C. Escher. \n\nIn the game you (represented as a gnome) have to collect stars while trying to avoid enemies (hexagons). Every level has increasing difficulty."
type infoLegend struct { type infoLegend struct {
ui.ControlBase ui.ControlBase
@ -22,44 +27,42 @@ type infoLegend struct {
} }
func (l *infoLegend) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 { func (l *infoLegend) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 {
return geom.PtF32(geom.NaN32(), .4*tins2021.TextureSize) textureHeight := float32(l.Icon.Height())
font := ctx.Fonts().Font("score")
fontHeight := float32(font.Height())
return geom.PtF32(textureHeight+infoLegendSeparatorMargin+font.WidthOf(l.Description), geom.Max32(textureHeight+2*infoLegendIconMargin, fontHeight))
} }
const infoLegenIconSize = .36
var infoLegendIconRectangle = geom.RectRelF32(0, 0, infoLegenIconSize*tins2021.TextureSize, infoLegenIconSize*tins2021.TextureSize)
func (l *infoLegend) Render(ctx ui.Context) { func (l *infoLegend) Render(ctx ui.Context) {
bounds := l.Bounds() bounds := l.Bounds()
separator := .3 * bounds.Dx()
textureHeight := float32(l.Icon.Height()) textureHeight := float32(l.Icon.Height())
renderer := ctx.Renderer() renderer := ctx.Renderer()
renderer.DrawTexturePointOptions(l.Icon, bounds.Min.Add2D(separator-textureHeight, 0), ui.DrawOptions{Source: &infoLegendIconRectangle}) renderer.DrawTexturePointOptions(l.Icon, bounds.Min, ui.DrawOptions{Source: &infoLegendIconRectangle})
font := ctx.Fonts().Font("score") font := ctx.Fonts().Font("score")
fontHeight := float32(font.Height()) fontHeight := float32(font.Height())
renderer.Text(font, bounds.Min.Add2D(separator+12, .5*(textureHeight-fontHeight)), ctx.Style().Palette.Text, l.Description) renderer.Text(font, bounds.Min.Add2D(textureHeight+infoLegendSeparatorMargin, .5*(textureHeight-fontHeight)+infoLegendIconMargin), ctx.Style().Palette.Text, l.Description)
} }
func newInfo(app *appContext, ctx ui.Context) ui.Control { func newInfo(app *appContext, ctx ui.Context) ui.Control {
legend := ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) { legend := ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("star", infoLegenIconSize), Icon: ctx.Textures().ScaledByName("star", infoLegendIconSize),
Description: "Collect them to complete the level.", Description: "Collect them to complete the level.",
}) })
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("heart", infoLegenIconSize), Icon: ctx.Textures().ScaledByName("heart", infoLegendIconSize),
Description: "Gives (back) a life.", Description: "Gives (back) a life.",
}) })
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("straight-walking-monster", infoLegenIconSize), Icon: ctx.Textures().ScaledByName("straight-walking-monster", infoLegendIconSize),
Description: "Monster that walks over a fixed diagonal.", Description: "Monster that walks over a fixed diagonal.",
}) })
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("random-walking-monster", infoLegenIconSize), Icon: ctx.Textures().ScaledByName("random-walking-monster", infoLegendIconSize),
Description: "Monster that walks randomly.", Description: "Monster that walks randomly.",
}) })
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("chasing-monster", infoLegenIconSize), Icon: ctx.Textures().ScaledByName("chasing-monster", infoLegendIconSize),
Description: "Monster that walks towards you.", Description: "Monster that walks towards you.",
}) })
}) })
@ -70,7 +73,7 @@ func newInfo(app *appContext, ctx ui.Context) ui.Control {
label("", "score"), // spacing label("", "score"), // spacing
paragraphOpts(infoText, "score", labelOptions{TextAlignment: ui.AlignCenter}), paragraphOpts(infoText, "score", labelOptions{TextAlignment: ui.AlignCenter}),
label("", "score"), // spacing label("", "score"), // spacing
legend, Center(legend),
}, },
}, },
Orientation: ui.OrientationVertical, Orientation: ui.OrientationVertical,

View File

@ -20,25 +20,60 @@ type levelController struct {
Level *tins2021.Level Level *tins2021.Level
Cubes cubeTexture Cube tins2021.NamedTexture
Inverted tins2021.NamedTexture
Animations map[string]*tins2021.Animations Animations map[string]*tins2021.Animations
IdleMonsters *tins2021.Animations IdleMonsters *tins2021.Animations
MovingMonsters *tins2021.Animations MovingMonsters *tins2021.Animations
SmallFont *tins2021.BitmapFont SmallFont *tins2021.BitmapFont
Controls map[ui.Key]tins2021.Direction
} }
func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *levelController { func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *levelController {
control := &levelController{app: app} control := &levelController{app: app}
textures := ctx.Textures() textures := ctx.Textures()
control.Cubes = newCubeTexture(textures, tins2021.Orange) control.Cube = tins2021.MustCreateNamedTextureImage(textures, "cube", tins2021.GenerateCube(tins2021.Orange))
control.Inverted = tins2021.MustCreateNamedTextureImage(textures, "cube_inverted", tins2021.GenerateHole(tins2021.Orange))
small, err := tins2021.NewBitmapFont(ctx.Renderer(), ctx.Fonts().Font("small"), tins2021.NumericCharacters...) small, err := tins2021.NewBitmapFont(ctx.Renderer(), ctx.Fonts().Font("small"), tins2021.NumericCharacters...)
if err != nil { if err != nil {
panic(err) panic(err)
} }
control.SmallFont = small control.SmallFont = small
switch app.Settings.Controls.Type {
case controlsTypeArrows:
control.Controls = map[ui.Key]tins2021.Direction{
ui.KeyUp: tins2021.DirectionUpLeft,
ui.KeyLeft: tins2021.DirectionDownLeft,
ui.KeyDown: tins2021.DirectionDownRight,
ui.KeyRight: tins2021.DirectionUpRight,
}
case controlsTypeCustom:
find := func(s string, defaultKey ui.Key) ui.Key {
for key, setting := range supportedCustomKeys {
if s == setting {
return key
}
}
return defaultKey
}
control.Controls = map[ui.Key]tins2021.Direction{
find(app.Settings.Controls.MoveUpLeft, ui.KeyW): tins2021.DirectionUpLeft,
find(app.Settings.Controls.MoveDownLeft, ui.KeyA): tins2021.DirectionDownLeft,
find(app.Settings.Controls.MoveDownRight, ui.KeyS): tins2021.DirectionDownRight,
find(app.Settings.Controls.MoveUpRight, ui.KeyD): tins2021.DirectionUpRight,
}
default:
control.Controls = map[ui.Key]tins2021.Direction{
ui.KeyW: tins2021.DirectionUpLeft,
ui.KeyA: tins2021.DirectionDownLeft,
ui.KeyS: tins2021.DirectionDownRight,
ui.KeyD: tins2021.DirectionUpRight,
}
}
control.Play(level) control.Play(level)
@ -75,15 +110,9 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
switch e := e.(type) { switch e := e.(type) {
case *ui.KeyDownEvent: case *ui.KeyDownEvent:
switch e.Key { dir, ok := r.Controls[e.Key]
case ui.KeyW: if ok {
r.Level.MovePlayer(tins2021.DirectionUpLeft) r.Level.MovePlayer(dir)
case ui.KeyD:
r.Level.MovePlayer(tins2021.DirectionUpRight)
case ui.KeyS:
r.Level.MovePlayer(tins2021.DirectionDownRight)
case ui.KeyA:
r.Level.MovePlayer(tins2021.DirectionDownLeft)
} }
} }
@ -146,17 +175,15 @@ func (r *levelController) Play(level *tins2021.Level) {
r.Level = level r.Level = level
r.Animations = map[string]*tins2021.Animations{ r.Animations = map[string]*tins2021.Animations{
"star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true), "star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true),
"heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true), "heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
"monster": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
} }
r.IdleMonsters = tins2021.NewAnimations(200*time.Millisecond, 100, false, false) r.IdleMonsters = tins2021.NewAnimations(200*time.Millisecond, 100, false, false)
r.MovingMonsters = tins2021.NewAnimations(16*time.Millisecond, 50, false, false) r.MovingMonsters = tins2021.NewAnimations(16*time.Millisecond, 50, false, false)
for monster := range level.Monsters { for monster := range level.Monsters {
r.IdleMonsters.Frame(monster) r.IdleMonsters.Frame(monster)
} }
for _, monster := range r.app.MonsterTextureNames {
r.Animations[monster] = tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true)
}
} }
const defaultAnimationFrames = 20 const defaultAnimationFrames = 20
@ -189,20 +216,21 @@ func (r levelController) Render(ctx ui.Context) {
} }
renderer := ctx.Renderer() renderer := ctx.Renderer()
textures := ctx.Textures() cube := r.Cube.Scaled(scale)
cubes := r.Cubes.Scaled(textures, scale) inverted := r.Inverted.Scaled(scale)
cubeWidth := float32(cubes.Normal.Width()) cubeWidth := float32(cube.Width())
cubeHeight := float32(cubes.Normal.Height()) cubeHeight := float32(cube.Height())
player := ctx.Textures().ScaledByName("dwarf", scale*.6) player := ctx.Textures().ScaledByName("dwarf", scale*.6)
star := tins2021.FitAnimatedTexture(textures, "star", scale*.4, defaultAnimationFrames) star := r.app.StarTexture.Scale(scale * .4)
heart := tins2021.FitAnimatedTexture(textures, "heart", scale*.4, defaultAnimationFrames) heart := r.app.HeartTexture.Scale(scale * .4)
monsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{} monsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{}
for typ, name := range r.app.MonsterTextureNames { for typ, animation := range r.app.MonsterTextures {
monsterTextures[typ] = tins2021.FitAnimatedTexture(textures, name, scale*.4, defaultAnimationFrames) monsterTextures[typ] = animation.Scale(scale * .4)
} }
propOffset := geom.PtF32(-.5*float32(star.Texture.Height()), -.8*float32(star.Texture.Height())) propHeight := star.FrameSize(0).Y
propOffset := geom.PtF32(-.5*float32(propHeight), -.8*float32(propHeight))
distances := r.Level.Tiles.Distances(r.Level.Player) distances := r.Level.Tiles.Distances(r.Level.Player)
@ -222,9 +250,9 @@ func (r levelController) Render(ctx ui.Context) {
continue continue
} }
screenPos, platformPos := positionOfTile(pos) screenPos, platformPos := positionOfTile(pos)
tileTexture := cubes.Normal.Texture tileTexture := cube
if tile.Inversed { if tile.Inversed {
tileTexture = cubes.Inversed.Texture tileTexture = inverted
} }
renderer.DrawTexturePoint(tileTexture, screenPos) renderer.DrawTexturePoint(tileTexture, screenPos)
if r.app.Debug { if r.app.Debug {
@ -253,7 +281,6 @@ func (r levelController) Render(ctx ui.Context) {
if tile == nil { if tile == nil {
continue continue
} }
name := r.app.MonsterTextureNames[monsterType.Type()]
texture := monsterTextures[monsterType.Type()] texture := monsterTextures[monsterType.Type()]
_, platformPos := positionOfTile(pos) _, platformPos := positionOfTile(pos)
if target, ok := r.Level.MonsterTargets[pos]; ok { if target, ok := r.Level.MonsterTargets[pos]; ok {
@ -262,15 +289,15 @@ func (r levelController) Render(ctx ui.Context) {
delta := targetPlatformPos.Sub(platformPos) delta := targetPlatformPos.Sub(platformPos)
curve := geom.PtF32(0, .6*geom.Sin32(dt*geom.Pi)*textureWidth) curve := geom.PtF32(0, .6*geom.Sin32(dt*geom.Pi)*textureWidth)
interpolatedPos := platformPos.Add(delta.Mul(dt)).Sub(curve) interpolatedPos := platformPos.Add(delta.Mul(dt)).Sub(curve)
texture.Draw(renderer, interpolatedPos.Add(propOffset), r.Animations[name].Frame(pos)) texture.Draw(renderer, interpolatedPos.Add(propOffset), r.Animations["monster"].Frame(pos))
} else { } else {
texture.Draw(renderer, platformPos.Add(propOffset), r.Animations[name].Frame(pos)) texture.Draw(renderer, platformPos.Add(propOffset), r.Animations["monster"].Frame(pos))
} }
} }
textColor := ctx.Style().Palette.Text textColor := ctx.Style().Palette.Text
scoreFont := ctx.Fonts().Font("score") scoreFont := ctx.Fonts().Font("score")
fontOffsetY := .5 * (float32(star.Texture.Height()) - scoreFont.Height()) fontOffsetY := .5 * (float32(propHeight) - scoreFont.Height())
// stars & hearts // stars & hearts
scoreTopLeft := scoreView.Min scoreTopLeft := scoreView.Min

View File

@ -37,6 +37,7 @@ func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
menu.Add("Play", func(ctx ui.Context) { menu.Add("Play", func(ctx ui.Context) {
app.Play(ctx) app.Play(ctx)
}) })
menu.Add("Controls", func(ctx ui.Context) { app.ShowSettings(ctx) })
menu.Add("Credits", func(ctx ui.Context) { app.ShowCredits(ctx) }) menu.Add("Credits", func(ctx ui.Context) { app.ShowCredits(ctx) })
menu.Add("Quit", func(ctx ui.Context) { ctx.Quit() }) menu.Add("Quit", func(ctx ui.Context) { ctx.Quit() })

View File

@ -1 +1 @@
dwarf: images/dwarf.png dwarf: resources/textures/gnome.png

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

347
cmd/tins2021/settings.go Normal file
View File

@ -0,0 +1,347 @@
package main
import (
"fmt"
"image"
"image/color"
"io/ioutil"
"github.com/golang/freetype/truetype"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg"
"opslag.de/schobers/geom"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
)
const keyboardKeyCornerRadius = .1 * keyboardKeyWidth
const keyboardKeyHeight = .2 * keyboardLayoutTextureWidth
const keyboardKeySkew = .15
const keyboardKeyWidth = .25 * keyboardLayoutTextureWidth
const keyboardLayoutTextureHeight = 256
const keyboardLayoutTextureWidth = 2 * keyboardLayoutTextureHeight
func drawKey(ctx *draw2dimg.GraphicContext, font *truetype.Font, center geom.PointF, key rune, color color.Color) {
const cornerRadius = keyboardKeyCornerRadius
const keyHeight_5 = .5 * keyboardKeyHeight
const keyWidth_5 = .5 * keyboardKeyWidth
skewed := func(x, y float64) (float64, float64) {
x, y = skewedKeyboardCoordinates(x, y)
return center.X + x, center.Y + y
}
corner := func(x, y, start float64) {
for a := start; a <= start+.25; a += .025 {
aa := a * 2 * geom.Pi
ctx.LineTo(skewed(x+cornerRadius*geom.Cos(aa), y-cornerRadius*geom.Sin(aa)))
}
}
ctx.SetLineWidth(3)
ctx.SetStrokeColor(color)
ctx.SetFillColor(color)
ctx.MoveTo(skewed(-keyWidth_5+cornerRadius, keyHeight_5))
ctx.LineTo(skewed(keyWidth_5-cornerRadius, keyHeight_5))
corner(keyWidth_5-cornerRadius, keyHeight_5-cornerRadius, .75)
ctx.LineTo(skewed(keyWidth_5, -keyHeight_5+cornerRadius))
corner(keyWidth_5-cornerRadius, -keyHeight_5+cornerRadius, 0)
ctx.LineTo(skewed(-keyWidth_5+cornerRadius, -keyHeight_5))
corner(-keyWidth_5+cornerRadius, -keyHeight_5+cornerRadius, .25)
ctx.LineTo(skewed(-keyWidth_5, keyHeight_5-cornerRadius))
corner(-keyWidth_5+cornerRadius, keyHeight_5-cornerRadius, .5)
ctx.Close()
ctx.Stroke()
ctx.FontCache = fontCache{font}
ctx.SetFont(font)
ctx.SetFontSize(keyHeight_5)
text := fmt.Sprintf("%c", key)
textLeft, textTop, textRight, textBottom := ctx.GetStringBounds(text)
textX, textY := skewed(-.5*(textRight-textLeft), .5*(textBottom-textTop))
ctx.FillStringAt(text, textX, textY)
}
type fontCache struct{ *truetype.Font }
func (f fontCache) Load(draw2d.FontData) (*truetype.Font, error) { return f.Font, nil }
func (fontCache) Store(draw2d.FontData, *truetype.Font) {}
func generateArrowKeys(resources ui.Resources) image.Image {
return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.53, .25), Key: '↑'},
keyboardLayoutKey{Position: geom.PtF(.2, .75), Key: '←'},
keyboardLayoutKey{Position: geom.PtF(.5, .75), Key: '↓'},
keyboardLayoutKey{Position: geom.PtF(.8, .75), Key: '→'},
)
}
func generateArrowKeysHighlight(resources ui.Resources, highlight [4]bool) image.Image {
spaceOrRune := func(r rune, space bool) rune {
if space {
return ' '
}
return r
}
return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.45, .25), Key: spaceOrRune('↑', !highlight[0]), Highlight: highlight[0]},
keyboardLayoutKey{Position: geom.PtF(.2, .75), Key: spaceOrRune('←', !highlight[1]), Highlight: highlight[1]},
keyboardLayoutKey{Position: geom.PtF(.5, .75), Key: spaceOrRune('↓', !highlight[2]), Highlight: highlight[2]},
keyboardLayoutKey{Position: geom.PtF(.8, .75), Key: spaceOrRune('→', !highlight[3]), Highlight: highlight[3]},
)
}
func generateCustomKeys(resources ui.Resources, keys [4]rune) image.Image {
return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.45, .25), Key: keys[0]},
keyboardLayoutKey{Position: geom.PtF(.2, .75), Key: keys[1]},
keyboardLayoutKey{Position: geom.PtF(.5, .75), Key: keys[2]},
keyboardLayoutKey{Position: geom.PtF(.8, .75), Key: keys[3]},
)
}
func generateKeys(resources ui.Resources, keys ...keyboardLayoutKey) image.Image {
im := image.NewRGBA(image.Rect(0, 0, keyboardLayoutTextureWidth, keyboardLayoutTextureHeight))
ctx := draw2dimg.NewGraphicContext(im)
font, err := parseFont(resources)
if err != nil {
panic(err)
}
for _, key := range keys {
var color color.Color = color.White
if key.Highlight {
color = zntg.MustHexColor(tins2021.Orange)
}
center := geom.PtF(key.Position.X*keyboardLayoutTextureWidth, key.Position.Y*keyboardLayoutTextureHeight)
drawKey(ctx, font, center, key.Key, color)
}
return im
}
func generateWASDKeys(resources ui.Resources) image.Image {
return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.45, .25), Key: 'W'},
keyboardLayoutKey{Position: geom.PtF(.2, .75), Key: 'A'},
keyboardLayoutKey{Position: geom.PtF(.5, .75), Key: 'S'},
keyboardLayoutKey{Position: geom.PtF(.8, .75), Key: 'D'},
)
}
type keyboardLayoutKey struct {
Position geom.PointF
Key rune
Highlight bool
}
func parseFont(resources ui.Resources) (*truetype.Font, error) {
ttf, err := resources.OpenResource("resources/fonts/FiraMono-Regular.ttf")
if err != nil {
return nil, err
}
defer ttf.Close()
data, err := ioutil.ReadAll(ttf)
if err != nil {
return nil, err
}
return truetype.Parse(data)
}
type settings struct {
ui.StackPanel
app *appContext
ActiveLayout int
SelectedLayout int
SelectingCustom int
}
const (
controlsTypeWASD = "wasd"
controlsTypeArrows = "arrows"
controlsTypeCustom = "custom"
)
func newSettings(app *appContext, ctx ui.Context) *settings {
ctx.Textures().CreateTextureGo("layout-wasd", generateWASDKeys(ctx.Resources()), true)
ctx.Textures().CreateTextureGo("layout-arrows", generateArrowKeys(ctx.Resources()), true)
ctx.Textures().CreateTextureGo("layout-select-1", generateArrowKeysHighlight(ctx.Resources(), [4]bool{true, false, false, false}), true)
ctx.Textures().CreateTextureGo("layout-select-2", generateArrowKeysHighlight(ctx.Resources(), [4]bool{false, true, false, false}), true)
ctx.Textures().CreateTextureGo("layout-select-3", generateArrowKeysHighlight(ctx.Resources(), [4]bool{false, false, true, false}), true)
ctx.Textures().CreateTextureGo("layout-select-4", generateArrowKeysHighlight(ctx.Resources(), [4]bool{false, false, false, true}), true)
var layout int
switch app.Settings.Controls.Type {
case controlsTypeArrows:
layout = 1
case controlsTypeCustom:
layout = 2
}
settings := &settings{app: app, ActiveLayout: layout, SelectedLayout: layout}
settings.renderCustomLayout(ctx)
return settings
}
var supportedCustomKeys = map[ui.Key]string{
ui.KeyA: "A",
ui.KeyB: "B",
ui.KeyC: "C",
ui.KeyD: "D",
ui.KeyE: "E",
ui.KeyF: "F",
ui.KeyG: "G",
ui.KeyH: "H",
ui.KeyI: "I",
ui.KeyJ: "J",
ui.KeyK: "K",
ui.KeyL: "L",
ui.KeyM: "M",
ui.KeyN: "N",
ui.KeyO: "O",
ui.KeyP: "P",
ui.KeyQ: "Q",
ui.KeyR: "R",
ui.KeyS: "S",
ui.KeyT: "T",
ui.KeyU: "U",
ui.KeyV: "V",
ui.KeyW: "W",
ui.KeyX: "X",
ui.KeyY: "Y",
ui.KeyZ: "Z",
}
func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
if s.StackPanel.Handle(ctx, e) {
return true
}
switch e := e.(type) {
case *ui.KeyDownEvent:
if s.SelectingCustom > 0 {
switch e.Key {
case ui.KeyEscape:
s.SelectingCustom = 0
return true
}
key, ok := supportedCustomKeys[e.Key]
if ok {
switch s.SelectingCustom {
case 1:
s.app.Settings.Controls.MoveUpLeft = key
case 2:
s.app.Settings.Controls.MoveDownLeft = key
case 3:
s.app.Settings.Controls.MoveDownRight = key
case 4:
s.app.Settings.Controls.MoveUpRight = key
}
s.renderCustomLayout(ctx)
s.SelectingCustom++
if s.SelectingCustom == 5 {
s.SelectingCustom = 0
s.SelectedLayout = 2
s.app.Settings.Controls.Type = controlsTypeCustom
}
}
return true
}
switch e.Key {
case ui.KeyEscape:
s.app.ShowMainMenu(ctx)
return true
case ui.KeyLeft:
s.ActiveLayout = (s.ActiveLayout + 2) % 3
case ui.KeyRight:
s.ActiveLayout = (s.ActiveLayout + 1) % 3
case ui.KeyEnter:
switch s.ActiveLayout {
case 0:
s.SelectedLayout = 0
s.app.Settings.Controls.Type = controlsTypeWASD
case 1:
s.SelectedLayout = 1
s.app.Settings.Controls.Type = controlsTypeArrows
case 2:
s.SelectingCustom = 1
}
}
}
return false
}
func (s *settings) Render(ctx ui.Context) {
bounds := s.Bounds()
center := bounds.Center()
width := bounds.Dx()
renderer := ctx.Renderer()
scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width)
font := ctx.Fonts().Font("default")
layouts := []string{
"WASD",
"ARROWS",
"CUSTOM",
}
layoutTextures := []string{"layout-wasd", "layout-arrows", "layout-custom"}
normalColor := ctx.Style().Palette.Text
highlightColor := ctx.Style().Palette.Primary
for i, layout := range layouts {
layoutLeft := (.04 + .32*float32(i)) * width
layoutCenter := layoutLeft + .14*width
textColor := normalColor
layoutColor := normalColor
if s.ActiveLayout == i {
textColor = highlightColor
}
if s.SelectedLayout == i {
layoutColor = highlightColor
}
renderer.TextAlign(font, geom.PtF32(layoutCenter, center.Y-2*font.Height()), textColor, layout, ui.AlignCenter)
renderer.DrawTexturePointOptions(ctx.Textures().ScaledByName(layoutTextures[i], scale), geom.PtF32(layoutLeft, center.Y), ui.DrawOptions{Tint: layoutColor})
}
if s.SelectingCustom > 0 {
renderer.FillRectangle(bounds, zntg.MustHexColor(`#000000CF`))
selectTexture := fmt.Sprintf("layout-select-%d", s.SelectingCustom)
layoutLeft := .36 * width
layoutCenter := layoutLeft + .14*width
renderer.TextAlign(font, geom.PtF32(layoutCenter, center.Y-2*font.Height()), normalColor, "PRESS KEY TO ASSIGN", ui.AlignCenter)
renderer.DrawTexturePoint(ctx.Textures().ScaledByName(selectTexture, scale), geom.PtF32(layoutLeft, center.Y))
}
}
func (s *settings) renderCustomLayout(ctx ui.Context) {
runeOrQuestionMark := func(s string) rune {
if len(s) == 0 {
return '?'
}
return []rune(s)[0]
}
customKeys := [4]rune{
runeOrQuestionMark(s.app.Settings.Controls.MoveUpLeft),
runeOrQuestionMark(s.app.Settings.Controls.MoveDownLeft),
runeOrQuestionMark(s.app.Settings.Controls.MoveDownRight),
runeOrQuestionMark(s.app.Settings.Controls.MoveUpRight),
}
ctx.Textures().CreateTextureGo("layout-custom", generateCustomKeys(ctx.Resources(), customKeys), true)
}
func skewedKeyboardCoordinates(x, y float64) (float64, float64) {
return x - keyboardKeySkew*y, y
}

View File

@ -0,0 +1,27 @@
package main
import (
"image"
"opslag.de/schobers/tins2021"
)
type textureGenerator struct{}
func (textureGenerator) Star() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateStar(5), tins2021.Yellow, defaultAnimationFrames, tins2021.MeshRotateAnimation{})
}
func (textureGenerator) Heart() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHeart(), tins2021.Red, defaultAnimationFrames, tins2021.MeshRotateAnimation{})
}
func (textureGenerator) monster(color string) image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), color, defaultAnimationFrames, tins2021.MeshWobbleTransformation{Wobble: 30})
}
func (g textureGenerator) ChasingMonster() image.Image { return g.monster(tins2021.Purple) }
func (g textureGenerator) RandomWalkingMonster() image.Image { return g.monster(tins2021.Blue) }
func (g textureGenerator) StraightWalkingMonster() image.Image { return g.monster(tins2021.Green) }

View File

@ -1,48 +1,28 @@
package main package main
import ( import (
"fmt"
"image" "image"
"image/png" "image/png"
"log"
"os" "os"
"path/filepath"
"time"
"opslag.de/schobers/tins2021" "opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
) )
type namedTexture struct { func chrono(action func()) time.Duration {
ui.Texture start := time.Now()
action()
Name string return time.Now().Sub(start)
} }
func newNamedTexture(textures *ui.Textures, name string, im image.Image) namedTexture { func chronoErr(action func() error) (time.Duration, error) {
texture, err := textures.CreateTextureGo(name, im, true) start := time.Now()
if err != nil { err := action()
panic(err) return time.Now().Sub(start), err
}
return namedTexture{texture, name}
}
func (t namedTexture) Scaled(textures *ui.Textures, scale float32) namedTexture {
return namedTexture{textures.ScaledByName(t.Name, scale), t.Name}
}
type cubeTexture struct {
Normal, Inversed namedTexture
}
func newCubeTexture(textures *ui.Textures, color string) cubeTexture {
return cubeTexture{
Normal: newNamedTexture(textures, "cube_"+color, tins2021.GenerateCube(color)),
Inversed: newNamedTexture(textures, "cube_"+color+"_inversed", tins2021.GenerateHole(color)),
}
}
func (t cubeTexture) Scaled(textures *ui.Textures, scale float32) cubeTexture {
return cubeTexture{
Normal: t.Normal.Scaled(textures, scale),
Inversed: t.Inversed.Scaled(textures, scale),
}
} }
func loadTextureImage(ctx ui.Context, resource string) image.Image { func loadTextureImage(ctx ui.Context, resource string) image.Image {
@ -58,26 +38,29 @@ func loadTextureImage(ctx ui.Context, resource string) image.Image {
return im return im
} }
func saveTextureImage(resource string, im image.Image) { func newAnimatedTexture(ctx ui.Context, name string, frames int, render func() image.Image) tins2021.AnimatedTexture {
out, err := os.Create(resource) resourceName := fmt.Sprintf("resources/textures/%s.png", name)
if err != nil { raw := loadTextureImage(ctx, resourceName)
return
}
defer out.Close()
png.Encode(out, im)
}
func newAnimatedTexture(ctx ui.Context, name, resource string, frames int, render animatedTextureRenderFn) tins2021.AnimatedTexture {
raw := loadTextureImage(ctx, resource)
if raw == nil { if raw == nil {
raw = render() renderTime := chrono(func() {
saveTextureImage(resource, raw) raw = render()
} })
texture, err := ctx.Textures().CreateTextureGo(name, raw, true) if err := saveTextureImage(resourceName, raw); err != nil {
if err != nil { log.Printf("failed to write animated texture \"%s\" to disk (%s); error: %v\n", name, resourceName, err)
panic(err) } else {
log.Printf("generated animated texture \"%s\" in %v (and saved to disk \"%s\")\n", name, renderTime, resourceName)
}
} }
texture := tins2021.MustCreateNamedTextureImage(ctx.Textures(), name, raw)
return tins2021.NewAnimatedTexture(texture, frames) return tins2021.NewAnimatedTexture(texture, frames)
} }
type animatedTextureRenderFn func() image.Image func saveTextureImage(resource string, im image.Image) error {
os.MkdirAll(filepath.Dir(resource), 0744)
out, err := os.Create(resource)
if err != nil {
return err
}
defer out.Close()
return png.Encode(out, im)
}

View File

@ -1,23 +1,20 @@
package main package main
import ( import (
"embed"
"flag" "flag"
"image/color" "image/color"
"log" "log"
rice "github.com/GeertJohan/go.rice"
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
"opslag.de/schobers/tins2021" "opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg" "opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/addons/riceres" "opslag.de/schobers/zntg/addons/embedres"
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
) )
// #cgo windows LDFLAGS: -Wl,-subsystem,windows //go:embed resources
import "C" var resources embed.FS
//go:generate go get -u github.com/GeertJohan/go.rice/rice
//go:generate rice embed
func main() { func main() {
err := run() err := run()
@ -27,18 +24,17 @@ func main() {
} }
func openResources() ui.Resources { func openResources() ui.Resources {
box := rice.MustFindBox(`resources`) embedded := embedres.New(resources)
embedded := riceres.New(box) return ui.NewFallbackResources(ui.NewPathResources(nil, `resources`), embedded)
return ui.NewFallbackResources(ui.NewPathResources(nil, box.Name()), embedded)
} }
func run() error { func run() error {
var extract bool var extract bool
flag.BoolVar(&extract, "extract-resources", false, "extracts all resources to the current working directory") flag.BoolVar(&extract, "extract", false, "extracts all resources to the current working directory")
flag.Parse() flag.Parse()
if extract { if extract {
return copyBoxToDisk() return copyBoxToDisk(resources)
} }
res := openResources() res := openResources()

View File

@ -0,0 +1,4 @@
package main
// #cgo windows,!console LDFLAGS: -Wl,-subsystem,windows
import "C"

197
meshanimation.go Normal file
View File

@ -0,0 +1,197 @@
package tins2021
import (
"fmt"
"image"
"os"
"runtime"
"sync"
"github.com/fogleman/fauxgl"
"github.com/nfnt/resize"
"golang.org/x/image/draw"
"opslag.de/schobers/geom"
"opslag.de/schobers/geom/ints"
"opslag.de/schobers/zntg/ui"
)
const (
fovy = 40 // vertical field of view in degrees
near = 1 // near clipping plane
far = 10 // far clipping plane
)
var (
eye = fauxgl.V(0, 0, 4) // camera position
center = fauxgl.V(0, 0, 0) // view center position
up = fauxgl.V(0, 1, 0) // up vector
light = fauxgl.V(.5, 1, .75).Normalize() // light direction
)
func animateMesh(mesh *fauxgl.Mesh, hexColor string, frames int, transform MeshAnimationTransformer) image.Image {
const scale = 4
const s = 1.1
mesh.BiUnitCube()
matrix := fauxgl.Orthographic(-s, s, -s, s, near, far).Mul(fauxgl.LookAt(eye, center, up))
animation := image.NewNRGBA(image.Rect(0, 0, TextureSize*frames, TextureSize))
threads := ints.Max(1, runtime.NumCPU())
framesC := make(chan int, threads)
wait := parallel(threads, func() {
context := fauxgl.NewContext(TextureSize*scale, TextureSize*scale)
color := fauxgl.HexColor(hexColor)
for i := range framesC {
context.ClearDepthBuffer()
context.ClearColorBufferWith(fauxgl.Transparent)
shader := fauxgl.NewPhongShader(matrix, light, eye)
shader.ObjectColor = color
shader.AmbientColor = fauxgl.MakeColor(mustHexColor(`#7F7F7F`))
context.Shader = shader
copy := mesh.Copy()
transform.transform(copy, FrameState{Current: i, TotalFrames: frames})
context.DrawMesh(copy)
frame := resize.Resize(TextureSize, TextureSize, context.Image(), resize.Bilinear)
draw.Copy(animation, image.Pt(i*TextureSize, 0), frame, frame.Bounds(), draw.Src, nil)
}
})
for f := 0; f < frames; f++ {
framesC <- f
}
close(framesC)
wait.Wait()
return animation
}
func AnimatePolygon(polygon geom.PolygonF, hexColor string, frames int, transform MeshAnimationTransformer) image.Image {
mesh := generateMeshFromPolygon(polygon, .2)
return animateMesh(mesh, hexColor, frames, transform)
}
func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, frames int, transform MeshAnimationTransformer) image.Image {
path, err := resources.FetchResource(name)
if err != nil {
panic(err)
}
mesh, err := fauxgl.LoadSTL(path)
if err != nil {
panic(err)
}
return animateMesh(mesh, hexColor, frames, transform)
}
type FrameState struct {
Current int
TotalFrames int
}
func (s FrameState) Animation() float64 { return float64(s.Current) / float64(s.TotalFrames) }
func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.Mesh {
vec := func(p geom.PointF, z float64) fauxgl.Vector { return fauxgl.V(p.X, p.Y, z) }
tri := fauxgl.NewTriangleForPoints
face := func(q, r, s geom.PointF, n float64) *fauxgl.Triangle {
return tri(vec(q, n*thickness), vec(r, n*thickness), vec(s, n*thickness))
}
var triangles []*fauxgl.Triangle
// generate front & back
for _, t := range polygon.Triangulate() {
triangles = append(triangles,
face(t.Points[0], t.Points[1], t.Points[2], 1), // front
face(t.Points[2], t.Points[1], t.Points[0], -1), // back
)
}
// generate side
back, front := -thickness, thickness
for i, p := range polygon.Points {
next := polygon.Points[(i+1)%len(polygon.Points)]
q, r, s, t := vec(p, back), vec(next, back), vec(next, front), vec(p, front)
triangles = append(triangles, tri(q, r, s), tri(q, s, t))
}
mesh := fauxgl.NewTriangleMesh(triangles)
return mesh
}
func iterate(n int, threads int) <-chan int {
iterator := make(chan int, threads)
go func() {
for i := 0; i < n; i++ {
iterator <- i
}
}()
return iterator
}
func parallel(n int, action func()) *sync.WaitGroup {
wait := &sync.WaitGroup{}
wait.Add(n)
for i := 0; i < n; i++ {
go func() {
action()
wait.Done()
}()
}
return wait
}
type MeshAnimationTransformer interface {
transform(*fauxgl.Mesh, FrameState)
}
type MeshRotateAnimation struct{}
func (MeshRotateAnimation) transform(mesh *fauxgl.Mesh, s FrameState) {
mesh.Transform(fauxgl.Rotate(up, 2*geom.Pi*s.Animation()))
}
type MeshWobbleTransformation struct {
Wobble float64
}
func (a MeshWobbleTransformation) animate(s FrameState) float64 {
animation := float64(s.Current) / float64(s.TotalFrames)
animation += .25
if animation >= 1 {
animation -= 1
}
return geom.Abs(animation*4-2) - 1
}
func (a MeshWobbleTransformation) transform(mesh *fauxgl.Mesh, s FrameState) {
mesh.Transform(fauxgl.Rotate(up, a.animate(s)*a.Wobble*geom.Pi/180))
}
func saveMeshSTL(path, name string, mesh *fauxgl.Mesh) error {
stl, err := os.Create(path)
if err != nil {
return err
}
defer stl.Close()
fmt.Fprintf(stl, "solid %s\n", name)
for _, triangle := range mesh.Triangles {
normal := triangle.Normal()
fmt.Fprintf(stl, " facet normal %f, %f, %f\n", normal.X, normal.Y, normal.Z)
fmt.Fprintf(stl, " outer loop\n")
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V1.Position.X, triangle.V1.Position.Y, triangle.V1.Position.Z)
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V2.Position.X, triangle.V2.Position.Y, triangle.V2.Position.Z)
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V3.Position.X, triangle.V3.Position.Y, triangle.V3.Position.Z)
fmt.Fprintf(stl, " endloop\n")
fmt.Fprintf(stl, " endfacet\n")
}
fmt.Fprintf(stl, "endsolid %s\n", name)
return nil
}
func SaveSTLFromPolygon(path, name string, polygon geom.PolygonF, thickness float64) {
mesh := generateMeshFromPolygon(polygon, thickness)
saveMeshSTL(path, name, mesh)
}

39
namedtexture.go Normal file
View File

@ -0,0 +1,39 @@
package tins2021
import (
"image"
"opslag.de/schobers/zntg/ui"
)
type NamedTexture struct {
textures *ui.Textures
name string
}
func NewNamedTexture(textures *ui.Textures, name string) NamedTexture {
return NamedTexture{
textures: textures,
name: name,
}
}
func CreateNamedTextureImage(textures *ui.Textures, name string, im image.Image) (NamedTexture, error) {
_, err := textures.CreateTextureGo(name, im, true)
if err != nil {
return NamedTexture{}, err
}
return NewNamedTexture(textures, name), nil
}
func MustCreateNamedTextureImage(textures *ui.Textures, name string, im image.Image) NamedTexture {
texture, err := CreateNamedTextureImage(textures, name, im)
if err != nil {
panic(err)
}
return texture
}
func (t NamedTexture) Scaled(scale float32) ui.Texture { return t.textures.ScaledByName(t.name, scale) }
func (t NamedTexture) Texture() ui.Texture { return t.textures.Texture(t.name) }

View File

@ -1,211 +0,0 @@
package tins2021
import (
"fmt"
"image"
"os"
"github.com/fogleman/fauxgl"
"github.com/nfnt/resize"
"golang.org/x/image/draw"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
const (
fovy = 40 // vertical field of view in degrees
near = 1 // near clipping plane
far = 10 // far clipping plane
)
var (
eye = fauxgl.V(0, 0, 4) // camera position
center = fauxgl.V(0, 0, 0) // view center position
up = fauxgl.V(0, 1, 0) // up vector
light = fauxgl.V(.5, 1, .75).Normalize() // light direction
)
func AnimatePolygon(polygon geom.PolygonF, hexColor string, renderer AnimationRenderer) image.Image {
mesh := generateMeshFromPolygon(polygon, .2)
renderer.setup(mesh)
return renderMeshAnimation(hexColor, renderer.frames(), renderer.render)
}
func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, renderer AnimationRenderer) image.Image {
path, err := resources.FetchResource(name)
if err != nil {
panic(err)
}
mesh, err := fauxgl.LoadSTL(path)
if err != nil {
panic(err)
}
renderer.setup(mesh)
return renderMeshAnimation(hexColor, renderer.frames(), renderer.render)
}
type animationRendererBase struct {
Frames int
Mesh *fauxgl.Mesh
}
func (r animationRendererBase) frames() int { return r.Frames }
func (r *animationRendererBase) setup(mesh *fauxgl.Mesh) {
r.Mesh = mesh
mesh.BiUnitCube()
}
var _ AnimationRenderer = &RotateAnimationRenderer{}
var _ AnimationRenderer = &WobbleAnimationRenderer{}
type AnimationRenderer interface {
frames() int
setup(*fauxgl.Mesh)
render(*fauxgl.Context, int, float64)
}
func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.Mesh {
vec := func(p geom.PointF, z float64) fauxgl.Vector { return fauxgl.V(p.X, p.Y, z) }
tri := fauxgl.NewTriangleForPoints
face := func(q, r, s geom.PointF, n float64) *fauxgl.Triangle {
return tri(vec(q, n*thickness), vec(r, n*thickness), vec(s, n*thickness))
}
var triangles []*fauxgl.Triangle
// generate front & back
for _, t := range polygon.Triangulate() {
triangles = append(triangles,
face(t.Points[0], t.Points[1], t.Points[2], 1), // front
face(t.Points[2], t.Points[1], t.Points[0], -1), // back
)
}
// generate side
back, front := -thickness, thickness
for i, p := range polygon.Points {
next := polygon.Points[(i+1)%len(polygon.Points)]
q, r, s, t := vec(p, back), vec(next, back), vec(next, front), vec(p, front)
triangles = append(triangles, tri(q, r, s), tri(q, s, t))
}
mesh := fauxgl.NewTriangleMesh(triangles)
return mesh
}
func renderMeshAnimation(hexColor string, frames int, render func(*fauxgl.Context, int, float64)) image.Image {
const scale = 4
context := fauxgl.NewContext(TextureSize*scale, TextureSize*scale)
// matrix := fauxgl.LookAt(eye, center, up).Perspective(fovy, 1, near, far)
const s = 1.1
// rot3 := func(m fauxgl.Matrix) fauxgl.Matrix {
// return fauxgl.Matrix{
// X00: m.X20, X01: m.X10, X02: m.X00, X03: m.X03,
// X10: m.X21, X11: m.X11, X12: m.X01, X13: m.X13,
// X20: m.X22, X21: m.X12, X22: m.X02, X23: m.X23,
// X30: m.X30, X31: m.X31, X32: m.X32, X33: m.X33,
// }
// }
// sqrt_6_1 := 1 / geom.Sqrt(6)
// iso := fauxgl.Matrix{
// X00: sqrt_6_1 * geom.Sqrt(3), X01: 0, X02: -sqrt_6_1 * geom.Sqrt(3), X03: 0,
// X10: sqrt_6_1, X11: 2 * sqrt_6_1, X12: sqrt_6_1, X13: 0,
// X20: sqrt_6_1 * geom.Sqrt(2), X21: -sqrt_6_1 * geom.Sqrt(2), X22: sqrt_6_1 * geom.Sqrt(2), X23: 0,
// X30: 0, X31: 0, X32: 0, X33: 1}
matrix := fauxgl.Orthographic(-s, s, -s, s, near, far).Mul(fauxgl.LookAt(eye, center, up))
color := fauxgl.HexColor(hexColor)
animation := image.NewNRGBA(image.Rect(0, 0, TextureSize*frames, TextureSize))
for i := 0; i < frames; i++ {
context.ClearDepthBuffer()
context.ClearColorBufferWith(fauxgl.Transparent)
shader := fauxgl.NewPhongShader(matrix, light, eye)
shader.ObjectColor = color
shader.AmbientColor = fauxgl.MakeColor(mustHexColor(`#7F7F7F`))
context.Shader = shader
render(context, i, float64(i)/float64(frames))
frame := resize.Resize(TextureSize, TextureSize, context.Image(), resize.Bilinear)
draw.Copy(animation, image.Pt(i*TextureSize, 0), frame, frame.Bounds(), draw.Src, nil)
}
return animation
}
type RotateAnimationRenderer struct {
animationRendererBase
Rotation float64
}
func NewRotateAnimation(frames int) AnimationRenderer {
return &RotateAnimationRenderer{
animationRendererBase: animationRendererBase{Frames: frames},
Rotation: 2 * geom.Pi / float64(frames),
}
}
func (a RotateAnimationRenderer) render(context *fauxgl.Context, _ int, _ float64) {
context.DrawMesh(a.Mesh)
a.Mesh.Transform(fauxgl.Rotate(up, a.Rotation))
}
func saveMeshSTL(path, name string, mesh *fauxgl.Mesh) error {
stl, err := os.Create(path)
if err != nil {
return err
}
defer stl.Close()
fmt.Fprintf(stl, "solid %s\n", name)
for _, triangle := range mesh.Triangles {
normal := triangle.Normal()
fmt.Fprintf(stl, " facet normal %f, %f, %f\n", normal.X, normal.Y, normal.Z)
fmt.Fprintf(stl, " outer loop\n")
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V1.Position.X, triangle.V1.Position.Y, triangle.V1.Position.Z)
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V2.Position.X, triangle.V2.Position.Y, triangle.V2.Position.Z)
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V3.Position.X, triangle.V3.Position.Y, triangle.V3.Position.Z)
fmt.Fprintf(stl, " endloop\n")
fmt.Fprintf(stl, " endfacet\n")
}
fmt.Fprintf(stl, "endsolid %s\n", name)
return nil
}
func SaveSTLFromPolygon(path, name string, polygon geom.PolygonF, thickness float64) {
mesh := generateMeshFromPolygon(polygon, thickness)
saveMeshSTL(path, name, mesh)
}
type WobbleAnimationRenderer struct {
animationRendererBase
Wobble float64
}
func NewWobbleAnimation(frames int, wobble float64) AnimationRenderer {
return &WobbleAnimationRenderer{
animationRendererBase: animationRendererBase{Frames: frames},
Wobble: wobble,
}
}
func (a WobbleAnimationRenderer) animate(frame float64) float64 {
frame += .25
if frame >= 1 {
frame -= 1
}
// return geom.Cos(float64(frame) * 2 * geom.Pi / float64(a.Frames))
return geom.Abs(frame*4-2) - 1
}
func (a WobbleAnimationRenderer) render(context *fauxgl.Context, frame int, animation float64) {
context.DrawMesh(a.Mesh)
curr := a.animate(animation)
next := a.animate(float64(frame+1) / float64(a.Frames))
a.Mesh.Transform(fauxgl.Rotate(up, (next-curr)*a.Wobble*geom.Pi/180))
}

View File

@ -8,7 +8,8 @@ import (
) )
type Settings struct { type Settings struct {
Window WindowSettings Controls ControlsSettings
Window WindowSettings
} }
func SettingsPath() (string, error) { func SettingsPath() (string, error) {
@ -34,6 +35,14 @@ func (s *Settings) Store() error {
return zntg.EncodeJSON(path, s) return zntg.EncodeJSON(path, s)
} }
type ControlsSettings struct {
Type string
MoveDownRight string
MoveDownLeft string
MoveUpLeft string
MoveUpRight string
}
type WindowSettings struct { type WindowSettings struct {
Location *geom.Point Location *geom.Point
Size *geom.Point Size *geom.Point