#8 首次啟動 seed 內建模型: - app.go 新增 seedUserDataDir() 在 server spawn 之前執行 - 若 user data-dir 缺 models.json,從 locateBundleDataDir() 複製 models.json + nef/ 預置模型過去 - 新增 locateBundleDataDir() / copyFile() / copyDirRecursive() helper - user 第一次開 app 會看到 8 個 Kneron 預置 .nef 模型(kl520×5 + kl720×3) #5 #6 #7 品牌視覺: - 新增 branding/ 目錄存放設計資產與生成工具 - logo.svg(向量原始稿) - icon-{16,...,1024}.png(10 種尺寸) - icon.ico(Windows 多解析度 ICO,PNG-in-ICO 格式) - icon.icns(macOS) - tools/gen_icon.go + gen_ico.go(純 Go 生成工具,未來調整 logo 用) - README.md + 色票表 - 部署: - visiona-local/build/appicon.png → Wails build 會嵌入 exe - visiona-local/frontend/icon.png → splash 使用 - frontend/src/app/favicon.ico + icon.png → Next.js App Router favicon - splash page 升級:加 logo icon + 品牌名 visionA Local + tagline Edge AI Workspace - Wails window title: "visionA Local — Edge AI Workspace" - wails.json productName: "visionA Local" - Next.js metadata title + icons - i18n: en/zh-TW 把殘留的 "Edge AI 平台" 字串改為 visionA Local 品牌 - .iss: SetupIconFile 指向 branding/icon.ico + UninstallDisplayIcon + ArchitecturesAllowed 改 x64compatible 修掉之前的 deprecation warning 品牌色票: - 主色 #4F7EFF(電子藍) - 輔色 #6EF3C5(mint 點綴) - 深色背景漸層 #1A1F36 → #0E1222 - 警示 #FF6B6B Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
290 lines
6.9 KiB
Go
290 lines
6.9 KiB
Go
// 一次性 icon 生成工具 — 產 visionA Local logo 的多種解析度 PNG
|
||
// 用法:go run gen_icon.go <output-dir>
|
||
//
|
||
// 此檔案為 standalone 工具,不屬於 visiona-local app。
|
||
// 用 build tag 避免被一般 go build 抓到(只有明確 go run 才執行)。
|
||
//go:build ignore
|
||
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/png"
|
||
"math"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
)
|
||
|
||
var (
|
||
bgTop = color.RGBA{0x1A, 0x1F, 0x36, 0xFF}
|
||
bgBottom = color.RGBA{0x0E, 0x12, 0x22, 0xFF}
|
||
lensTop = color.RGBA{0x6E, 0xA8, 0xFF, 0xFF}
|
||
lensBot = color.RGBA{0x4F, 0x7E, 0xFF, 0xFF}
|
||
lensMid = color.RGBA{0x4F, 0x7E, 0xFF, 0x99}
|
||
centerGlow = color.RGBA{0x6E, 0xF3, 0xC5, 0xE0}
|
||
white = color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}
|
||
mint = color.RGBA{0x6E, 0xF3, 0xC5, 0xFF}
|
||
lightBlue = color.RGBA{0x6E, 0xA8, 0xFF, 0xFF}
|
||
)
|
||
|
||
func lerpColor(a, b color.RGBA, t float64) color.RGBA {
|
||
if t < 0 {
|
||
t = 0
|
||
}
|
||
if t > 1 {
|
||
t = 1
|
||
}
|
||
return color.RGBA{
|
||
R: uint8(float64(a.R)*(1-t) + float64(b.R)*t),
|
||
G: uint8(float64(a.G)*(1-t) + float64(b.G)*t),
|
||
B: uint8(float64(a.B)*(1-t) + float64(b.B)*t),
|
||
A: uint8(float64(a.A)*(1-t) + float64(b.A)*t),
|
||
}
|
||
}
|
||
|
||
func blendAlpha(dst, src color.RGBA) color.RGBA {
|
||
if src.A == 0 {
|
||
return dst
|
||
}
|
||
if src.A == 0xFF {
|
||
return src
|
||
}
|
||
sa := float64(src.A) / 255.0
|
||
da := float64(dst.A) / 255.0
|
||
out := 1.0 - (1.0-sa)*(1.0-da)
|
||
if out == 0 {
|
||
return dst
|
||
}
|
||
return color.RGBA{
|
||
R: uint8((float64(src.R)*sa + float64(dst.R)*da*(1-sa)) / out),
|
||
G: uint8((float64(src.G)*sa + float64(dst.G)*da*(1-sa)) / out),
|
||
B: uint8((float64(src.B)*sa + float64(dst.B)*da*(1-sa)) / out),
|
||
A: uint8(out * 255),
|
||
}
|
||
}
|
||
|
||
func rgba(c color.Color) color.RGBA {
|
||
if r, ok := c.(color.RGBA); ok {
|
||
return r
|
||
}
|
||
r, g, b, a := c.RGBA()
|
||
return color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
|
||
}
|
||
|
||
func drawRoundedRect(img *image.RGBA, size int, radius float64, colorAt func(y int) color.RGBA) {
|
||
cx, cy := float64(size)/2, float64(size)/2
|
||
for y := 0; y < size; y++ {
|
||
c := colorAt(y)
|
||
for x := 0; x < size; x++ {
|
||
dx := math.Max(0, math.Abs(float64(x)-cx+0.5)-(float64(size)/2-radius))
|
||
dy := math.Max(0, math.Abs(float64(y)-cy+0.5)-(float64(size)/2-radius))
|
||
d := math.Hypot(dx, dy)
|
||
if d <= radius {
|
||
a := 1.0
|
||
if d > radius-1 {
|
||
a = radius - d
|
||
}
|
||
if a < 0 {
|
||
continue
|
||
}
|
||
if a > 1 {
|
||
a = 1
|
||
}
|
||
cc := c
|
||
cc.A = uint8(float64(cc.A) * a)
|
||
img.Set(x, y, blendAlpha(rgba(img.At(x, y)), cc))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func drawCircleRing(img *image.RGBA, cx, cy, rOuter, rInner float64, c color.RGBA) {
|
||
size := img.Bounds().Dx()
|
||
for y := int(cy-rOuter-1) - 1; y <= int(cy+rOuter+1)+1; y++ {
|
||
for x := int(cx-rOuter-1) - 1; x <= int(cx+rOuter+1)+1; x++ {
|
||
if x < 0 || y < 0 || x >= size || y >= size {
|
||
continue
|
||
}
|
||
d := math.Hypot(float64(x)-cx+0.5, float64(y)-cy+0.5)
|
||
if d <= rOuter && d >= rInner {
|
||
a := 1.0
|
||
if d > rOuter-1 {
|
||
a = rOuter - d
|
||
} else if d < rInner+1 {
|
||
a = d - rInner
|
||
}
|
||
if a < 0 {
|
||
continue
|
||
}
|
||
if a > 1 {
|
||
a = 1
|
||
}
|
||
cc := c
|
||
cc.A = uint8(float64(cc.A) * a)
|
||
img.Set(x, y, blendAlpha(rgba(img.At(x, y)), cc))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func drawFilledCircle(img *image.RGBA, cx, cy, r float64, c color.RGBA) {
|
||
size := img.Bounds().Dx()
|
||
for y := int(cy-r-1) - 1; y <= int(cy+r+1)+1; y++ {
|
||
for x := int(cx-r-1) - 1; x <= int(cx+r+1)+1; x++ {
|
||
if x < 0 || y < 0 || x >= size || y >= size {
|
||
continue
|
||
}
|
||
d := math.Hypot(float64(x)-cx+0.5, float64(y)-cy+0.5)
|
||
if d <= r {
|
||
a := 1.0
|
||
if d > r-1 {
|
||
a = r - d
|
||
}
|
||
if a < 0 {
|
||
continue
|
||
}
|
||
cc := c
|
||
cc.A = uint8(float64(cc.A) * a)
|
||
img.Set(x, y, blendAlpha(rgba(img.At(x, y)), cc))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func drawRadialGlow(img *image.RGBA, cx, cy, r float64, c color.RGBA) {
|
||
size := img.Bounds().Dx()
|
||
for y := int(cy-r-1) - 1; y <= int(cy+r+1)+1; y++ {
|
||
for x := int(cx-r-1) - 1; x <= int(cx+r+1)+1; x++ {
|
||
if x < 0 || y < 0 || x >= size || y >= size {
|
||
continue
|
||
}
|
||
d := math.Hypot(float64(x)-cx+0.5, float64(y)-cy+0.5)
|
||
if d <= r {
|
||
t := 1.0 - d/r
|
||
t = t * t
|
||
cc := c
|
||
cc.A = uint8(float64(cc.A) * t)
|
||
img.Set(x, y, blendAlpha(rgba(img.At(x, y)), cc))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func drawLine(img *image.RGBA, x1, y1, x2, y2, width float64, c color.RGBA) {
|
||
size := img.Bounds().Dx()
|
||
r := width / 2
|
||
minX := int(math.Min(x1, x2) - r - 1)
|
||
maxX := int(math.Max(x1, x2) + r + 1)
|
||
minY := int(math.Min(y1, y2) - r - 1)
|
||
maxY := int(math.Max(y1, y2) + r + 1)
|
||
dx := x2 - x1
|
||
dy := y2 - y1
|
||
lenSq := dx*dx + dy*dy
|
||
for y := minY; y <= maxY; y++ {
|
||
for x := minX; x <= maxX; x++ {
|
||
if x < 0 || y < 0 || x >= size || y >= size {
|
||
continue
|
||
}
|
||
px := float64(x) + 0.5
|
||
py := float64(y) + 0.5
|
||
var t float64
|
||
if lenSq > 0 {
|
||
t = ((px-x1)*dx + (py-y1)*dy) / lenSq
|
||
if t < 0 {
|
||
t = 0
|
||
}
|
||
if t > 1 {
|
||
t = 1
|
||
}
|
||
}
|
||
cx := x1 + t*dx
|
||
cy := y1 + t*dy
|
||
d := math.Hypot(px-cx, py-cy)
|
||
if d <= r {
|
||
a := 1.0
|
||
if d > r-1 {
|
||
a = r - d
|
||
}
|
||
if a < 0 {
|
||
continue
|
||
}
|
||
cc := c
|
||
cc.A = uint8(float64(cc.A) * a)
|
||
img.Set(x, y, blendAlpha(rgba(img.At(x, y)), cc))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func renderLogo(size int) *image.RGBA {
|
||
img := image.NewRGBA(image.Rect(0, 0, size, size))
|
||
s := float64(size)
|
||
k := s / 1024.0
|
||
|
||
radius := 220.0 * k
|
||
drawRoundedRect(img, size, radius, func(y int) color.RGBA {
|
||
t := float64(y) / s
|
||
return lerpColor(bgTop, bgBottom, t)
|
||
})
|
||
|
||
cx := 512.0 * k
|
||
cy := 512.0 * k
|
||
outerR := 360.0 * k
|
||
outerW := 36.0 * k
|
||
drawCircleRing(img, cx, cy, outerR, outerR-outerW, lensTop)
|
||
drawCircleRing(img, cx, cy, outerR, outerR-outerW/2, lerpColor(lensTop, lensBot, 0.6))
|
||
|
||
midR := 296.0 * k
|
||
midW := 14.0 * k
|
||
drawCircleRing(img, cx, cy, midR, midR-midW, lensMid)
|
||
|
||
glowR := 240.0 * k
|
||
drawRadialGlow(img, cx, cy, glowR, centerGlow)
|
||
|
||
lineW := 44.0 * k
|
||
drawLine(img, 360*k, 360*k, 512*k, 640*k, lineW, white)
|
||
drawLine(img, 664*k, 360*k, 512*k, 640*k, lineW, white)
|
||
|
||
drawFilledCircle(img, 360*k, 360*k, 22*k, mint)
|
||
drawFilledCircle(img, 664*k, 360*k, 22*k, lightBlue)
|
||
drawFilledCircle(img, 512*k, 640*k, 26*k, white)
|
||
|
||
drawFilledCircle(img, 760*k, 264*k, 14*k, mint)
|
||
drawCircleRing(img, 760*k, 264*k, 24*k, 21*k, color.RGBA{0x6E, 0xF3, 0xC5, 0x66})
|
||
|
||
return img
|
||
}
|
||
|
||
func savePNG(img *image.RGBA, path string) error {
|
||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||
return err
|
||
}
|
||
f, err := os.Create(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer f.Close()
|
||
return png.Encode(f, img)
|
||
}
|
||
|
||
func main() {
|
||
if len(os.Args) < 2 {
|
||
fmt.Fprintln(os.Stderr, "usage: go run gen_icon.go <output-dir>")
|
||
os.Exit(1)
|
||
}
|
||
outDir := os.Args[1]
|
||
sizes := []int{16, 24, 32, 48, 64, 96, 128, 256, 512, 1024}
|
||
for _, s := range sizes {
|
||
img := renderLogo(s)
|
||
path := filepath.Join(outDir, "icon-"+strconv.Itoa(s)+".png")
|
||
if err := savePNG(img, path); err != nil {
|
||
fmt.Fprintln(os.Stderr, "save", path, err)
|
||
os.Exit(1)
|
||
}
|
||
fmt.Println("wrote", path)
|
||
}
|
||
}
|