The raycaster and piano examples would be better off in their own repo

This commit is contained in:
Sasha Koshka 2023-05-03 01:26:51 -04:00 committed by Sasha Koshka
parent abe63f4118
commit 6e1369da5c
12 changed files with 12 additions and 1123 deletions

View File

@ -1,98 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/flow"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 192, 192))
window.SetTitle("adventure")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
var world flow.Flow
world.Transition = container.DisownAll
world.Stages = map [string] func () {
"start": func () {
label := elements.NewLabelWrapped (
"you are standing next to a river.")
button0 := elements.NewButton("go in the river")
button0.OnClick(world.SwitchFunc("wet"))
button1 := elements.NewButton("walk along the river")
button1.OnClick(world.SwitchFunc("house"))
button2 := elements.NewButton("turn around")
button2.OnClick(world.SwitchFunc("bear"))
container.AdoptExpand(label)
container.Adopt(button0, button1, button2)
button0.Focus()
},
"wet": func () {
label := elements.NewLabelWrapped (
"you get completely soaked.\n" +
"you die of hypothermia.")
button0 := elements.NewButton("try again")
button0.OnClick(world.SwitchFunc("start"))
button1 := elements.NewButton("exit")
button1.OnClick(tomo.Stop)
container.AdoptExpand(label)
container.Adopt(button0, button1)
button0.Focus()
},
"house": func () {
label := elements.NewLabelWrapped (
"you are standing in front of a delapidated " +
"house.")
button1 := elements.NewButton("go inside")
button1.OnClick(world.SwitchFunc("inside"))
button0 := elements.NewButton("turn back")
button0.OnClick(world.SwitchFunc("start"))
container.AdoptExpand(label)
container.Adopt(button0, button1)
button1.Focus()
},
"inside": func () {
label := elements.NewLabelWrapped (
"you are standing inside of the house.\n" +
"it is dark, but rays of light stream " +
"through the window.\n" +
"there is nothing particularly interesting " +
"here.")
button0 := elements.NewButton("go back outside")
button0.OnClick(world.SwitchFunc("house"))
container.AdoptExpand(label)
container.Adopt(button0)
button0.Focus()
},
"bear": func () {
label := elements.NewLabelWrapped (
"you come face to face with a bear.\n" +
"it eats you (it was hungry).")
button0 := elements.NewButton("try again")
button0.OnClick(world.SwitchFunc("start"))
button1 := elements.NewButton("exit")
button1.OnClick(tomo.Stop)
container.AdoptExpand(label)
container.Adopt(button0, button1)
button0.Focus()
},
}
world.Switch("start")
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,19 +1,20 @@
package main
import "os"
import "time"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
os.Exit(0)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216))
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 200, 216))
if err != nil { return err }
window.SetTitle("Clock")
window.SetApplicationName("TomoClock")
container := elements.NewVBox(elements.SpaceBoth)
@ -24,9 +25,10 @@ func run () {
container.AdoptExpand(clock)
container.Adopt(label)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
go tick(label, clock)
return nil
}
func formatTime () (timeString string) {
@ -35,7 +37,7 @@ func formatTime () (timeString string) {
func tick (label *elements.Label, clock *fun.AnalogClock) {
for {
tomo.Do (func () {
nasin.Do (func () {
label.SetText(formatTime())
clock.SetTime(time.Now())
})

View File

@ -1,9 +1,9 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/nasin"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)

View File

@ -1,331 +0,0 @@
package main
import "math"
import "time"
import "errors"
import "github.com/faiface/beep"
import "github.com/faiface/beep/speaker"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
const sampleRate = 44100
const bufferSize = 256
var tuning = music.EqualTemparment { A4: 440 }
var waveform = 0
var playing = map[music.Note] *toneStreamer { }
var adsr = ADSR {
Attack: 5 * time.Millisecond,
Decay: 400 * time.Millisecond,
Sustain: 0.7,
Release: 500 * time.Millisecond,
}
var gain = 0.3
func main () {
speaker.Init(sampleRate, bufferSize)
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Piano")
container := containers.NewContainer(layouts.Vertical { true, true })
controlBar := containers.NewContainer(layouts.Horizontal { true, false })
waveformColumn := containers.NewContainer(layouts.Vertical { true, false })
waveformList := elements.NewList (
elements.NewListEntry("Sine", func(){ waveform = 0 }),
elements.NewListEntry("Triangle", func(){ waveform = 3 }),
elements.NewListEntry("Square", func(){ waveform = 1 }),
elements.NewListEntry("Saw", func(){ waveform = 2 }),
elements.NewListEntry("Supersaw", func(){ waveform = 4 }),
)
waveformList.OnNoEntrySelected (func(){waveformList.Select(0)})
waveformList.Select(0)
adsrColumn := containers.NewContainer(layouts.Vertical { true, false })
adsrGroup := containers.NewContainer(layouts.Horizontal { true, false })
attackSlider := elements.NewLerpSlider(0, 3 * time.Second, adsr.Attack, true)
decaySlider := elements.NewLerpSlider(0, 3 * time.Second, adsr.Decay, true)
sustainSlider := elements.NewSlider(adsr.Sustain, true)
releaseSlider := elements.NewLerpSlider(0, 3 * time.Second, adsr.Release, true)
gainSlider := elements.NewSlider(math.Sqrt(gain), false)
attackSlider.OnRelease (func () {
adsr.Attack = attackSlider.Value()
})
decaySlider.OnRelease (func () {
adsr.Decay = decaySlider.Value()
})
sustainSlider.OnRelease (func () {
adsr.Sustain = sustainSlider.Value()
})
releaseSlider.OnRelease (func () {
adsr.Release = releaseSlider.Value()
})
gainSlider.OnRelease (func () {
gain = math.Pow(gainSlider.Value(), 2)
})
patchColumn := containers.NewContainer(layouts.Vertical { true, false })
patch := func (w int, a, d time.Duration, s float64, r time.Duration) func () {
return func () {
waveform = w
adsr = ADSR {
a * time.Millisecond,
d * time.Millisecond,
s,
r * time.Millisecond,
}
waveformList.Select(w)
attackSlider .SetValue(adsr.Attack)
decaySlider .SetValue(adsr.Decay)
sustainSlider.SetValue(adsr.Sustain)
releaseSlider.SetValue(adsr.Release)
}
}
patchList := elements.NewList (
elements.NewListEntry ("Bones", patch (
0, 0, 100, 0.0, 0)),
elements.NewListEntry ("Staccato", patch (
4, 70, 500, 0, 0)),
elements.NewListEntry ("Sustain", patch (
4, 70, 200, 0.8, 500)),
elements.NewListEntry ("Upright", patch (
1, 0, 500, 0.4, 70)),
elements.NewListEntry ("Space Pad", patch (
4, 1500, 0, 1.0, 3000)),
elements.NewListEntry ("Popcorn", patch (
2, 0, 40, 0.0, 0)),
elements.NewListEntry ("Racer", patch (
3, 70, 0, 0.7, 400)),
elements.NewListEntry ("Reverse", patch (
2, 3000, 60, 0, 0)),
)
patchList.Collapse(0, 32)
patchScrollBox := containers.NewScrollContainer(false, true)
piano := fun.NewPiano(2, 5)
piano.OnPress(playNote)
piano.OnRelease(stopNote)
// honestly, if you were doing something like this for real, i'd
// encourage you to build a custom layout because this is a bit cursed.
// i need to add more layouts...
window.Adopt(container)
controlBar.Adopt(patchColumn, true)
patchColumn.Adopt(elements.NewLabel("Presets", false), false)
patchColumn.Adopt(patchScrollBox, true)
patchScrollBox.Adopt(patchList)
controlBar.Adopt(elements.NewSpacer(true), false)
controlBar.Adopt(waveformColumn, false)
waveformColumn.Adopt(elements.NewLabel("Waveform", false), false)
waveformColumn.Adopt(waveformList, true)
controlBar.Adopt(elements.NewSpacer(true), false)
adsrColumn.Adopt(elements.NewLabel("ADSR", false), false)
adsrGroup.Adopt(attackSlider, false)
adsrGroup.Adopt(decaySlider, false)
adsrGroup.Adopt(sustainSlider, false)
adsrGroup.Adopt(releaseSlider, false)
adsrColumn.Adopt(adsrGroup, true)
adsrColumn.Adopt(gainSlider, false)
controlBar.Adopt(adsrColumn, false)
container.Adopt(controlBar, true)
container.Adopt(piano, false)
piano.Focus()
window.OnClose(tomo.Stop)
window.Show()
}
type Patch struct {
ADSR
Waveform int
}
func stopNote (note music.Note) {
if _, is := playing[note]; !is { return }
speaker.Lock()
playing[note].Release()
delete(playing, note)
speaker.Unlock()
}
func playNote (note music.Note) {
streamer, _ := Tone (
sampleRate,
int(tuning.Tune(note)),
waveform,
gain,
adsr)
stopNote(note)
speaker.Lock()
playing[note] = streamer
speaker.Unlock()
speaker.Play(playing[note])
}
// https://github.com/faiface/beep/blob/v1.1.0/generators/toner.go
// Adapted to be a bit more versatile.
type toneStreamer struct {
position float64
cycles uint64
delta float64
waveform int
gain float64
adsr ADSR
released bool
complete bool
adsrPhase int
adsrPosition float64
adsrDeltas [4]float64
}
type ADSR struct {
Attack time.Duration
Decay time.Duration
Sustain float64
Release time.Duration
}
func Tone (
sampleRate beep.SampleRate,
frequency int,
waveform int,
gain float64,
adsr ADSR,
) (
*toneStreamer,
error,
) {
if int(sampleRate) / frequency < 2 {
return nil, errors.New (
"tone generator: samplerate must be at least " +
"2 times greater then frequency")
}
tone := new(toneStreamer)
tone.waveform = waveform
tone.position = 0.0
steps := float64(sampleRate) / float64(frequency)
tone.delta = 1.0 / steps
tone.gain = gain
if adsr.Attack < time.Millisecond { adsr.Attack = time.Millisecond }
if adsr.Decay < time.Millisecond { adsr.Decay = time.Millisecond }
if adsr.Release < time.Millisecond { adsr.Release = time.Millisecond }
tone.adsr = adsr
attackSteps := adsr.Attack.Seconds() * float64(sampleRate)
decaySteps := adsr.Decay.Seconds() * float64(sampleRate)
releaseSteps := adsr.Release.Seconds() * float64(sampleRate)
tone.adsrDeltas[0] = 1 / attackSteps
tone.adsrDeltas[1] = 1 / decaySteps
tone.adsrDeltas[2] = 0
tone.adsrDeltas[3] = 1 / releaseSteps
return tone, nil
}
func (tone *toneStreamer) nextSample () (sample float64) {
switch tone.waveform {
case 0:
sample = math.Sin(tone.position * 2.0 * math.Pi)
case 1:
if tone.position > 0.5 {
sample = 1
} else {
sample = -1
}
case 2:
sample = (tone.position - 0.5) * 2
case 3:
sample = 1 - math.Abs(tone.position - 0.5) * 4
case 4:
unison := 5
detuneDelta := 0.00005
detune := 0.0 - (float64(unison) / 2) * detuneDelta
for i := 0; i < unison; i ++ {
_, offset := math.Modf(detune * float64(tone.cycles) + tone.position)
sample += (offset - 0.5) * 2
detune += detuneDelta
}
sample /= float64(unison)
}
adsrGain := 0.0
switch tone.adsrPhase {
case 0: adsrGain = tone.adsrPosition
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.adsrPhase = 1
}
case 1: adsrGain = 1 + tone.adsrPosition * (tone.adsr.Sustain - 1)
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.adsrPhase = 2
}
case 2: adsrGain = tone.adsr.Sustain
if tone.released {
tone.adsrPhase = 3
}
case 3: adsrGain = (1 - tone.adsrPosition) * tone.adsr.Sustain
if tone.adsrPosition > 1 {
tone.adsrPosition = 0
tone.complete = true
}
}
sample *= adsrGain * adsrGain
tone.adsrPosition += tone.adsrDeltas[tone.adsrPhase]
_, tone.position = math.Modf(tone.position + tone.delta)
tone.cycles ++
return
}
func (tone *toneStreamer) Stream (buf [][2]float64) (int, bool) {
if tone.complete {
return 0, false
}
for i := 0; i < len(buf); i++ {
sample := 0.0
if !tone.complete {
sample = tone.nextSample() * tone.gain
}
buf[i] = [2]float64{sample, sample}
}
return len(buf), true
}
func (tone *toneStreamer) Err () error {
return nil
}
func (tone *toneStreamer) Release () {
tone.released = true
}

Binary file not shown.

View File

@ -1,124 +0,0 @@
package main
import "time"
import "git.tebibyte.media/sashakoshka/tomo"
type Game struct {
*Raycaster
running bool
tickChan <- chan time.Time
stopChan chan bool
stamina float64
health float64
controlState ControlState
onStatUpdate func ()
}
func NewGame (world World, textures Textures) (game *Game) {
game = &Game {
Raycaster: NewRaycaster(world, textures),
stopChan: make(chan bool),
}
game.Raycaster.OnControlStateChange (func (state ControlState) {
game.controlState = state
})
game.stamina = 0.5
game.health = 1
return
}
func (game *Game) Start () {
if game.running == true { return }
game.running = true
go game.run()
}
func (game *Game) Stop () {
select {
case game.stopChan <- true:
default:
}
}
func (game *Game) Stamina () float64 {
return game.stamina
}
func (game *Game) Health () float64 {
return game.health
}
func (game *Game) OnStatUpdate (callback func ()) {
game.onStatUpdate = callback
}
func (game *Game) tick () {
moved := false
statUpdate := false
speed := 0.07
if game.controlState.Sprint {
speed = 0.16
}
if game.stamina <= 0 {
speed = 0
}
if game.controlState.WalkForward {
game.Walk(speed)
moved = true
}
if game.controlState.WalkBackward {
game.Walk(-speed)
moved = true
}
if game.controlState.StrafeLeft {
game.Strafe(-speed)
moved = true
}
if game.controlState.StrafeRight {
game.Strafe(speed)
moved = true
}
if game.controlState.LookLeft {
game.Rotate(-0.1)
}
if game.controlState.LookRight {
game.Rotate(0.1)
}
if moved {
game.stamina -= speed / 50
statUpdate = true
} else if game.stamina < 1 {
game.stamina += 0.005
statUpdate = true
}
if game.stamina > 1 {
game.stamina = 1
}
if game.stamina < 0 {
game.stamina = 0
}
tomo.Do(game.Invalidate)
if statUpdate && game.onStatUpdate != nil {
tomo.Do(game.onStatUpdate)
}
}
func (game *Game) run () {
ticker := time.NewTicker(time.Second / 30)
game.tickChan = ticker.C
for game.running {
select {
case <- game.tickChan:
game.tick()
case <- game.stopChan:
ticker.Stop()
}
}
}

View File

@ -1,78 +0,0 @@
package main
import "bytes"
import _ "embed"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
//go:embed wall.png
var wallTextureBytes []uint8
func main () {
tomo.Run(run)
}
// FIXME this entire example seems to be broken
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 640, 480))
window.SetTitle("Raycaster")
container := elements.NewVBox(elements.SpaceNone)
window.Adopt(container)
wallTexture, _ := TextureFrom(bytes.NewReader(wallTextureBytes))
game := NewGame (World {
Data: []int {
1,1,1,1,1,1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,1,1,1,1,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,1,0,1,
1,0,0,0,0,0,0,0,1,0,0,0,1,
1,0,0,0,0,0,0,0,1,0,1,1,1,
1,1,1,1,1,1,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,0,1,1,
1,0,0,1,0,0,0,0,0,0,0,0,1,
1,0,1,1,1,0,0,0,0,0,0,0,1,
1,0,0,1,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,1,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,
},
Stride: 13,
}, Textures {
wallTexture,
})
topBar := elements.NewHBox(elements.SpaceBoth)
staminaBar := elements.NewProgressBar(game.Stamina())
healthBar := elements.NewProgressBar(game.Health())
topBar.Adopt(elements.NewLabel("Stamina:"))
topBar.AdoptExpand(staminaBar)
topBar.Adopt(elements.NewLabel("Health:"))
topBar.AdoptExpand(healthBar)
container.Adopt(topBar)
container.AdoptExpand(game.Raycaster)
game.Focus()
game.OnStatUpdate (func () {
staminaBar.SetProgress(game.Stamina())
})
game.Start()
window.OnClose(tomo.Stop)
window.Show()
popups.NewDialog (
popups.DialogKindInfo,
window,
"Welcome to the backrooms",
"You've no-clipped into the backrooms!\n" +
"Move with WASD, and look with the arrow keys.\n" +
"Keep an eye on your health and stamina.")
}

View File

@ -1,193 +0,0 @@
package main
import "math"
import "image"
type World struct {
Data []int
Stride int
}
func (world World) At (position image.Point) int {
if position.X < 0 { return 0 }
if position.Y < 0 { return 0 }
if position.X >= world.Stride { return 0 }
index := position.X + position.Y * world.Stride
if index >= len(world.Data) { return 0 }
return world.Data[index]
}
type Vector struct {
X, Y float64
}
func (vector Vector) Point () (image.Point) {
return image.Pt(int(vector.X), int(vector.Y))
}
func (vector Vector) Add (other Vector) Vector {
return Vector {
vector.X + other.X,
vector.Y + other.Y,
}
}
func (vector Vector) Sub (other Vector) Vector {
return Vector {
vector.X - other.X,
vector.Y - other.Y,
}
}
func (vector Vector) Mul (by float64) Vector {
return Vector {
vector.X * by,
vector.Y * by,
}
}
func (vector Vector) Hypot () float64 {
return math.Hypot(vector.X, vector.Y)
}
type Camera struct {
Vector
Angle float64
Fov float64
}
func (camera *Camera) Rotate (by float64) {
camera.Angle += by
if camera.Angle < 0 { camera.Angle += math.Pi * 2 }
if camera.Angle > math.Pi * 2 { camera.Angle = 0 }
}
func (camera *Camera) Walk (by float64) {
delta := camera.Delta()
camera.X += delta.X * by
camera.Y += delta.Y * by
}
func (camera *Camera) Strafe (by float64) {
delta := camera.OffsetDelta()
camera.X += delta.X * by
camera.Y += delta.Y * by
}
func (camera *Camera) Delta () Vector {
return Vector {
math.Cos(camera.Angle),
math.Sin(camera.Angle),
}
}
func (camera *Camera) OffsetDelta () Vector {
offset := math.Pi / 2
return Vector {
math.Cos(camera.Angle + offset),
math.Sin(camera.Angle + offset),
}
}
type Ray struct {
Vector
Angle float64
}
func (ray *Ray) Cast (
world World,
max int,
) (
distance float64,
hit Vector,
wall int,
horizontal bool,
) {
// return ray.castV(world, max)
cellAt := world.At(ray.Point())
if cellAt > 0 {
return 0, Vector { }, cellAt, false
}
hDistance, hPos, hWall := ray.castH(world, max)
vDistance, vPos, vWall := ray.castV(world, max)
if hDistance < vDistance {
return hDistance, hPos, hWall, true
} else {
return vDistance, vPos, vWall, false
}
}
func (ray *Ray) castH (world World, max int) (distance float64, hit Vector, wall int) {
var position Vector
var delta Vector
var offset Vector
ray.Angle = math.Mod(ray.Angle, math.Pi * 2)
if ray.Angle < 0 {
ray.Angle += math.Pi * 2
}
tan := math.Tan(math.Pi - ray.Angle)
if ray.Angle > math.Pi {
// facing up
position.Y = math.Floor(ray.Y)
delta.Y = -1
offset.Y = -1
} else if ray.Angle < math.Pi {
// facing down
position.Y = math.Floor(ray.Y) + 1
delta.Y = 1
} else {
// facing straight left or right
return float64(max), Vector { }, 0
}
position.X = ray.X + (ray.Y - position.Y) / tan
delta.X = -delta.Y / tan
// cast da ray
steps := 0
for {
cell := world.At(position.Add(offset).Point())
if cell > 0 || steps > max { break }
position = position.Add(delta)
steps ++
}
return position.Sub(ray.Vector).Hypot(),
position,
world.At(position.Add(offset).Point())
}
func (ray *Ray) castV (world World, max int) (distance float64, hit Vector, wall int) {
var position Vector
var delta Vector
var offset Vector
tan := math.Tan(math.Pi - ray.Angle)
offsetAngle := math.Mod(ray.Angle + math.Pi / 2, math.Pi * 2)
if offsetAngle > math.Pi {
// facing left
position.X = math.Floor(ray.X)
delta.X = -1
offset.X = -1
} else if offsetAngle < math.Pi {
// facing right
position.X = math.Floor(ray.X) + 1
delta.X = 1
} else {
// facing straight left or right
return float64(max), Vector { }, 0
}
position.Y = ray.Y + (ray.X - position.X) * tan
delta.Y = -delta.X * tan
// cast da ray
steps := 0
for {
cell := world.At(position.Add(offset).Point())
if cell > 0 || steps > max { break }
position = position.Add(delta)
steps ++
}
return position.Sub(ray.Vector).Hypot(),
position,
world.At(position.Add(offset).Point())
}

View File

@ -1,241 +0,0 @@
package main
// import "fmt"
import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type ControlState struct {
WalkForward bool
WalkBackward bool
StrafeLeft bool
StrafeRight bool
LookLeft bool
LookRight bool
Sprint bool
}
type Raycaster struct {
entity tomo.FocusableEntity
config config.Wrapped
Camera
controlState ControlState
world World
textures Textures
onControlStateChange func (ControlState)
renderDistance int
}
func NewRaycaster (world World, textures Textures) (element *Raycaster) {
element = &Raycaster {
Camera: Camera {
Vector: Vector {
X: 1,
Y: 1,
},
Angle: math.Pi / 3,
Fov: 1,
},
world: world,
textures: textures,
renderDistance: 8,
}
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.entity.SetMinimumSize(64, 64)
return
}
func (element *Raycaster) Entity () tomo.Entity {
return element.entity
}
func (element *Raycaster) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
// artist.FillRectangle(element.core, artist.Uhex(0x000000FF), bounds)
width := bounds.Dx()
height := bounds.Dy()
halfway := bounds.Max.Y - height / 2
ray := Ray { Angle: element.Camera.Angle - element.Camera.Fov / 2 }
for x := 0; x < width; x ++ {
ray.X = element.Camera.X
ray.Y = element.Camera.Y
distance, hitPoint, wall, horizontal := ray.Cast (
element.world, element.renderDistance)
distance *= math.Cos(ray.Angle - element.Camera.Angle)
textureX := math.Mod(hitPoint.X + hitPoint.Y, 1)
if textureX < 0 { textureX += 1 }
wallHeight := height
if distance > 0 {
wallHeight = int((float64(height) / 2.0) / float64(distance))
}
shade := 1.0
if horizontal {
shade *= 0.8
}
shade *= 1 - distance / float64(element.renderDistance)
if shade < 0 { shade = 0 }
ceilingColor := color.RGBA { 0x00, 0x00, 0x00, 0xFF }
floorColor := color.RGBA { 0x39, 0x49, 0x25, 0xFF }
// draw
data, stride := destination.Buffer()
wallStart := halfway - wallHeight
wallEnd := halfway + wallHeight
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
switch {
case y < wallStart:
data[y * stride + x + bounds.Min.X] = ceilingColor
case y < wallEnd:
textureY :=
float64(y - halfway) /
float64(wallEnd - wallStart) + 0.5
// fmt.Println(textureY)
wallColor := element.textures.At (wall, Vector {
textureX,
textureY,
})
wallColor = shadeColor(wallColor, shade)
data[y * stride + x + bounds.Min.X] = wallColor
default:
data[y * stride + x + bounds.Min.X] = floorColor
}
}
// increment angle
ray.Angle += element.Camera.Fov / float64(width)
}
// element.drawMinimap()
}
func (element *Raycaster) Invalidate () {
element.entity.Invalidate()
}
func (element *Raycaster) OnControlStateChange (callback func (ControlState)) {
element.onControlStateChange = callback
}
func (element *Raycaster) Focus () {
element.entity.Focus()
}
func (element *Raycaster) SetEnabled (bool) { }
func (element *Raycaster) Enabled () bool { return true }
func (element *Raycaster) HandleFocusChange () { }
func (element *Raycaster) HandleMouseDown (x, y int, button input.Button) {
element.entity.Focus()
}
func (element *Raycaster) HandleMouseUp (x, y int, button input.Button) { }
func (element *Raycaster) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
switch key {
case input.KeyLeft: element.controlState.LookLeft = true
case input.KeyRight: element.controlState.LookRight = true
case 'a', 'A': element.controlState.StrafeLeft = true
case 'd', 'D': element.controlState.StrafeRight = true
case 'w', 'W': element.controlState.WalkForward = true
case 's', 'S': element.controlState.WalkBackward = true
case input.KeyLeftControl: element.controlState.Sprint = true
default: return
}
if element.onControlStateChange != nil {
element.onControlStateChange(element.controlState)
}
}
func (element *Raycaster) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
switch key {
case input.KeyLeft: element.controlState.LookLeft = false
case input.KeyRight: element.controlState.LookRight = false
case 'a', 'A': element.controlState.StrafeLeft = false
case 'd', 'D': element.controlState.StrafeRight = false
case 'w', 'W': element.controlState.WalkForward = false
case 's', 'S': element.controlState.WalkBackward = false
case input.KeyLeftControl: element.controlState.Sprint = false
default: return
}
if element.onControlStateChange != nil {
element.onControlStateChange(element.controlState)
}
}
func shadeColor (c color.RGBA, brightness float64) color.RGBA {
return color.RGBA {
uint8(float64(c.R) * brightness),
uint8(float64(c.G) * brightness),
uint8(float64(c.B) * brightness),
c.A,
}
}
func (element *Raycaster) drawMinimap (destination canvas.Canvas) {
bounds := element.entity.Bounds()
scale := 8
for y := 0; y < len(element.world.Data) / element.world.Stride; y ++ {
for x := 0; x < element.world.Stride; x ++ {
cellPt := image.Pt(x, y)
cell := element.world.At(cellPt)
cellBounds :=
image.Rectangle {
cellPt.Mul(scale),
cellPt.Add(image.Pt(1, 1)).Mul(scale),
}.Add(bounds.Min)
cellColor := color.RGBA { 0x22, 0x22, 0x22, 0xFF }
if cell > 0 {
cellColor = color.RGBA { 0xFF, 0xFF, 0xFF, 0xFF }
}
shapes.FillColorRectangle (
destination,
cellColor,
cellBounds.Inset(1))
}}
playerPt := element.Camera.Mul(float64(scale)).Point().Add(bounds.Min)
playerAnglePt :=
element.Camera.Add(element.Camera.Delta()).
Mul(float64(scale)).Point().Add(bounds.Min)
ray := Ray { Vector: element.Camera.Vector, Angle: element.Camera.Angle }
_, hit, _, _ := ray.Cast(element.world, 8)
hitPt := hit.Mul(float64(scale)).Point().Add(bounds.Min)
playerBounds := image.Rectangle { playerPt, playerPt }.Inset(scale / -8)
shapes.FillColorEllipse (
destination,
artist.Hex(0xFFFFFFFF),
playerBounds)
shapes.ColorLine (
destination,
artist.Hex(0xFFFFFFFF), 1,
playerPt,
playerAnglePt)
shapes.ColorLine (
destination,
artist.Hex(0x00FF00FF), 1,
playerPt,
hitPt)
}

