package splashmenu import ( "cosmos/diego/groovy" splashmenu "cosmos/diego/groovy/examples/splashmenu/fonts" "fmt" "image/color" "math" "strconv" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text" "github.com/hajimehoshi/ebiten/v2/vector" ) const ( BGCOLOR = 0xCCCCCC LINECOLOR = 0x8a8a8a SQUARECOLOR = 0x199a19 RADIUSCOLOR = 0xFF0000 AXISCOLOR = 0xf100c1 SEMIAXISCOLOR = 0x178fc5 //0x1e1e1e //0xdccdFF ) type ColorData struct { selectionColor color.RGBA squareColor color.RGBA radiusColor color.RGBA axisColor color.RGBA semiAxisColor color.RGBA } type Coords struct { X float32 Y float32 } func (c Coords) DistanceFrom(pos Coords) float32 { return float32(math.Sqrt(float64((pos.X-c.X)*(pos.X-c.X) + (pos.Y-c.Y)*(pos.Y-c.Y)))) } func (c Coords) SignedDistanceFrom(pos Coords) Coords { return Coords{X: pos.X - c.X, Y: pos.Y - c.Y} } func (c Coords) Magnitude() float32 { return float32(math.Sqrt(float64(c.X*c.X + c.Y*c.Y))) } type Rays struct { events map[groovy.SceneEvent]func() increment int Dimensions groovy.Area coordinates Coords selection Coords interacting bool bgcolor color.RGBA colorData ColorData } func hexToRGBA(hexcolor int) color.RGBA { return color.RGBA{ R: uint8(hexcolor >> 0x10 & 0xff), G: uint8(hexcolor >> 0x08 & 0xff), B: uint8(hexcolor >> 0x00 & 0xff), A: 0xff, } } func NewRays() Rays { r := Rays{ events: make(map[groovy.SceneEvent]func()), increment: 0, bgcolor: hexToRGBA(BGCOLOR), coordinates: Coords{X: 0, Y: 0}, selection: Coords{X: 0, Y: 0}, } r.colorData.selectionColor = hexToRGBA(LINECOLOR) r.colorData.squareColor = hexToRGBA(SQUARECOLOR) r.colorData.radiusColor = hexToRGBA(RADIUSCOLOR) r.colorData.axisColor = hexToRGBA(AXISCOLOR) r.colorData.semiAxisColor = hexToRGBA(SEMIAXISCOLOR) return r } func gimmeFloatCursor() (float32, float32) { x, y := ebiten.CursorPosition() return float32(x), float32(y) } func (r *Rays) Update() error { r.increment++ x, y := gimmeFloatCursor() mouseCoords := Coords{X: x, Y: y} newRad := mouseCoords.DistanceFrom(r.selection) xMin := r.selection.X - newRad xMax := r.selection.X + newRad yMin := r.selection.Y - newRad yMax := r.selection.Y + newRad if 0 <= xMin && xMax < float32(r.Dimensions.Width) && 0 <= yMin && yMax < float32(r.Dimensions.Height) { r.coordinates = mouseCoords } if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { r.selection = mouseCoords r.interacting = true } if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { //r.selection = r.coordinates r.interacting = false } if !r.interacting { r.selection = r.coordinates } return nil } func (r *Rays) Draw(screen *ebiten.Image) { screen.Fill(r.bgcolor) r.DrawTheAngleBits(screen) x, y := gimmeFloatCursor() text.Draw(screen, fmt.Sprintf("Mouse x, y: (%.1f, %.1f)", x, y), splashmenu.SplashFont.Menu, 20, 20, color.White) text.Draw(screen, fmt.Sprintf("Selection x, y: (%.1f, %.1f)", r.selection.X, r.selection.Y), splashmenu.SplashFont.Menu, 20, 40, color.White) text.Draw(screen, fmt.Sprintf("Radius: %.1f", r.GetPrimaryRadius()), splashmenu.SplashFont.Menu, 20, 60, color.White) text.Draw(screen, fmt.Sprintf("Interaction: %s", strconv.FormatBool(r.interacting)), splashmenu.SplashFont.Menu, 20, 80, color.White) } func (r *Rays) SetDimensions(a groovy.Area) { r.Dimensions = a } func (r *Rays) SetEventHandler(event groovy.SceneEvent, f func()) { r.events[event] = f } func (r *Rays) DrawTheAngleBits(screen *ebiten.Image) { if r.interacting { r.DrawTrigElementAxis(screen, r.selection, r.coordinates, r.colorData) r.DrawTrigElement(screen, r.selection, r.coordinates, r.colorData) r.DrawCircleElement(screen, r.selection, r.coordinates, r.colorData) dist := r.coordinates.SignedDistanceFrom(r.selection) angle := math.Atan2(float64(dist.Y), float64(dist.X)) for i := 0; i < 4; i++ { var xSpot Coords xSpot.X = r.selection.X + float32(math.Cos(angle))*dist.Magnitude()*float32(i) xSpot.Y = r.selection.Y + float32(math.Sin(angle))*dist.Magnitude()*float32(i) var nSpot Coords //our limit is pi/2, but we want to stretch this out over 30 frames u := (math.Pi / 2) * 30 v := float64(r.increment) //our angular frequency is simply the remainder of our current frame count / our limit w := (v/u - math.Floor(v/u)) //now scale the circles nSpot.X = xSpot.X + float32(math.Cos(angle))*dist.Magnitude()*float32(math.Sin(w)) nSpot.Y = xSpot.Y + float32(math.Sin(angle))*dist.Magnitude()*float32(math.Sin(w)) r.DrawCircleElement(screen, xSpot, nSpot, r.colorData) } } } // Returns the primary circle radius value of the current scene func (r *Rays) GetPrimaryRadius() float32 { return r.coordinates.DistanceFrom(r.selection) } // draws circle func (r *Rays) DrawCircleElement(img *ebiten.Image, origin Coords, dest Coords, colorData ColorData) { dist := dest.SignedDistanceFrom(origin) vector.StrokeCircle(img, origin.X, origin.Y, dist.Magnitude(), 2, colorData.selectionColor, true) } // draws circle trig relationships with the x/y axis of the circles origin and a radius indicator func (r *Rays) DrawTrigElement(img *ebiten.Image, origin Coords, dest Coords, colorData ColorData) { dist := dest.SignedDistanceFrom(origin) //circle and radius marker vector.StrokeCircle(img, origin.X, origin.Y, dist.Magnitude(), 2, colorData.selectionColor, true) vector.StrokeLine(img, origin.X, origin.Y, dest.X, dest.Y, 1, colorData.radiusColor, true) //draw rectangle from origin to dest vector.StrokeRect(img, origin.X, origin.Y, dest.X-origin.X, dest.Y-origin.Y, 1, colorData.squareColor, true) //compute directionality (1) dirX := dist.X / float32(math.Abs(float64(dist.X))) dirY := dist.Y / float32(math.Abs(float64(dist.Y))) //semi-axis within circle vector.StrokeLine(img, origin.X, origin.Y, origin.X-dirX*dist.Magnitude(), origin.Y, 1, colorData.axisColor, true) vector.StrokeLine(img, origin.X, origin.Y, origin.X, origin.Y-dirY*dist.Magnitude(), 1, colorData.axisColor, true) } func (r *Rays) DrawTrigElementAxis(img *ebiten.Image, origin Coords, dest Coords, colorData ColorData) { dist := dest.SignedDistanceFrom(origin) //origin to BORDER // case 1. origin.x = destination.x, means intercept should be vertical line // case 2. we have a slope if dist.X == 0 { vector.StrokeLine(img, origin.X, 0, origin.X, float32(r.Dimensions.Height), 1, colorData.semiAxisColor, true) } else { m := dist.Y / dist.X /* y - y1 = m(x - x1) y = m(x - x1) + y1 x = (y - y1)/m + x1 find the intercepts: x = Dimensions.Width x = 0 y = 0 y = Dimensions.Height */ //find our y intercept for the start of our segment var xZero Coords xZero.X = 0 xZero.Y = m*(xZero.X-origin.X) + origin.Y //in the even our point lies outside the visible screen boundaries, //we can save some pixel operations by restricting to the bounds if xZero.Y > float32(r.Dimensions.Height) { xZero.Y = float32(r.Dimensions.Height) xZero.X = (xZero.Y-origin.Y)/m + origin.X } if xZero.Y < 0 { xZero.Y = 0 xZero.X = (xZero.Y-origin.Y)/m + origin.X } var xMax Coords xMax.X = float32(r.Dimensions.Width) xMax.Y = m*(xMax.X-origin.X) + origin.Y if xMax.Y > float32(r.Dimensions.Height) { xMax.Y = float32(r.Dimensions.Height) xMax.X = (xMax.Y-origin.Y)/m + origin.X } if xMax.Y < 0 { xMax.Y = 0 xMax.X = (xMax.Y-origin.Y)/m + origin.X } var slashLineOrigin Coords var slashLineDest Coords slashLineOrigin = xZero slashLineDest = xMax var xSpot Coords angle := math.Atan2(float64(dist.Y), float64(dist.X)) xSpot.X = origin.X + float32(math.Cos(angle))*dist.Magnitude() xSpot.Y = origin.Y + float32(math.Sin(angle))*dist.Magnitude() vector.StrokeLine(img, slashLineOrigin.X, slashLineOrigin.Y, slashLineDest.X, slashLineDest.Y, 1, colorData.semiAxisColor, true) text.Draw(img, fmt.Sprintf("%0.1f : (%0.1f, %0.1f) (%0.1f, %0.1f)", m, slashLineOrigin.X, slashLineOrigin.Y, slashLineDest.X, slashLineDest.Y), splashmenu.SplashFont.Menu, 20, 100, color.White) //text.Draw(img, "x", splashmenu.SplashFont.Title, int(xSpot.X), int(xSpot.Y), color.Black) vector.DrawFilledCircle(img, xSpot.X, xSpot.Y, 5, color.Black, true) vector.DrawFilledCircle(img, origin.X, origin.Y, 5, colorData.radiusColor, true) } }