diff --git a/elements/flaskRoundedBottom.go b/elements/flaskRoundedBottom.go new file mode 100644 index 0000000..5a7f413 --- /dev/null +++ b/elements/flaskRoundedBottom.go @@ -0,0 +1,157 @@ +package elements + +import ( + "fluids/fluid" + "fluids/gamedata" + "fluids/resources" + "image/color" + + "github.com/hajimehoshi/ebiten/v2" +) + +const ( + RoundedBottomFlaskWidth = 32 //pixels + RoundedBottomFlaskHeight = 32 //pixels + RoundedBottomFlaskFluidRadius = 9 //pixels: represents spherical portion of the flask where fluid will be contained + RoundedBottomFlaskFluidOriginX = 16 //pixels: x origin of fluid area + RoundedBottomFlaskFluidOriginY = 20 //pixels: y origin of fluid area + RoundedBottomFlaskFluidWidth = 0.5 //meters + RoundedBottomFlaskFluidHeight = 0.5 //meters + RoundedBottomFlaskFluidResolution = 20 +) + +type RoundedBottomFlask struct { + MappedEntityBase + fluid *fluid.Fluid //our physical representation of the fluid + fluidbuff *ebiten.Image //predraw for the fluid + fluidcellbuff *ebiten.Image //persistent fluid sprite, we redraw this everywhere we want to represent fluid + flaskbase *ebiten.Image //flask background (container) + flaskhighlight *ebiten.Image //flask foreground (glassware highlights) + fieldscale gamedata.Vector //used for transforming from fluid-space to sprite-space + angle float64 + + //fluid color business + fluidcolor color.RGBA //premultiplied fluid color, as set by external sources + fluidcolorF []float32 //for caching of individual color values, we compute rarely +} + +func NewRoundedBottomFlask() *RoundedBottomFlask { + flask := &RoundedBottomFlask{ + fluidbuff: ebiten.NewImage(RoundedBottomFlaskFluidRadius*2, RoundedBottomFlaskFluidRadius*2), + fluidcellbuff: ebiten.NewImage(1, 1), + flaskbase: ebiten.NewImageFromImage(resources.ImageBank[resources.RoundedBottomFlaskBase]), + flaskhighlight: ebiten.NewImageFromImage(resources.ImageBank[resources.RoundedBottomFlaskHighlights]), + angle: 0, + fluidcolorF: make([]float32, 4), //one field each for R,G,B,A + } + flask.Initialize() + return flask +} + +func (flask *RoundedBottomFlask) Initialize() { + //prepare our internal data + flask.dimensions = gamedata.Vector{ + X: RoundedBottomFlaskWidth, + Y: RoundedBottomFlaskHeight, + } + flask.Sprite = ebiten.NewImage(RoundedBottomFlaskWidth, RoundedBottomFlaskHeight) + flask.fluidcellbuff.Fill(color.White) + + //prepare and initialize the fluid + fluiddimensions := fluid.FieldVector{ + X: RoundedBottomFlaskFluidWidth, + Y: RoundedBottomFlaskFluidHeight, + } + flask.fluid = fluid.NewFluid(fluiddimensions, RoundedBottomFlaskFluidHeight/RoundedBottomFlaskFluidResolution) + flask.fluid.Initialize() + flask.fluid.Block = false //rounded flask, not a rect volume + + //compute fieldscale using newly created fluid + flask.fieldscale = gamedata.Vector{ + X: RoundedBottomFlaskFluidRadius * 2 / float64(flask.fluid.Field.Nx-1), + Y: RoundedBottomFlaskFluidRadius * 2 / float64(flask.fluid.Field.Ny-1), + } + + //setup default fluid color + flask.SetFluidColor(color.RGBA{R: 0x0, G: 0x0, B: 0xff, A: 0xff}) +} + +func (flask *RoundedBottomFlask) Update() { + if flask.paused { + return + } + + flask.fluid.Step() +} + +func (flask *RoundedBottomFlask) Draw() { + flask.Sprite.Clear() + + //render flask background + flask.Sprite.DrawImage(flask.flaskbase, nil) + + //render fluid + flask.RenderFluid() + + //render flask foreground + flask.Sprite.DrawImage(flask.flaskhighlight, nil) + +} + +func (flask *RoundedBottomFlask) SetAngle(angle float64) { + flask.angle = angle + flask.fluid.SetAngle(float32(flask.angle)) +} + +func (flask *RoundedBottomFlask) GetAngle() float64 { + return flask.angle +} + +func (flask *RoundedBottomFlask) RenderFluid() { + flask.fluidbuff.Clear() + + //construct fluid buffer from fluid simulation + for i := range flask.fluid.Field.Nx { + for j := range flask.fluid.Field.Ny { + + idx := i*flask.fluid.Field.Ny + j + + if flask.fluid.Field.CellType[idx] != fluid.CellTypeFluid { + continue + } + + celldensity := flask.fluid.ParticleDensity[idx] / flask.fluid.ParticleRestDensity + /*if celldensity > 0.8 { + celldensity = 1 + }*/ + + ox := float64(i) * flask.fieldscale.X + oy := float64(j) * flask.fieldscale.Y + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(-.5, -.5) + op.GeoM.Scale(flask.fieldscale.X, flask.fieldscale.Y) + op.GeoM.Translate(ox, oy) + op.ColorScale.ScaleAlpha(celldensity) + op.ColorScale.Scale(flask.fluidcolorF[0], flask.fluidcolorF[1], flask.fluidcolorF[2], flask.fluidcolorF[3]) + // op.ColorM.Scale(0, 0, 1, 1) + //flask.Sprite.DrawImage(flask.fluidcellbuff, op) + flask.fluidbuff.DrawImage(flask.fluidcellbuff, op) + } + } + + //transform buffer for our flask space + op := &ebiten.DrawImageOptions{} + + op.GeoM.Translate(-RoundedBottomFlaskFluidRadius, -RoundedBottomFlaskFluidRadius) + op.GeoM.Scale(1, -1) + op.GeoM.Translate(RoundedBottomFlaskFluidOriginX, RoundedBottomFlaskFluidOriginY) + flask.Sprite.DrawImage(flask.fluidbuff, op) +} + +func (flask *RoundedBottomFlask) SetFluidColor(c color.RGBA) { + flask.fluidcolor = c + flask.fluidcolorF[0] = float32(flask.fluidcolor.R) / float32(flask.fluidcolor.A) + flask.fluidcolorF[1] = float32(flask.fluidcolor.G) / float32(flask.fluidcolor.A) + flask.fluidcolorF[2] = float32(flask.fluidcolor.B) / float32(flask.fluidcolor.A) + flask.fluidcolorF[3] = float32(flask.fluidcolor.A) / 0xff +} diff --git a/game/game.go b/game/game.go index 5c5ef83..99f767f 100644 --- a/game/game.go +++ b/game/game.go @@ -4,6 +4,7 @@ import ( "fluids/elements" "fluids/gamedata" "fmt" + "image/color" "math" "github.com/hajimehoshi/ebiten/v2" @@ -16,6 +17,7 @@ const ( GameHeight = 360 GameFSDW = 200 GameFSDH = 100 + GameSims = 3 //number of total sims ) type Game struct { @@ -28,6 +30,7 @@ type Game struct { fluidsimd *elements.FluidSimD fluidsim10 []*elements.FluidSim10 alertbox *elements.Alert + flask *elements.RoundedBottomFlask //cache elements fluidsim10width float64 @@ -37,6 +40,11 @@ type Game struct { fluidsim10angle float64 //purely for debugging mdown bool mdx, mdy int + mdangle float64 //angle at mousedown + mfangle float64 //last flask angle + + //colors + flaskcolor color.RGBA } func NewGame() *Game { @@ -45,6 +53,7 @@ func NewGame() *Game { fluidsimidx: 0, alertbox: elements.NewAlert(), fluidsimd: elements.NewFluidSimD(), + flask: elements.NewRoundedBottomFlask(), //fluidsim10: elements.NewFluidSim10(gamedata.Vector{X: GameFSDW, Y: GameFSDH}), //fluidsimgpt: elements.NewFlipFluidEntity(640, 480, 2, 1, 100), @@ -66,6 +75,8 @@ func (g *Game) Update() error { case 1: //g.fluidsimgpt.Update() g.UpdateFluidsim10() + case 2: + g.UpdateFlask() default: break } @@ -90,6 +101,8 @@ func (g *Game) Draw(screen *ebiten.Image) { g.RenderFluidSimD(screen) case 1: g.RenderFluidSim10(screen) + case 2: + g.RenderFlask(screen) default: break } @@ -144,8 +157,9 @@ func (g *Game) ParseInputs() { case 0: g.ManageFluidSimDInputs() case 1: - //g.ManageFluidSimGPTInputs() g.ManageFluidSim10Inputs() + case 2: + g.ManageFlaskInputs() default: break } @@ -159,13 +173,13 @@ func (g *Game) ParseInputs() { //swap fluid simulations if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) { - g.fluidsimidx = (g.fluidsimidx + 1) % 2 + g.fluidsimidx = (g.fluidsimidx + 1) % GameSims } if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) { g.fluidsimidx = g.fluidsimidx - 1 if g.fluidsimidx < 0 { - g.fluidsimidx = 1 + g.fluidsimidx = GameSims - 1 } } @@ -250,9 +264,9 @@ func (g *Game) ManageFluidSim10Inputs() { func (g *Game) Initialize() { + //10MP Fluid Simulation Initialization g.fluidsim10 = append(g.fluidsim10, elements.NewFluidSim10()) g.fluidsim10 = append(g.fluidsim10, elements.NewFluidSim10()) - //g.fluidsim10 = append(g.fluidsim10, elements.NewFluidSim10()) g.fluidsim10width = float64(g.fluidsim10[0].GetSprite().Bounds().Dx()) g.fluidsim10height = float64(g.fluidsim10[0].GetSprite().Bounds().Dy()) @@ -268,4 +282,81 @@ func (g *Game) Initialize() { sim.SetPosition(pos) } + //Flask Initialization + pos := gamedata.Vector{ + X: GameWidth / 2, + Y: GameHeight / 2, + } + g.flask.SetPosition(pos) + g.flaskcolor = color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff} + g.flask.SetFluidColor(g.flaskcolor) + +} + +func (g *Game) ManageFlaskInputs() { + if inpututil.IsKeyJustPressed(ebiten.KeyR) { + g.flask.Initialize() + g.mfangle = 0 + g.flask.SetAngle(g.mfangle) + g.flask.SetFluidColor(g.flaskcolor) + } + + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + //angle at mouse down is the zero angle + g.mdown = true + g.mdx, g.mdy = ebiten.CursorPosition() + + dx := float64(g.mdx) - g.flask.GetPosition().X + dy := float64(g.mdy) - g.flask.GetPosition().Y + g.mdangle = math.Atan2(dy, dx) + } + + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + + if g.mdown { + mx, my := ebiten.CursorPosition() + + dx := float64(mx) - g.flask.GetPosition().X + dy := float64(my) - g.flask.GetPosition().Y + angle := math.Atan2(dy, dx) + + g.flask.SetAngle(g.mfangle + (angle - g.mdangle)) + } + } + + if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { + g.mdown = false + + mx, my := ebiten.CursorPosition() + + dx := float64(mx) - g.flask.GetPosition().X + dy := float64(my) - g.flask.GetPosition().Y + angle := math.Atan2(dy, dx) + + g.mfangle = g.mfangle + (angle - g.mdangle) + } +} + +func (g *Game) UpdateFlask() { + g.flask.Update() +} + +func (g *Game) RenderFlask(img *ebiten.Image) { + + g.flask.Draw() + + angle := g.flask.GetAngle() + dim := g.flask.GetDimensions() + + // //TODO: use flask position for rendering, not screen + pos := g.flask.GetPosition() + + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(-dim.X/2, -dim.Y/2) + op.GeoM.Rotate(angle) + op.GeoM.Scale(2, 2) + op.GeoM.Translate(pos.X, pos.Y) + + img.DrawImage(g.flask.GetSprite(), op) + } diff --git a/main.go b/main.go index 168e6e5..7d23919 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fluids/game" + "fluids/resources" "fmt" "log" @@ -16,6 +17,10 @@ const ( func main() { fmt.Println("fluid experiments") + //preload assets + resources.LoadImages() + + //initialize new game instance g := game.NewGame() ebiten.SetWindowTitle("fluids") diff --git a/resources/images.go b/resources/images.go new file mode 100644 index 0000000..1f712f9 --- /dev/null +++ b/resources/images.go @@ -0,0 +1,41 @@ +package resources + +import ( + "bytes" + "image" + "log" + + _ "embed" + + "github.com/hajimehoshi/ebiten/v2" +) + +type ImageName string + +const ( + RoundedBottomFlaskBase ImageName = "RoundedBottomFlaskBase" + RoundedBottomFlaskHighlights ImageName = "RoundedBottomFlaskHighlights" +) + +var ( + ImageBank map[ImageName]*ebiten.Image + + //go:embed rounded_bottom_flask_base.png + rounded_bottom_flask_base []byte + //go:embed rounded_bottom_flask_highlights.png + rounded_bottom_flask_highlitsh []byte +) + +func LoadImages() { + ImageBank = make(map[ImageName]*ebiten.Image) + ImageBank[RoundedBottomFlaskBase] = LoadImagesFatal(rounded_bottom_flask_base) + ImageBank[RoundedBottomFlaskHighlights] = LoadImagesFatal(rounded_bottom_flask_highlitsh) +} + +func LoadImagesFatal(b []byte) *ebiten.Image { + img, _, err := image.Decode(bytes.NewReader(b)) + if err != nil { + log.Fatal(err) + } + return ebiten.NewImageFromImage(img) +} diff --git a/resources/rounded_bottom_flask_base.png b/resources/rounded_bottom_flask_base.png new file mode 100644 index 0000000..cf22025 Binary files /dev/null and b/resources/rounded_bottom_flask_base.png differ diff --git a/resources/rounded_bottom_flask_highlights.png b/resources/rounded_bottom_flask_highlights.png new file mode 100644 index 0000000..0778b6d Binary files /dev/null and b/resources/rounded_bottom_flask_highlights.png differ