Loading...
Loading...
Compare original and translation side by side
html-to-imagehtml-to-imagegit clone https://github.com/joshluongo/frameme.git /tmp/frameme
cd /tmp/frameme && swift build -c release
strip .build/release/frameme
cp .build/release/frameme /usr/local/bin/framemegit clone https://github.com/joshluongo/frameme.git /tmp/frameme
cd /tmp/frameme && swift build -c release
strip .build/release/frameme
cp .build/release/frameme /usr/local/bin/framemeundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedproject/
├── public/
│ ├── app-icon.png # User's app icon
│ ├── framed/ # FrameMe output (device-framed screenshots)
│ │ ├── iphone/
│ │ │ ├── en/ # Per-locale if localized UI screenshots
│ │ │ │ ├── home_framed.png
│ │ │ │ └── ...
│ │ │ └── de/ # (if single language, skip locale dirs)
│ │ │ └── ...
│ │ └── ipad/
│ │ └── ...
│ └── screenshots/ # Raw app screenshots (before framing)
│ ├── iphone/
│ │ ├── en/
│ │ │ ├── home.png
│ │ │ └── ...
│ │ └── de/
│ │ └── ...
│ └── ipad/
│ └── ...
├── src/app/
│ ├── copy.json # Localized copy (editable without touching code)
│ ├── layout.tsx # Font setup
│ └── page.tsx # The screenshot generator (single file)
└── package.jsonpage.tsxcopy.jsoncopy.jsonproject/
├── public/
│ ├── app-icon.png # 用户的应用图标
│ ├── framed/ # FrameMe输出(带设备边框的截图)
│ │ ├── iphone/
│ │ │ ├── en/ # 多语言版本的本地化截图
│ │ │ │ ├── home_framed.png
│ │ │ │ └── ...
│ │ │ └── de/ # (如果是单语言,可跳过语言目录)
│ │ │ └── ...
│ │ └── ipad/
│ │ └── ...
│ └── screenshots/ # 原始应用截图(未添加边框前)
│ ├── iphone/
│ │ ├── en/
│ │ │ ├── home.png
│ │ │ └── ...
│ │ └── de/
│ │ └── ...
│ └── ipad/
│ └── ...
├── src/app/
│ ├── copy.json # 本地化文案(无需修改代码即可编辑)
│ ├── layout.tsx # 字体设置
│ └── page.tsx # 截图生成器(单文件实现)
└── package.jsonpage.tsxcopy.jsoncopy.json// src/app/layout.tsx
import { YourFont } from "next/font/google"; // Use whatever font the user specified
const font = YourFont({ subsets: ["latin"] });
export default function Layout({ children }: { children: React.ReactNode }) {
return <html><body className={font.className}>{children}</body></html>;
}// src/app/layout.tsx
import { YourFont } from "next/font/google"; // 使用用户指定的字体
const font = YourFont({ subsets: ["latin"] });
export default function Layout({ children }: { children: React.ReactNode }) {
return <html><body className={font.className}>{children}</body></html>;
}| Bezel file | Device | Image size |
|---|---|---|
| iPhone 17 Pro (6.1") | 1350×2760 |
| iPhone 17 Pro Max (6.9") | 1470×3000 |
| iPad Pro 11" M4 | 1880×2640 |
| iPad Pro 13" M4 | 2300×3000 |
| 边框文件 | 对应设备 | 图像尺寸 |
|---|---|---|
| iPhone 17 Pro(6.1英寸) | 1350×2760 |
| iPhone 17 Pro Max(6.9英寸) | 1470×3000 |
| iPad Pro 11英寸 M4 | 1880×2640 |
| iPad Pro 13英寸 M4 | 2300×3000 |
SKILL_DIR="<path to this skill's directory>"SKILL_DIR="<本技能的目录路径>"
If the app UI doesn't change across languages (only marketing text differs), frame once under `en/` — the Device component's `onError` fallback will use English images for all locales.
FrameMe automatically:
- Detects the screen area from the bezel's alpha channel
- Creates a per-scanline mask (handles Dynamic Island, rounded corners)
- Composites the screenshot behind the bezel
- Outputs `filename_framed.png`
如果应用UI在不同语言中保持一致(仅营销文案变化),只需在`en/`目录下生成一次带边框的图像——Device组件的`onError`回退机制会在其他语言图像缺失时自动使用英语版本。
FrameMe会自动完成以下操作:
- 从边框的Alpha通道检测屏幕区域
- 创建逐行扫描蒙版(处理灵动岛、圆角)
- 将截图合成到边框后方
- 输出文件名为`filename_framed.png`page.tsx| Slot | Purpose | Notes |
|---|---|---|
| #1 | Hero / Main Benefit | App icon + tagline + home screen. This is the ONLY one most people see. |
| #2 | Differentiator | What makes this app unique vs competitors |
| #3 | Ecosystem | Widgets, extensions, watch — beyond the main app. Skip if N/A. |
| #4+ | Core Features | One feature per slide, most important first |
| 2nd to last | Trust Signal | Identity/craft — "made for people who [X]" |
| Last | More Features | Pills listing extras + coming soon. Skip if few features. |
| 插槽 | 用途 | 说明 |
|---|---|---|
| #1 | 主视觉/核心卖点 | 应用图标 + 标语 + 主屏幕。这是大多数用户会看到的唯一一张截图。 |
| #2 | 差异化优势 | 你的应用与竞品的独特之处 |
| #3 | 生态系统 | 小组件、扩展、手表应用——超出主应用的附加功能。如果没有可跳过。 |
| #4+ | 核心功能 | 每张幻灯片一个功能,按优先级排序 |
| 倒数第二张 | 信任信号 | 定位/调性——"为[某类人群]打造" |
| 最后一张 | 更多功能 | 列出额外功能的胶囊式标签 + 即将推出的功能。如果功能较少可跳过。 |
<br /><br />| Type | What it does | Example |
|---|---|---|
| Paint a moment | You picture yourself doing it | "Check your coffee without opening the app." |
| State an outcome | What your life looks like after | "A home for every coffee you buy." |
| Kill a pain | Name a problem and destroy it | "Never waste a great bag of coffee." |
| 类型 | 作用 | 示例 |
|---|---|---|
| 描绘场景 | 让用户想象自己使用的场景 | "无需打开应用,即可查看咖啡状态。" |
| 陈述结果 | 告诉用户使用后的生活变化 | "为你购买的每一杯咖啡找到归宿。" |
| 解决痛点 | 指出问题并提供解决方案 | "再也不会浪费一袋好咖啡。" |
page.tsx
├── Constants (IPHONE_W, IPHONE_H, IPAD_W, IPAD_H, design tokens)
├── LOCALES (code, label, asc locale code for App Store Connect)
├── ASC_DEVICE (maps "iphone" → "APP_IPHONE_67", "ipad" → "APP_IPAD_PRO_3GEN_129")
├── COPY (imported from copy.json — localized strings per language, keyed by slide name)
├── Device component (renders a pre-framed device image — just an <img>)
├── Caption component (label + headline)
├── Decorative components (blobs, glows, shapes — based on style direction)
├── Screenshot1..N components (one per slide, with iPhone and/or iPad variants)
├── SCREENSHOTS array (registry, with device type tag)
├── ScreenshotPreview (ResizeObserver scaling + hover export)
└── ScreenshotsPage (grid + toolbar with language switcher + device filter + Export/Export All Languages)page.tsx
├── 常量(IPHONE_W, IPHONE_H, IPAD_W, IPAD_H, 设计令牌)
├── LOCALES(代码、标签、用于App Store Connect的ASC语言代码)
├── ASC_DEVICE(映射关系:"iphone" → "APP_IPHONE_67", "ipad" → "APP_IPAD_PRO_3GEN_129")
├── COPY(从copy.json导入——每种语言的本地化字符串,按幻灯片名称索引)
├── Device组件(渲染带边框的设备图像——仅为<img>标签)
├── Caption组件(标签 + 标题)
├── 装饰组件(blob图形、光晕、形状——基于风格方向)
├── Screenshot1..N组件(每张幻灯片一个组件,包含iPhone和/或iPad变体)
├── SCREENSHOTS数组(注册表,带设备类型标签)
├── ScreenshotPreview(ResizeObserver缩放 + 悬停导出)
└── ScreenshotsPage(网格 + 工具栏,包含语言切换器 + 设备过滤器 + 导出/导出所有语言)// iPhone 6.9" — covers all iPhone sizes
const IPHONE_W = 1320;
const IPHONE_H = 2868;
// iPad 13" — covers all iPad sizes
const IPAD_W = 2064;
const IPAD_H = 2752;// iPhone 6.9英寸 — 覆盖所有iPhone尺寸
const IPHONE_W = 1320;
const IPHONE_H = 2868;
// iPad 13英寸 — 覆盖所有iPad尺寸
const IPAD_W = 2064;
const IPAD_H = 2752;transform: scale()position: absolute; left: -9999px<h2>iPhone</h2><h2>iPad</h2>SCREENSHOTS.filter(s => s.device === "iphone")transform: scale()position: absolute; left: -9999px<h2>iPhone</h2><h2>iPad</h2>SCREENSHOTS.filter(s => s.device === "iphone")copy.jsonsrc/app/copy.json{
"en": {
"iphone-hero": { "headline": "Spam texts,<br/>silenced.", "pills": ["Smart", "Private"] },
"iphone-privacy": { "label": "On-device AI", "headline": "Completely<br/>private.", "sub": "..." }
},
"de": {
"iphone-hero": { "headline": "Spam-SMS,<br/>verstummt.", "pills": ["Intelligent", "Datenschutz"] },
"iphone-privacy": { "label": "KI auf dem Gerät", "headline": "Vollständig<br/>privat.", "sub": "..." }
}
}page.tsximport COPY_JSON from "./copy.json";
const COPY = COPY_JSON as Record<Locale, Record<string, SlideCopy>>;COPY[locale][slideName]<select>locale{label} ({code})01-iphone-hero-en-1320x2868.png01-iphone-hero-de-1320x2868.pngpublic/framed/iphone/en/public/framed/iphone/de/src={}enonErrorcopy.jsonsrc/app/copy.json{
"en": {
"iphone-hero": { "headline": "Spam texts,<br/>silenced.", "pills": ["Smart", "Private"] },
"iphone-privacy": { "label": "On-device AI", "headline": "Completely<br/>private.", "sub": "..." }
},
"de": {
"iphone-hero": { "headline": "Spam-SMS,<br/>verstummt.", "pills": ["Intelligent", "Datenschutz"] },
"iphone-privacy": { "label": "KI auf dem Gerät", "headline": "Vollständig<br/>privat.", "sub": "..." }
}
}page.tsximport COPY_JSON from "./copy.json";
const COPY = COPY_JSON as Record<Locale, Record<string, SlideCopy>>;COPY[locale][slideName]<select>locale{label} ({code})01-iphone-hero-en-1320x2868.png01-iphone-hero-de-1320x2868.pngpublic/framed/iphone/en/public/framed/iphone/de/src={}enonError<img>function Device({ src, alt, style }: {
src: string; alt: string; style?: React.CSSProperties;
}) {
return (
<img
src={src}
alt={alt}
style={style}
draggable={false}
onError={(e) => {
// Fall back to English if localized image is missing
const img = e.currentTarget;
const enSrc = img.src.replace(/\/(?!en\/)[a-z]{2}\//, "/en/");
if (img.src !== enSrc) img.src = enSrc;
}}
/>
);
}onError<img>function Device({ src, alt, style }: {
src: string; alt: string; style?: React.CSSProperties;
}) {
return (
<img
src={src}
alt={alt}
style={style}
draggable={false}
onError={(e) => {
// 如果本地化图像缺失,回退到英语版本
const img = e.currentTarget;
const enSrc = img.src.replace(/\/(?!en\/)[a-z]{2}\//, "/en/");
if (img.src !== enSrc) img.src = enSrc;
}}
/>
);
}onErrorIPHONE_WIPAD_W| Element | Size | Weight | Line Height |
|---|---|---|---|
| Category label | | 600 (semibold) | default |
| Headline | | 700 (bold) | 1.0 |
| Hero headline | | 700 (bold) | 0.92 |
IPHONE_WIPAD_W| 元素 | 尺寸 | 字重 | 行高 |
|---|---|---|---|
| 分类标签 | | 600(半粗体) | 默认 |
| 标题 | | 700(粗体) | 1.0 |
| 主视觉标题 | | 700(粗体) | 0.92 |
bottom: 0, width: "82-86%", translateX(-50%) translateY(12-14%)Back: left: "-8%", width: "65%", rotate(-4deg), opacity: 0.55
Front: right: "-4%", width: "82%", translateY(10%)Cards should NOT block the device's main content.
Position at edges, slight rotation (2-5deg), drop shadows.
If distracting, push partially off-screen or make smaller.bottom: 0, width: "82-86%", translateX(-50%) translateY(12-14%)后方设备: left: "-8%", width: "65%", rotate(-4deg), opacity: 0.55
前方设备: right: "-4%", width: "82%", translateY(10%)卡片不应遮挡设备的主要内容。
放置在边缘,轻微旋转(2-5度),添加阴影。
如果分散注意力,可部分移出屏幕或缩小尺寸。html2canvashtml-to-imagehtml2canvashtml-to-imageimport { toPng } from "html-to-image";
// CRITICAL: el must be the slide component's root div (position: relative),
// NOT the offscreen wrapper. Use ref on wrapper, then ref.firstElementChild.
// toPng must capture the element that owns the positioned children.
const wrapper = el.parentElement!;
// Before capture: move wrapper on-screen
wrapper.style.left = "0px";
wrapper.style.opacity = "1";
wrapper.style.zIndex = "-1";
const opts = { width: W, height: H, pixelRatio: 1, cacheBust: true };
// CRITICAL: Double-call trick — first warms up fonts/images, second produces clean output
await toPng(el, opts);
const dataUrl = await toPng(el, opts);
// After capture: move wrapper back off-screen
wrapper.style.left = "-9999px";
wrapper.style.opacity = "";
wrapper.style.zIndex = "";W = IPHONE_W, H = IPHONE_HW = IPAD_W, H = IPAD_Hposition: absolute; left: -9999pxfontFamilyfirstElementChildref={(el) => { if (el?.firstElementChild) exportRefs.current.set(i, el.firstElementChild as HTMLDivElement); }}import { toPng } from "html-to-image";
// 关键:el必须是幻灯片组件的根div(position: relative),
// 而非屏幕外的包装器。在包装器上使用ref,然后取ref.firstElementChild。
// toPng必须捕获拥有定位子元素的元素。
const wrapper = el.parentElement!;
// 捕获前:将包装器移到屏幕内
wrapper.style.left = "0px";
wrapper.style.opacity = "1";
wrapper.style.zIndex = "-1";
const opts = { width: W, height: H, pixelRatio: 1, cacheBust: true };
// 关键:双重调用技巧——第一次调用预加载字体/图像,第二次生成清晰的输出
await toPng(el, opts);
const dataUrl = await toPng(el, opts);
// 捕获后:将包装器移回屏幕外
wrapper.style.left = "-9999px";
wrapper.style.opacity = "";
wrapper.style.zIndex = "";W = IPHONE_W, H = IPHONE_HW = IPAD_W, H = IPAD_Hposition: absolute; left: -9999pxfontFamilyfirstElementChildref={(el) => { if (el?.firstElementChild) exportRefs.current.set(i, el.firstElementChild as HTMLDivElement); }}toPng()left: 0toPngposition: absolute; left: -9999pxfixedwidthheighten-US/APP_IPHONE_67/01_hero.png
en-US/APP_IPAD_PRO_3GEN_129/01_hero.png
de-DE/APP_IPHONE_67/01_hero.pngiphone → APP_IPHONE_67ipad → APP_IPAD_PRO_3GEN_129asc{ code: "en", label: "English", asc: "en-US" }01_name.png01-iphone-hero-en-1320x2868.pngfontFamilyString(index + 1).padStart(2, "0")toPng()toPngposition: absolute; left: -9999pxfixedwidthheighten-US/APP_IPHONE_67/01_hero.png
en-US/APP_IPAD_PRO_3GEN_129/01_hero.png
de-DE/APP_IPHONE_67/01_hero.pngiphone → APP_IPHONE_67ipad → APP_IPAD_PRO_3GEN_129asc{ code: "en", label: "English", asc: "en-US" }01_name.png01-iphone-hero-en-1320x2868.pngfontFamilyString(index + 1).padStart(2, "0")| Mistake | Fix |
|---|---|
| All slides look the same | Vary device position (center, left, right, two-device, no-device) |
| Decorative elements invisible | Increase size and opacity — better too visible than invisible |
| Copy is too complex | "One second at arm's length" test |
| Floating elements block the device | Move off-screen edges or above the device |
| Plain white/black background | Use gradients — even subtle ones add depth |
| Too cluttered | Remove floating elements, simplify to device + caption |
| Too simple/empty | Add larger decorative elements, floating items at edges |
| Headlines use "and" | Split into two slides or pick one idea |
| No visual contrast across slides | Mix light and dark backgrounds |
| Export is blank | Use double-call trick; move element on-screen before capture |
| Export has empty top, content at bottom | |
| FrameMe not installed | Build from source: clone repo, |
| Framed image looks wrong | Ensure raw screenshot matches the bezel's screen resolution |
| 错误 | 修复方法 |
|---|---|
| 所有幻灯片看起来都一样 | 改变设备位置(居中、左对齐、右对齐、双设备、无设备) |
| 装饰元素不可见 | 增大尺寸和透明度——宁可过于明显,也不要看不见 |
| 文案过于复杂 | 进行"一臂距离1秒阅读"测试 |
| 浮动元素遮挡设备 | 移到屏幕边缘或设备上方 |
| 纯白/纯黑背景 | 使用渐变——即使是细微的渐变也能增加深度 |
| 过于拥挤 | 移除浮动元素,简化为设备 + 文案 |
| 过于简单/空白 | 添加更大的装饰元素,在边缘添加浮动元素 |
| 标题使用"和" | 拆分为两张幻灯片或选择一个核心思想 |
| 幻灯片之间无视觉对比 | 混合使用浅色和深色背景 |
| 导出内容为空 | 使用双重调用技巧;捕获前将元素移到屏幕内 |
| 导出内容顶部空白,内容在底部 | |
| 未安装FrameMe | 从源码编译:克隆仓库,执行 |
| 带边框的图像显示异常 | 确保原始截图与边框的屏幕分辨率匹配 |