@veolab/discoverylab 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-plugin/marketplace.json +15 -0
- package/.claude-plugin/plugin.json +12 -0
- package/.mcp.json +6 -0
- package/README.md +214 -0
- package/assets/applab-discovery.jpeg +0 -0
- package/assets/backgrounds/abstract-colorful-gradient-orange-background.jpg +0 -0
- package/assets/backgrounds/blurred-colorful-luxury-gradient-rainbow-abstract.jpg +0 -0
- package/assets/backgrounds/glowing-neon-moving-continuously-looking-bright.jpg +0 -0
- package/assets/backgrounds/glowing-neon-moving-continuously-looking-bright2.jpg +0 -0
- package/assets/backgrounds/macos-big-sur-apple-layers-fluidic-colorful-wwdc-stock-4096x2304-1455.jpg +0 -0
- package/assets/backgrounds/macos-sierra-mountain-peak-sunset-evening-stock-5k-5120x3684-3987.jpg +0 -0
- package/assets/backgrounds/macos-tahoe-26-5120x2880-22674.jpg +0 -0
- package/assets/backgrounds/macos-tahoe-26-5120x2880-22675.jpg +0 -0
- package/assets/backgrounds/view-of-the-sea-from-the-window-of-an-airplane-2024-10-21-11-25-30-utc.jpg +0 -0
- package/assets/cursor/cursor-blue.png +0 -0
- package/assets/icons/android-head_3D.png +0 -0
- package/assets/icons/apple-logo.png +0 -0
- package/assets/icons/apple-logo.svg +4 -0
- package/assets/icons/claude-ai-icon.svg +1 -0
- package/assets/icons/icons8-apple-intelligence-48.png +0 -0
- package/assets/icons/icons8-apple-intelligence-96.png +0 -0
- package/dist/chunk-7IDQLLBW.js +311 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-MN6LCZHZ.js +1320 -0
- package/dist/chunk-PBHUHSC3.js +6002 -0
- package/dist/chunk-QJXXHOV7.js +205 -0
- package/dist/chunk-SSRXIO2V.js +6822 -0
- package/dist/chunk-VY3BLXBW.js +329 -0
- package/dist/chunk-W3WJGYR6.js +354 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +120 -0
- package/dist/db-IWIL65EX.js +33 -0
- package/dist/gridCompositor-ENKLFPWR.js +409 -0
- package/dist/index.d.ts +1648 -0
- package/dist/index.js +869 -0
- package/dist/ocr-UTWC7537.js +21 -0
- package/dist/server-3FBHBA7L.js +15 -0
- package/dist/server-NM5CKDUU.js +13 -0
- package/dist/setup-27CQAX6K.js +17 -0
- package/dist/tools-75BAPCUM.js +177 -0
- package/package.json +84 -0
- package/skills/generate-assets/SKILL.md +44 -0
- package/skills/mobile-test/SKILL.md +33 -0
- package/skills/open-ui/SKILL.md +24 -0
- package/skills/quick-capture/SKILL.md +28 -0
- package/skills/task-hub/SKILL.md +44 -0
- package/skills/web-test/SKILL.md +41 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import open from "open";
|
|
7
|
+
var program = new Command();
|
|
8
|
+
program.name("discoverylab").description("AI-powered app testing & evidence generator - Claude Code Plugin").version("0.1.0");
|
|
9
|
+
program.command("serve").description("Start the DiscoveryLab web UI server").option("-p, --port <number>", "Port to listen on", "3847").option("-o, --open", "Open browser automatically", false).action(async (options) => {
|
|
10
|
+
const port = parseInt(options.port, 10);
|
|
11
|
+
console.log(chalk.cyan("\n DiscoveryLab"));
|
|
12
|
+
console.log(chalk.gray(" AI-powered app testing & evidence generator\n"));
|
|
13
|
+
try {
|
|
14
|
+
const { startServer } = await import("./server-NM5CKDUU.js");
|
|
15
|
+
await startServer(port);
|
|
16
|
+
console.log(chalk.green(` Server running at http://localhost:${port}`));
|
|
17
|
+
console.log(chalk.gray(" Press Ctrl+C to stop\n"));
|
|
18
|
+
if (options.open) {
|
|
19
|
+
await open(`http://localhost:${port}`);
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(chalk.red(` Failed to start server: ${error}`));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
program.command("setup").description("Check and configure DiscoveryLab dependencies").action(async () => {
|
|
27
|
+
console.log(chalk.cyan("\n DiscoveryLab Setup\n"));
|
|
28
|
+
try {
|
|
29
|
+
const { setupStatusTool } = await import("./setup-27CQAX6K.js");
|
|
30
|
+
const result = await setupStatusTool.handler({});
|
|
31
|
+
if (result.isError) {
|
|
32
|
+
console.error(chalk.red(" Setup check failed"));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const data = JSON.parse(result.content[0].text);
|
|
36
|
+
console.log(chalk.gray(` Platform: ${data.platform}`));
|
|
37
|
+
console.log(chalk.gray(` Data directory: ${data.dataDirectory.path}`));
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(chalk.white(" Dependencies:"));
|
|
40
|
+
for (const dep of data.dependencies) {
|
|
41
|
+
const status = dep.installed ? chalk.green(` ${dep.name} ${dep.version}`) : chalk.red(` ${dep.name} (not installed)`);
|
|
42
|
+
const required = dep.required ? chalk.yellow(" [required]") : chalk.gray(" [optional]");
|
|
43
|
+
console.log(`${status}${required}`);
|
|
44
|
+
if (!dep.installed && dep.installHint) {
|
|
45
|
+
console.log(chalk.gray(` Install: ${dep.installHint}`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
console.log();
|
|
49
|
+
if (data.ready) {
|
|
50
|
+
console.log(chalk.green(" Ready to use!"));
|
|
51
|
+
console.log(chalk.gray(" Run: discoverylab serve"));
|
|
52
|
+
} else {
|
|
53
|
+
console.log(chalk.yellow(" Some required dependencies are missing."));
|
|
54
|
+
console.log(chalk.gray(" Install them and run setup again."));
|
|
55
|
+
}
|
|
56
|
+
console.log();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(chalk.red(` Setup failed: ${error}`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
program.command("init").description("Initialize DiscoveryLab data directories").action(async () => {
|
|
63
|
+
console.log(chalk.cyan("\n Initializing DiscoveryLab...\n"));
|
|
64
|
+
try {
|
|
65
|
+
const { getDatabase, DATA_DIR, PROJECTS_DIR, EXPORTS_DIR } = await import("./db-IWIL65EX.js");
|
|
66
|
+
getDatabase();
|
|
67
|
+
console.log(chalk.green(" Created directories:"));
|
|
68
|
+
console.log(chalk.gray(` ${DATA_DIR}`));
|
|
69
|
+
console.log(chalk.gray(` ${PROJECTS_DIR}`));
|
|
70
|
+
console.log(chalk.gray(` ${EXPORTS_DIR}`));
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(chalk.green(" Database initialized successfully!"));
|
|
73
|
+
console.log();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(chalk.red(` Initialization failed: ${error}`));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
program.command("mcp").description("Run as MCP server (for Claude Code integration)").action(async () => {
|
|
80
|
+
try {
|
|
81
|
+
const { getDatabase } = await import("./db-IWIL65EX.js");
|
|
82
|
+
getDatabase();
|
|
83
|
+
const { mcpServer } = await import("./server-3FBHBA7L.js");
|
|
84
|
+
const { uiTools, projectTools, setupTools, captureTools, analyzeTools, canvasTools, exportTools, testingTools, integrationTools } = await import("./tools-75BAPCUM.js");
|
|
85
|
+
mcpServer.registerTools([
|
|
86
|
+
...uiTools,
|
|
87
|
+
...projectTools,
|
|
88
|
+
...setupTools,
|
|
89
|
+
...captureTools,
|
|
90
|
+
...analyzeTools,
|
|
91
|
+
...canvasTools,
|
|
92
|
+
...exportTools,
|
|
93
|
+
...testingTools,
|
|
94
|
+
...integrationTools
|
|
95
|
+
]);
|
|
96
|
+
await mcpServer.runStdio();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`MCP server error: ${error}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
program.command("info").description("Show version and configuration info").action(async () => {
|
|
103
|
+
console.log(chalk.cyan("\n DiscoveryLab v0.1.0\n"));
|
|
104
|
+
console.log(chalk.gray(" AI-powered app testing & evidence generator"));
|
|
105
|
+
console.log(chalk.gray(" Claude Code Plugin\n"));
|
|
106
|
+
try {
|
|
107
|
+
const { DATA_DIR, DB_PATH } = await import("./db-IWIL65EX.js");
|
|
108
|
+
console.log(chalk.white(" Paths:"));
|
|
109
|
+
console.log(chalk.gray(` Data: ${DATA_DIR}`));
|
|
110
|
+
console.log(chalk.gray(` Database: ${DB_PATH}`));
|
|
111
|
+
console.log();
|
|
112
|
+
} catch {
|
|
113
|
+
console.log(chalk.gray(" (Database not initialized)"));
|
|
114
|
+
console.log();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
program.parse();
|
|
118
|
+
if (!process.argv.slice(2).length) {
|
|
119
|
+
program.outputHelp();
|
|
120
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DATA_DIR,
|
|
3
|
+
DB_PATH,
|
|
4
|
+
EXPORTS_DIR,
|
|
5
|
+
FRAMES_DIR,
|
|
6
|
+
PROJECTS_DIR,
|
|
7
|
+
closeDatabase,
|
|
8
|
+
exportDestinations,
|
|
9
|
+
exportRules,
|
|
10
|
+
frames,
|
|
11
|
+
getDatabase,
|
|
12
|
+
getSqlite,
|
|
13
|
+
projectExports,
|
|
14
|
+
projects,
|
|
15
|
+
settings
|
|
16
|
+
} from "./chunk-VY3BLXBW.js";
|
|
17
|
+
import "./chunk-MLKGABMK.js";
|
|
18
|
+
export {
|
|
19
|
+
DATA_DIR,
|
|
20
|
+
DB_PATH,
|
|
21
|
+
EXPORTS_DIR,
|
|
22
|
+
FRAMES_DIR,
|
|
23
|
+
PROJECTS_DIR,
|
|
24
|
+
closeDatabase,
|
|
25
|
+
exportDestinations,
|
|
26
|
+
exportRules,
|
|
27
|
+
frames,
|
|
28
|
+
getDatabase,
|
|
29
|
+
getSqlite,
|
|
30
|
+
projectExports,
|
|
31
|
+
projects,
|
|
32
|
+
settings
|
|
33
|
+
};
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import "./chunk-MLKGABMK.js";
|
|
2
|
+
|
|
3
|
+
// src/core/canvas/gridCompositor.ts
|
|
4
|
+
import { createCanvas, loadImage } from "canvas";
|
|
5
|
+
import { existsSync, writeFileSync, mkdirSync, readdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
var ASPECT_RATIOS = {
|
|
8
|
+
"9:16": { width: 1080, height: 1920 },
|
|
9
|
+
"1:1": { width: 1080, height: 1080 },
|
|
10
|
+
"16:9": { width: 1920, height: 1080 }
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_CONFIG = {
|
|
13
|
+
aspectRatio: "9:16",
|
|
14
|
+
layout: "quad",
|
|
15
|
+
background: {
|
|
16
|
+
type: "gradient",
|
|
17
|
+
gradientStart: "#1a1a2e",
|
|
18
|
+
gradientEnd: "#16213e",
|
|
19
|
+
gradientAngle: 135
|
|
20
|
+
},
|
|
21
|
+
padding: 40,
|
|
22
|
+
cellPadding: 16,
|
|
23
|
+
cornerRadius: 16,
|
|
24
|
+
shadowEnabled: true,
|
|
25
|
+
shadowBlur: 20,
|
|
26
|
+
outputWidth: 1080,
|
|
27
|
+
imageFit: "cover"
|
|
28
|
+
};
|
|
29
|
+
function calculateCells(layout, canvasWidth, canvasHeight, padding, cellPadding, imageCount) {
|
|
30
|
+
const contentWidth = canvasWidth - padding * 2;
|
|
31
|
+
const contentHeight = canvasHeight - padding * 2;
|
|
32
|
+
const cells = [];
|
|
33
|
+
switch (layout) {
|
|
34
|
+
case "single": {
|
|
35
|
+
cells.push({
|
|
36
|
+
x: padding,
|
|
37
|
+
y: padding,
|
|
38
|
+
width: contentWidth,
|
|
39
|
+
height: contentHeight
|
|
40
|
+
});
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case "duo-h": {
|
|
44
|
+
const cellWidth = (contentWidth - cellPadding) / 2;
|
|
45
|
+
cells.push(
|
|
46
|
+
{ x: padding, y: padding, width: cellWidth, height: contentHeight },
|
|
47
|
+
{ x: padding + cellWidth + cellPadding, y: padding, width: cellWidth, height: contentHeight }
|
|
48
|
+
);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case "duo-v": {
|
|
52
|
+
const cellHeight = (contentHeight - cellPadding) / 2;
|
|
53
|
+
cells.push(
|
|
54
|
+
{ x: padding, y: padding, width: contentWidth, height: cellHeight },
|
|
55
|
+
{ x: padding, y: padding + cellHeight + cellPadding, width: contentWidth, height: cellHeight }
|
|
56
|
+
);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case "trio-h": {
|
|
60
|
+
const cellWidth = (contentWidth - cellPadding * 2) / 3;
|
|
61
|
+
for (let i = 0; i < 3; i++) {
|
|
62
|
+
cells.push({
|
|
63
|
+
x: padding + i * (cellWidth + cellPadding),
|
|
64
|
+
y: padding,
|
|
65
|
+
width: cellWidth,
|
|
66
|
+
height: contentHeight
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "trio-v": {
|
|
72
|
+
const cellHeight = (contentHeight - cellPadding * 2) / 3;
|
|
73
|
+
for (let i = 0; i < 3; i++) {
|
|
74
|
+
cells.push({
|
|
75
|
+
x: padding,
|
|
76
|
+
y: padding + i * (cellHeight + cellPadding),
|
|
77
|
+
width: contentWidth,
|
|
78
|
+
height: cellHeight
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "quad": {
|
|
84
|
+
const cellWidth = (contentWidth - cellPadding) / 2;
|
|
85
|
+
const cellHeight = (contentHeight - cellPadding) / 2;
|
|
86
|
+
for (let row = 0; row < 2; row++) {
|
|
87
|
+
for (let col = 0; col < 2; col++) {
|
|
88
|
+
cells.push({
|
|
89
|
+
x: padding + col * (cellWidth + cellPadding),
|
|
90
|
+
y: padding + row * (cellHeight + cellPadding),
|
|
91
|
+
width: cellWidth,
|
|
92
|
+
height: cellHeight
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "featured": {
|
|
99
|
+
const leftWidth = contentWidth * 0.6 - cellPadding / 2;
|
|
100
|
+
const rightWidth = contentWidth * 0.4 - cellPadding / 2;
|
|
101
|
+
const rightHeight = (contentHeight - cellPadding) / 2;
|
|
102
|
+
cells.push(
|
|
103
|
+
{ x: padding, y: padding, width: leftWidth, height: contentHeight },
|
|
104
|
+
{ x: padding + leftWidth + cellPadding, y: padding, width: rightWidth, height: rightHeight },
|
|
105
|
+
{ x: padding + leftWidth + cellPadding, y: padding + rightHeight + cellPadding, width: rightWidth, height: rightHeight }
|
|
106
|
+
);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "masonry": {
|
|
110
|
+
const colWidth = (contentWidth - cellPadding * 2) / 3;
|
|
111
|
+
const heights = [0, 0, 0];
|
|
112
|
+
const count = Math.min(imageCount, 6);
|
|
113
|
+
for (let i = 0; i < count; i++) {
|
|
114
|
+
const col = i % 3;
|
|
115
|
+
const cellHeight = i < 3 ? contentHeight * 0.55 : contentHeight * 0.45 - cellPadding;
|
|
116
|
+
cells.push({
|
|
117
|
+
x: padding + col * (colWidth + cellPadding),
|
|
118
|
+
y: padding + heights[col],
|
|
119
|
+
width: colWidth,
|
|
120
|
+
height: cellHeight
|
|
121
|
+
});
|
|
122
|
+
heights[col] += cellHeight + cellPadding;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "carousel": {
|
|
127
|
+
const cardWidth = contentWidth * 0.7;
|
|
128
|
+
const cardHeight = contentHeight * 0.85;
|
|
129
|
+
const overlap = cardWidth * 0.3;
|
|
130
|
+
const count = Math.min(imageCount, 3);
|
|
131
|
+
for (let i = 0; i < count; i++) {
|
|
132
|
+
const scale = 1 - i * 0.05;
|
|
133
|
+
const w = cardWidth * scale;
|
|
134
|
+
const h = cardHeight * scale;
|
|
135
|
+
cells.push({
|
|
136
|
+
x: padding + (contentWidth - w) / 2 + (i - 1) * overlap,
|
|
137
|
+
y: padding + (contentHeight - h) / 2 + i * 10,
|
|
138
|
+
width: w,
|
|
139
|
+
height: h
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "stacked": {
|
|
145
|
+
const cardWidth = contentWidth * 0.8;
|
|
146
|
+
const cardHeight = contentHeight * 0.7;
|
|
147
|
+
const count = Math.min(imageCount, 4);
|
|
148
|
+
const offsetY = 30;
|
|
149
|
+
const offsetX = 15;
|
|
150
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
151
|
+
cells.push({
|
|
152
|
+
x: padding + (contentWidth - cardWidth) / 2 + i * offsetX,
|
|
153
|
+
y: padding + (contentHeight - cardHeight) / 2 - i * offsetY,
|
|
154
|
+
width: cardWidth,
|
|
155
|
+
height: cardHeight
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
default:
|
|
161
|
+
cells.push({
|
|
162
|
+
x: padding,
|
|
163
|
+
y: padding,
|
|
164
|
+
width: contentWidth,
|
|
165
|
+
height: contentHeight
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return cells;
|
|
169
|
+
}
|
|
170
|
+
function drawRoundedRect(ctx, x, y, width, height, radius) {
|
|
171
|
+
ctx.beginPath();
|
|
172
|
+
ctx.moveTo(x + radius, y);
|
|
173
|
+
ctx.lineTo(x + width - radius, y);
|
|
174
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
175
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
176
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
177
|
+
ctx.lineTo(x + radius, y + height);
|
|
178
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
179
|
+
ctx.lineTo(x, y + radius);
|
|
180
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
181
|
+
ctx.closePath();
|
|
182
|
+
}
|
|
183
|
+
function drawBackground(ctx, width, height, config, bgImage) {
|
|
184
|
+
if (config.type === "image" && bgImage) {
|
|
185
|
+
const scale = Math.max(width / bgImage.width, height / bgImage.height);
|
|
186
|
+
const scaledWidth = bgImage.width * scale;
|
|
187
|
+
const scaledHeight = bgImage.height * scale;
|
|
188
|
+
const offsetX = (width - scaledWidth) / 2;
|
|
189
|
+
const offsetY = (height - scaledHeight) / 2;
|
|
190
|
+
ctx.drawImage(bgImage, offsetX, offsetY, scaledWidth, scaledHeight);
|
|
191
|
+
} else if (config.type === "gradient") {
|
|
192
|
+
const angle = (config.gradientAngle || 135) * Math.PI / 180;
|
|
193
|
+
const length = Math.sqrt(width * width + height * height);
|
|
194
|
+
const x1 = width / 2 - Math.cos(angle) * length / 2;
|
|
195
|
+
const y1 = height / 2 - Math.sin(angle) * length / 2;
|
|
196
|
+
const x2 = width / 2 + Math.cos(angle) * length / 2;
|
|
197
|
+
const y2 = height / 2 + Math.sin(angle) * length / 2;
|
|
198
|
+
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
|
|
199
|
+
gradient.addColorStop(0, config.gradientStart || "#1a1a2e");
|
|
200
|
+
gradient.addColorStop(1, config.gradientEnd || "#16213e");
|
|
201
|
+
ctx.fillStyle = gradient;
|
|
202
|
+
ctx.fillRect(0, 0, width, height);
|
|
203
|
+
} else {
|
|
204
|
+
ctx.fillStyle = config.color || "#1a1a2e";
|
|
205
|
+
ctx.fillRect(0, 0, width, height);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function drawShadow(ctx, x, y, width, height, radius, blur) {
|
|
209
|
+
ctx.save();
|
|
210
|
+
ctx.shadowColor = "rgba(0, 0, 0, 0.4)";
|
|
211
|
+
ctx.shadowBlur = blur;
|
|
212
|
+
ctx.shadowOffsetX = 0;
|
|
213
|
+
ctx.shadowOffsetY = blur / 3;
|
|
214
|
+
ctx.fillStyle = "white";
|
|
215
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
216
|
+
ctx.fill();
|
|
217
|
+
ctx.restore();
|
|
218
|
+
}
|
|
219
|
+
function calculateImageBounds(image, cell, imageFit) {
|
|
220
|
+
const imageAspect = image.width / image.height;
|
|
221
|
+
const cellAspect = cell.width / cell.height;
|
|
222
|
+
let drawWidth;
|
|
223
|
+
let drawHeight;
|
|
224
|
+
let offsetX;
|
|
225
|
+
let offsetY;
|
|
226
|
+
if (imageFit === "cover") {
|
|
227
|
+
if (imageAspect > cellAspect) {
|
|
228
|
+
drawHeight = cell.height;
|
|
229
|
+
drawWidth = cell.height * imageAspect;
|
|
230
|
+
offsetX = (cell.width - drawWidth) / 2;
|
|
231
|
+
offsetY = 0;
|
|
232
|
+
} else {
|
|
233
|
+
drawWidth = cell.width;
|
|
234
|
+
drawHeight = cell.width / imageAspect;
|
|
235
|
+
offsetX = 0;
|
|
236
|
+
offsetY = (cell.height - drawHeight) / 2;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
x: cell.x,
|
|
240
|
+
y: cell.y,
|
|
241
|
+
width: cell.width,
|
|
242
|
+
height: cell.height,
|
|
243
|
+
drawWidth,
|
|
244
|
+
drawHeight,
|
|
245
|
+
offsetX,
|
|
246
|
+
offsetY
|
|
247
|
+
};
|
|
248
|
+
} else {
|
|
249
|
+
if (imageAspect > cellAspect) {
|
|
250
|
+
drawWidth = cell.width;
|
|
251
|
+
drawHeight = cell.width / imageAspect;
|
|
252
|
+
offsetX = 0;
|
|
253
|
+
offsetY = (cell.height - drawHeight) / 2;
|
|
254
|
+
} else {
|
|
255
|
+
drawHeight = cell.height;
|
|
256
|
+
drawWidth = cell.height * imageAspect;
|
|
257
|
+
offsetX = (cell.width - drawWidth) / 2;
|
|
258
|
+
offsetY = 0;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
x: cell.x + offsetX,
|
|
262
|
+
y: cell.y + offsetY,
|
|
263
|
+
width: drawWidth,
|
|
264
|
+
height: drawHeight,
|
|
265
|
+
drawWidth,
|
|
266
|
+
drawHeight,
|
|
267
|
+
offsetX,
|
|
268
|
+
offsetY
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function drawImageInCell(ctx, image, cell, cornerRadius, imageFit = "cover", shadowEnabled = false, shadowBlur = 20) {
|
|
273
|
+
const bounds = calculateImageBounds(image, cell, imageFit);
|
|
274
|
+
if (shadowEnabled && imageFit === "contain") {
|
|
275
|
+
drawShadow(ctx, bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius, shadowBlur);
|
|
276
|
+
}
|
|
277
|
+
ctx.save();
|
|
278
|
+
if (imageFit === "cover") {
|
|
279
|
+
drawRoundedRect(ctx, cell.x, cell.y, cell.width, cell.height, cornerRadius);
|
|
280
|
+
} else {
|
|
281
|
+
drawRoundedRect(ctx, bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius);
|
|
282
|
+
}
|
|
283
|
+
ctx.clip();
|
|
284
|
+
ctx.drawImage(
|
|
285
|
+
image,
|
|
286
|
+
cell.x + bounds.offsetX,
|
|
287
|
+
cell.y + bounds.offsetY,
|
|
288
|
+
bounds.drawWidth,
|
|
289
|
+
bounds.drawHeight
|
|
290
|
+
);
|
|
291
|
+
ctx.restore();
|
|
292
|
+
}
|
|
293
|
+
async function composeGrid(images, config = {}, outputPath) {
|
|
294
|
+
try {
|
|
295
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
296
|
+
const aspectDef = ASPECT_RATIOS[cfg.aspectRatio];
|
|
297
|
+
const scale = cfg.outputWidth / aspectDef.width;
|
|
298
|
+
const canvasWidth = Math.round(aspectDef.width * scale);
|
|
299
|
+
const canvasHeight = Math.round(aspectDef.height * scale);
|
|
300
|
+
const canvas = createCanvas(canvasWidth, canvasHeight);
|
|
301
|
+
const ctx = canvas.getContext("2d");
|
|
302
|
+
let bgImage;
|
|
303
|
+
if (cfg.background.type === "image" && cfg.background.imagePath && existsSync(cfg.background.imagePath)) {
|
|
304
|
+
bgImage = await loadImage(cfg.background.imagePath);
|
|
305
|
+
}
|
|
306
|
+
drawBackground(ctx, canvasWidth, canvasHeight, cfg.background, bgImage);
|
|
307
|
+
const cells = calculateCells(
|
|
308
|
+
cfg.layout,
|
|
309
|
+
canvasWidth,
|
|
310
|
+
canvasHeight,
|
|
311
|
+
cfg.padding,
|
|
312
|
+
cfg.cellPadding,
|
|
313
|
+
images.length
|
|
314
|
+
);
|
|
315
|
+
for (let i = 0; i < Math.min(images.length, cells.length); i++) {
|
|
316
|
+
const cell = cells[i];
|
|
317
|
+
const imageCell = images[i];
|
|
318
|
+
if (!existsSync(imageCell.imagePath)) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const image = await loadImage(imageCell.imagePath);
|
|
322
|
+
if (cfg.shadowEnabled && cfg.imageFit === "cover") {
|
|
323
|
+
drawShadow(ctx, cell.x, cell.y, cell.width, cell.height, cfg.cornerRadius, cfg.shadowBlur);
|
|
324
|
+
}
|
|
325
|
+
drawImageInCell(ctx, image, cell, cfg.cornerRadius, cfg.imageFit, cfg.shadowEnabled, cfg.shadowBlur);
|
|
326
|
+
}
|
|
327
|
+
const buffer = canvas.toBuffer("image/png");
|
|
328
|
+
const outputDir = outputPath.substring(0, outputPath.lastIndexOf("/"));
|
|
329
|
+
if (outputDir && !existsSync(outputDir)) {
|
|
330
|
+
mkdirSync(outputDir, { recursive: true });
|
|
331
|
+
}
|
|
332
|
+
writeFileSync(outputPath, buffer);
|
|
333
|
+
return {
|
|
334
|
+
success: true,
|
|
335
|
+
outputPath,
|
|
336
|
+
width: canvasWidth,
|
|
337
|
+
height: canvasHeight
|
|
338
|
+
};
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const message = error instanceof Error ? error.message : "Grid composition failed";
|
|
341
|
+
return {
|
|
342
|
+
success: false,
|
|
343
|
+
error: message
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function recommendLayout(imageCount, aspectRatio) {
|
|
348
|
+
if (imageCount === 1) return "single";
|
|
349
|
+
if (imageCount === 2) return aspectRatio === "16:9" ? "duo-h" : "duo-v";
|
|
350
|
+
if (imageCount === 3) return aspectRatio === "9:16" ? "trio-v" : "featured";
|
|
351
|
+
if (imageCount === 4) return "quad";
|
|
352
|
+
if (imageCount >= 5) return "masonry";
|
|
353
|
+
return "quad";
|
|
354
|
+
}
|
|
355
|
+
function getLayoutInfo(layout) {
|
|
356
|
+
const layouts = {
|
|
357
|
+
"single": { name: "Single", maxImages: 1, description: "One large image" },
|
|
358
|
+
"duo-h": { name: "Side by Side", maxImages: 2, description: "Two images horizontally" },
|
|
359
|
+
"duo-v": { name: "Stacked", maxImages: 2, description: "Two images vertically" },
|
|
360
|
+
"trio-h": { name: "3 Across", maxImages: 3, description: "Three images in a row" },
|
|
361
|
+
"trio-v": { name: "3 Down", maxImages: 3, description: "Three images in a column" },
|
|
362
|
+
"quad": { name: "Grid 2x2", maxImages: 4, description: "Four images in a grid" },
|
|
363
|
+
"featured": { name: "Featured", maxImages: 3, description: "One large + two small" },
|
|
364
|
+
"masonry": { name: "Masonry", maxImages: 6, description: "Pinterest-style columns" },
|
|
365
|
+
"carousel": { name: "Carousel", maxImages: 3, description: "Overlapping cards" },
|
|
366
|
+
"stacked": { name: "3D Stack", maxImages: 4, description: "Perspective stack effect" }
|
|
367
|
+
};
|
|
368
|
+
return layouts[layout];
|
|
369
|
+
}
|
|
370
|
+
function getAllLayouts() {
|
|
371
|
+
return ["single", "duo-h", "duo-v", "trio-h", "trio-v", "quad", "featured", "masonry", "carousel", "stacked"];
|
|
372
|
+
}
|
|
373
|
+
function getAvailableBackgrounds(backgroundsDir) {
|
|
374
|
+
if (!existsSync(backgroundsDir)) {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
const files = readdirSync(backgroundsDir).filter((f) => /\.(jpg|jpeg|png|webp)$/i.test(f));
|
|
378
|
+
return files.map((f) => ({
|
|
379
|
+
id: f.replace(/\.[^.]+$/, ""),
|
|
380
|
+
name: f.replace(/[-_]/g, " ").replace(/\.[^.]+$/, ""),
|
|
381
|
+
path: join(backgroundsDir, f)
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
var PRESET_GRADIENTS = [
|
|
385
|
+
{ id: "dark-blue", name: "Dark Blue", start: "#1a1a2e", end: "#16213e" },
|
|
386
|
+
{ id: "midnight", name: "Midnight", start: "#0f0c29", end: "#302b63" },
|
|
387
|
+
{ id: "ocean", name: "Ocean", start: "#2193b0", end: "#6dd5ed" },
|
|
388
|
+
{ id: "sunset", name: "Sunset", start: "#ff7e5f", end: "#feb47b" },
|
|
389
|
+
{ id: "aurora", name: "Aurora", start: "#00c6ff", end: "#0072ff" },
|
|
390
|
+
{ id: "forest", name: "Forest", start: "#134e5e", end: "#71b280" },
|
|
391
|
+
{ id: "berry", name: "Berry", start: "#8e2de2", end: "#4a00e0" },
|
|
392
|
+
{ id: "noir", name: "Noir", start: "#0a0a0a", end: "#1a1a1a" }
|
|
393
|
+
];
|
|
394
|
+
var PRESET_SOLID_COLORS = [
|
|
395
|
+
{ id: "black", name: "Black", color: "#000000" },
|
|
396
|
+
{ id: "dark-gray", name: "Dark Gray", color: "#1a1a1a" },
|
|
397
|
+
{ id: "navy", name: "Navy", color: "#0a1628" },
|
|
398
|
+
{ id: "charcoal", name: "Charcoal", color: "#2d2d2d" },
|
|
399
|
+
{ id: "white", name: "White", color: "#ffffff" }
|
|
400
|
+
];
|
|
401
|
+
export {
|
|
402
|
+
PRESET_GRADIENTS,
|
|
403
|
+
PRESET_SOLID_COLORS,
|
|
404
|
+
composeGrid,
|
|
405
|
+
getAllLayouts,
|
|
406
|
+
getAvailableBackgrounds,
|
|
407
|
+
getLayoutInfo,
|
|
408
|
+
recommendLayout
|
|
409
|
+
};
|