Files
caire/gui.go

566 lines
14 KiB
Go

package caire
import (
"fmt"
"image"
"image/color"
"image/draw"
"math"
"math/rand"
"time"
"gioui.org/app"
"gioui.org/f32"
"gioui.org/font/gofont"
"gioui.org/io/key"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/esimov/caire/imop"
"github.com/esimov/caire/utils"
)
type hudControlType int
const (
hudShowSeams hudControlType = iota
hudShowDebugMask
)
const (
// The starting colors for the linear gradient, used when the image is resized both horizontally and vertically.
// In this case the preview mode is deactivated and a dynamic gradient overlay is shown.
redStart = 137
greenStart = 47
blueStart = 54
// The ending colors for the linear gradient. The starting colors and ending colors are lerped.
redEnd = 255
greenEnd = 112
blueEnd = 105
)
var (
maxScreenX float32 = 1280
maxScreenY float32 = 720
defaultBkgColor = color.Transparent
defaultFillColor = color.Black
)
type interval struct {
min, max float64
}
// Gui is the basic struct containing all of the information needed for the UI operation.
// It receives the resized image transferred through a channel which is called in a separate goroutine.
type Gui struct {
cfg struct {
x interval
y interval
chrot bool
angle float32
window struct {
width float32
height float32
title string
}
color struct {
randR uint8
randG uint8
randB uint8
background color.Color
fill color.Color
}
timeStamp time.Time
}
process struct {
isDone bool
img image.Image
seams []Seam
worker <-chan worker
err chan<- error
}
proc *Processor
compOp *imop.Composite
blendOp *imop.Blend
theme *material.Theme
ctx layout.Context
huds map[hudControlType]*hudCtrl
view struct {
huds layout.List
}
}
type hudCtrl struct {
enabled widget.Bool
hudType hudControlType
title string
}
// NewGUI initializes the Gio interface.
func NewGUI(width, height int) *Gui {
defaultColor := color.NRGBA{R: 0x2d, G: 0x23, B: 0x2e, A: 0xff}
gui := &Gui{
ctx: layout.Context{
Ops: new(op.Ops),
Constraints: layout.Constraints{
Max: image.Pt(width, height),
},
},
compOp: imop.InitOp(),
blendOp: imop.NewBlend(),
theme: material.NewTheme(),
huds: make(map[hudControlType]*hudCtrl),
}
gui.theme.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
gui.theme.TextSize = unit.Sp(16)
gui.theme.Palette.ContrastBg = defaultColor
gui.theme.FingerSize = 10
gui.initWindow(width, height)
return gui
}
// AddHudControl adds a new hud control for debugging.
func (g *Gui) AddHudControl(hudControlType hudControlType, title string, enabled bool) {
control := &hudCtrl{
hudType: hudControlType,
title: title,
enabled: widget.Bool{},
}
control.enabled.Value = enabled
g.huds[hudControlType] = control
}
// initWindow creates and initializes the GUI window.
func (g *Gui) initWindow(width, height int) {
rand.NewSource(time.Now().UnixNano())
g.cfg.angle = 45
g.cfg.color.randR = uint8(random(1, 2))
g.cfg.color.randG = uint8(random(1, 2))
g.cfg.color.randB = uint8(random(1, 2))
g.cfg.window.width, g.cfg.window.height = float32(width), float32(height)
g.cfg.x = interval{min: 0, max: float64(width)}
g.cfg.y = interval{min: 0, max: float64(height)}
g.cfg.color.background = defaultBkgColor
g.cfg.color.fill = defaultFillColor
if !resizeXY {
g.cfg.window.width, g.cfg.window.height = g.getWindowSize()
}
g.cfg.window.title = "Preview process..."
}
// getWindowSize returns the resized image dimension.
func (g *Gui) getWindowSize() (float32, float32) {
w, h := g.cfg.window.width, g.cfg.window.height
// Maintain the image aspect ratio in case the image width and height is greater than the predefined window.
r := getRatio(w, h)
if w > maxScreenX && h > maxScreenY {
w = w * r
h = h * r
}
return w, h
}
// Run is the core method of the Gio GUI application.
// This updates the window with the resized image received from a channel
// and terminates when the image resizing operation completes.
func (g *Gui) Run() error {
var (
rc uint8 = redStart
gc uint8 = greenStart
bc uint8 = blueStart
descRed, descGreen, descBlue bool
)
width := unit.Dp(g.cfg.window.width)
height := unit.Dp(g.cfg.window.height)
w := new(app.Window)
w.Option(
app.Title(g.cfg.window.title),
app.Size(width, height),
app.MinSize(width, height),
app.MaxSize(width, height),
)
// Center the window.
w.Perform(system.ActionCenter)
g.cfg.timeStamp = time.Now()
if g.proc.Debug {
g.AddHudControl(hudShowSeams, "Show seams", true)
if len(g.proc.MaskPath) > 0 || len(g.proc.RMaskPath) > 0 || g.proc.FaceDetect {
g.AddHudControl(hudShowDebugMask, "Debug mode", false)
}
}
abortFn := func() {
var dx, dy int
if g.process.img != nil {
bounds := g.process.img.Bounds()
dx, dy = bounds.Max.X, bounds.Max.Y
}
if !g.process.isDone {
if (g.proc.NewWidth > 0 && g.proc.NewWidth != dx) ||
(g.proc.NewHeight > 0 && g.proc.NewHeight != dy) {
errorMsg := fmt.Sprintf("%s %s %s",
utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
utils.DecorateText("⇢ process aborted by the user...", utils.DefaultMessage),
utils.DecorateText("✘\n", utils.ErrorMessage),
)
g.proc.Spinner.StopMsg = errorMsg
g.proc.Spinner.Stop()
}
}
g.proc.Spinner.RestoreCursor()
}
for {
select {
case res := <-g.process.worker:
if res.done {
w.Option(app.Title("Done!"))
g.process.isDone = true
break
}
if resizeXY {
continue
}
g.process.img = res.img
g.process.seams = res.seams
if mask, ok := g.huds[hudShowDebugMask]; ok {
if mask.enabled.Value && res.mask != nil {
bounds := res.img.Bounds()
srcBitmap := imop.NewBitmap(bounds)
dstBitmap := imop.NewBitmap(bounds)
uniformCol := image.NewNRGBA(bounds)
col := color.RGBA{R: 0x2f, G: 0xf3, B: 0xe0, A: 0xff}
draw.Draw(uniformCol, uniformCol.Bounds(), &image.Uniform{col}, image.Point{}, draw.Src)
_ = g.compOp.Set(imop.DstIn)
g.compOp.Draw(srcBitmap, res.mask, uniformCol, nil)
_ = g.blendOp.Set(imop.Screen)
_ = g.compOp.Set(imop.SrcAtop)
g.compOp.Draw(dstBitmap, res.img, srcBitmap.Img, g.blendOp)
g.process.img = dstBitmap.Img
}
}
if g.proc.vRes {
g.process.img = rotateImage270(g.process.img.(*image.NRGBA))
}
w.Invalidate()
default:
switch e := w.Event().(type) {
case app.FrameEvent:
g.ctx = app.NewContext(g.ctx.Ops, e)
for {
event, ok := g.ctx.Event(key.Filter{
Name: key.NameEscape,
})
if !ok {
break
}
switch event := event.(type) {
case key.Event:
switch event.Name {
case key.NameEscape:
w.Perform(system.ActionClose)
abortFn()
return nil
}
}
}
{ // red
if descRed {
rc--
} else {
rc++
}
if rc >= redEnd {
descRed = !descRed
}
if rc == redStart {
descRed = !descRed
}
}
{ // green
if descGreen {
gc--
} else {
gc++
}
if gc >= greenEnd {
descGreen = !descGreen
}
if gc == greenStart {
descGreen = !descGreen
}
}
{ // blue
if descBlue {
bc--
} else {
bc++
}
if bc >= blueEnd {
descBlue = !descBlue
}
if bc == blueStart {
descBlue = !descBlue
}
}
g.draw(color.NRGBA{R: rc, G: gc, B: bc})
e.Frame(g.ctx.Ops)
case app.DestroyEvent:
abortFn()
return e.Err
}
}
}
}
type (
C = layout.Context
D = layout.Dimensions
)
// draw draws the resized image in the GUI window (obtained from a channel)
// and in case the debug mode is activated it prints out the seams.
func (g *Gui) draw(bgColor color.NRGBA) {
g.ctx.Execute(op.InvalidateCmd{})
c := g.setColor(g.cfg.color.background)
paint.Fill(g.ctx.Ops, c)
if g.process.img != nil {
src := paint.NewImageOp(g.process.img)
src.Add(g.ctx.Ops)
layout.Stack{}.Layout(g.ctx,
layout.Stacked(func(gtx C) D {
paint.FillShape(gtx.Ops, c,
clip.Rect{Max: g.ctx.Constraints.Max}.Op(),
)
return layout.UniformInset(unit.Dp(0)).Layout(gtx,
func(gtx C) D {
widget.Image{
Src: src,
Scale: 1 / float32(unit.Dp(1)),
Fit: widget.Contain,
}.Layout(gtx)
if seam, ok := g.huds[hudShowSeams]; ok {
if seam.enabled.Value {
tr := f32.Affine2D{}
screen := layout.FPt(g.ctx.Constraints.Max)
width, height := float32(g.process.img.Bounds().Dx()), float32(g.process.img.Bounds().Dy())
sw, sh := float32(screen.X), float32(screen.Y)
if sw > width {
ratio := sw / width
tr = tr.Scale(f32.Pt(sw/2, sh/2), f32.Pt(1, ratio))
} else if sh > height {
ratio := sh / height
tr = tr.Scale(f32.Pt(sw/2, sh/2), f32.Pt(ratio, 1))
}
if g.proc.vRes {
angle := float32(270 * math.Pi / 180)
half := float32(math.Round(float64(sh*0.5-height*0.5) * 0.5))
ox := math.Abs(float64(sw - (sw - (sw/2 - sh/2))))
oy := math.Abs(float64(sh - (sh - (sw/2 - height/2 + half))))
tr = tr.Rotate(f32.Pt(sw/2, sh/2), -angle)
if screen.X > screen.Y {
tr = tr.Offset(f32.Pt(float32(ox), float32(oy)))
} else {
tr = tr.Offset(f32.Pt(float32(-ox), float32(-oy)))
}
}
op.Affine(tr).Add(gtx.Ops)
for _, s := range g.process.seams {
dpx := gtx.Dp(unit.Dp(s.X))
dpy := gtx.Dp(unit.Dp(s.Y))
g.DrawSeam(g.proc.ShapeType, float32(dpx), float32(dpy), 1.0)
}
}
}
return layout.Dimensions{Size: gtx.Constraints.Max}
})
}),
)
}
if g.proc.Debug {
layout.Stack{}.Layout(g.ctx,
layout.Stacked(func(gtx C) D {
hudHeight := 30
r := image.Rectangle{
Max: image.Point{
X: gtx.Constraints.Max.X,
Y: hudHeight,
},
}
defer op.Offset(image.Pt(0, gtx.Constraints.Max.Y-hudHeight)).Push(gtx.Ops).Pop()
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx C) D {
paint.FillShape(gtx.Ops, color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xcc}, clip.Rect(r).Op())
return layout.Dimensions{Size: r.Max}
}),
layout.Stacked(func(gtx C) D {
border := image.Rectangle{
Max: image.Point{
X: gtx.Constraints.Max.X,
Y: gtx.Dp(unit.Dp(0.5)),
},
}
paint.FillShape(gtx.Ops, color.NRGBA{R: 0xd0, G: 0xcd, B: 0xd7, A: 0xaa}, clip.Rect(border).Op())
return layout.Dimensions{Size: r.Max}
}),
layout.Stacked(func(gtx C) D {
return g.view.huds.Layout(gtx, len(g.huds),
func(gtx layout.Context, index int) D {
if hud, ok := g.huds[hudControlType(index)]; ok {
checkbox := material.CheckBox(g.theme, &hud.enabled, fmt.Sprintf("%v", hud.title))
checkbox.Size = 20
return checkbox.Layout(gtx)
}
return D{}
})
}),
)
}),
)
}
// Disable the preview mode and warn the user in case the image is resized both horizontally and vertically.
if resizeXY {
var msg string
if !g.process.isDone {
msg = "Preview is not available while the image is resized both horizontally and vertically!"
} else {
msg = "Done, you may close this window!"
bgColor = color.NRGBA{R: 45, G: 45, B: 42, A: 0xff}
}
g.displayMessage(g.ctx, bgColor, msg)
}
}
// displayMessage show a static message when the image is resized both horizontally and vertically.
func (g *Gui) displayMessage(ctx layout.Context, bgCol color.NRGBA, msg string) {
g.theme.Palette.Fg = color.NRGBA{R: 251, G: 254, B: 249, A: 0xff}
paint.ColorOp{Color: bgCol}.Add(ctx.Ops)
rect := image.Rectangle{
Max: ctx.Constraints.Max,
}
defer clip.Rect(rect).Push(ctx.Ops).Pop()
paint.PaintOp{}.Add(ctx.Ops)
layout.Stack{}.Layout(ctx,
layout.Stacked(func(gtx C) D {
return layout.UniformInset(unit.Dp(4)).Layout(ctx, func(gtx C) D {
if !g.process.isDone {
gtx.Constraints.Min.Y = 0
tr := f32.Affine2D{}
dr := image.Rectangle{Max: gtx.Constraints.Min}
tr = tr.Rotate(f32.Pt(float32(ctx.Constraints.Max.X/2), float32(ctx.Constraints.Max.Y/2)), 0.005*-g.cfg.angle)
op.Affine(tr).Add(gtx.Ops)
since := time.Since(g.cfg.timeStamp)
if since.Seconds() > 5 {
g.cfg.timeStamp = time.Now()
g.cfg.color.randR = uint8(random(1, 2))
g.cfg.color.randG = uint8(random(1, 2))
g.cfg.color.randB = uint8(random(1, 2))
}
paint.LinearGradientOp{
Stop1: layout.FPt(dr.Min.Div(2)),
Stop2: layout.FPt(dr.Max.Mul(2)),
Color1: color.NRGBA{R: 41, G: bgCol.G * g.cfg.color.randG, B: bgCol.B * g.cfg.color.randB, A: 0xFF},
Color2: color.NRGBA{R: bgCol.R * g.cfg.color.randR, G: 29, B: 54, A: 0xFF},
}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
if g.cfg.chrot {
g.cfg.angle--
} else {
g.cfg.angle++
}
if g.cfg.angle == -90 || g.cfg.angle == 90 {
g.cfg.chrot = !g.cfg.chrot
}
}
return layout.Dimensions{
Size: gtx.Constraints.Max,
}
})
}),
layout.Stacked(func(gtx C) D {
return layout.UniformInset(unit.Dp(4)).Layout(ctx, func(gtx C) D {
return layout.Center.Layout(ctx, func(gtx C) D {
m := material.Label(g.theme, unit.Sp(40), msg)
m.Alignment = text.Middle
return m.Layout(gtx)
})
})
}),
layout.Stacked(func(gtx C) D {
info := "(You will be notified once the process is finished.)"
if g.process.isDone {
return layout.Dimensions{}
}
return layout.Inset{Top: 70}.Layout(ctx, func(gtx C) D {
return layout.Center.Layout(ctx, func(gtx C) D {
return material.Label(g.theme, unit.Sp(13), info).Layout(gtx)
})
})
}),
)
}
// random generates a random number between two numbers.
func random(min, max float32) float32 {
return rand.Float32()*(max-min) + min
}