tray: a click on the bird flaps it — the wing beats twice and the tail flicks up once, 16 frames over about half a second (build/icon/gen.go tray-flap bends the outline about the tail's root and the shoulder, eased, so it stays one line); and the usage beside it no longer sits hard against it, a thin space between (Image #13: 图标与用量之间没有任何的间距)

This commit is contained in:
yetone committed 2026-09-28 16:15:40 +08:00
1 parent ec10f99113
commit ebc33c721b
20 files changed
+133 -1

No files matched your search

+1
View File
@@ -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
+94
View File
@@ -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).
+33 -1
View File
@@ -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)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

+5
View File
@@ -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)