266 lines
8.7 KiB
Go
266 lines
8.7 KiB
Go
package elements
|
|
|
|
import (
|
|
"fluids/fluid"
|
|
"fluids/gamedata"
|
|
"fluids/resources"
|
|
"image/color"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
)
|
|
|
|
const (
|
|
RoundedBottomFlaskWidth = 32 //pixels
|
|
RoundedBottomFlaskHeight = 32 //pixels
|
|
RBFlaskMaskWidth = 46 //pixels
|
|
RBFlaskMaskHeight = 46 //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
|
|
fluidbuffS *ebiten.Image //predraw for the fluid, static
|
|
fluidbuffD *ebiten.Image //predraw for the fluid, dynamic
|
|
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)
|
|
flaskboundarymask *ebiten.Image //flask boundary mask
|
|
fieldscaleStatic gamedata.Vector //used for transforming from fluid-space to sprite-space: static
|
|
fieldscaleDyanmic gamedata.Vector //used for transforming from fluid-space to sprite-space: dynamic
|
|
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
|
|
|
|
//boundary
|
|
boundaryinitialized bool
|
|
container *fluid.Boundary
|
|
}
|
|
|
|
func NewRoundedBottomFlask() *RoundedBottomFlask {
|
|
flask := &RoundedBottomFlask{
|
|
fluidbuffS: ebiten.NewImage(RoundedBottomFlaskFluidRadius*2, RoundedBottomFlaskFluidRadius*2),
|
|
fluidbuffD: ebiten.NewImage(RBFlaskMaskWidth, RBFlaskMaskHeight),
|
|
fluidcellbuff: ebiten.NewImage(1, 1),
|
|
flaskbase: ebiten.NewImageFromImage(resources.ImageBank[resources.RoundedBottomFlaskBase]),
|
|
flaskhighlight: ebiten.NewImageFromImage(resources.ImageBank[resources.RoundedBottomFlaskHighlights]),
|
|
flaskboundarymask: ebiten.NewImageFromImage(resources.ImageBank[resources.RoundedBottomFlaskBoundaryMap46]),
|
|
//flaskboundarymask: ebiten.NewImageFromImage(resources.ImageBank[resources.RoundedBottomFlaskBoundaryMap]),
|
|
angle: 0,
|
|
fluidcolorF: make([]float32, 4), //one field each for R,G,B,A
|
|
boundaryinitialized: false,
|
|
}
|
|
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 fieldscales using newly created fluid
|
|
flask.fieldscaleStatic = gamedata.Vector{
|
|
X: RoundedBottomFlaskFluidRadius * 2 / float64(flask.fluid.Field.Nx-1),
|
|
Y: RoundedBottomFlaskFluidRadius * 2 / float64(flask.fluid.Field.Ny-1),
|
|
}
|
|
|
|
flask.fieldscaleDyanmic = gamedata.Vector{
|
|
X: RBFlaskMaskWidth / float64(flask.fluid.Field.Nx-2),
|
|
Y: RBFlaskMaskHeight / float64(flask.fluid.Field.Ny-2),
|
|
}
|
|
|
|
//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
|
|
if flask.boundaryinitialized {
|
|
flask.RenderFluidDynamic()
|
|
} else {
|
|
flask.RenderFluidStatic()
|
|
}
|
|
|
|
//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) RenderFluidDynamic() {
|
|
flask.fluidbuffD.Clear()
|
|
//flask.fluidbuffD.Fill(color.White)
|
|
//vector.StrokeRect(flask.fluidbuffD, 0, 0, 46, 46, 1, color.White, true)
|
|
|
|
//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.fieldscaleDyanmic.X
|
|
oy := float64(j) * flask.fieldscaleDyanmic.Y
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Translate(-.5, -.5)
|
|
op.GeoM.Scale(flask.fieldscaleDyanmic.X, flask.fieldscaleDyanmic.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])
|
|
flask.fluidbuffD.DrawImage(flask.fluidcellbuff, op)
|
|
}
|
|
}
|
|
|
|
//transform buffer for our flask space
|
|
|
|
s := float64(RoundedBottomFlaskWidth) / RBFlaskMaskWidth * 67. / 104
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Translate(-RBFlaskMaskWidth/2, -RBFlaskMaskHeight/2)
|
|
op.GeoM.Scale(s, -s*1.15)
|
|
op.GeoM.Translate(RoundedBottomFlaskWidth/2, RoundedBottomFlaskHeight/2+2)
|
|
//op.GeoM.Translate(RoundedBottomFlaskFluidOriginX, RoundedBottomFlaskFluidOriginY)
|
|
flask.Sprite.DrawImage(flask.fluidbuffD, op)
|
|
}
|
|
|
|
func (flask *RoundedBottomFlask) RenderFluidStatic() {
|
|
flask.fluidbuffS.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.fieldscaleStatic.X
|
|
oy := float64(j) * flask.fieldscaleStatic.Y
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Translate(-.5, -.5)
|
|
op.GeoM.Scale(flask.fieldscaleStatic.X, flask.fieldscaleStatic.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])
|
|
flask.fluidbuffS.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.fluidbuffS, 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
|
|
}
|
|
|
|
func (flask *RoundedBottomFlask) InitializeBoundary() {
|
|
|
|
//prepare the dimensions of our boundary map
|
|
dimensions := fluid.BoundaryDimensions{
|
|
X: RBFlaskMaskWidth,
|
|
Y: RBFlaskMaskHeight,
|
|
}
|
|
|
|
//instantiate the boundary structure
|
|
flask.container = fluid.NewBoundary(dimensions)
|
|
|
|
//load pixel data from boundary
|
|
var pixels []byte = make([]byte, dimensions.X*dimensions.Y*4)
|
|
flask.flaskboundarymask.ReadPixels(pixels)
|
|
|
|
//populate our boundary map based on the pixel data
|
|
for i := 0; i < len(pixels); i += 4 {
|
|
cellidx := i / 4
|
|
boundary := pixels[i] == 0
|
|
flask.container.Cells[cellidx] = !boundary
|
|
}
|
|
|
|
//apply to fluid simulation
|
|
flask.fluid.SetBoundary(flask.container)
|
|
flask.boundaryinitialized = true
|
|
}
|
|
|
|
// set new boundary mask and reinitialize associated data, including computing and
|
|
// passing that into the associated fluid simulation
|
|
func (flask *RoundedBottomFlask) SetBoundaryMap(img *ebiten.Image) {
|
|
flask.flaskboundarymask = img
|
|
flask.InitializeBoundary()
|
|
}
|
|
|
|
func (flask *RoundedBottomFlask) ToggleBoundaryMask() {
|
|
if flask.boundaryinitialized {
|
|
flask.fluid.SetBoundary(nil)
|
|
flask.boundaryinitialized = false
|
|
} else {
|
|
flask.InitializeBoundary()
|
|
flask.boundaryinitialized = true
|
|
}
|
|
}
|