main.go (8823B)
1 package main 2 3 import ( 4 "bytes" 5 "image" 6 "image/color" 7 "image/gif" 8 "log" 9 "os" 10 "path/filepath" 11 "strconv" 12 "time" 13 14 "gioui.org/app" 15 "gioui.org/font/gofont" 16 "gioui.org/io/system" 17 "gioui.org/layout" 18 l "gioui.org/layout" 19 "gioui.org/op" 20 "gioui.org/op/paint" 21 "gioui.org/unit" 22 "gioui.org/widget" 23 "gioui.org/widget/material" 24 m "gioui.org/widget/material" 25 c "gioui.org/x/component" 26 "github.com/ncruces/zenity" 27 ) 28 29 func main() { 30 go func() { 31 ui := UI{ 32 Window: app.NewWindow( 33 app.Title("Giffer"), 34 app.MinSize(unit.Dp(800), unit.Dp(425)), 35 ), 36 Th: material.NewTheme(gofont.Collection()), 37 Giffer: Giffer{ 38 Store: &gifdb{ 39 Dir: filepath.Join(os.TempDir(), "giffer"), 40 }, 41 }, 42 } 43 if err := ui.Loop(); err != nil { 44 log.Fatalf("error: %v", err) 45 } 46 os.Exit(0) 47 }() 48 app.Main() 49 } 50 51 type ( 52 C = layout.Context 53 D = layout.Dimensions 54 ) 55 56 type UI struct { 57 *app.Window 58 Giffer Giffer 59 Th *m.Theme 60 Form Form 61 GifPlayer GifPlayer 62 Processing bool 63 cache *PreparedGif 64 done chan *PreparedGif 65 } 66 67 // PreparedGif helper wraps a Gif with FPS metadata. 68 // This is needed because Gif itself cannot be relied upon to contain proper FPS data in it's delay 69 // slice. 70 type PreparedGif struct { 71 *gif.GIF 72 FPS float64 73 } 74 75 // Loop runs the event loop until terminated. 76 func (ui *UI) Loop() error { 77 var ( 78 ops op.Ops 79 events = ui.Window.Events() 80 ) 81 ui.Init() 82 for event := range events { 83 switch event := (event).(type) { 84 case system.DestroyEvent: 85 return event.Err 86 case system.FrameEvent: 87 gtx := layout.NewContext(&ops, event) 88 ui.Update(gtx) 89 ui.Layout(gtx) 90 event.Frame(gtx.Ops) 91 } 92 } 93 return nil 94 } 95 96 func (ui *UI) Init() { 97 ui.Form.FPS.SetText("12") 98 ui.Form.Width.SetText("400") 99 ui.Form.Height.SetText("350") 100 ui.Form.Start.SetText("0") 101 ui.Form.End.SetText("3") 102 ui.done = make(chan *PreparedGif) 103 } 104 105 func (ui *UI) Update(gtx C) { 106 if ui.Form.SaveBtn.Clicked() && ui.cache != nil { 107 path, err := zenity.SelectFileSave(zenity.Title("Save Gif"), zenity.ConfirmOverwrite()) 108 if err != nil { 109 log.Printf("error: selecting save file: %v", err) 110 } 111 savef, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0664) 112 if err != nil { 113 log.Printf("error: opening save file: %v", err) 114 } 115 defer func() { go savef.Close() }() 116 if err := gif.EncodeAll(savef, ui.cache.GIF); err != nil { 117 log.Printf("error: saving gif to %q: %v", path, err) 118 } 119 ui.cache = nil 120 } 121 if ui.Form.SubmitBtn.Clicked() { 122 ui.GifPlayer.Clear() 123 ui.Processing = true 124 var ( 125 url = ui.Form.URL.Text() 126 fuzz = 0 127 ) 128 // TODO(jfm): show as validation errors. 129 start, err := strconv.ParseFloat(ui.Form.Start.Text(), 64) 130 if err != nil { 131 log.Printf("error: start must be a floating point number") 132 } 133 end, err := strconv.ParseFloat(ui.Form.End.Text(), 64) 134 if err != nil { 135 log.Printf("error: end must be a floating point number") 136 } 137 fps, err := strconv.ParseFloat(ui.Form.FPS.Text(), 64) 138 if err != nil { 139 log.Printf("error: fps must be a floating point number") 140 } 141 width, err := strconv.Atoi(ui.Form.Width.Text()) 142 if err != nil { 143 log.Printf("error: width must be an integer number") 144 } 145 height, err := strconv.Atoi(ui.Form.Height.Text()) 146 if err != nil { 147 log.Printf("error: height must be an integer number") 148 } 149 go func() { 150 g, err := ui.Giffer.GififyURL( 151 url, 152 start, 153 end, 154 fps, 155 width, 156 height, 157 fuzz, 158 ) 159 if err != nil { 160 log.Printf("error: fetching gif: %v", err) 161 return 162 } 163 img, err := gif.DecodeAll(bytes.NewReader(g.Data)) 164 if err != nil { 165 log.Printf("error: decoding gif: %v", err) 166 return 167 } 168 ui.GifPlayer.FPS = fps 169 ui.done <- &PreparedGif{ 170 GIF: img, 171 FPS: fps, 172 } 173 }() 174 } 175 select { 176 case img := <-ui.done: 177 ui.cache = img 178 ui.Processing = false 179 ui.GifPlayer.Load(img) 180 default: 181 } 182 } 183 184 func (ui *UI) Layout(gtx C) D { 185 return layout.Stack{}.Layout( 186 gtx, 187 layout.Stacked(func(gtx C) D { 188 return l.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx C) D { 189 return l.Flex{ 190 Axis: l.Horizontal, 191 }.Layout( 192 gtx, 193 l.Flexed(1, func(gtx C) D { 194 return l.Center.Layout(gtx, func(gtx C) D { 195 var ( 196 cs = >x.Constraints 197 max = gtx.Dp(400) 198 ) 199 if cs.Max.X > max { 200 cs.Max.X = max 201 } 202 if ui.Processing { 203 gtx.Queue = nil 204 } 205 return l.Flex{ 206 Axis: l.Vertical, 207 }.Layout( 208 gtx, 209 l.Rigid(func(gtx C) D { 210 return ui.Form.LayoutFields(gtx, ui.Th) 211 }), 212 l.Flexed(1, func(gtx C) D { 213 return D{Size: gtx.Constraints.Max} 214 }), 215 l.Rigid(func(gtx C) D { 216 return l.Flex{ 217 Axis: l.Horizontal, 218 }.Layout( 219 gtx, 220 l.Rigid(func(gtx C) D { 221 return m.Button(ui.Th, &ui.Form.SubmitBtn, "Create"). 222 Layout(gtx) 223 }), 224 l.Rigid(func(gtx C) D { 225 return D{Size: image.Point{X: 10}} 226 }), 227 l.Rigid(func(gtx C) D { 228 if ui.cache == nil { 229 gtx.Queue = nil 230 } 231 return m.Button(ui.Th, &ui.Form.SaveBtn, "Save"). 232 Layout(gtx) 233 }), 234 ) 235 }), 236 ) 237 }) 238 }), 239 l.Rigid(func(gtx C) D { 240 return D{Size: image.Point{X: 10}} 241 }), 242 l.Flexed(1, func(gtx C) D { 243 return ui.GifPlayer.Layout(gtx) 244 }), 245 ) 246 }) 247 }), 248 layout.Expanded(func(gtx C) D { 249 if !ui.Processing { 250 return D{} 251 } 252 return c.Rect{ 253 Color: color.NRGBA{A: 100}, 254 Size: gtx.Constraints.Max, 255 }.Layout(gtx) 256 }), 257 layout.Expanded(func(gtx C) D { 258 if !ui.Processing { 259 return D{} 260 } 261 cs := >x.Constraints 262 cs.Max.X = 50 263 cs.Max.Y = 50 264 return l.Center.Layout(gtx, func(gtx C) D { 265 return m.Loader(ui.Th).Layout(gtx) 266 }) 267 }), 268 ) 269 } 270 271 // Form holds state for form inputs. 272 type Form struct { 273 URL c.TextField 274 Start c.TextField 275 End c.TextField 276 Width c.TextField 277 Height c.TextField 278 FPS c.TextField 279 SubmitBtn widget.Clickable 280 SaveBtn widget.Clickable 281 } 282 283 func (f *Form) LayoutFields(gtx C, th *m.Theme) D { 284 return l.Flex{ 285 Axis: l.Vertical, 286 }.Layout( 287 gtx, 288 l.Rigid(func(gtx C) D { 289 return f.URL.Layout(gtx, th, "url") 290 }), 291 l.Rigid(func(gtx C) D { 292 return f.Start.Layout(gtx, th, "start (seconds)") 293 }), 294 l.Rigid(func(gtx C) D { 295 return f.End.Layout(gtx, th, "end (seconds)") 296 }), 297 l.Rigid(func(gtx C) D { 298 return f.Width.Layout(gtx, th, "width (pixels)") 299 }), 300 l.Rigid(func(gtx C) D { 301 return f.Height.Layout(gtx, th, "height (pixels)") 302 }), 303 l.Rigid(func(gtx C) D { 304 return f.FPS.Layout(gtx, th, "fps (integer)") 305 }), 306 l.Rigid(func(gtx C) D { 307 return D{Size: image.Point{Y: 10}} 308 }), 309 ) 310 } 311 312 func (f *Form) LayoutActions(gtx C, th *m.Theme) D { 313 return l.Flex{ 314 Axis: l.Horizontal, 315 }.Layout( 316 gtx, 317 l.Rigid(func(gtx C) D { 318 return m.Button(th, &f.SubmitBtn, "Create").Layout(gtx) 319 }), 320 l.Rigid(func(gtx C) D { 321 return D{Size: image.Point{X: 10}} 322 }), 323 l.Rigid(func(gtx C) D { 324 return m.Button(th, &f.SaveBtn, "Save").Layout(gtx) 325 }), 326 ) 327 } 328 329 // GifPlayer animates through a series of frames. 330 // 331 // TODO(jfm): Fix artifacting from the ImageStack implementation. 332 // TODO(jfm): Properly crop gif. 333 type GifPlayer struct { 334 Frames []paint.ImageOp 335 Cursor int 336 FPS float64 337 since time.Time 338 img widget.Image 339 } 340 341 // Load a Gif image to render. 342 func (g *GifPlayer) Load(src *PreparedGif) { 343 g.FPS = float64(src.FPS) 344 g.Frames = make([]paint.ImageOp, len(src.Image)) 345 for ii := range src.Image { 346 s := ImageStack{Config: src.Config} 347 for jj := ii; jj >= 0; jj-- { 348 s.Stack = append(s.Stack, src.Image[jj]) 349 } 350 g.Frames[ii] = paint.NewImageOp(s) 351 } 352 } 353 354 // Clear the player state. 355 // Stops playing any current gif. 356 func (g *GifPlayer) Clear() { 357 g.Frames = g.Frames[:] 358 g.Cursor = 0 359 g.FPS = 0 360 } 361 362 // Ready if the next frame is ready to be displayed. 363 func (g *GifPlayer) Ready(gtx C) bool { 364 var ( 365 now = gtx.Now 366 since = g.since 367 latency = time.Second / time.Duration(g.FPS) 368 ) 369 return now.Sub(since).Milliseconds() >= latency.Milliseconds() 370 } 371 372 // Next loads the next frame in the series. 373 func (g *GifPlayer) Next(gtx C) { 374 defer func() { 375 g.Cursor++ 376 if g.Cursor > len(g.Frames)-1 { 377 g.Cursor = 0 378 } 379 g.since = gtx.Now 380 }() 381 g.img.Src = g.Frames[g.Cursor] 382 } 383 384 // Current returns the current frame. 385 func (g *GifPlayer) Current() paint.ImageOp { 386 return g.Frames[g.Cursor] 387 } 388 389 func (g *GifPlayer) Layout(gtx C) D { 390 if g == nil || len(g.Frames) == 0 { 391 return c.Rect{ 392 Size: image.Point{ 393 X: gtx.Constraints.Max.X, 394 Y: gtx.Constraints.Max.Y, 395 }, 396 Color: color.NRGBA{A: 100}, 397 }.Layout(gtx) 398 } 399 op.InvalidateOp{}.Add(gtx.Ops) 400 if g.Ready(gtx) { 401 g.Next(gtx) 402 } 403 g.img.Fit = widget.Fill 404 return g.img.Layout(gtx) 405 }