Files
wuguanganandClaude Opus 5 11786954cf docs: 功能长图 + 全部截图用演示数据重截
## 长图

docs/poster.png:六段式,一张图讲完这个工具是什么。
生成脚本 Resources/make-poster.swift,改文案或调顺序改那个数组即可。

修了个坑:NSImage.size 返回的是**点**不是像素,这批截图是 2x 抓的,
1880px 的图它报 940,于是大图被当小图处理,第一版只占了半幅。

## 截图

scripts/shoot-screenshots.sh:用 HUB_DEMO=1 + HUB_ISLAND_STATE 把形态钉死,
自动截 + 自动裁掉透明留白,五种形态全自动。

之前那批是改名前截的,岛头还写着旧名。重截之后顺带修好三件事:
- 演示工作区路径 claude-hub-demo → vibe-foreman-demo
- 新图带上了「在问你」横幅和状态徽章,比旧图更能说明问题
- 删掉 04-main-window.png:验收页本来就带主窗口侧边栏,一张顶两张,
  而且那张的标题栏是旧名

两个配套小工具:
- scripts/find-window.swift  拿窗口 id 给 screencapture -l 用。
  CGWindowList 报的是显示名不是可执行名,写死 ClaudeHub 一个都找不到。
- scripts/crop-alpha.swift   裁到不透明内容的外接矩形。岛画在 640×720 的
  透明容器窗口里,不裁的话拼进长图就是一大块空白。

截图脚本踩了 NOTES.md 里记过的那个 bash 坑:变量后面紧跟中文标点
(`$state)`)会被算进变量名,set -u 下直接报未定义。加花括号。

窗口探测改成轮询而不是固定 sleep —— 实测 4 秒对 hover 够、对 expanded 不够。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 00:04:25 +08:00

72 lines
2.6 KiB
Swift
Raw Permalink 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.
import CoreGraphics
import Foundation
import ImageIO
// 把 PNG 裁到「不透明内容」的外接矩形。
//
// 岛是画在一个 640×720 的透明容器窗口里的(这样形态变化时不用改窗口尺寸),
// 直接 `screencapture -l` 截下来四周有大片透明。不裁的话拼进长图里
// 就是一块巨大的空白,看起来像排版塌了。
//
// 阈值取 20 而不是 0:岛有一圈很淡的外发光,按 alpha>0 裁会把那圈
// 几乎看不见的雾也算进来,边距忽大忽小、每张都不一样。
let args = Array(CommandLine.arguments.dropFirst())
guard args.count >= 1 else {
FileHandle.standardError.write(Data("用法:crop-alpha.swift <输入.png> [输出.png]\n".utf8))
exit(1)
}
let input = URL(fileURLWithPath: args[0])
let output = URL(fileURLWithPath: args.count >= 2 ? args[1] : args[0])
guard let source = CGImageSourceCreateWithURL(input as CFURL, nil),
let image = CGImageSourceCreateImageAtIndex(source, 0, nil)
else { exit(1) }
let width = image.width
let height = image.height
// 重画一遍进已知布局的缓冲区 —— 直接读 image.dataProvider 拿到的字节序
// 依赖源图的 bitmapInfo,PNG 的排列方式不止一种,猜错就裁在莫名其妙的地方。
var pixels = [UInt8](repeating: 0, count: width * height * 4)
guard let ctx = pixels.withUnsafeMutableBytes({ raw -> CGContext? in
CGContext(
data: raw.baseAddress, width: width, height: height,
bitsPerComponent: 8, bytesPerRow: width * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)
}) else { exit(1) }
ctx.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
let threshold: UInt8 = 20
var minX = width, minY = height, maxX = -1, maxY = -1
for y in 0..<height {
let row = y * width * 4
for x in 0..<width where pixels[row + x * 4 + 3] > threshold {
if x < minX { minX = x }
if x > maxX { maxX = x }
if y < minY { minY = y }
if y > maxY { maxY = y }
}
}
guard maxX >= minX, maxY >= minY else {
FileHandle.standardError.write(Data("整张都是透明的\n".utf8))
exit(1)
}
let rect = CGRect(
x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1
)
guard let cropped = image.cropping(to: rect),
let dest = CGImageDestinationCreateWithURL(
output as CFURL, "public.png" as CFString, 1, nil
)
else { exit(1) }
CGImageDestinationAddImage(dest, cropped, nil)
guard CGImageDestinationFinalize(dest) else { exit(1) }
print("\(width)×\(height) → \(Int(rect.width))×\(Int(rect.height))")