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
scripts/build
cmd/tins2021/rice-box.go

View File

@ -19,7 +19,6 @@
* [go-colurful](###go-colurful)
* [resize](###resize)
* [testify](###testify)
* [go.rice](###rice)
## 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).
## 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
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,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
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.
- [ ] Add music & sounds.
- [ ] Keep score/difficulty level (resume & restart).
- [ ] Explain controls on info page.
- [ ] Fix usage of go/embed (and remove rice again).
- [X] ~~Explain controls on info page~~ add settings for controls.
- [X] Fix usage of go/embed (and remove rice again).
- [X] Add monster animations (~~jumping on tile &~~ towards new tile).
- [ ] Scale icons (heart & star on right side) when playing.
- [ ] 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"
"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 {
LastUpdate time.Time
Frame int

View File

@ -35,18 +35,18 @@ const fpsOverlayName = `fps`
func (a *app) Init(ctx ui.Context) error {
if err := a.loadFonts(ctx,
fontDescriptor{"debug", "fonts/FiraMono-Regular.ttf", 12},
fontDescriptor{"default", "fonts/escheresk.ttf", 48},
fontDescriptor{"small", "fonts/escheresk.ttf", 16},
fontDescriptor{"score", "fonts/FiraMono-Regular.ttf", 24},
fontDescriptor{"title", "fonts/escher.ttf", 80},
fontDescriptor{"debug", "resources/fonts/FiraMono-Regular.ttf", 12},
fontDescriptor{"default", "resources/fonts/escheresk.ttf", 48},
fontDescriptor{"small", "resources/fonts/escheresk.ttf", 16},
fontDescriptor{"score", "resources/fonts/FiraMono-Regular.ttf", 24},
fontDescriptor{"title", "resources/fonts/escher.ttf", 80},
); err != nil {
return err
}
textureLoader := tins2021.NewResourceLoader()
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)
return err
}); err != nil {
@ -55,7 +55,7 @@ func (a *app) Init(ctx ui.Context) error {
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.context.ShowMainMenu(ctx)

View File

@ -1,8 +1,6 @@
package main
import (
"image"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui"
)
@ -10,36 +8,29 @@ import (
type appContext struct {
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 {
newAnimatedTexture(ctx, "star", "images/star.png", defaultAnimationFrames, func() image.Image {
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))
})
func newAppContext(ctx ui.Context, settings *tins2021.Settings, setView func(ui.Control)) *appContext {
textures := textureGenerator{}
app := &appContext{
setView: setView,
MonsterTextureNames: map[tins2021.MonsterType]string{
tins2021.MonsterTypeStraight: "straight-walking-monster",
tins2021.MonsterTypeRandom: "random-walking-monster",
tins2021.MonsterTypeChaser: "chasing-monster",
setView: setView,
Settings: settings,
StarTexture: newAnimatedTexture(ctx, "star", defaultAnimationFrames, textures.Star),
HeartTexture: newAnimatedTexture(ctx, "heart", defaultAnimationFrames, textures.Heart),
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
}
@ -73,6 +64,10 @@ func (app *appContext) ShowCredits(ctx ui.Context) {
app.setView(newCredits(app, ctx))
}
func (app *appContext) ShowSettings(ctx ui.Context) {
app.setView(newSettings(app, ctx))
}
func (app *appContext) ShowInfo(ctx ui.Context) {
app.setView(newInfo(app, ctx))
}

View File

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

View File

@ -1,12 +1,11 @@
package main
import (
"embed"
"io"
"io/fs"
"os"
"path/filepath"
rice "github.com/GeertJohan/go.rice"
)
func copyFile(path string, file fs.File) error {
@ -21,19 +20,19 @@ func copyFile(path string, file fs.File) error {
return err
}
func copyBoxToDisk() error {
box := rice.MustFindBox(`resources`)
return box.Walk("", func(path string, info os.FileInfo, err error) error {
func copyBoxToDisk(resources embed.FS) error {
return fs.WalkDir(resources, ".", func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if info.IsDir() {
if entry.IsDir() {
return nil
}
src, err := box.Open(path)
src, err := resources.Open(path)
if err != nil {
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"
)
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 {
ui.StackPanel
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 {
ui.ControlBase
@ -22,44 +27,42 @@ type infoLegend struct {
}
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) {
bounds := l.Bounds()
separator := .3 * bounds.Dx()
textureHeight := float32(l.Icon.Height())
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")
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 {
legend := ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("star", infoLegenIconSize),
Icon: ctx.Textures().ScaledByName("star", infoLegendIconSize),
Description: "Collect them to complete the level.",
})
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("heart", infoLegenIconSize),
Icon: ctx.Textures().ScaledByName("heart", infoLegendIconSize),
Description: "Gives (back) a life.",
})
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.",
})
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("random-walking-monster", infoLegenIconSize),
Icon: ctx.Textures().ScaledByName("random-walking-monster", infoLegendIconSize),
Description: "Monster that walks randomly.",
})
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("chasing-monster", infoLegenIconSize),
Icon: ctx.Textures().ScaledByName("chasing-monster", infoLegendIconSize),
Description: "Monster that walks towards you.",
})
})
@ -70,7 +73,7 @@ func newInfo(app *appContext, ctx ui.Context) ui.Control {
label("", "score"), // spacing
paragraphOpts(infoText, "score", labelOptions{TextAlignment: ui.AlignCenter}),
label("", "score"), // spacing
legend,
Center(legend),
},
},
Orientation: ui.OrientationVertical,

View File

@ -20,25 +20,60 @@ type levelController struct {
Level *tins2021.Level
Cubes cubeTexture
Cube tins2021.NamedTexture
Inverted tins2021.NamedTexture
Animations map[string]*tins2021.Animations
IdleMonsters *tins2021.Animations
MovingMonsters *tins2021.Animations
SmallFont *tins2021.BitmapFont
Controls map[ui.Key]tins2021.Direction
}
func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *levelController {
control := &levelController{app: app}
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...)
if err != nil {
panic(err)
}
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)
@ -75,15 +110,9 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
switch e := e.(type) {
case *ui.KeyDownEvent:
switch e.Key {
case ui.KeyW:
r.Level.MovePlayer(tins2021.DirectionUpLeft)
case ui.KeyD:
r.Level.MovePlayer(tins2021.DirectionUpRight)
case ui.KeyS:
r.Level.MovePlayer(tins2021.DirectionDownRight)
case ui.KeyA:
r.Level.MovePlayer(tins2021.DirectionDownLeft)
dir, ok := r.Controls[e.Key]
if ok {
r.Level.MovePlayer(dir)
}
}
@ -146,17 +175,15 @@ func (r *levelController) Play(level *tins2021.Level) {
r.Level = level
r.Animations = map[string]*tins2021.Animations{
"star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true),
"heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
"star": tins2021.NewAnimations(50*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.MovingMonsters = tins2021.NewAnimations(16*time.Millisecond, 50, false, false)
for monster := range level.Monsters {
r.IdleMonsters.Frame(monster)
}
for _, monster := range r.app.MonsterTextureNames {
r.Animations[monster] = tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true)
}
}
const defaultAnimationFrames = 20
@ -189,20 +216,21 @@ func (r levelController) Render(ctx ui.Context) {
}
renderer := ctx.Renderer()
textures := ctx.Textures()
cubes := r.Cubes.Scaled(textures, scale)
cube := r.Cube.Scaled(scale)
inverted := r.Inverted.Scaled(scale)
cubeWidth := float32(cubes.Normal.Width())
cubeHeight := float32(cubes.Normal.Height())
cubeWidth := float32(cube.Width())
cubeHeight := float32(cube.Height())
player := ctx.Textures().ScaledByName("dwarf", scale*.6)
star := tins2021.FitAnimatedTexture(textures, "star", scale*.4, defaultAnimationFrames)
heart := tins2021.FitAnimatedTexture(textures, "heart", scale*.4, defaultAnimationFrames)
star := r.app.StarTexture.Scale(scale * .4)
heart := r.app.HeartTexture.Scale(scale * .4)
monsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{}
for typ, name := range r.app.MonsterTextureNames {
monsterTextures[typ] = tins2021.FitAnimatedTexture(textures, name, scale*.4, defaultAnimationFrames)
for typ, animation := range r.app.MonsterTextures {
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)
@ -222,9 +250,9 @@ func (r levelController) Render(ctx ui.Context) {
continue
}
screenPos, platformPos := positionOfTile(pos)
tileTexture := cubes.Normal.Texture
tileTexture := cube
if tile.Inversed {
tileTexture = cubes.Inversed.Texture
tileTexture = inverted
}
renderer.DrawTexturePoint(tileTexture, screenPos)
if r.app.Debug {
@ -253,7 +281,6 @@ func (r levelController) Render(ctx ui.Context) {
if tile == nil {
continue
}
name := r.app.MonsterTextureNames[monsterType.Type()]
texture := monsterTextures[monsterType.Type()]
_, platformPos := positionOfTile(pos)
if target, ok := r.Level.MonsterTargets[pos]; ok {
@ -262,15 +289,15 @@ func (r levelController) Render(ctx ui.Context) {
delta := targetPlatformPos.Sub(platformPos)
curve := geom.PtF32(0, .6*geom.Sin32(dt*geom.Pi)*textureWidth)
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 {
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
scoreFont := ctx.Fonts().Font("score")
fontOffsetY := .5 * (float32(star.Texture.Height()) - scoreFont.Height())
fontOffsetY := .5 * (float32(propHeight) - scoreFont.Height())
// stars & hearts
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) {
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("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
import (
"fmt"
"image"
"image/png"
"log"
"os"
"path/filepath"
"time"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui"
)
type namedTexture struct {
ui.Texture
Name string
func chrono(action func()) time.Duration {
start := time.Now()
action()
return time.Now().Sub(start)
}
func newNamedTexture(textures *ui.Textures, name string, im image.Image) namedTexture {
texture, err := textures.CreateTextureGo(name, im, true)
if err != nil {
panic(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 chronoErr(action func() error) (time.Duration, error) {
start := time.Now()
err := action()
return time.Now().Sub(start), err
}
func loadTextureImage(ctx ui.Context, resource string) image.Image {
@ -58,26 +38,29 @@ func loadTextureImage(ctx ui.Context, resource string) image.Image {
return im
}
func saveTextureImage(resource string, im image.Image) {
out, err := os.Create(resource)
if err != nil {
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)
func newAnimatedTexture(ctx ui.Context, name string, frames int, render func() image.Image) tins2021.AnimatedTexture {
resourceName := fmt.Sprintf("resources/textures/%s.png", name)
raw := loadTextureImage(ctx, resourceName)
if raw == nil {
raw = render()
saveTextureImage(resource, raw)
}
texture, err := ctx.Textures().CreateTextureGo(name, raw, true)
if err != nil {
panic(err)
renderTime := chrono(func() {
raw = render()
})
if err := saveTextureImage(resourceName, raw); err != nil {
log.Printf("failed to write animated texture \"%s\" to disk (%s); error: %v\n", name, resourceName, 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)
}
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
import (
"embed"
"flag"
"image/color"
"log"
rice "github.com/GeertJohan/go.rice"
"opslag.de/schobers/geom"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/addons/riceres"
"opslag.de/schobers/zntg/addons/embedres"
"opslag.de/schobers/zntg/ui"
)
// #cgo windows LDFLAGS: -Wl,-subsystem,windows
import "C"
//go:generate go get -u github.com/GeertJohan/go.rice/rice
//go:generate rice embed
//go:embed resources
var resources embed.FS
func main() {
err := run()
@ -27,18 +24,17 @@ func main() {
}
func openResources() ui.Resources {
box := rice.MustFindBox(`resources`)
embedded := riceres.New(box)
return ui.NewFallbackResources(ui.NewPathResources(nil, box.Name()), embedded)
embedded := embedres.New(resources)
return ui.NewFallbackResources(ui.NewPathResources(nil, `resources`), embedded)
}
func run() error {
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()
if extract {
return copyBoxToDisk()
return copyBoxToDisk(resources)
}
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 {
Window WindowSettings
Controls ControlsSettings
Window WindowSettings
}
func SettingsPath() (string, error) {
@ -34,6 +35,14 @@ func (s *Settings) Store() error {
return zntg.EncodeJSON(path, s)
}
type ControlsSettings struct {
Type string
MoveDownRight string
MoveDownLeft string
MoveUpLeft string
MoveUpRight string
}
type WindowSettings struct {
Location *geom.Point
Size *geom.Point