diff --git a/Makefile b/Makefile index ca6bc2f5..41becc30 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,7 @@ app: build icons: @go run build/icon/gen.go tray internal/gui/tray.png + @go run build/icon/gen.go tray-flap internal/gui/trayflap @go run build/icon/gen.go app 64 internal/gui/icon.png @go run build/icon/gen.go app 1024 internal/gui/icon-1024.png @rm -rf build/darwin/magpie.iconset && mkdir -p build/darwin/magpie.iconset diff --git a/build/icon/gen.go b/build/icon/gen.go index 78e0bfb6..d51b678c 100644 --- a/build/icon/gen.go +++ b/build/icon/gen.go @@ -9,6 +9,7 @@ // internal/gui/assets/index.html. // // go run build/icon/gen.go tray internal/gui/tray.png # 44px black template icon (macOS menu bar) +// go run build/icon/gen.go tray-flap internal/gui/trayflap # its frames when clicked (a size after the folder for a bigger look) // go run build/icon/gen.go app 64 internal/gui/icon.png # app icon at a given size // make icons # regenerates all of them plus magpie.icns package main @@ -38,6 +39,19 @@ func main() { switch { case len(os.Args) == 3 && os.Args[1] == "tray": img = tray() + case len(os.Args) == 3 && os.Args[1] == "tray-flap": + if err := trayFlap(os.Args[2], 44); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + return + case len(os.Args) == 4 && os.Args[1] == "tray-flap": + n, _ := strconv.Atoi(os.Args[3]) + if err := trayFlap(os.Args[2], n); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + return case len(os.Args) == 4 && os.Args[1] == "glyph": n, _ := strconv.Atoi(os.Args[2]) img = glyph(n) @@ -71,6 +85,86 @@ func tray() image.Image { return img } +// flapFrames is how many pictures one flap of the tray bird is drawn in. +const flapFrames = 16 + +// trayFlap writes the frames the tray icon plays when it is clicked, +// flap-01.png to flap-16.png: the wing beats twice and the tail flicks up +// once, both easing out of and back into the still bird. The drawing is +// bent, not cut: every point of the outline near the tail turns about the +// tail's root, and every point near the wing about the shoulder, by less +// the nearer it is to the pivot, so the outline stays whole. +func trayFlap(dir string, S int) error { + base := parse(smallSVG) + for f := 1; f <= flapFrames; f++ { + t := float64(f) / (flapFrames + 1) + env := math.Sin(math.Pi * t) // 0 at both ends + tail := 14 * env * math.Sin(math.Pi*t) + wing := 8 * env * math.Sin(4*math.Pi*t) + if wing < 0 { + wing /= 2 // a beat is mostly up; down only a little, into the belly + } + rings := make([][][2]float64, len(base)) + for i, r := range base { + rings[i] = make([][2]float64, len(r)) + for j, p := range r { + // Tail and wing meet in a notch of the outline (ring 0): above + // it the outline bends with the tail, below it with the wing, + // eased across it so it stays one line. The holes, the + // shoulder and the belly, move only where they meet the wing. + up, down := 0.0, smooth((p[1]-25)/3) + if i == 0 { + up = 1 - smooth((p[1]-24)/2.5) + } + p = bend(p, [2]float64{12, 24}, [2]float64{2, 6}, 5, tail*up) + p = bend(p, [2]float64{29, 19.5}, [2]float64{8, 30}, 3.5, wing*down) + rings[i][j] = p + } + } + k := float64(S) / 44 + cov := coverage(rings, S, 8, k, 0, 0) + img := image.NewNRGBA(image.Rect(0, 0, S, S)) + for i, a := range cov { + img.SetNRGBA(i%S, i/S, color.NRGBA{0, 0, 0, uint8(a*255 + 0.5)}) + } + out, err := os.Create(fmt.Sprintf("%s/flap-%02d.png", dir, f)) + if err != nil { + return err + } + err = png.Encode(out, img) + out.Close() + if err != nil { + return err + } + } + return nil +} + +// bend turns p about pivot by deg degrees (clockwise on screen, which +// raises what points left), by all of it at tip's end of the axis from pivot +// to tip and by none at the pivot or behind it, fading too with p's distance +// from that axis past width. +func bend(p, pivot, tip [2]float64, width, deg float64) [2]float64 { + ax, ay := tip[0]-pivot[0], tip[1]-pivot[1] + l := math.Hypot(ax, ay) + dx, dy := p[0]-pivot[0], p[1]-pivot[1] + along := (dx*ax + dy*ay) / (l * l) + across := math.Abs(dx*ay-dy*ax) / l + w := smooth(along/0.45) * (1 - smooth((across-width)/width)) + if w <= 0 { + return p + } + a := deg * w * math.Pi / 180 + s, c := math.Sin(a), math.Cos(a) + return [2]float64{pivot[0] + dx*c - dy*s, pivot[1] + dx*s + dy*c} +} + +// smooth eases 0..1 (clamped) in and out. +func smooth(x float64) float64 { + x = math.Max(0, math.Min(1, x)) + return x * x * (3 - 2*x) +} + // glyph is the bird alone, black on clear, as big on an n-pixel canvas as // app draws it on its tile: the layer of build/darwin/AppIcon.icon, which // macOS 26 lays on a tile of its own, light or dark (#117). diff --git a/internal/gui/app.go b/internal/gui/app.go index 74568a2d..359af6a7 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -4,8 +4,9 @@ package gui import ( "crypto/sha256" - _ "embed" + "embed" "encoding/hex" + "io/fs" "log" "net/http" "os" @@ -26,6 +27,12 @@ import ( //go:embed tray.png var trayIcon []byte // black glyph, tinted by the macOS menu bar +// trayFlap is the bird beating its wing and flicking its tail, played on +// the tray icon when it is clicked (build/icon/gen.go tray-flap). +// +//go:embed trayflap/*.png +var trayFlap embed.FS + //go:embed icon.png var appIcon []byte // coloured, for other trays @@ -56,6 +63,9 @@ type host struct { panel *application.WebviewWindow main *application.WebviewWindow tray *application.SystemTray + // flapping is set while the tray bird plays its flap, so a second + // click in it doesn't start another over it + flapping atomic.Bool panelHeight int glides atomic.Int64 // the newest panel glide; older ones stop @@ -302,6 +312,9 @@ func Run(version string, showMain bool, link string) error { // the quick panel by the icon, or the main window if the user would // rather (Settings → Tray icon) h.tray.OnClick(func() { + if runtime.GOOS == "darwin" { + go h.flap() + } if settings.Load().Tray == "window" { h.ShowMain("") return @@ -375,3 +388,22 @@ func abs(n int) int { } return n } + +// flap plays trayFlap on the tray icon, about half a second, and ends on the +// still bird. +func (h *host) flap() { + if !h.flapping.CompareAndSwap(false, true) { + return + } + defer h.flapping.Store(false) + names, _ := fs.Glob(trayFlap, "trayflap/*.png") + for _, n := range names { + b, err := trayFlap.ReadFile(n) + if err != nil { + break + } + h.tray.SetTemplateIcon(b) + time.Sleep(28 * time.Millisecond) + } + h.tray.SetTemplateIcon(trayIcon) +} diff --git a/internal/gui/trayflap/flap-01.png b/internal/gui/trayflap/flap-01.png new file mode 100644 index 00000000..530d13e7 Binary files /dev/null and b/internal/gui/trayflap/flap-01.png differ diff --git a/internal/gui/trayflap/flap-02.png b/internal/gui/trayflap/flap-02.png new file mode 100644 index 00000000..888a1aaa Binary files /dev/null and b/internal/gui/trayflap/flap-02.png differ diff --git a/internal/gui/trayflap/flap-03.png b/internal/gui/trayflap/flap-03.png new file mode 100644 index 00000000..ba487829 Binary files /dev/null and b/internal/gui/trayflap/flap-03.png differ diff --git a/internal/gui/trayflap/flap-04.png b/internal/gui/trayflap/flap-04.png new file mode 100644 index 00000000..e76506e0 Binary files /dev/null and b/internal/gui/trayflap/flap-04.png differ diff --git a/internal/gui/trayflap/flap-05.png b/internal/gui/trayflap/flap-05.png new file mode 100644 index 00000000..f04dc5f6 Binary files /dev/null and b/internal/gui/trayflap/flap-05.png differ diff --git a/internal/gui/trayflap/flap-06.png b/internal/gui/trayflap/flap-06.png new file mode 100644 index 00000000..501b20e2 Binary files /dev/null and b/internal/gui/trayflap/flap-06.png differ diff --git a/internal/gui/trayflap/flap-07.png b/internal/gui/trayflap/flap-07.png new file mode 100644 index 00000000..aa03e34a Binary files /dev/null and b/internal/gui/trayflap/flap-07.png differ diff --git a/internal/gui/trayflap/flap-08.png b/internal/gui/trayflap/flap-08.png new file mode 100644 index 00000000..7d0a2c73 Binary files /dev/null and b/internal/gui/trayflap/flap-08.png differ diff --git a/internal/gui/trayflap/flap-09.png b/internal/gui/trayflap/flap-09.png new file mode 100644 index 00000000..6ba50812 Binary files /dev/null and b/internal/gui/trayflap/flap-09.png differ diff --git a/internal/gui/trayflap/flap-10.png b/internal/gui/trayflap/flap-10.png new file mode 100644 index 00000000..1ede17e6 Binary files /dev/null and b/internal/gui/trayflap/flap-10.png differ diff --git a/internal/gui/trayflap/flap-11.png b/internal/gui/trayflap/flap-11.png new file mode 100644 index 00000000..eaf54d2e Binary files /dev/null and b/internal/gui/trayflap/flap-11.png differ diff --git a/internal/gui/trayflap/flap-12.png b/internal/gui/trayflap/flap-12.png new file mode 100644 index 00000000..a8496aa8 Binary files /dev/null and b/internal/gui/trayflap/flap-12.png differ diff --git a/internal/gui/trayflap/flap-13.png b/internal/gui/trayflap/flap-13.png new file mode 100644 index 00000000..b504df5e Binary files /dev/null and b/internal/gui/trayflap/flap-13.png differ diff --git a/internal/gui/trayflap/flap-14.png b/internal/gui/trayflap/flap-14.png new file mode 100644 index 00000000..355202f4 Binary files /dev/null and b/internal/gui/trayflap/flap-14.png differ diff --git a/internal/gui/trayflap/flap-15.png b/internal/gui/trayflap/flap-15.png new file mode 100644 index 00000000..dbd0900a Binary files /dev/null and b/internal/gui/trayflap/flap-15.png differ diff --git a/internal/gui/trayflap/flap-16.png b/internal/gui/trayflap/flap-16.png new file mode 100644 index 00000000..ac14fc70 Binary files /dev/null and b/internal/gui/trayflap/flap-16.png differ diff --git a/internal/gui/trayusage_tray.go b/internal/gui/trayusage_tray.go index 10651adb..4af996ff 100644 --- a/internal/gui/trayusage_tray.go +++ b/internal/gui/trayusage_tray.go @@ -4,6 +4,7 @@ package gui import ( "context" + "runtime" "time" "github.com/yetone/magpie/internal/provider" @@ -35,6 +36,10 @@ func (h *host) watchTrayUsage() { } } cancel() + if label != "" && runtime.GOOS == "darwin" { + // the menu bar sets the text hard against the icon + label = "\u2009" + label + } if label+"\x00"+tip != shown { shown = label + "\x00" + tip h.tray.SetLabel(label)