mirror of
https://github.com/vsariola/sointu.git
synced 2026-01-30 20:30:11 -05:00
169 lines
4.9 KiB
Go
169 lines
4.9 KiB
Go
package gioui
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/io/event"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
)
|
|
|
|
type (
|
|
Plot struct {
|
|
origXlim, origYlim plotRange
|
|
|
|
xScale, yScale float32
|
|
xOffset float32
|
|
dragging bool
|
|
dragId pointer.ID
|
|
dragStartPoint f32.Point
|
|
}
|
|
|
|
PlotStyle struct {
|
|
CurveColors [2]color.NRGBA `yaml:",flow"`
|
|
LimitColor color.NRGBA `yaml:",flow"`
|
|
CursorColor color.NRGBA `yaml:",flow"`
|
|
}
|
|
|
|
PlotDataFunc func(chn int, xr plotRange) (yr plotRange)
|
|
PlotTickFunc func(r plotRange, yield func(pos float32, label string))
|
|
plotRange struct{ a, b float32 }
|
|
plotRel float32
|
|
plotPx int
|
|
plotLogScale float32
|
|
)
|
|
|
|
func NewPlot(xlim, ylim plotRange) *Plot {
|
|
return &Plot{
|
|
origXlim: xlim,
|
|
origYlim: ylim,
|
|
}
|
|
}
|
|
|
|
func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cursornx float32, numchns int) D {
|
|
p.update(gtx)
|
|
t := TrackerFromContext(gtx)
|
|
style := t.Theme.Oscilloscope
|
|
s := gtx.Constraints.Max
|
|
if s.X <= 1 || s.Y <= 1 {
|
|
return D{}
|
|
}
|
|
defer clip.Rect(image.Rectangle{Max: s}).Push(gtx.Ops).Pop()
|
|
event.Op(gtx.Ops, p)
|
|
|
|
xlim := p.xlim()
|
|
ylim := p.ylim()
|
|
|
|
// draw tick marks
|
|
paint.ColorOp{Color: style.LimitColor}.Add(gtx.Ops)
|
|
|
|
xticks(xlim, func(x float32, label string) {
|
|
sx := plotPx(s.X).toScreen(xlim.toRelative(x))
|
|
fillRect(gtx, clip.Rect{Min: image.Pt(sx, 0), Max: image.Pt(sx+1, s.Y)})
|
|
})
|
|
|
|
yticks(ylim, func(y float32, label string) {
|
|
sy := plotPx(s.Y).toScreen(ylim.toRelative(y))
|
|
fillRect(gtx, clip.Rect{Min: image.Pt(0, sy), Max: image.Pt(s.X, sy+1)})
|
|
})
|
|
|
|
// draw cursor
|
|
paint.ColorOp{Color: style.CursorColor}.Add(gtx.Ops)
|
|
csx := plotPx(s.X).toScreen(xlim.toRelative(cursornx))
|
|
fillRect(gtx, clip.Rect{Min: image.Pt(csx, 0), Max: image.Pt(csx+1, s.Y)})
|
|
|
|
// draw curves
|
|
for chn := range numchns {
|
|
paint.ColorOp{Color: style.CurveColors[chn]}.Add(gtx.Ops)
|
|
right := xlim.fromRelative(plotPx(s.X).fromScreen(0))
|
|
for sx := range s.X {
|
|
// left and right is the sample range covered by the pixel
|
|
left := right
|
|
right = xlim.fromRelative(plotPx(s.X).fromScreen(sx + 1))
|
|
yr := data(chn, plotRange{left, right})
|
|
if yr.b < yr.a {
|
|
continue
|
|
}
|
|
y1 := plotPx(s.Y).toScreen(ylim.toRelative(yr.a))
|
|
y2 := plotPx(s.Y).toScreen(ylim.toRelative(yr.b))
|
|
fillRect(gtx, clip.Rect{Min: image.Pt(sx, y1), Max: image.Pt(sx+1, y2+1)})
|
|
}
|
|
}
|
|
return D{Size: s}
|
|
}
|
|
|
|
func (r plotRange) toRelative(f float32) plotRel { return plotRel((f - r.a) / (r.b - r.a)) }
|
|
func (r plotRange) fromRelative(pr plotRel) float32 { return float32(pr)*(r.b-r.a) + r.a }
|
|
func (r plotRange) offset(o float32) plotRange { return plotRange{r.a + o, r.b + o} }
|
|
func (r plotRange) scale(logScale float32) plotRange {
|
|
s := float32(math.Exp(float64(logScale)))
|
|
return plotRange{r.a * s, r.b * s}
|
|
}
|
|
|
|
func (s plotPx) toScreen(pr plotRel) int { return int(float32(pr)*float32(s-1) + 0.5) }
|
|
func (s plotPx) fromScreen(px int) plotRel { return plotRel(float32(px) / float32(s-1)) }
|
|
func (s plotPx) fromScreenF32(px float32) plotRel { return plotRel(px / float32(s-1)) }
|
|
|
|
func (o *Plot) xlim() plotRange { return o.origXlim.scale(o.xScale).offset(o.xOffset) }
|
|
func (o *Plot) ylim() plotRange { return o.origYlim.scale(o.yScale) }
|
|
|
|
func fillRect(gtx C, rect clip.Rect) {
|
|
stack := rect.Push(gtx.Ops)
|
|
paint.PaintOp{}.Add(gtx.Ops)
|
|
stack.Pop()
|
|
}
|
|
|
|
func (o *Plot) update(gtx C) {
|
|
s := gtx.Constraints.Max
|
|
for {
|
|
ev, ok := gtx.Event(pointer.Filter{
|
|
Target: o,
|
|
Kinds: pointer.Scroll | pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
|
|
ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6},
|
|
})
|
|
if !ok {
|
|
break
|
|
}
|
|
if e, ok := ev.(pointer.Event); ok {
|
|
switch e.Kind {
|
|
case pointer.Scroll:
|
|
x1 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(e.Position.X))
|
|
o.xScale += float32(min(max(-1, int(e.Scroll.Y)), 1)) * 0.1
|
|
x2 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(e.Position.X))
|
|
o.xOffset += x1 - x2
|
|
case pointer.Press:
|
|
if e.Buttons&pointer.ButtonSecondary != 0 {
|
|
o.xOffset = 0
|
|
o.xScale = 0
|
|
o.yScale = 0
|
|
}
|
|
if e.Buttons&pointer.ButtonPrimary != 0 {
|
|
o.dragging = true
|
|
o.dragId = e.PointerID
|
|
o.dragStartPoint = e.Position
|
|
}
|
|
case pointer.Drag:
|
|
if e.Buttons&pointer.ButtonPrimary != 0 && o.dragging && e.PointerID == o.dragId {
|
|
x1 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(o.dragStartPoint.X))
|
|
x2 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(e.Position.X))
|
|
o.xOffset += x1 - x2
|
|
|
|
num := o.ylim().fromRelative(plotPx(s.Y).fromScreenF32(e.Position.Y))
|
|
den := o.ylim().fromRelative(plotPx(s.Y).fromScreenF32(o.dragStartPoint.Y))
|
|
if l := math.Abs(float64(num / den)); l > 1e-3 && l < 1e3 {
|
|
o.yScale -= float32(math.Log(l))
|
|
o.yScale = min(max(o.yScale, -1e3), 1e3)
|
|
}
|
|
o.dragStartPoint = e.Position
|
|
}
|
|
case pointer.Release | pointer.Cancel:
|
|
o.dragging = false
|
|
}
|
|
}
|
|
}
|
|
}
|