Compare commits
4 Commits
64ff5ac78e
...
000348339d
Author | SHA1 | Date | |
---|---|---|---|
000348339d | |||
165d1fcd26 | |||
f9da21b61c | |||
7053e7b9f2 |
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
.vscode/launch.json
|
||||
|
||||
scripts/build
|
||||
cmd/tins2021/rice-box.go
|
||||
|
28
README.md
@ -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.
|
||||
|
4
TODO.md
@ -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
@ -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
|
||||
}
|
39
animation.go
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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",
|
||||
"",
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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() })
|
||||
|
||||
|
@ -1 +1 @@
|
||||
dwarf: images/dwarf.png
|
||||
dwarf: resources/textures/gnome.png
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
347
cmd/tins2021/settings.go
Normal 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
|
||||
}
|
27
cmd/tins2021/texturegenerator.go
Normal 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) }
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
4
cmd/tins2021/tins2021_console.go
Normal file
@ -0,0 +1,4 @@
|
||||
package main
|
||||
|
||||
// #cgo windows,!console LDFLAGS: -Wl,-subsystem,windows
|
||||
import "C"
|
197
meshanimation.go
Normal 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
@ -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) }
|
@ -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))
|
||||
}
|
11
settings.go
@ -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
|
||||
|