diff options
author | Martin Ashby <martin@martin-laptop.lan> | 2018-05-28 16:31:18 +0100 |
---|---|---|
committer | Martin Ashby <martin@martin-laptop.lan> | 2018-05-28 16:31:18 +0100 |
commit | 28f0ac19eb3b42601b0511131b5e39c77b80da9c (patch) | |
tree | a0765d2742926bb418ce623175876a68cb000c8b /unicorn | |
parent | 5766b0c285052d48ab7756c9c3eef6ae41ecd36e (diff) | |
parent | f20896ef3b147ac07769eb36c67ba436c6d2ed22 (diff) | |
download | unicornpaint-28f0ac19eb3b42601b0511131b5e39c77b80da9c.tar.gz unicornpaint-28f0ac19eb3b42601b0511131b5e39c77b80da9c.tar.bz2 unicornpaint-28f0ac19eb3b42601b0511131b5e39c77b80da9c.tar.xz unicornpaint-28f0ac19eb3b42601b0511131b5e39c77b80da9c.zip |
Merge branch 'master' of github.com:MFAshby/unicornpaint
Diffstat (limited to 'unicorn')
-rw-r--r-- | unicorn/BaseFakeUnicorn.go | 78 | ||||
-rw-r--r-- | unicorn/FakeUnicorn.go | 66 | ||||
-rw-r--r-- | unicorn/FakeUnicorn2.go | 70 | ||||
-rw-r--r-- | unicorn/RealUnicorn.go | 3 | ||||
-rw-r--r-- | unicorn/RealUnicorn2.go | 69 | ||||
-rw-r--r-- | unicorn/SpiRenderDevice.go | 19 | ||||
-rw-r--r-- | unicorn/Unicorn2.go | 40 | ||||
-rw-r--r-- | unicorn/Unicorn2_test.go | 61 | ||||
-rw-r--r-- | unicorn/Unicorn_test2.go | 21 | ||||
-rw-r--r-- | unicorn/bindata.go | 25 | ||||
-rwxr-xr-x | unicorn/build_bindata.sh | 1 | ||||
-rw-r--r-- | unicorn/data/sample2.gif | bin | 0 -> 1783 bytes |
12 files changed, 331 insertions, 122 deletions
diff --git a/unicorn/BaseFakeUnicorn.go b/unicorn/BaseFakeUnicorn.go new file mode 100644 index 0000000..7910814 --- /dev/null +++ b/unicorn/BaseFakeUnicorn.go @@ -0,0 +1,78 @@ +package unicorn + +import "github.com/veandco/go-sdl2/sdl" + +// BaseFakeUnicorn ... +// The base for FakeUnicorn & FakeUnicorn2 +// Share the SDL code. +type BaseFakeUnicorn struct { + displayWidth int32 + displayHeight int32 + window *sdl.Window + renderer *sdl.Renderer +} + +func NewBaseFakeUnicorn(width, height int32) (*BaseFakeUnicorn, error) { + if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil { + return nil, err + } + + unicorn := &BaseFakeUnicorn{ + displayWidth: width, + displayHeight: height, + window: nil, + renderer: nil, + } + if err := unicorn.createWindow(); err != nil { + unicorn.Close() + return nil, err + } + if err := unicorn.createRenderer(); err != nil { + unicorn.Close() + return nil, err + } + return unicorn, nil +} + +func (f *BaseFakeUnicorn) createWindow() error { + window, err := sdl.CreateWindow("Fake Unicorn", + sdl.WINDOWPOS_UNDEFINED, + sdl.WINDOWPOS_UNDEFINED, + f.displayWidth, + f.displayHeight, + sdl.WINDOW_SHOWN) + f.window = window + return err +} + +func (f *BaseFakeUnicorn) createRenderer() error { + renderer, err := sdl.CreateRenderer(f.window, -1, sdl.RENDERER_ACCELERATED) + f.renderer = renderer + return err +} + +func (f *BaseFakeUnicorn) Close() error { + if f.window != nil { + f.window.Destroy() + } + if f.renderer != nil { + f.renderer.Destroy() + } + return nil +} + +// MainLoop ... +// Handle UI events so OS doesn't think we're frozen +func (f *BaseFakeUnicorn) MainLoop() { + running := true + for running { + for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { + switch event.(type) { + case *sdl.QuitEvent: + println("Quit") + running = false + break + } + } + } +} diff --git a/unicorn/FakeUnicorn.go b/unicorn/FakeUnicorn.go index eaec4f2..8db71d5 100644 --- a/unicorn/FakeUnicorn.go +++ b/unicorn/FakeUnicorn.go @@ -8,10 +8,7 @@ import ( type FakeUnicorn struct { BaseUnicorn - displayWidth int32 - displayHeight int32 - window *sdl.Window - renderer *sdl.Renderer + *BaseFakeUnicorn } // NewUnicorn ... @@ -19,7 +16,9 @@ type FakeUnicorn struct { func NewUnicorn() (*FakeUnicorn, error) { width := uint8(16) height := uint8(16) - if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil { + + baseFake, err := NewBaseFakeUnicorn(300, 300) + if err != nil { return nil, err } @@ -27,47 +26,10 @@ func NewUnicorn() (*FakeUnicorn, error) { BaseUnicorn{ pixels: makePixels(width, height), }, - 300, - 300, - nil, - nil, - } - if err := unicorn.createWindow(); err != nil { - unicorn.Close() - return nil, err - } - if err := unicorn.createRenderer(); err != nil { - unicorn.Close() - return nil, err + baseFake, } - return unicorn, nil -} -func (f *FakeUnicorn) createWindow() error { - window, err := sdl.CreateWindow("Fake Unicorn", - sdl.WINDOWPOS_UNDEFINED, - sdl.WINDOWPOS_UNDEFINED, - f.displayWidth, - f.displayHeight, - sdl.WINDOW_SHOWN) - f.window = window - return err -} - -func (f *FakeUnicorn) createRenderer() error { - renderer, err := sdl.CreateRenderer(f.window, -1, sdl.RENDERER_ACCELERATED) - f.renderer = renderer - return err -} - -func (f *FakeUnicorn) Close() error { - if f.window != nil { - f.window.Destroy() - } - if f.renderer != nil { - f.renderer.Destroy() - } - return nil + return unicorn, nil } func (f *FakeUnicorn) Show() { @@ -96,19 +58,3 @@ func (f *FakeUnicorn) Show() { func (f *FakeUnicorn) Off() { f.Close() } - -// MainLoop ... -// Handle UI events so OS doesn't think we're frozen -func (f *FakeUnicorn) MainLoop() { - running := true - for running { - for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { - switch event.(type) { - case *sdl.QuitEvent: - println("Quit") - running = false - break - } - } - } -} diff --git a/unicorn/FakeUnicorn2.go b/unicorn/FakeUnicorn2.go index 73ce8ab..27629d2 100644 --- a/unicorn/FakeUnicorn2.go +++ b/unicorn/FakeUnicorn2.go @@ -8,50 +8,42 @@ import ( type FakeUnicorn2 struct { BaseUnicorn2 + *BaseFakeUnicorn } -func renderImage(im image.Image) { - // b := im.Bounds() - // width := b.Dx() - // height := b.Dy() - // for x := 0; x < width; x++ { - // for y := 0; y < height; y++ { - // r, g, b, _ := im.At(x, y).RGBA() - // un.SetPixel(uint8(x), uint8(y), uint8(r), uint8(g), uint8(b)) - // } - // } - // un.Show() -} - -func render() { - // for !stop { - // for i := 0; i < len(gf.Image); i++ { - // im := gf.Image[i] - // delay := gf.Delay[i] //100ths of a second - // renderImage(un, im) - // time.Sleep(time.Duration(delay * 10000000)) // nanoseconds 10^-9 sec - // } - // } -} - -func (u *FakeUnicorn2) StartRender() { - -} - -func MainLoop() { - running := true - for running { - for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { - switch event.(type) { - case *sdl.QuitEvent: - println("Quit") - running = false - break +func (u *FakeUnicorn2) renderImage(im image.Image) { + b := im.Bounds() + width, height := b.Dx(), b.Dy() + for x := 0; x < width; x++ { + for y := 0; y < height; y++ { + col := im.At(x, y) + r, g, b, _ := col.RGBA() + // Ignore alpha for now, not worked out how it should work on real unicorn + if err := u.renderer.SetDrawColor(uint8(r), uint8(g), uint8(b), uint8(255)); err != nil { + panic(err) + } + cellWidth := u.displayWidth / int32(width) + cellHeight := u.displayHeight / int32(height) + if err := u.renderer.FillRect(&sdl.Rect{ + X: cellWidth * int32(x), + Y: u.displayHeight - (cellHeight * int32(y)) - cellHeight, // SDL Y coordinate is from the top + W: cellWidth, + H: cellHeight, + }); err != nil { + panic(err) } } } + u.renderer.Present() } -func NewUnicorn2() *FakeUnicorn2 { - return &FakeUnicorn2{} +func NewUnicorn2() (*FakeUnicorn2, error) { + baseFake, err := NewBaseFakeUnicorn(300, 300) + if err != nil { + return nil, err + } + return &FakeUnicorn2{ + BaseUnicorn2{}, + baseFake, + }, nil } diff --git a/unicorn/RealUnicorn.go b/unicorn/RealUnicorn.go index b3c1df3..6174ccf 100644 --- a/unicorn/RealUnicorn.go +++ b/unicorn/RealUnicorn.go @@ -3,11 +3,12 @@ package unicorn import ( - "github.com/ecc1/spi" "log" "os" "os/signal" "syscall" + + "github.com/ecc1/spi" ) type RealUnicorn struct { diff --git a/unicorn/RealUnicorn2.go b/unicorn/RealUnicorn2.go new file mode 100644 index 0000000..991e79f --- /dev/null +++ b/unicorn/RealUnicorn2.go @@ -0,0 +1,69 @@ +// +build linux,arm linux,arm64 + +package unicorn + +import ( + "image" + "log" + "os" + "os/signal" + "syscall" + + "github.com/ecc1/spi" +) + +type RealUnicorn2 struct { + BaseUnicorn2 + device *spi.Device +} + +func (u *RealUnicorn2) renderImage(im image.Image) { + b := im.Bounds() + width, height := b.Dx(), b.Dy() + sz := (width * height * 3) + 1 + write := make([]byte, sz) + + // Write leading bit + write[0] = 0x72 + + // Write color values + ix := 1 + for x := 0; x < width; x++ { + for y := 0; y < height; y++ { + col := im.At(x, y) + r, g, b, _ := col.RGBA() + write[ix] = byte(r) + ix++ + write[ix] = byte(g) + ix++ + write[ix] = byte(b) + ix++ + } + } + // Write to the device + err := u.device.Transfer(write) + if err != nil { + log.Printf("Error writing to SPI device %v", err) + } +} + +// NewUnicorn2 ... +// Constructs a new and improved unicorn from stuff and things +func NewUnicorn2() (*RealUnicorn2, error) { + dev, err := spi.Open("/dev/spidev0.0", 9000000, 0) + if err != nil { + return nil, err + } + return &RealUnicorn2{ + BaseUnicorn2{}, + dev, + }, nil +} + +// MainLoop ... +// Just blocks until sigterm +func (u *RealUnicorn2) MainLoop() { + c := make(chan os.Signal, 2) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c +} diff --git a/unicorn/SpiRenderDevice.go b/unicorn/SpiRenderDevice.go new file mode 100644 index 0000000..140326a --- /dev/null +++ b/unicorn/SpiRenderDevice.go @@ -0,0 +1,19 @@ +package unicorn + +import ( + "github.com/ecc1/spi" +) + +type SpiRenderDevice struct { + device *spi.Device +} + +func NewSpiRenderDevice() (*SpiRenderDevice, error) { + dev, err := spi.Open("/dev/spidev0.0", 9000000, 0) + if err != nil { + return nil, err + } + return &SpiRenderDevice{ + device: dev, + }, nil +} diff --git a/unicorn/Unicorn2.go b/unicorn/Unicorn2.go index 716fe4d..04722f7 100644 --- a/unicorn/Unicorn2.go +++ b/unicorn/Unicorn2.go @@ -4,21 +4,32 @@ package unicorn import ( + "image" "image/gif" + "time" ) // Unicorn2 ... // Interface for interacting with the Unicorn HAT HD // Implemented by both real & fake unicorns. type Unicorn2 interface { + // Change the image GetGif() *gif.GIF SetGif(*gif.GIF) + // Starts the rendering goroutine StartRender() + + // Must be implemented to actually render the image to device + renderImage(image.Image) + // Required for os to not think we're stuck MainLoop() } +// BaseUnicorn2 ... +// Common to both real & fake unicorns! +// timing code for rendering & stopping rendering type BaseUnicorn2 struct { g *gif.GIF } @@ -30,3 +41,32 @@ func (u *BaseUnicorn2) GetGif() *gif.GIF { func (u *BaseUnicorn2) SetGif(g *gif.GIF) { u.g = g } + +// StartRender ... +// Starts rendering the image. If it's an animated image, +// renders animation frames. Return a channel to stop the +// image rendering. +func (u *FakeUnicorn2) StartRender() chan bool { + stopChan := make(chan bool) + go func() { + timer := time.NewTimer(0) + imageIndex := 0 + running := true + for running { + select { + case <-stopChan: + timer.Stop() + running = false + case <-timer.C: + gf := u.GetGif() + im := gf.Image[imageIndex] + delay := gf.Delay[imageIndex] //100ths of a second, 10^-2 + u.renderImage(im) + + timer.Reset(time.Duration(delay * 10000000)) // nanoseconds 10^-9 sec + imageIndex = (imageIndex + 1) % len(gf.Image) + } + } + }() + return stopChan +}
\ No newline at end of file diff --git a/unicorn/Unicorn2_test.go b/unicorn/Unicorn2_test.go new file mode 100644 index 0000000..c74571f --- /dev/null +++ b/unicorn/Unicorn2_test.go @@ -0,0 +1,61 @@ +package unicorn + +import ( + "bytes" + "image/gif" + "testing" + "time" +) + +func gifAsset(name string) (*gif.GIF, error) { + data, err := Asset(name) + if err != nil { + return nil, err + } + + g, err := gif.DecodeAll(bytes.NewReader(data)) + if err != nil { + return nil, err + } + + return g, nil +} + +func TestAnimated(t *testing.T) { + un, err := NewUnicorn2() + if err != nil { + t.Errorf("Failed to create fake unicorn :( %v", err) + return + } + defer un.Close() + + g, err := gifAsset("data/sample.gif") + if err != nil { + t.Errorf("Failed to load asset %v", err) + return + } + + un.SetGif(g) + stopChan := un.StartRender() + + // Stop after 3 + time.Sleep(3 * time.Second) + stopChan <- true + + // Leave it for a sec + time.Sleep(1 * time.Second) + g2, err := gifAsset("data/sample2.gif") + if err != nil { + t.Errorf("Failed to load asset %v", err) + return + } + un.SetGif(g2) + stopChan = un.StartRender() + + // Stop after 5 + time.Sleep(5 * time.Second) + stopChan <- true + + // Make sure it's stopped + time.Sleep(2 * time.Second) +} diff --git a/unicorn/Unicorn_test2.go b/unicorn/Unicorn_test2.go deleted file mode 100644 index 74cffd1..0000000 --- a/unicorn/Unicorn_test2.go +++ /dev/null @@ -1,21 +0,0 @@ -package unicorn - -import ( - "testing" -) - -func TestAnimated(t *testing.T) { - // data, err := Asset("sample.gif") - // if err != nil { - // t.Errorf("Failed to load asset %v", err) - // } - - // g, err := gif.DecodeAll(bytes.NewReader(data)) - // if err != nil { - // t.Errorf("Failed to decode gif from asset %v", err) - // } - - // un := unicorn.NewUnicorn2() - // un.SetGif(g) - // un.MainLoop() -} diff --git a/unicorn/bindata.go b/unicorn/bindata.go index f4395cc..1bd572c 100644 --- a/unicorn/bindata.go +++ b/unicorn/bindata.go @@ -1,6 +1,7 @@ // Code generated by go-bindata. DO NOT EDIT. // sources: // data/sample.gif +// data/sample2.gif package unicorn import ( @@ -81,7 +82,27 @@ func dataSampleGif() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "data/sample.gif", size: 1773, mode: os.FileMode(420), modTime: time.Unix(1527458100, 0)} + info := bindataFileInfo{name: "data/sample.gif", size: 1773, mode: os.FileMode(420), modTime: time.Unix(1527504134, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _dataSample2Gif = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x93\xb1\x8b\x5d\x45\x14\xc6\x3f\x31\xe8\x26\x0a\xee\x53\x14\x0b\x0b\xf3\x60\x21\x48\x90\xe4\x2d\x0b\x6a\x6c\xd6\xcc\x8b\x0c\xde\xcd\x2c\xea\x41\x2d\x2e\x6a\xb2\x5c\x08\xd8\x68\x91\x5b\x5a\xf8\xc4\x09\xa6\x30\xb9\x46\x1c\x08\x97\xd5\x05\x8b\xd3\x0a\x0b\x32\x8d\x5b\xb9\xc5\x01\xc5\x46\xb7\x18\xb0\x11\xb4\x19\x14\x04\x95\x45\x47\xce\x80\x45\xfe\x87\x7d\xcd\x39\x9c\xf3\x8a\xdf\xfc\xee\x77\x9e\xb7\x17\x9e\x7a\xfa\xcd\x65\x2c\x03\xc0\xc9\x72\xdf\xc5\xf9\xcb\x2f\x9d\x5f\xdf\x9c\xcf\x9e\x3c\x73\xf7\x5d\x3a\xfa\xfb\x18\x66\x00\x4e\xeb\x5e\xff\xf6\x81\x36\x30\x80\x07\x44\xab\x31\x30\x1e\x46\x74\xe0\x0d\xbc\x87\x17\x08\x20\x06\xe2\x21\x82\x94\x26\x93\x09\x40\x40\x00\x92\x56\x22\x34\x01\x36\xe9\x20\x34\x08\x01\x43\x42\x02\x92\x45\x1a\x90\x47\xe4\x3c\x9d\x4e\x81\x0e\x60\x20\x6b\xed\x3a\x10\xc3\x65\x1d\x30\x81\x19\x63\x46\x06\xb2\x43\x21\x14\x46\x29\xab\xab\xab\x40\x0f\x44\xa0\x68\xed\x7b\xb4\x11\x54\x74\x10\x5b\xc4\x08\x2e\x28\x30\x4a\xec\x0d\x44\xc1\xf5\xe7\x8d\x11\x7d\x81\x37\xc6\x7b\xe3\xc5\x08\x8c\x18\x23\xde\x88\x98\xca\x4d\xa0\x06\xc1\x22\x11\x81\x88\x9a\x26\x58\x9b\x9a\x80\x26\x34\x4d\x08\x76\x48\x36\xc1\x26\x6b\xd3\x60\x53\x72\x39\x2b\x2f\x3a\x02\x3b\xe4\xae\x43\xd7\x75\x44\xec\x5c\x26\x06\x31\x11\xb3\x1b\xb3\xcb\x70\xd9\xb9\x3c\x52\x61\x2a\x45\x79\xd1\xb7\x88\x84\xd2\xf7\xe8\xfb\xbe\x6d\x23\x51\x69\x23\xda\xd8\xb6\x31\x12\x17\x2a\xa0\x42\x5e\x15\x8b\x9a\x56\x74\xef\x8d\xa8\x72\xed\xbc\xf7\xe2\x05\x5e\x8c\x17\xef\x45\xf4\x83\xc0\x28\x26\xc2\x80\xa4\xa6\x9b\x26\x34\x61\xb0\x29\x04\x84\xd0\x84\x10\x86\x21\x0d\x09\x43\xb2\x43\x1a\x86\x94\x42\x35\xae\x98\xe0\x11\x59\x4d\x13\x31\xf1\xe8\x32\x33\x98\x89\x99\xc7\x31\x8f\x19\x63\x76\x63\x1e\xc7\x9c\xb9\x14\x15\xac\xae\x23\xa3\xa8\xe9\xb6\x8d\x6d\x64\x2a\x31\x22\xc6\x36\xc6\xc8\x5c\xb8\x80\x0b\x71\x61\x81\x6a\x95\x2a\x5d\x8c\x52\x4a\x7d\x83\x78\x91\xba\x93\xda\x49\x8d\x97\xc0\xa7\x1a\x0e\x8d\x86\xb5\xa9\xba\x4e\x03\xd4\x74\x45\x4f\x75\x97\x6a\xa7\x09\x82\x55\xaf\x35\x1c\xd9\x41\x05\x57\xd7\x9a\xa9\xd1\xe5\x8a\x9e\xeb\x2e\xd7\x2e\x6b\x56\xa0\x5e\x6b\x38\x34\x4a\x44\xa5\xba\xd6\x4c\x31\x95\x8a\x5e\xea\xae\xd4\x6e\x69\x03\xf8\x71\xf9\xd5\xef\x7e\x5a\x5f\x1c\x7f\xe8\x41\xb3\xbf\xf9\xd9\xf9\xf7\x27\x2b\x17\x1f\xfb\x6b\x73\xfa\x65\x2b\xa7\x66\x87\x7f\x3e\x70\xf9\xb9\xeb\x72\xe6\xea\x1f\xa7\x66\x3b\x5f\xdf\x28\x57\x6f\x6c\xfd\x3a\xc1\x37\x9f\x9c\x3d\xf7\xc5\xc7\x07\xb3\xcb\x1b\x37\x4f\x6e\xe4\x85\x7d\xe7\x87\xf9\xad\xe9\xda\x89\xfb\x97\x8e\x2e\xec\xe8\xc2\x8e\x2e\xec\xce\x0b\x7b\xfd\xff\x0b\xc3\x89\x63\x4f\x98\xfd\xdf\x1e\xdf\x5b\x60\x6b\x69\xe1\x3f\xfa\x7c\x7e\xed\xd1\xb5\x67\x9e\xdd\xff\xfd\xd2\x85\xf7\x1e\x5e\x7f\x64\x6b\xf7\xc3\x83\xbd\x6b\xf7\x86\x7f\x0e\x5f\xf9\xfe\x92\xed\x56\x5e\xdb\xbd\x7e\xe5\xc5\x6d\x79\x37\x76\xf9\xad\xe3\x6f\xac\xb8\xed\xd3\x6f\xdf\xf3\xf3\x95\x9b\xbf\x34\x9f\xae\xdd\xfa\xf7\xf6\x57\x2f\xec\x7c\x7b\xfb\xac\xd3\x9b\x3b\xf7\x5f\x00\x00\x00\xff\xff\x24\xe4\xf7\x2e\xf7\x06\x00\x00") + +func dataSample2GifBytes() ([]byte, error) { + return bindataRead( + _dataSample2Gif, + "data/sample2.gif", + ) +} + +func dataSample2Gif() (*asset, error) { + bytes, err := dataSample2GifBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "data/sample2.gif", size: 1783, mode: os.FileMode(420), modTime: time.Unix(1527519351, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -139,6 +160,7 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "data/sample.gif": dataSampleGif, + "data/sample2.gif": dataSample2Gif, } // AssetDir returns the file names below a certain @@ -183,6 +205,7 @@ type bintree struct { var _bintree = &bintree{nil, map[string]*bintree{ "data": &bintree{nil, map[string]*bintree{ "sample.gif": &bintree{dataSampleGif, map[string]*bintree{}}, + "sample2.gif": &bintree{dataSample2Gif, map[string]*bintree{}}, }}, }} diff --git a/unicorn/build_bindata.sh b/unicorn/build_bindata.sh new file mode 100755 index 0000000..2265ce0 --- /dev/null +++ b/unicorn/build_bindata.sh @@ -0,0 +1 @@ +~/go/bin/go-bindata -pkg unicorn data/ diff --git a/unicorn/data/sample2.gif b/unicorn/data/sample2.gif Binary files differnew file mode 100644 index 0000000..2dfc764 --- /dev/null +++ b/unicorn/data/sample2.gif |