View File

@ -1,48 +0,0 @@
package main
import "io"
import "image"
import "image/color"
type Textures []Texture
type Texture struct {
Data []color.RGBA
Stride int
}
func (texture Textures) At (wall int, offset Vector) color.RGBA {
wall --
if wall < 0 || wall >= len(texture) { return color.RGBA { } }
image := texture[wall]
xOffset := int(offset.X * float64(image.Stride))
yOffset := int(offset.Y * float64(len(image.Data) / image.Stride))
index := xOffset + yOffset * image.Stride
if index < 0 { return color.RGBA { } }
if index >= len(image.Data) { return color.RGBA { } }
return image.Data[index]
}
func TextureFrom (source io.Reader) (texture Texture, err error) {
sourceImage, _, err := image.Decode(source)
if err != nil { return }
bounds := sourceImage.Bounds()
texture.Stride = bounds.Dx()
texture.Data = make([]color.RGBA, bounds.Dx() * bounds.Dy())
index := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
r, g, b, a := sourceImage.At(x, y).RGBA()
texture.Data[index] = color.RGBA {
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: uint8(a >> 8),
}
index ++
}}
return texture, nil
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -37,7 +37,7 @@ func NewDialog (
window tomo.Window,
) {
if parent == nil {
window, _ = tomo.NewWindow(image.Rectangle { })
window, _ = tomo.GetBackend().NewWindow(image.Rectangle { })
} else {
window, _ = parent.NewModal(image.Rectangle { })
}