mirror of
https://github.com/esimov/caire.git
synced 2025-09-26 12:31:16 +08:00
566 lines
14 KiB
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
|
|
}
|