forked from go/golangs_learn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
379 lines
9.2 KiB
379 lines
9.2 KiB
3 years ago
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||
|
|
||
|
package main
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"image"
|
||
|
"image/color"
|
||
|
"log"
|
||
|
"math"
|
||
|
"strconv"
|
||
|
"time"
|
||
|
|
||
|
"golang.org/x/exp/rand"
|
||
|
"golang.org/x/exp/shiny/materialdesign/icons"
|
||
|
|
||
|
"gonum.org/v1/gonum/spatial/r2"
|
||
|
|
||
|
"gioui.org/app"
|
||
|
"gioui.org/f32"
|
||
|
"gioui.org/font/gofont"
|
||
|
"gioui.org/io/pointer"
|
||
|
"gioui.org/io/system"
|
||
|
"gioui.org/layout"
|
||
|
"gioui.org/op"
|
||
|
"gioui.org/op/clip"
|
||
|
"gioui.org/op/paint"
|
||
|
"gioui.org/unit"
|
||
|
"gioui.org/widget"
|
||
|
"gioui.org/widget/material"
|
||
|
)
|
||
|
|
||
|
// distribution tracks useful minimum and maximum information about
|
||
|
// the stars.
|
||
|
type distribution struct {
|
||
|
min, max r2.Vec
|
||
|
maxSpeed float64
|
||
|
meanSpeed float64
|
||
|
minMass, maxMass float64
|
||
|
|
||
|
speedSum float64
|
||
|
speedSamples int
|
||
|
}
|
||
|
|
||
|
// Update ensures that the distribution contains accurate min/max
|
||
|
// data for the slice of stars provided.
|
||
|
func (d *distribution) Update(stars []*mass) {
|
||
|
var (
|
||
|
speedSum float64
|
||
|
speedSamples int
|
||
|
)
|
||
|
for i, s := range stars {
|
||
|
speed := distance(s.v, s.d)
|
||
|
if i == 0 {
|
||
|
d.minMass = s.m
|
||
|
}
|
||
|
if s.d.X < d.min.X {
|
||
|
d.min.X = s.d.X
|
||
|
}
|
||
|
if s.d.Y < d.min.Y {
|
||
|
d.min.Y = s.d.Y
|
||
|
}
|
||
|
if s.d.X > d.max.X {
|
||
|
d.max.X = s.d.X
|
||
|
}
|
||
|
if s.d.Y > d.max.Y {
|
||
|
d.max.Y = s.d.Y
|
||
|
}
|
||
|
if s.m > d.maxMass {
|
||
|
d.maxMass = s.m
|
||
|
}
|
||
|
if s.m < d.minMass {
|
||
|
d.minMass = s.m
|
||
|
}
|
||
|
if speed > d.maxSpeed {
|
||
|
d.maxSpeed = speed
|
||
|
}
|
||
|
speedSamples++
|
||
|
speedSum += speed
|
||
|
}
|
||
|
d.meanSpeed = speedSum / float64(speedSamples)
|
||
|
}
|
||
|
|
||
|
// EnsureSquare adjusts the distribution so that the min and max
|
||
|
// coordinates are the corners of a square (by padding one axis
|
||
|
// equally across the top and bottom). This helps to prevent visual
|
||
|
// distortion during the visualization, though it does not stop it
|
||
|
// completely.
|
||
|
func (d *distribution) EnsureSquare() {
|
||
|
diff := d.max.Sub(d.min)
|
||
|
if diff.X > diff.Y {
|
||
|
padding := (diff.X - diff.Y) / 2
|
||
|
d.max.Y += padding
|
||
|
d.min.Y -= padding
|
||
|
} else if diff.Y > diff.X {
|
||
|
padding := (diff.Y - diff.X) / 2
|
||
|
d.max.X += padding
|
||
|
d.min.X -= padding
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// String describes the distribution in text form.
|
||
|
func (d distribution) String() string {
|
||
|
return fmt.Sprintf("distance: (min: %v max: %v), mass: (min: %v, max: %v)", d.min, d.max, d.minMass, d.maxMass)
|
||
|
}
|
||
|
|
||
|
// Scale uses the min/max data within the distribution to compute the
|
||
|
// position, speed, and size of the star.
|
||
|
func (d distribution) Scale(star *mass) Star {
|
||
|
s := Star{}
|
||
|
s.X = float32((star.d.X - d.min.X) / (d.max.X - d.min.X))
|
||
|
s.Y = float32((star.d.Y - d.min.Y) / (d.max.Y - d.min.Y))
|
||
|
speed := math.Log(distance(star.v, star.d)) / math.Log(d.maxSpeed)
|
||
|
s.Speed = float32(speed)
|
||
|
s.Size = unit.Dp(float32(1 + ((star.m / (d.maxMass - d.minMass)) * 10)))
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
// distance implements the simple two-dimensional euclidean distance function.
|
||
|
func distance(a, b r2.Vec) float64 {
|
||
|
return math.Sqrt((b.X-a.X)*(b.X-a.X) + (b.Y-a.Y)*(b.Y-a.Y))
|
||
|
}
|
||
|
|
||
|
var PlayIcon = func() *widget.Icon {
|
||
|
ic, _ := widget.NewIcon(icons.AVPlayArrow)
|
||
|
return ic
|
||
|
}()
|
||
|
|
||
|
var PauseIcon = func() *widget.Icon {
|
||
|
ic, _ := widget.NewIcon(icons.AVPause)
|
||
|
return ic
|
||
|
}()
|
||
|
|
||
|
var ClearIcon = func() *widget.Icon {
|
||
|
ic, _ := widget.NewIcon(icons.ContentClear)
|
||
|
return ic
|
||
|
}()
|
||
|
|
||
|
// viewport models a region of a larger space. Offset is the location
|
||
|
// of the upper-left corner of the view within the larger space. size
|
||
|
// is the dimensions of the viewport within the larger space.
|
||
|
type viewport struct {
|
||
|
offset f32.Point
|
||
|
size f32.Point
|
||
|
}
|
||
|
|
||
|
// subview modifies v to describe a smaller region by zooming into the
|
||
|
// space described by v using other.
|
||
|
func (v *viewport) subview(other *viewport) {
|
||
|
v.offset.X += other.offset.X * v.size.X
|
||
|
v.offset.Y += other.offset.Y * v.size.Y
|
||
|
v.size.X *= other.size.X
|
||
|
v.size.Y *= other.size.Y
|
||
|
}
|
||
|
|
||
|
// ensureSquare returns a copy of the rectangle that has been padded to
|
||
|
// be square by increasing the maximum coordinate.
|
||
|
func ensureSquare(r image.Rectangle) image.Rectangle {
|
||
|
dx := r.Dx()
|
||
|
dy := r.Dy()
|
||
|
if dx > dy {
|
||
|
r.Max.Y = r.Min.Y + dx
|
||
|
} else if dy > dx {
|
||
|
r.Max.X = r.Min.X + dy
|
||
|
}
|
||
|
return r
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
ops op.Ops
|
||
|
play, clear widget.Clickable
|
||
|
playing = false
|
||
|
th = material.NewTheme(gofont.Collection())
|
||
|
selected image.Rectangle
|
||
|
selecting = false
|
||
|
view *viewport
|
||
|
)
|
||
|
|
||
|
func main() {
|
||
|
th.Palette.Fg, th.Palette.Bg = th.Palette.Bg, th.Palette.Fg
|
||
|
dist := distribution{}
|
||
|
|
||
|
seed := time.Now().UnixNano()
|
||
|
rnd := rand.New(rand.NewSource(uint64(seed)))
|
||
|
|
||
|
// Make 1000 stars in random locations.
|
||
|
stars, plane := galaxy(1000, rnd)
|
||
|
dist.Update(stars)
|
||
|
|
||
|
desiredSize := unit.Dp(800)
|
||
|
window := app.NewWindow(
|
||
|
app.Size(desiredSize, desiredSize),
|
||
|
app.Title("Seed: "+strconv.Itoa(int(seed))),
|
||
|
)
|
||
|
|
||
|
iterateSim := func() {
|
||
|
if !playing {
|
||
|
return
|
||
|
}
|
||
|
simulate(stars, plane, &dist)
|
||
|
window.Invalidate()
|
||
|
}
|
||
|
for {
|
||
|
select {
|
||
|
case ev := <-window.Events():
|
||
|
switch ev := ev.(type) {
|
||
|
case system.DestroyEvent:
|
||
|
if ev.Err != nil {
|
||
|
log.Fatal(ev.Err)
|
||
|
}
|
||
|
return
|
||
|
case system.FrameEvent:
|
||
|
gtx := layout.NewContext(&ops, ev)
|
||
|
paint.Fill(gtx.Ops, th.Palette.Bg)
|
||
|
|
||
|
layout.Center.Layout(gtx, func(gtx C) D {
|
||
|
return widget.Border{
|
||
|
Color: th.Fg,
|
||
|
Width: unit.Dp(1),
|
||
|
}.Layout(gtx, func(gtx C) D {
|
||
|
if gtx.Constraints.Max.X > gtx.Constraints.Max.Y {
|
||
|
gtx.Constraints.Max.X = gtx.Constraints.Max.Y
|
||
|
} else {
|
||
|
gtx.Constraints.Max.Y = gtx.Constraints.Max.X
|
||
|
}
|
||
|
gtx.Constraints.Min = gtx.Constraints.Max
|
||
|
|
||
|
if clear.Clicked() {
|
||
|
view = nil
|
||
|
}
|
||
|
if play.Clicked() {
|
||
|
playing = !playing
|
||
|
}
|
||
|
|
||
|
layoutSelectionLayer(gtx)
|
||
|
|
||
|
for _, s := range stars {
|
||
|
dist.Scale(s).Layout(gtx, view)
|
||
|
}
|
||
|
layoutControls(gtx)
|
||
|
return D{Size: gtx.Constraints.Max}
|
||
|
})
|
||
|
})
|
||
|
|
||
|
ev.Frame(gtx.Ops)
|
||
|
iterateSim()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func layoutControls(gtx C) D {
|
||
|
layout.N.Layout(gtx, func(gtx C) D {
|
||
|
return material.Body1(th, "Click and drag to zoom in on a region").Layout(gtx)
|
||
|
})
|
||
|
layout.S.Layout(gtx, func(gtx C) D {
|
||
|
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||
|
return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
|
||
|
return layout.Flex{
|
||
|
Spacing: layout.SpaceEvenly,
|
||
|
}.Layout(gtx,
|
||
|
layout.Rigid(func(gtx C) D {
|
||
|
var btn material.IconButtonStyle
|
||
|
if playing {
|
||
|
btn = material.IconButton(th, &play, PauseIcon, "Pause Simulation")
|
||
|
} else {
|
||
|
btn = material.IconButton(th, &play, PlayIcon, "Play Simulation")
|
||
|
}
|
||
|
return btn.Layout(gtx)
|
||
|
}),
|
||
|
layout.Rigid(func(gtx C) D {
|
||
|
if view == nil {
|
||
|
gtx = gtx.Disabled()
|
||
|
}
|
||
|
return material.IconButton(th, &clear, ClearIcon, "Reset Viewport").Layout(gtx)
|
||
|
}),
|
||
|
)
|
||
|
})
|
||
|
})
|
||
|
return D{}
|
||
|
}
|
||
|
|
||
|
func layoutSelectionLayer(gtx C) D {
|
||
|
for _, event := range gtx.Events(&selected) {
|
||
|
switch event := event.(type) {
|
||
|
case pointer.Event:
|
||
|
var intPt image.Point
|
||
|
intPt.X = int(event.Position.X)
|
||
|
intPt.Y = int(event.Position.Y)
|
||
|
switch event.Type {
|
||
|
case pointer.Press:
|
||
|
selecting = true
|
||
|
selected.Min = intPt
|
||
|
selected.Max = intPt
|
||
|
case pointer.Drag:
|
||
|
if intPt.X >= selected.Min.X && intPt.Y >= selected.Min.Y {
|
||
|
selected.Max = intPt
|
||
|
} else {
|
||
|
selected.Min = intPt
|
||
|
}
|
||
|
selected = ensureSquare(selected)
|
||
|
case pointer.Release:
|
||
|
selecting = false
|
||
|
newView := &viewport{
|
||
|
offset: f32.Point{
|
||
|
X: float32(selected.Min.X) / float32(gtx.Constraints.Max.X),
|
||
|
Y: float32(selected.Min.Y) / float32(gtx.Constraints.Max.Y),
|
||
|
},
|
||
|
size: f32.Point{
|
||
|
X: float32(selected.Dx()) / float32(gtx.Constraints.Max.X),
|
||
|
Y: float32(selected.Dy()) / float32(gtx.Constraints.Max.Y),
|
||
|
},
|
||
|
}
|
||
|
if view == nil {
|
||
|
view = newView
|
||
|
} else {
|
||
|
view.subview(newView)
|
||
|
}
|
||
|
case pointer.Cancel:
|
||
|
selecting = false
|
||
|
selected = image.Rectangle{}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if selecting {
|
||
|
paint.FillShape(gtx.Ops, color.NRGBA{R: 255, A: 100}, clip.Rect(selected).Op())
|
||
|
}
|
||
|
pr := clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops)
|
||
|
pointer.CursorNameOp{Name: pointer.CursorCrossHair}.Add(gtx.Ops)
|
||
|
pointer.InputOp{
|
||
|
Tag: &selected,
|
||
|
Types: pointer.Press | pointer.Release | pointer.Drag,
|
||
|
}.Add(gtx.Ops)
|
||
|
pr.Pop()
|
||
|
|
||
|
return D{Size: gtx.Constraints.Max}
|
||
|
}
|
||
|
|
||
|
// Star represents a point of mass rendered within a specific region of a canvas.
|
||
|
type Star struct {
|
||
|
X, Y float32
|
||
|
Speed float32
|
||
|
Size unit.Value
|
||
|
}
|
||
|
|
||
|
type (
|
||
|
C = layout.Context
|
||
|
D = layout.Dimensions
|
||
|
)
|
||
|
|
||
|
// Layout renders the star into the gtx assuming that it is visible within the
|
||
|
// provided viewport. Stars outside of the viewport will be skipped.
|
||
|
func (s Star) Layout(gtx layout.Context, view *viewport) layout.Dimensions {
|
||
|
px := gtx.Px(s.Size)
|
||
|
if view != nil {
|
||
|
if s.X < view.offset.X || s.X > view.offset.X+view.size.X {
|
||
|
return D{}
|
||
|
}
|
||
|
if s.Y < view.offset.Y || s.Y > view.offset.Y+view.size.Y {
|
||
|
return D{}
|
||
|
}
|
||
|
s.X = (s.X - view.offset.X) / view.size.X
|
||
|
s.Y = (s.Y - view.offset.Y) / view.size.Y
|
||
|
}
|
||
|
rr := float32(px / 2)
|
||
|
x := s.X*float32(gtx.Constraints.Max.X) - rr
|
||
|
y := s.Y*float32(gtx.Constraints.Max.Y) - rr
|
||
|
|
||
|
defer op.Offset(f32.Pt(x, y)).Push(gtx.Ops).Pop()
|
||
|
rect := f32.Rectangle{
|
||
|
Max: f32.Pt(float32(px), float32(px)),
|
||
|
}
|
||
|
fill := color.NRGBA{R: 0xff, G: 128, B: 0xff, A: 50}
|
||
|
fill.R = 255 - uint8(255*s.Speed)
|
||
|
fill.B = uint8(255 * s.Speed)
|
||
|
paint.FillShape(gtx.Ops, fill, clip.UniformRRect(rect, rr).Op(gtx.Ops))
|
||
|
return D{}
|
||
|
}
|