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 } }