jim800121chen 50a3f73acd feat(local-tool): 品牌視覺設計 + 內建模型首次啟動 seed
#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>
2026-04-12 04:42:41 +08:00

290 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 一次性 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)
}
}