local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
Wails IPC raise endpoint, stale process cleanup
.autoflow/: full PRD / Design Spec / Architecture / Testing docs
(4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
96 lines
2.4 KiB
Go
96 lines
2.4 KiB
Go
package camera
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/jpeg"
|
|
"time"
|
|
)
|
|
|
|
type MockCamera struct {
|
|
width int
|
|
height int
|
|
frameCount int
|
|
}
|
|
|
|
func NewMockCamera(width, height int) *MockCamera {
|
|
return &MockCamera{width: width, height: height}
|
|
}
|
|
|
|
func (mc *MockCamera) ReadFrame() ([]byte, error) {
|
|
mc.frameCount++
|
|
return mc.generateTestCard()
|
|
}
|
|
|
|
func (mc *MockCamera) generateTestCard() ([]byte, error) {
|
|
img := image.NewRGBA(image.Rect(0, 0, mc.width, mc.height))
|
|
offset := mc.frameCount % mc.width
|
|
|
|
for y := 0; y < mc.height; y++ {
|
|
for x := 0; x < mc.width; x++ {
|
|
pos := (x + offset) % mc.width
|
|
ratio := float64(pos) / float64(mc.width)
|
|
var r, g, b uint8
|
|
if ratio < 0.33 {
|
|
r = uint8(255 * (1 - ratio/0.33))
|
|
g = uint8(255 * ratio / 0.33)
|
|
} else if ratio < 0.66 {
|
|
g = uint8(255 * (1 - (ratio-0.33)/0.33))
|
|
b = uint8(255 * (ratio - 0.33) / 0.33)
|
|
} else {
|
|
b = uint8(255 * (1 - (ratio-0.66)/0.34))
|
|
r = uint8(255 * (ratio - 0.66) / 0.34)
|
|
}
|
|
img.SetRGBA(x, y, color.RGBA{R: r, G: g, B: b, A: 255})
|
|
}
|
|
}
|
|
|
|
// Draw dark overlay bar at top for text area
|
|
for y := 0; y < 40; y++ {
|
|
for x := 0; x < mc.width; x++ {
|
|
img.SetRGBA(x, y, color.RGBA{R: 0, G: 0, B: 0, A: 180})
|
|
}
|
|
}
|
|
|
|
// Draw "MOCK CAMERA" text block and frame counter using simple rectangles
|
|
drawTextBlock(img, 10, 10, fmt.Sprintf("MOCK CAMERA | Frame: %d | %s", mc.frameCount, time.Now().Format("15:04:05")))
|
|
|
|
// Draw center crosshair
|
|
cx, cy := mc.width/2, mc.height/2
|
|
for i := -20; i <= 20; i++ {
|
|
if cx+i >= 0 && cx+i < mc.width {
|
|
img.SetRGBA(cx+i, cy, color.RGBA{R: 255, G: 255, B: 255, A: 200})
|
|
}
|
|
if cy+i >= 0 && cy+i < mc.height {
|
|
img.SetRGBA(cx, cy+i, color.RGBA{R: 255, G: 255, B: 255, A: 200})
|
|
}
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 75}); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func drawTextBlock(img *image.RGBA, x, y int, text string) {
|
|
white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
|
// Simple pixel-based text rendering: each character is a 5x7 block
|
|
for i, ch := range text {
|
|
if ch == ' ' {
|
|
continue
|
|
}
|
|
px := x + i*6
|
|
// Draw a small white dot for each character position
|
|
for dy := 0; dy < 5; dy++ {
|
|
for dx := 0; dx < 4; dx++ {
|
|
if px+dx < img.Bounds().Max.X && y+dy < img.Bounds().Max.Y {
|
|
img.SetRGBA(px+dx, y+dy, white)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|