screencraft 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +30 -0
- package/.env.example +3 -0
- package/MCP_README.md +200 -0
- package/README.md +148 -0
- package/bin/screencraft.js +61 -0
- package/package.json +31 -0
- package/src/auth/keystore.js +148 -0
- package/src/commands/init.js +119 -0
- package/src/commands/launch.js +405 -0
- package/src/detectors/detectBrand.js +1222 -0
- package/src/detectors/simulator.js +317 -0
- package/src/generators/analyzeStyleReference.js +471 -0
- package/src/generators/compositePSD.js +682 -0
- package/src/generators/copy.js +147 -0
- package/src/mcp/index.js +394 -0
- package/src/pipeline/aeSwap.js +369 -0
- package/src/pipeline/download.js +32 -0
- package/src/pipeline/queue.js +101 -0
- package/src/server/index.js +627 -0
- package/src/server/public/app.js +738 -0
- package/src/server/public/index.html +255 -0
- package/src/server/public/style.css +751 -0
- package/src/server/session.js +36 -0
- package/templates/ae/(Footage)/Assets/This Hip-Hop Upbeat (Short version).wav +0 -0
- package/templates/ae/(Footage)/Assets/screen_01_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_02_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_03_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_04_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_05_raw.png +0 -0
- package/templates/ae/(Footage)/Assets/screen_06_raw.png +0 -0
- package/templates/ae/Motion Forge Test 1.0 (converted).aep +0 -0
- package/templates/ae_swap.jsx +284 -0
- package/templates/layouts/minimal.psd +0 -0
- package/templates/screencraft.config.example.js +165 -0
- package/test/output/layout_test.png +0 -0
- package/test/output/style_profile.json +64 -0
- package/test/reference.png +0 -0
- package/test/test_brand.js +69 -0
- package/test/test_psd.js +83 -0
- package/test/test_style_analysis.js +114 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/session.js
|
|
3
|
+
* ---------------------
|
|
4
|
+
* Shared session state between Express server and MCP server.
|
|
5
|
+
* Single-user tool — one session at a time.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const session = {
|
|
9
|
+
projectPath: null,
|
|
10
|
+
outputDir: null,
|
|
11
|
+
framework: null,
|
|
12
|
+
brand: null,
|
|
13
|
+
screenshots: [],
|
|
14
|
+
suggestions: [],
|
|
15
|
+
approvedTexts: [],
|
|
16
|
+
selectedTemplate: null,
|
|
17
|
+
renderStatus: { phase: 'idle', progress: 0, message: '' },
|
|
18
|
+
outputs: [],
|
|
19
|
+
_simDevice: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function resetSession() {
|
|
23
|
+
session.projectPath = null;
|
|
24
|
+
session.outputDir = null;
|
|
25
|
+
session.framework = null;
|
|
26
|
+
session.brand = null;
|
|
27
|
+
session.screenshots = [];
|
|
28
|
+
session.suggestions = [];
|
|
29
|
+
session.approvedTexts = [];
|
|
30
|
+
session.selectedTemplate = null;
|
|
31
|
+
session.renderStatus = { phase: 'idle', progress: 0, message: '' };
|
|
32
|
+
session.outputs = [];
|
|
33
|
+
session._simDevice = null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { session, resetSession };
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* templates/ae_swap.jsx
|
|
3
|
+
* ---------------------
|
|
4
|
+
* ExtendScript for After Effects.
|
|
5
|
+
*
|
|
6
|
+
* Reads a render_manifest.json from the same directory as the AEP,
|
|
7
|
+
* then walks all comps/layers looking for [SWAP_*] tags and replaces:
|
|
8
|
+
*
|
|
9
|
+
* [SWAP_COLOR_*] — updates Color Control effect value
|
|
10
|
+
* [SWAP_TEXT_*] — updates source text
|
|
11
|
+
* [SWAP_SCREENSHOT_*] — replaces footage item with new PNG
|
|
12
|
+
* [SWAP_IMAGE_*] — replaces footage item with new image
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* 1. Open the AE template project
|
|
16
|
+
* 2. File → Scripts → Run Script File → ae_swap.jsx
|
|
17
|
+
* -- or --
|
|
18
|
+
* aerender -project "template.aep" -s ae_swap.jsx
|
|
19
|
+
*
|
|
20
|
+
* The script expects render_manifest.json in the project's root folder:
|
|
21
|
+
* {
|
|
22
|
+
* "brand": {
|
|
23
|
+
* "primary": "#7068B1",
|
|
24
|
+
* "secondary": "#435B98",
|
|
25
|
+
* "accent": "#7068b1",
|
|
26
|
+
* "background": "#1a1a2e",
|
|
27
|
+
* "appName": "My App"
|
|
28
|
+
* },
|
|
29
|
+
* "texts": [
|
|
30
|
+
* { "white": "Transform your", "accent": "inner voice" },
|
|
31
|
+
* ...
|
|
32
|
+
* ],
|
|
33
|
+
* "screenshots": [
|
|
34
|
+
* { "file": "screenshots/screen_01_raw.png" },
|
|
35
|
+
* ...
|
|
36
|
+
* ]
|
|
37
|
+
* }
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
(function () {
|
|
41
|
+
// ── JSON polyfill (ExtendScript has no native JSON) ─────────────
|
|
42
|
+
if (typeof JSON === "undefined") {
|
|
43
|
+
JSON = {};
|
|
44
|
+
}
|
|
45
|
+
if (typeof JSON.parse !== "function") {
|
|
46
|
+
JSON.parse = function (str) {
|
|
47
|
+
return eval("(" + str + ")");
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (typeof JSON.stringify !== "function") {
|
|
51
|
+
JSON.stringify = function (obj) {
|
|
52
|
+
if (obj === null) return "null";
|
|
53
|
+
if (typeof obj === "string") return '"' + obj.replace(/"/g, '\\"') + '"';
|
|
54
|
+
if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
|
|
55
|
+
if (obj instanceof Array) {
|
|
56
|
+
var arr = [];
|
|
57
|
+
for (var i = 0; i < obj.length; i++) arr.push(JSON.stringify(obj[i]));
|
|
58
|
+
return "[" + arr.join(",") + "]";
|
|
59
|
+
}
|
|
60
|
+
if (typeof obj === "object") {
|
|
61
|
+
var pairs = [];
|
|
62
|
+
for (var k in obj) {
|
|
63
|
+
if (obj.hasOwnProperty(k)) {
|
|
64
|
+
pairs.push('"' + k + '":' + JSON.stringify(obj[k]));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return "{" + pairs.join(",") + "}";
|
|
68
|
+
}
|
|
69
|
+
return "undefined";
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Locate manifest ────────────────────────────────────────────
|
|
74
|
+
var projectFolder = app.project.file.parent;
|
|
75
|
+
var manifestFile = new File(projectFolder.fsName + "/render_manifest.json");
|
|
76
|
+
|
|
77
|
+
if (!manifestFile.exists) {
|
|
78
|
+
alert("ScreenCraft: render_manifest.json not found in:\n" + projectFolder.fsName);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
manifestFile.open("r");
|
|
83
|
+
var raw = manifestFile.read();
|
|
84
|
+
manifestFile.close();
|
|
85
|
+
|
|
86
|
+
var manifest = JSON.parse(raw);
|
|
87
|
+
var brand = manifest.brand || {};
|
|
88
|
+
var texts = manifest.texts || [];
|
|
89
|
+
var screenshots = manifest.screenshots || [];
|
|
90
|
+
|
|
91
|
+
// ── Build color map ────────────────────────────────────────────
|
|
92
|
+
var colorMap = {
|
|
93
|
+
"primary": brand.primary || null,
|
|
94
|
+
"secondary": brand.secondary || null,
|
|
95
|
+
"accent": brand.accent || null,
|
|
96
|
+
"background": brand.background || null
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ── Build text map ─────────────────────────────────────────────
|
|
100
|
+
// Per-scene numbered text (for AE with multiple scenes in one comp):
|
|
101
|
+
// [SWAP_TEXT_1_headline] → texts[0].white
|
|
102
|
+
// [SWAP_TEXT_1_headline_accent]→ texts[0].accent
|
|
103
|
+
// [SWAP_TEXT_2_headline] → texts[1].white
|
|
104
|
+
// ...
|
|
105
|
+
// Unnumbered fallback (for single-scene or PSD):
|
|
106
|
+
// [SWAP_TEXT_headline] → texts[0].white
|
|
107
|
+
// [SWAP_TEXT_headline_accent] → texts[0].accent
|
|
108
|
+
// Global:
|
|
109
|
+
// [SWAP_TEXT_app_name] → brand.appName
|
|
110
|
+
// [SWAP_TEXT_tagline] → brand.tagline
|
|
111
|
+
var textMap = {
|
|
112
|
+
"headline": texts[0] ? texts[0].white : null,
|
|
113
|
+
"headline_accent": texts[0] ? texts[0].accent : null,
|
|
114
|
+
"app_name": brand.appName || null,
|
|
115
|
+
"tagline": brand.tagline || null
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Add per-scene numbered text entries
|
|
119
|
+
for (var i = 0; i < texts.length; i++) {
|
|
120
|
+
var n = String(i + 1);
|
|
121
|
+
textMap[n + "_headline"] = texts[i].white;
|
|
122
|
+
textMap[n + "_headline_accent"] = texts[i].accent;
|
|
123
|
+
textMap["screenshot_" + n + "_label"] = texts[i].white + " " + texts[i].accent;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Build screenshot map ───────────────────────────────────────
|
|
127
|
+
// Maps [SWAP_SCREENSHOT_1] → absolute path to PNG
|
|
128
|
+
var screenshotMap = {};
|
|
129
|
+
for (var i = 0; i < screenshots.length; i++) {
|
|
130
|
+
var ssPath = screenshots[i].file || screenshots[i].png || "";
|
|
131
|
+
// Resolve relative paths against project folder
|
|
132
|
+
if (ssPath.indexOf("/") !== 0 && ssPath.indexOf("\\") !== 0) {
|
|
133
|
+
ssPath = projectFolder.fsName + "/" + ssPath;
|
|
134
|
+
}
|
|
135
|
+
screenshotMap[String(i + 1)] = ssPath;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Walk all comps and layers ──────────────────────────────────
|
|
139
|
+
var swapCount = 0;
|
|
140
|
+
|
|
141
|
+
for (var c = 1; c <= app.project.numItems; c++) {
|
|
142
|
+
var item = app.project.item(c);
|
|
143
|
+
if (!(item instanceof CompItem)) continue;
|
|
144
|
+
|
|
145
|
+
for (var l = 1; l <= item.numLayers; l++) {
|
|
146
|
+
var layer = item.layer(l);
|
|
147
|
+
var name = layer.name;
|
|
148
|
+
|
|
149
|
+
// Skip layers without [SWAP_ tag
|
|
150
|
+
if (name.indexOf("[SWAP_") === -1) continue;
|
|
151
|
+
|
|
152
|
+
// Extract the tag: [SWAP_TYPE_identifier]
|
|
153
|
+
var match = name.match(/\[SWAP_(\w+?)_(\w+)\]/);
|
|
154
|
+
if (!match) continue;
|
|
155
|
+
|
|
156
|
+
var swapType = match[1]; // COLOR, TEXT, SCREENSHOT, IMAGE
|
|
157
|
+
var swapId = match[2]; // primary, headline, 1, etc.
|
|
158
|
+
|
|
159
|
+
// Also extract full identifier after type (for multi-word ids like headline_accent)
|
|
160
|
+
var fullMatch = name.match(/\[SWAP_\w+?_([\w_]+)\]/);
|
|
161
|
+
var fullId = fullMatch ? fullMatch[1] : swapId;
|
|
162
|
+
|
|
163
|
+
// ── COLOR SWAP ─────────────────────────────────────────
|
|
164
|
+
if (swapType === "COLOR") {
|
|
165
|
+
var hex = colorMap[fullId];
|
|
166
|
+
if (hex) {
|
|
167
|
+
var rgb = hexToRgb(hex);
|
|
168
|
+
// Look for Color Control effect
|
|
169
|
+
var colorCtrl = findEffect(layer, "Color Control");
|
|
170
|
+
if (colorCtrl) {
|
|
171
|
+
colorCtrl.property("Color").setValue(rgb);
|
|
172
|
+
swapCount++;
|
|
173
|
+
} else {
|
|
174
|
+
// No Color Control — try to set the layer's solid color
|
|
175
|
+
// (works if it's a solid layer)
|
|
176
|
+
try {
|
|
177
|
+
if (layer.source instanceof FootageItem && layer.source.mainSource instanceof SolidSource) {
|
|
178
|
+
layer.source.mainSource.color = rgb;
|
|
179
|
+
swapCount++;
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── TEXT SWAP ──────────────────────────────────────────
|
|
187
|
+
if (swapType === "TEXT") {
|
|
188
|
+
var newText = textMap[fullId];
|
|
189
|
+
if (newText && layer.property("Source Text")) {
|
|
190
|
+
var textDoc = layer.property("Source Text").value;
|
|
191
|
+
textDoc.text = newText;
|
|
192
|
+
|
|
193
|
+
// If this is an accent text, set the fill color to brand accent
|
|
194
|
+
if (fullId.indexOf("accent") !== -1 && colorMap["accent"]) {
|
|
195
|
+
var accentRgb = hexToRgb(colorMap["accent"]);
|
|
196
|
+
textDoc.fillColor = accentRgb;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
layer.property("Source Text").setValue(textDoc);
|
|
200
|
+
swapCount++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── SCREENSHOT SWAP ────────────────────────────────────
|
|
205
|
+
if (swapType === "SCREENSHOT") {
|
|
206
|
+
var ssFile = screenshotMap[fullId];
|
|
207
|
+
if (ssFile) {
|
|
208
|
+
var newFile = new File(ssFile);
|
|
209
|
+
if (newFile.exists) {
|
|
210
|
+
try {
|
|
211
|
+
layer.source.replace(newFile);
|
|
212
|
+
swapCount++;
|
|
213
|
+
} catch (e) {
|
|
214
|
+
// Layer might not have replaceable source
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── IMAGE SWAP (app icon, logo, etc.) ──────────────────
|
|
221
|
+
if (swapType === "IMAGE") {
|
|
222
|
+
var imgPath = null;
|
|
223
|
+
if (fullId === "app_icon" && brand.icon) imgPath = brand.icon;
|
|
224
|
+
if (fullId === "store_badge") imgPath = null; // keep default
|
|
225
|
+
if (fullId.indexOf("logo") !== -1 && brand.logo) imgPath = brand.logo;
|
|
226
|
+
|
|
227
|
+
if (imgPath) {
|
|
228
|
+
var imgFile = new File(imgPath);
|
|
229
|
+
if (imgFile.exists) {
|
|
230
|
+
try {
|
|
231
|
+
layer.source.replace(imgFile);
|
|
232
|
+
swapCount++;
|
|
233
|
+
} catch (e) {}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Done ───────────────────────────────────────────────────────
|
|
241
|
+
if (swapCount > 0) {
|
|
242
|
+
app.project.save();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Write a status file so the Node pipeline knows we're done
|
|
246
|
+
var statusFile = new File(projectFolder.fsName + "/swap_complete.json");
|
|
247
|
+
statusFile.open("w");
|
|
248
|
+
statusFile.write(JSON.stringify({
|
|
249
|
+
success: true,
|
|
250
|
+
swapsApplied: swapCount,
|
|
251
|
+
timestamp: new Date().toString()
|
|
252
|
+
}));
|
|
253
|
+
statusFile.close();
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Convert hex color to AE [r, g, b] array (0-1 range).
|
|
260
|
+
*/
|
|
261
|
+
function hexToRgb(hex) {
|
|
262
|
+
hex = hex.replace("#", "");
|
|
263
|
+
var r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
264
|
+
var g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
265
|
+
var b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
266
|
+
return [r, g, b];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Find an effect by match name or display name on a layer.
|
|
271
|
+
*/
|
|
272
|
+
function findEffect(layer, effectName) {
|
|
273
|
+
if (!layer.property("Effects")) return null;
|
|
274
|
+
var effects = layer.property("Effects");
|
|
275
|
+
for (var i = 1; i <= effects.numProperties; i++) {
|
|
276
|
+
var eff = effects.property(i);
|
|
277
|
+
if (eff.name === effectName || eff.matchName === "ADBE Color Control") {
|
|
278
|
+
return eff;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
})();
|
|
Binary file
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screencraft.config.js
|
|
3
|
+
* ---------------------
|
|
4
|
+
* Place this file in your project root.
|
|
5
|
+
* All fields are optional except `screenshots[].route`.
|
|
6
|
+
* Everything else is auto-detected from your codebase.
|
|
7
|
+
*
|
|
8
|
+
* Run `npx screencraft init` to generate this file automatically
|
|
9
|
+
* with detected values pre-filled.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
|
|
14
|
+
// ─────────────────────────────────────────────────────────────
|
|
15
|
+
// APP INFO
|
|
16
|
+
// Auto-detected from: Info.plist, app.json, pubspec.yaml,
|
|
17
|
+
// AndroidManifest.xml, package.json
|
|
18
|
+
// Override here if detection is wrong or you want different copy.
|
|
19
|
+
// ─────────────────────────────────────────────────────────────
|
|
20
|
+
app: {
|
|
21
|
+
name: "Navi", // App display name
|
|
22
|
+
tagline: "Navigate anything.", // One-line tagline (used in video title card)
|
|
23
|
+
cta: "Download Free", // CTA button text in video
|
|
24
|
+
category: "productivity", // App Store category (used for icon style selection)
|
|
25
|
+
platforms: ["ios", "android"], // Which store badges to include in video
|
|
26
|
+
storeUrl: {
|
|
27
|
+
ios: "apps.apple.com/app/navi/id123456789",
|
|
28
|
+
android: "play.google.com/store/apps/details?id=com.navi.app"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// ─────────────────────────────────────────────────────────────
|
|
33
|
+
// SCREENSHOTS
|
|
34
|
+
// The only required section. Define up to 6 screens.
|
|
35
|
+
// `route` is the deep link / navigation path to reach each screen.
|
|
36
|
+
// `label` is the short callout text that appears beside the screen
|
|
37
|
+
// in the video. Max 5 words. Auto-generated by AI if omitted.
|
|
38
|
+
// `state` lets you set up specific UI state before screenshotting.
|
|
39
|
+
// ─────────────────────────────────────────────────────────────
|
|
40
|
+
screenshots: [
|
|
41
|
+
{
|
|
42
|
+
route: "/onboarding",
|
|
43
|
+
label: "Get started in seconds",
|
|
44
|
+
state: null // No special state needed
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
route: "/home",
|
|
48
|
+
label: "Everything at a glance",
|
|
49
|
+
state: {
|
|
50
|
+
user: "demo", // Log in as demo user so home screen is populated
|
|
51
|
+
mock: "home_populated" // Use mock data preset (see Mocks section below)
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
route: "/search",
|
|
56
|
+
label: "Find anything instantly",
|
|
57
|
+
state: {
|
|
58
|
+
mock: "search_results"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
route: "/profile",
|
|
63
|
+
label: "Your space, your way",
|
|
64
|
+
state: {
|
|
65
|
+
user: "demo"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
route: "/settings",
|
|
70
|
+
label: "Built for how you work",
|
|
71
|
+
state: null
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
route: "/home",
|
|
75
|
+
label: "Easy on the eyes",
|
|
76
|
+
state: {
|
|
77
|
+
theme: "dark", // Force dark mode for this screenshot
|
|
78
|
+
mock: "home_populated"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
|
|
83
|
+
// ─────────────────────────────────────────────────────────────
|
|
84
|
+
// BRAND
|
|
85
|
+
// Auto-detected from: theme.js, colors.js, colors.swift,
|
|
86
|
+
// Color.xcassets, theme.dart, styles/colors.css, tokens.json
|
|
87
|
+
// Override here if you want to use different colors in the
|
|
88
|
+
// marketing materials vs the app itself.
|
|
89
|
+
// ─────────────────────────────────────────────────────────────
|
|
90
|
+
brand: {
|
|
91
|
+
primary: null, // e.g. "#1B3A6B" — null = auto-detect
|
|
92
|
+
secondary: null, // e.g. "#F5F7FA" — null = auto-detect
|
|
93
|
+
accent: null, // e.g. "#C9A84C" — null = auto-detect
|
|
94
|
+
font: null, // e.g. "Inter" — null = auto-detect, falls back to system
|
|
95
|
+
icon: null, // Path to app icon PNG (1024x1024). null = auto-detect
|
|
96
|
+
// Searched: ios/AppIcon.appiconset, assets/icon.png,
|
|
97
|
+
// android/app/src/main/res/mipmap-xxxhdpi
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// ─────────────────────────────────────────────────────────────
|
|
101
|
+
// DEVICE
|
|
102
|
+
// Which device frame to use for screenshots and video.
|
|
103
|
+
// Auto-selected based on platform if not specified.
|
|
104
|
+
// ─────────────────────────────────────────────────────────────
|
|
105
|
+
device: {
|
|
106
|
+
model: "iphone-15-pro", // iphone-15-pro | iphone-15 | pixel-8 | pixel-8-pro
|
|
107
|
+
color: "black-titanium", // black-titanium | white-titanium | natural | blue
|
|
108
|
+
showNotch: true, // Show status bar / dynamic island area
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// ─────────────────────────────────────────────────────────────
|
|
112
|
+
// VIDEO
|
|
113
|
+
// Controls the generated preview video.
|
|
114
|
+
// ─────────────────────────────────────────────────────────────
|
|
115
|
+
video: {
|
|
116
|
+
duration: 30, // 30 or 45 seconds
|
|
117
|
+
music: "upbeat", // upbeat | calm | cinematic | none
|
|
118
|
+
showRating: false, // Include App Store rating scene (requires rating + reviewCount)
|
|
119
|
+
rating: null, // e.g. "4.8" — shown as star rating in social proof scene
|
|
120
|
+
reviewCount: null, // e.g. "12,400+" — shown beneath stars
|
|
121
|
+
reviewQuote: null, // Short user review quote, max 10 words
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// ─────────────────────────────────────────────────────────────
|
|
125
|
+
// OUTPUT
|
|
126
|
+
// Controls what gets generated and where.
|
|
127
|
+
// ─────────────────────────────────────────────────────────────
|
|
128
|
+
output: {
|
|
129
|
+
dir: "./launch-kit", // Output directory (relative to project root)
|
|
130
|
+
screenshots: true, // Always true — free tier
|
|
131
|
+
video: true, // Watermarked always; clean requires license
|
|
132
|
+
aepFile: true, // Requires Launch ($49) or Pro ($99) license
|
|
133
|
+
psds: true, // Requires Pro ($99) license
|
|
134
|
+
socialCut: true, // Square 1:1 MP4 — requires Pro ($99) license
|
|
135
|
+
storeCopy: true, // App Store description + keywords — requires Pro
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// ─────────────────────────────────────────────────────────────
|
|
139
|
+
// MOCKS (optional)
|
|
140
|
+
// Define named data states the simulator can be put into
|
|
141
|
+
// before a screenshot. Reference these in screenshots[].state.mock
|
|
142
|
+
// ScreenCraft will look for a matching setup function in:
|
|
143
|
+
// screencraft.mocks.js (you create this — see docs)
|
|
144
|
+
// ─────────────────────────────────────────────────────────────
|
|
145
|
+
mocks: {
|
|
146
|
+
enabled: false, // Set to true if you have screencraft.mocks.js
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// ─────────────────────────────────────────────────────────────
|
|
150
|
+
// SIMULATOR (rarely needed — auto-configured per framework)
|
|
151
|
+
// ─────────────────────────────────────────────────────────────
|
|
152
|
+
simulator: {
|
|
153
|
+
ios: {
|
|
154
|
+
device: "iPhone 15 Pro", // Simulator device name
|
|
155
|
+
os: "17.0", // iOS version
|
|
156
|
+
timeout: 8000, // ms to wait after navigation before screenshot
|
|
157
|
+
},
|
|
158
|
+
android: {
|
|
159
|
+
device: "Pixel 8 Pro",
|
|
160
|
+
api: 34,
|
|
161
|
+
timeout: 8000,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"layout": {
|
|
3
|
+
"type": "multi-phone",
|
|
4
|
+
"phoneCount": 3,
|
|
5
|
+
"arrangement": "stacked-cascade",
|
|
6
|
+
"tiltAngles": [-14, -7, 0],
|
|
7
|
+
"phoneScales": [0.65, 0.8, 1.0],
|
|
8
|
+
"phonePositions": [
|
|
9
|
+
{ "anchor": "left", "xOffset": -28, "yOffset": 8 },
|
|
10
|
+
{ "anchor": "center", "xOffset": -10, "yOffset": 4 },
|
|
11
|
+
{ "anchor": "right", "xOffset": 12, "yOffset": 0 }
|
|
12
|
+
],
|
|
13
|
+
"textPosition": "top-left"
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
"background": {
|
|
17
|
+
"type": "solid",
|
|
18
|
+
"color": "#5C3317",
|
|
19
|
+
"gradientEndColor": null,
|
|
20
|
+
"gradientAngle": null,
|
|
21
|
+
"noiseAmount": 0.08,
|
|
22
|
+
"adaptToBrand": true
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
"phoneFrame": {
|
|
26
|
+
"style": "white-minimal",
|
|
27
|
+
"borderColor": null,
|
|
28
|
+
"borderWidth": 0,
|
|
29
|
+
"shadowStyle": "soft",
|
|
30
|
+
"shadowColor": "#00000055",
|
|
31
|
+
"cornerRadius": 44,
|
|
32
|
+
"adaptBorderToBrand": false
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
"typography": {
|
|
36
|
+
"headlineStyle": "large-bold-sans",
|
|
37
|
+
"headlineSize": "xxl",
|
|
38
|
+
"headlineCase": "title",
|
|
39
|
+
"headlinePosition": "top-left",
|
|
40
|
+
"subheadPresent": true,
|
|
41
|
+
"subheadSize": "sm",
|
|
42
|
+
"usesEmoji": false,
|
|
43
|
+
"textColor": "#FFFFFF",
|
|
44
|
+
"accentTextColor": null,
|
|
45
|
+
"lineStyle": "tight",
|
|
46
|
+
"fontPersonality": "bold-sans"
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
"spacing": {
|
|
50
|
+
"density": "airy",
|
|
51
|
+
"phonePaddingPercent": 6,
|
|
52
|
+
"textToPhoneGap": "generous"
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
"colorExtraction": {
|
|
56
|
+
"dominantBgColor": "#5C3317",
|
|
57
|
+
"phoneFrameColor": "#FFFFFF",
|
|
58
|
+
"headlineTextColor": "#FFFFFF",
|
|
59
|
+
"accentColor": null,
|
|
60
|
+
"overallMood": "warm"
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
"layoutDescription": "Three white-framed phones in a stacked left-to-right cascade at increasing scale, small-to-large, with bold left-aligned headline text occupying the upper-left quadrant above the phone stack."
|
|
64
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test/test_brand.js
|
|
3
|
+
* ------------------
|
|
4
|
+
* Tests brand detection against a real project directory.
|
|
5
|
+
*
|
|
6
|
+
* Run: node test/test_brand.js /path/to/your/app
|
|
7
|
+
* or: node test/test_brand.js (uses current directory)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { detectBrand, detectFramework } = require('../src/detectors/detectBrand');
|
|
12
|
+
|
|
13
|
+
const targetDir = process.argv[2] || process.cwd();
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log(' Testing brand detection on:', targetDir);
|
|
18
|
+
console.log('');
|
|
19
|
+
|
|
20
|
+
const framework = await detectFramework(targetDir);
|
|
21
|
+
console.log(' Framework:', framework.name);
|
|
22
|
+
console.log(' Platform:', framework.platform.join(', ') || 'none detected');
|
|
23
|
+
console.log(' Config file:', framework.configFile || 'none');
|
|
24
|
+
console.log('');
|
|
25
|
+
|
|
26
|
+
const brand = await detectBrand(targetDir, {});
|
|
27
|
+
console.log(' Brand results:');
|
|
28
|
+
console.log(' ├─ App name: ', brand.appName || '(not detected)');
|
|
29
|
+
console.log(' ├─ Primary: ', brand.primary);
|
|
30
|
+
console.log(' ├─ Secondary: ', brand.secondary);
|
|
31
|
+
console.log(' ├─ Accent: ', brand.accent);
|
|
32
|
+
console.log(' ├─ Background:', brand.background);
|
|
33
|
+
console.log(' ├─ Icon: ', brand.icon || '(not detected)');
|
|
34
|
+
console.log(' ├─ Font: ', brand.font?.family ? `${brand.font.family}${brand.font.weight ? ` (${brand.font.weight})` : ''}` : '(not detected)');
|
|
35
|
+
console.log(' │ File: ', brand.font?.file || '(none)');
|
|
36
|
+
console.log(' └─ Logo: ', brand.logo || '(not detected)');
|
|
37
|
+
console.log('');
|
|
38
|
+
|
|
39
|
+
// Visual color swatches in terminal (if supported)
|
|
40
|
+
const swatches = [
|
|
41
|
+
['Primary', brand.primary],
|
|
42
|
+
['Secondary', brand.secondary],
|
|
43
|
+
['Accent', brand.accent],
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
console.log(' Color swatches:');
|
|
47
|
+
for (const [name, hex] of swatches) {
|
|
48
|
+
const { r, g, b } = hexToRgb(hex);
|
|
49
|
+
// ANSI background color block
|
|
50
|
+
const swatch = `\x1b[48;2;${r};${g};${b}m \x1b[0m`;
|
|
51
|
+
console.log(` ${swatch} ${name.padEnd(10)} ${hex}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hexToRgb(hex) {
|
|
58
|
+
const h = hex.replace('#', '');
|
|
59
|
+
return {
|
|
60
|
+
r: parseInt(h.substring(0, 2), 16),
|
|
61
|
+
g: parseInt(h.substring(2, 4), 16),
|
|
62
|
+
b: parseInt(h.substring(4, 6), 16),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
main().catch(err => {
|
|
67
|
+
console.error(' ✗ Error:', err.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|