appshot-cli 0.8.7 → 0.9.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/README.md +480 -15
- package/dist/cli.js +19 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +16 -2
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/preset.d.ts +5 -0
- package/dist/commands/preset.d.ts.map +1 -0
- package/dist/commands/preset.js +191 -0
- package/dist/commands/preset.js.map +1 -0
- package/dist/commands/quickstart.d.ts +3 -0
- package/dist/commands/quickstart.d.ts.map +1 -0
- package/dist/commands/quickstart.js +326 -0
- package/dist/commands/quickstart.js.map +1 -0
- package/dist/commands/style.d.ts.map +1 -1
- package/dist/commands/style.js +98 -0
- package/dist/commands/style.js.map +1 -1
- package/dist/commands/template.d.ts +3 -0
- package/dist/commands/template.d.ts.map +1 -0
- package/dist/commands/template.js +399 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/core/compose.d.ts +16 -0
- package/dist/core/compose.d.ts.map +1 -1
- package/dist/core/compose.js +342 -83
- package/dist/core/compose.js.map +1 -1
- package/dist/services/doctor.js +1 -1
- package/dist/templates/registry.d.ts +73 -0
- package/dist/templates/registry.d.ts.map +1 -0
- package/dist/templates/registry.js +724 -0
- package/dist/templates/registry.js.map +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/validation.d.ts +36 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +143 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +3 -2
package/dist/core/compose.js
CHANGED
|
@@ -47,20 +47,39 @@ function escapeXml(text) {
|
|
|
47
47
|
/**
|
|
48
48
|
* Generate SVG with text, background, and border support
|
|
49
49
|
*/
|
|
50
|
-
function generateCaptionSVG(lines, width, height, fontSize, fontFamily, fontStyle, fontWeight, textColor, lineHeight, captionConfig) {
|
|
51
|
-
const backgroundConfig = captionConfig.background;
|
|
52
|
-
const borderConfig = captionConfig.border;
|
|
50
|
+
function generateCaptionSVG(lines, width, height, fontSize, fontFamily, fontStyle, fontWeight, textColor, lineHeight, captionConfig, deviceConfig = {}) {
|
|
51
|
+
const backgroundConfig = deviceConfig.captionBackground || captionConfig.background;
|
|
52
|
+
const borderConfig = deviceConfig.captionBorder || captionConfig.border;
|
|
53
|
+
const verticalAlign = captionConfig.box?.verticalAlign || 'center';
|
|
54
|
+
const align = captionConfig.align || 'center';
|
|
53
55
|
// Calculate text positioning
|
|
54
56
|
const totalTextHeight = lines.length * fontSize * lineHeight;
|
|
55
|
-
const
|
|
57
|
+
const bgPadding = backgroundConfig?.padding || 20;
|
|
58
|
+
const startY = verticalAlign === 'top'
|
|
59
|
+
? Math.max(fontSize, bgPadding + fontSize)
|
|
60
|
+
: (height - totalTextHeight) / 2 + fontSize;
|
|
61
|
+
// Horizontal alignment
|
|
62
|
+
const sideMargin = backgroundConfig?.sideMargin ?? 30;
|
|
63
|
+
const leftX = sideMargin + bgPadding;
|
|
64
|
+
const rightX = width - (sideMargin + bgPadding);
|
|
65
|
+
const centerX = Math.floor(width / 2);
|
|
66
|
+
let textAnchor = 'middle';
|
|
67
|
+
let textX = centerX;
|
|
68
|
+
if (align === 'left') {
|
|
69
|
+
textAnchor = 'start';
|
|
70
|
+
textX = leftX;
|
|
71
|
+
}
|
|
72
|
+
else if (align === 'right') {
|
|
73
|
+
textAnchor = 'end';
|
|
74
|
+
textX = rightX;
|
|
75
|
+
}
|
|
56
76
|
// SVG elements array
|
|
57
77
|
const svgElements = [];
|
|
58
78
|
// Add background rectangle if configured
|
|
59
79
|
if (backgroundConfig?.color) {
|
|
60
|
-
const
|
|
61
|
-
const bgOpacity = backgroundConfig.opacity || 0.8;
|
|
80
|
+
const bgOpacity = backgroundConfig.opacity !== undefined ? backgroundConfig.opacity : 0.8;
|
|
62
81
|
// Use full width minus margins for uniform appearance
|
|
63
|
-
const sideMargin = 30; // Margin from edges
|
|
82
|
+
const sideMargin = backgroundConfig.sideMargin ?? 30; // Margin from edges
|
|
64
83
|
const bgWidth = width - (sideMargin * 2);
|
|
65
84
|
const bgHeight = totalTextHeight + bgPadding * 2;
|
|
66
85
|
const bgX = sideMargin;
|
|
@@ -75,7 +94,7 @@ function generateCaptionSVG(lines, width, height, fontSize, fontFamily, fontStyl
|
|
|
75
94
|
const borderWidth = borderConfig.width;
|
|
76
95
|
const borderRadius = borderConfig.radius || 12;
|
|
77
96
|
// Use full width minus margins for uniform appearance
|
|
78
|
-
const sideMargin = 30; // Margin from edges
|
|
97
|
+
const sideMargin = backgroundConfig?.sideMargin ?? 30; // Margin from edges
|
|
79
98
|
const rectWidth = width - (sideMargin * 2);
|
|
80
99
|
const rectHeight = totalTextHeight + bgPadding * 2;
|
|
81
100
|
const rectX = sideMargin;
|
|
@@ -86,13 +105,13 @@ function generateCaptionSVG(lines, width, height, fontSize, fontFamily, fontStyl
|
|
|
86
105
|
// Add text elements
|
|
87
106
|
const textElements = lines.map((line, index) => {
|
|
88
107
|
const y = startY + (index * fontSize * lineHeight);
|
|
89
|
-
return `<text x="${
|
|
108
|
+
return `<text x="${textX}" y="${y}" ` +
|
|
90
109
|
`font-family="${fontFamily}" ` +
|
|
91
110
|
`font-size="${fontSize}" ` +
|
|
92
111
|
`font-style="${fontStyle}" ` +
|
|
93
112
|
`font-weight="${fontWeight}" ` +
|
|
94
113
|
`fill="${textColor}" ` +
|
|
95
|
-
`text-anchor="
|
|
114
|
+
`text-anchor="${textAnchor}">${escapeXml(line)}</text>`;
|
|
96
115
|
});
|
|
97
116
|
svgElements.push(...textElements);
|
|
98
117
|
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -108,18 +127,30 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
108
127
|
// Check for device-specific override first
|
|
109
128
|
const captionPosition = deviceConfig.captionPosition || captionConfig.position || 'above';
|
|
110
129
|
const partialFrame = deviceConfig.partialFrame || false;
|
|
111
|
-
const frameOffset = deviceConfig.frameOffset
|
|
112
|
-
const framePosition = deviceConfig.framePosition
|
|
130
|
+
const frameOffset = deviceConfig.frameOffset !== undefined ? deviceConfig.frameOffset : 25; // Default 25% cut off
|
|
131
|
+
const framePosition = deviceConfig.framePosition !== undefined ? deviceConfig.framePosition : 'center';
|
|
113
132
|
const deviceFrameScale = deviceConfig.frameScale;
|
|
133
|
+
const boxCfg = deviceConfig.captionBox || captionConfig.box || {};
|
|
134
|
+
const marginTop = boxCfg.marginTop || 0;
|
|
135
|
+
const marginBottom = boxCfg.marginBottom || 0;
|
|
136
|
+
const sideMarginDbg = captionConfig.background?.sideMargin ?? 30;
|
|
137
|
+
if (verbose) {
|
|
138
|
+
console.log(pc.dim(' Layout settings:'));
|
|
139
|
+
console.log(pc.dim(` Caption position: ${captionPosition}`));
|
|
140
|
+
console.log(pc.dim(` Frame position: ${String(framePosition)}`));
|
|
141
|
+
console.log(pc.dim(` Frame scale: ${deviceFrameScale ?? '(auto)'}`));
|
|
142
|
+
console.log(pc.dim(` Margins: top=${marginTop}px, bottom=${marginBottom}px, side=${sideMarginDbg}px`));
|
|
143
|
+
}
|
|
114
144
|
// Calculate dimensions based on output
|
|
115
145
|
// Pre-calculate device dimensions for caption height calculation
|
|
116
146
|
let targetDeviceHeight = 0;
|
|
117
147
|
let deviceTop = 0;
|
|
148
|
+
let croppedPixels = 0; // Track cropped pixels for partial frames
|
|
118
149
|
if (frame && frameMetadata) {
|
|
119
150
|
const originalFrameHeight = frameMetadata.frameHeight;
|
|
120
151
|
const availableHeight = Math.max(100, outputHeight - 100); // Temporary estimate
|
|
121
152
|
const scaleY = availableHeight / originalFrameHeight;
|
|
122
|
-
const scale = deviceConfig.frameScale ? scaleY * deviceConfig.frameScale : scaleY * 0.9;
|
|
153
|
+
const scale = deviceConfig.frameScale !== undefined ? scaleY * deviceConfig.frameScale : scaleY * 0.9;
|
|
123
154
|
targetDeviceHeight = Math.floor(originalFrameHeight * scale);
|
|
124
155
|
// Calculate preliminary device position
|
|
125
156
|
if (typeof framePosition === 'number') {
|
|
@@ -139,6 +170,10 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
139
170
|
// Calculate caption height based on position
|
|
140
171
|
let captionHeight = 0;
|
|
141
172
|
let captionLines = [];
|
|
173
|
+
const backgroundCfg = deviceConfig.captionBackground || captionConfig.background || {};
|
|
174
|
+
const sideMargin = backgroundCfg.sideMargin ?? 30;
|
|
175
|
+
const boxPadding = backgroundCfg.padding ?? 20;
|
|
176
|
+
const wrapWidth = Math.max(50, outputWidth - (sideMargin * 2) - (boxPadding * 2));
|
|
142
177
|
if ((captionPosition === 'above' || captionPosition === 'below') && caption) {
|
|
143
178
|
const isWatch = outputWidth < 500;
|
|
144
179
|
const captionFontSize = deviceConfig.captionSize || captionConfig.fontsize;
|
|
@@ -158,7 +193,7 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
158
193
|
// Use smaller font size for watch (36px max)
|
|
159
194
|
const watchFontSize = Math.min(36, captionFontSize);
|
|
160
195
|
// Use wrapText which now accounts for watch padding - allow 3 lines for watch
|
|
161
|
-
captionLines = wrapText(caption,
|
|
196
|
+
captionLines = wrapText(caption, wrapWidth, watchFontSize, 3);
|
|
162
197
|
if (verbose) {
|
|
163
198
|
console.log(pc.dim(` Watch mode: font reduced to ${watchFontSize}px`));
|
|
164
199
|
console.log(pc.dim(` Wrap width: ${outputWidth}px (with padding)`));
|
|
@@ -166,9 +201,22 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
166
201
|
}
|
|
167
202
|
else if (autoSize) {
|
|
168
203
|
// Use adaptive caption height
|
|
169
|
-
const result = calculateAdaptiveCaptionHeight(caption, captionFontSize,
|
|
204
|
+
const result = calculateAdaptiveCaptionHeight(caption, captionFontSize, wrapWidth, outputHeight, deviceTop, targetDeviceHeight, framePosition);
|
|
170
205
|
captionHeight = result.height;
|
|
171
206
|
captionLines = result.lines;
|
|
207
|
+
// Respect min/max height if provided (helps verticalAlign be visible)
|
|
208
|
+
const minH = captionBoxConfig.minHeight;
|
|
209
|
+
const maxH = captionBoxConfig.maxHeight;
|
|
210
|
+
if (typeof minH === 'number')
|
|
211
|
+
captionHeight = Math.max(minH, captionHeight);
|
|
212
|
+
if (typeof maxH === 'number')
|
|
213
|
+
captionHeight = Math.min(maxH, captionHeight);
|
|
214
|
+
// Ensure the SVG canvas can fully contain the background rect
|
|
215
|
+
const computedLineHeight = (captionConfig.box && captionConfig.box.lineHeight) || 1.4;
|
|
216
|
+
const minSvgHeight = Math.ceil((captionLines.length * captionFontSize * computedLineHeight) + (boxPadding * 2));
|
|
217
|
+
if (captionHeight < minSvgHeight) {
|
|
218
|
+
captionHeight = minSvgHeight;
|
|
219
|
+
}
|
|
172
220
|
if (verbose) {
|
|
173
221
|
console.log(pc.dim(` Adaptive height: ${captionHeight}px`));
|
|
174
222
|
}
|
|
@@ -176,7 +224,7 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
176
224
|
else {
|
|
177
225
|
// Use fixed height with text wrapping
|
|
178
226
|
const maxLines = captionBoxConfig.maxLines || 3;
|
|
179
|
-
captionLines = wrapText(caption,
|
|
227
|
+
captionLines = wrapText(caption, wrapWidth, captionFontSize, maxLines);
|
|
180
228
|
const lineHeight = captionBoxConfig.lineHeight || 1.4;
|
|
181
229
|
const textHeight = captionLines.length * captionFontSize * lineHeight;
|
|
182
230
|
captionHeight = captionConfig.paddingTop + textHeight + (captionConfig.paddingBottom || 60);
|
|
@@ -187,6 +235,12 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
187
235
|
if (captionBoxConfig.maxHeight) {
|
|
188
236
|
captionHeight = Math.min(captionBoxConfig.maxHeight, captionHeight);
|
|
189
237
|
}
|
|
238
|
+
// Ensure the SVG canvas can fully contain the background rect
|
|
239
|
+
const computedLineHeight = (captionConfig.box && captionConfig.box.lineHeight) || 1.4;
|
|
240
|
+
const minSvgHeight = Math.ceil((captionLines.length * captionFontSize * computedLineHeight) + (boxPadding * 2));
|
|
241
|
+
if (captionHeight < minSvgHeight) {
|
|
242
|
+
captionHeight = minSvgHeight;
|
|
243
|
+
}
|
|
190
244
|
if (verbose) {
|
|
191
245
|
console.log(pc.dim(` Max lines: ${maxLines}`));
|
|
192
246
|
console.log(pc.dim(` Line height: ${lineHeight}`));
|
|
@@ -195,7 +249,7 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
195
249
|
}
|
|
196
250
|
if (verbose && captionLines.length > 0) {
|
|
197
251
|
console.log(pc.dim(` Lines wrapped: ${captionLines.length}`));
|
|
198
|
-
console.log(pc.dim(` Wrap width: ${
|
|
252
|
+
console.log(pc.dim(` Wrap width: ${wrapWidth}px`));
|
|
199
253
|
}
|
|
200
254
|
}
|
|
201
255
|
// Calculate total canvas dimensions (should be output dimensions)
|
|
@@ -245,6 +299,7 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
245
299
|
// Get caption box config
|
|
246
300
|
const captionBoxConfig = deviceConfig.captionBox || captionConfig.box || {};
|
|
247
301
|
const lineHeight = captionBoxConfig.lineHeight || 1.4;
|
|
302
|
+
const topMargin = captionBoxConfig.marginTop || 0;
|
|
248
303
|
if (captionLines.length === 0) {
|
|
249
304
|
// Fallback if no lines were calculated
|
|
250
305
|
captionLines = [caption];
|
|
@@ -264,15 +319,40 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
264
319
|
console.log(pc.dim(` Style: ${fontStyle}, Weight: ${fontWeight}`));
|
|
265
320
|
}
|
|
266
321
|
}
|
|
267
|
-
|
|
322
|
+
// Ensure 'below' captions anchor their background box at the top of the
|
|
323
|
+
// reserved area so the box does not extend upward into the device area.
|
|
324
|
+
// If a verticalAlign is already provided, respect it; otherwise default to 'top'.
|
|
325
|
+
const adjustedCaptionConfig = {
|
|
326
|
+
...captionConfig,
|
|
327
|
+
box: {
|
|
328
|
+
...(captionConfig.box || {}),
|
|
329
|
+
verticalAlign: (captionConfig.box && captionConfig.box.verticalAlign) ? captionConfig.box.verticalAlign : 'top'
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
svgText = generateCaptionSVG(captionLines, canvasWidth, captionHeight, fontSize, fontFamily, fontStyle, fontWeight, adjustedCaptionConfig.color, lineHeight, adjustedCaptionConfig, deviceConfig);
|
|
268
333
|
const captionImage = await sharp(Buffer.from(svgText))
|
|
269
334
|
.png()
|
|
270
335
|
.toBuffer();
|
|
271
336
|
composites.push({
|
|
272
337
|
input: captionImage,
|
|
273
|
-
top: 0,
|
|
338
|
+
top: Math.max(0, topMargin),
|
|
274
339
|
left: 0
|
|
275
340
|
});
|
|
341
|
+
if (options.onDebug) {
|
|
342
|
+
const bm = (deviceConfig.captionBox || captionConfig.box || {}).marginBottom || 0;
|
|
343
|
+
options.onDebug({
|
|
344
|
+
mode: 'above',
|
|
345
|
+
framePosition,
|
|
346
|
+
frameScale: deviceFrameScale,
|
|
347
|
+
deviceTop,
|
|
348
|
+
deviceBottom: deviceTop + targetDeviceHeight,
|
|
349
|
+
deviceHeight: targetDeviceHeight,
|
|
350
|
+
captionTop: Math.max(0, topMargin),
|
|
351
|
+
captionHeight,
|
|
352
|
+
marginTop: topMargin,
|
|
353
|
+
marginBottom: bm
|
|
354
|
+
});
|
|
355
|
+
}
|
|
276
356
|
}
|
|
277
357
|
catch {
|
|
278
358
|
// If text rendering fails, just add transparent area
|
|
@@ -311,19 +391,25 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
311
391
|
let availableHeight;
|
|
312
392
|
// For 'above' positioning, reduce available height by caption
|
|
313
393
|
// For 'below' positioning, we'll adjust positioning later but calculate scale normally
|
|
394
|
+
const boxConfig = deviceConfig.captionBox || captionConfig.box || {};
|
|
395
|
+
const topMargin = boxConfig.marginTop || 0;
|
|
396
|
+
const bottomMargin = boxConfig.marginBottom || 0;
|
|
314
397
|
if (captionPosition === 'above') {
|
|
315
|
-
availableHeight = Math.max(100, outputHeight - captionHeight);
|
|
398
|
+
availableHeight = Math.max(100, outputHeight - captionHeight - topMargin - bottomMargin);
|
|
316
399
|
}
|
|
317
400
|
else if (captionPosition === 'below') {
|
|
318
|
-
availableHeight = Math.max(100, outputHeight - captionHeight);
|
|
401
|
+
availableHeight = Math.max(100, outputHeight - captionHeight - bottomMargin);
|
|
319
402
|
}
|
|
320
403
|
else {
|
|
321
404
|
// 'overlay' positioning doesn't reduce available space
|
|
322
|
-
availableHeight = outputHeight;
|
|
405
|
+
availableHeight = outputHeight - bottomMargin;
|
|
323
406
|
}
|
|
324
|
-
//
|
|
407
|
+
// Respect explicit frameScale by basing scale on the full output height.
|
|
408
|
+
// This keeps device size under user control and avoids unexpected shrinking
|
|
409
|
+
// when switching caption modes. Caption placement logic will adapt if the
|
|
410
|
+
// device intrudes into reserved areas.
|
|
325
411
|
if (deviceFrameScale !== undefined) {
|
|
326
|
-
availableHeight = outputHeight;
|
|
412
|
+
availableHeight = outputHeight;
|
|
327
413
|
}
|
|
328
414
|
// Calculate scale to fit within available space while maintaining aspect ratio
|
|
329
415
|
const scaleX = availableWidth / originalFrameWidth;
|
|
@@ -458,9 +544,11 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
458
544
|
// If partial frame, crop the bottom
|
|
459
545
|
if (partialFrame) {
|
|
460
546
|
const cropHeight = Math.floor(originalFrameHeight * (1 - frameOffset / 100));
|
|
547
|
+
croppedPixels = Math.floor((originalFrameHeight - cropHeight) * scale); // Scaled cropped amount
|
|
461
548
|
if (verbose) {
|
|
462
549
|
console.log(pc.dim(` Partial frame: cropping ${frameOffset}% from bottom`));
|
|
463
550
|
console.log(pc.dim(` Crop height: ${cropHeight}px`));
|
|
551
|
+
console.log(pc.dim(` Cropped pixels (scaled): ${croppedPixels}px`));
|
|
464
552
|
}
|
|
465
553
|
try {
|
|
466
554
|
deviceComposite = await sharp(deviceComposite)
|
|
@@ -496,76 +584,182 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
496
584
|
}
|
|
497
585
|
}
|
|
498
586
|
// Recalculate position with actual caption height
|
|
587
|
+
let availableSpace = 0; // Declare here for wider scope
|
|
499
588
|
if (captionPosition === 'above') {
|
|
500
589
|
// Caption above: position device below caption area
|
|
501
590
|
if (typeof framePosition === 'number') {
|
|
502
591
|
// Custom position as percentage from top (0-100)
|
|
503
|
-
const
|
|
504
|
-
|
|
592
|
+
const boxConfig = deviceConfig.captionBox || captionConfig.box || {};
|
|
593
|
+
const topMargin = boxConfig.marginTop || 0;
|
|
594
|
+
const bottomMargin = boxConfig.marginBottom || 0;
|
|
595
|
+
availableSpace = canvasHeight - topMargin - captionHeight - bottomMargin - targetDeviceHeight;
|
|
596
|
+
// Fix for watch: if there's no available space, adjust positioning to work
|
|
597
|
+
if (availableSpace < 0 && frameMetadata.deviceType === 'watch') {
|
|
598
|
+
// For watch with negative space, position relative to full canvas minus device height
|
|
599
|
+
// This allows the watch to overlap with the caption area when needed
|
|
600
|
+
const totalAvailable = canvasHeight - bottomMargin - targetDeviceHeight;
|
|
601
|
+
if (verbose) {
|
|
602
|
+
console.log(pc.dim(' Watch positioning fix applied (overlap mode)'));
|
|
603
|
+
console.log(pc.dim(` Original available: ${availableSpace}px`));
|
|
604
|
+
console.log(pc.dim(` Using total available: ${totalAvailable}px`));
|
|
605
|
+
}
|
|
606
|
+
// Position from top of canvas, allowing overlap with caption
|
|
607
|
+
deviceTop = Math.floor(totalAvailable * (framePosition / 100));
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
// Normal positioning for other devices or when space is available
|
|
611
|
+
deviceTop = topMargin + captionHeight + Math.floor(availableSpace * (framePosition / 100));
|
|
612
|
+
if (verbose) {
|
|
613
|
+
console.log(pc.dim(' Frame positioning (above mode):'));
|
|
614
|
+
console.log(pc.dim(` Canvas height: ${canvasHeight}px`));
|
|
615
|
+
console.log(pc.dim(` Caption height: ${captionHeight}px`));
|
|
616
|
+
console.log(pc.dim(` Device height: ${targetDeviceHeight}px`));
|
|
617
|
+
console.log(pc.dim(` Top margin: ${topMargin}px`));
|
|
618
|
+
console.log(pc.dim(` Bottom margin: ${bottomMargin}px`));
|
|
619
|
+
console.log(pc.dim(` Available space: ${availableSpace}px`));
|
|
620
|
+
console.log(pc.dim(` Frame position %: ${framePosition}`));
|
|
621
|
+
console.log(pc.dim(` Calculated deviceTop: ${deviceTop}px`));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// Debug logging for watch positioning issue
|
|
625
|
+
if (verbose && frameMetadata.deviceType === 'watch') {
|
|
626
|
+
console.log(pc.dim(' Watch positioning debug:'));
|
|
627
|
+
console.log(pc.dim(` Canvas height: ${canvasHeight}px`));
|
|
628
|
+
console.log(pc.dim(` Caption height: ${captionHeight}px`));
|
|
629
|
+
console.log(pc.dim(` Target device height: ${targetDeviceHeight}px`));
|
|
630
|
+
console.log(pc.dim(` Available space: ${availableSpace}px`));
|
|
631
|
+
console.log(pc.dim(` Device top: ${deviceTop}px`));
|
|
632
|
+
}
|
|
505
633
|
}
|
|
506
634
|
else if (framePosition === 'top') {
|
|
507
|
-
|
|
635
|
+
const boxConfig = deviceConfig.captionBox || captionConfig.box || {};
|
|
636
|
+
const topMargin = boxConfig.marginTop || 0;
|
|
637
|
+
deviceTop = topMargin + captionHeight;
|
|
508
638
|
}
|
|
509
639
|
else if (framePosition === 'bottom') {
|
|
510
|
-
|
|
640
|
+
const boxConfig = deviceConfig.captionBox || captionConfig.box || {};
|
|
641
|
+
const bottomMargin = boxConfig.marginBottom || 0;
|
|
642
|
+
deviceTop = canvasHeight - bottomMargin - targetDeviceHeight;
|
|
511
643
|
}
|
|
512
644
|
else if (framePosition === 'center') {
|
|
513
645
|
// Default centered positioning
|
|
514
|
-
const
|
|
515
|
-
|
|
646
|
+
const boxConfig = deviceConfig.captionBox || captionConfig.box || {};
|
|
647
|
+
const topMargin = boxConfig.marginTop || 0;
|
|
648
|
+
const bottomMargin = boxConfig.marginBottom || 0;
|
|
649
|
+
const availableSpace2 = canvasHeight - topMargin - captionHeight - bottomMargin;
|
|
650
|
+
deviceTop = topMargin + captionHeight + Math.floor((availableSpace2 - targetDeviceHeight) / 2);
|
|
516
651
|
}
|
|
517
652
|
else {
|
|
518
|
-
|
|
519
|
-
|
|
653
|
+
const boxConfig = deviceConfig.captionBox || captionConfig.box || {};
|
|
654
|
+
const topMargin = boxConfig.marginTop || 0;
|
|
655
|
+
deviceTop = topMargin + captionHeight;
|
|
520
656
|
}
|
|
521
657
|
// Ensure device doesn't go off canvas
|
|
522
|
-
|
|
658
|
+
const boxConfig2 = deviceConfig.captionBox || captionConfig.box || {};
|
|
659
|
+
const bottomMargin2 = boxConfig2.marginBottom || 0;
|
|
660
|
+
// For watch with negative available space, allow overlap with caption
|
|
661
|
+
if (frameMetadata.deviceType === 'watch' && typeof framePosition === 'number' && availableSpace < 0) {
|
|
662
|
+
// Only clamp to canvas bounds, not caption height
|
|
663
|
+
// With partialFrame, allow device to extend beyond canvas by the cropped amount
|
|
664
|
+
const bottomAdjustment = partialFrame ? croppedPixels : 0;
|
|
665
|
+
deviceTop = Math.floor(Math.max(0, Math.min(deviceTop, canvasHeight - bottomMargin2 - targetDeviceHeight + bottomAdjustment)));
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
// Normal clamping for other devices
|
|
669
|
+
const topMargin2 = (deviceConfig.captionBox || captionConfig.box || {}).marginTop || 0;
|
|
670
|
+
// With partialFrame, allow device to extend beyond canvas by the cropped amount
|
|
671
|
+
const bottomAdjustment = partialFrame ? croppedPixels : 0;
|
|
672
|
+
deviceTop = Math.floor(Math.max(topMargin2 + captionHeight, Math.min(deviceTop, canvasHeight - bottomMargin2 - targetDeviceHeight + bottomAdjustment)));
|
|
673
|
+
}
|
|
523
674
|
}
|
|
524
675
|
else if (captionPosition === 'below') {
|
|
525
676
|
// Caption below: position device in upper area, leaving space for caption at bottom
|
|
526
|
-
const
|
|
677
|
+
const captionBoxConfig = deviceConfig.captionBox || captionConfig.box || {};
|
|
678
|
+
// Apply a sensible default gap when marginTop is undefined to meet user expectations
|
|
679
|
+
// of a small clearance between device and caption. If marginTop is explicitly set,
|
|
680
|
+
// it wins. Otherwise we default to max(12px, half border width).
|
|
681
|
+
const borderWidthForGap = (deviceConfig.captionBorder || captionConfig.border)?.width || 0;
|
|
682
|
+
const defaultBelowGap = Math.max(12, Math.round(borderWidthForGap / 2));
|
|
683
|
+
const marginTop = (captionBoxConfig.marginTop !== undefined)
|
|
684
|
+
? captionBoxConfig.marginTop
|
|
685
|
+
: defaultBelowGap;
|
|
686
|
+
const marginBottom = captionBoxConfig.marginBottom || 0;
|
|
687
|
+
// Calculate ideal positions
|
|
688
|
+
// Device area is everything except caption, marginTop gap, and marginBottom
|
|
689
|
+
const deviceAreaHeight = canvasHeight - captionHeight - marginTop - marginBottom;
|
|
690
|
+
const spaceWithMargins = deviceAreaHeight - targetDeviceHeight;
|
|
691
|
+
const maxOverlapSlack = (canvasHeight - captionHeight) - targetDeviceHeight;
|
|
692
|
+
const effectiveAvailableSpace = spaceWithMargins >= 0
|
|
693
|
+
? spaceWithMargins
|
|
694
|
+
: Math.max(0, maxOverlapSlack);
|
|
527
695
|
if (typeof framePosition === 'number') {
|
|
528
696
|
// Custom position as percentage within device area
|
|
529
|
-
|
|
530
|
-
deviceTop = Math.floor(availableSpace * (framePosition / 100));
|
|
697
|
+
deviceTop = Math.floor(effectiveAvailableSpace * (framePosition / 100));
|
|
531
698
|
}
|
|
532
699
|
else if (framePosition === 'top') {
|
|
533
700
|
deviceTop = 0;
|
|
534
701
|
}
|
|
535
702
|
else if (framePosition === 'bottom') {
|
|
536
|
-
deviceTop =
|
|
703
|
+
deviceTop = effectiveAvailableSpace;
|
|
537
704
|
}
|
|
538
705
|
else if (framePosition === 'center') {
|
|
539
706
|
// Default centered positioning in device area
|
|
540
|
-
deviceTop = Math.floor(
|
|
707
|
+
deviceTop = Math.floor(effectiveAvailableSpace / 2);
|
|
541
708
|
}
|
|
542
709
|
else {
|
|
543
710
|
// Default to centered in device area
|
|
544
|
-
deviceTop = Math.floor(
|
|
711
|
+
deviceTop = Math.floor(effectiveAvailableSpace / 2);
|
|
545
712
|
}
|
|
546
|
-
// Ensure device doesn't go off canvas
|
|
547
|
-
|
|
713
|
+
// Ensure device doesn't go off canvas
|
|
714
|
+
// With partialFrame, allow device to extend beyond canvas by the cropped amount
|
|
715
|
+
const bottomAdjustmentBelow = partialFrame ? croppedPixels : 0;
|
|
716
|
+
// Clamp device position to available space (allowing overlap slack when needed)
|
|
717
|
+
const maxDeviceTop = effectiveAvailableSpace + bottomAdjustmentBelow;
|
|
718
|
+
deviceTop = Math.floor(Math.max(0, Math.min(deviceTop, maxDeviceTop)));
|
|
548
719
|
}
|
|
549
720
|
else {
|
|
550
|
-
// 'overlay' positioning: position device
|
|
721
|
+
// 'overlay' positioning: position device using the full canvas height.
|
|
722
|
+
// Per layout invariants, overlay is bottom-anchored by the caption box;
|
|
723
|
+
// top margin should not influence device placement. Respect explicit
|
|
724
|
+
// bottom spacing only.
|
|
725
|
+
const marginTop = 0; // ignore top margin for overlay
|
|
726
|
+
const marginBottom = (deviceConfig.captionBox || captionConfig.box || {}).marginBottom || 0;
|
|
551
727
|
if (typeof framePosition === 'number') {
|
|
552
|
-
|
|
553
|
-
|
|
728
|
+
// Custom position as percentage within available space (accounting for margins)
|
|
729
|
+
const availableSpace = canvasHeight - marginTop - marginBottom - targetDeviceHeight;
|
|
730
|
+
deviceTop = marginTop + Math.floor(availableSpace * (framePosition / 100));
|
|
731
|
+
if (verbose) {
|
|
732
|
+
console.log(pc.dim(' Frame positioning (overlay mode):'));
|
|
733
|
+
console.log(pc.dim(` Canvas height: ${canvasHeight}px`));
|
|
734
|
+
console.log(pc.dim(` Device height: ${targetDeviceHeight}px`));
|
|
735
|
+
console.log(pc.dim(` Top margin: ${marginTop}px`));
|
|
736
|
+
console.log(pc.dim(` Bottom margin: ${marginBottom}px`));
|
|
737
|
+
console.log(pc.dim(` Available space: ${availableSpace}px`));
|
|
738
|
+
console.log(pc.dim(` Frame position %: ${framePosition}`));
|
|
739
|
+
console.log(pc.dim(` Calculated deviceTop: ${deviceTop}px`));
|
|
740
|
+
}
|
|
554
741
|
}
|
|
555
742
|
else if (framePosition === 'top') {
|
|
556
|
-
deviceTop =
|
|
743
|
+
deviceTop = marginTop;
|
|
557
744
|
}
|
|
558
745
|
else if (framePosition === 'bottom') {
|
|
559
|
-
|
|
746
|
+
// With partialFrame, allow device to extend beyond canvas by the cropped amount
|
|
747
|
+
const bottomAdjustment = partialFrame ? croppedPixels : 0;
|
|
748
|
+
deviceTop = canvasHeight - marginBottom - targetDeviceHeight + bottomAdjustment;
|
|
560
749
|
}
|
|
561
750
|
else if (framePosition === 'center') {
|
|
562
|
-
|
|
751
|
+
const availableSpace = canvasHeight - marginTop - marginBottom - targetDeviceHeight;
|
|
752
|
+
deviceTop = marginTop + Math.floor(availableSpace / 2);
|
|
563
753
|
}
|
|
564
754
|
else {
|
|
565
|
-
|
|
755
|
+
// Default to center
|
|
756
|
+
const availableSpace = canvasHeight - marginTop - marginBottom - targetDeviceHeight;
|
|
757
|
+
deviceTop = marginTop + Math.floor(availableSpace / 2);
|
|
566
758
|
}
|
|
567
759
|
// Ensure device doesn't go off canvas
|
|
568
|
-
|
|
760
|
+
// With partialFrame, allow device to extend beyond canvas by the cropped amount
|
|
761
|
+
const bottomAdjustmentOverlay = partialFrame ? croppedPixels : 0;
|
|
762
|
+
deviceTop = Math.floor(Math.max(0, Math.min(deviceTop, canvasHeight - marginBottom - targetDeviceHeight + bottomAdjustmentOverlay)));
|
|
569
763
|
}
|
|
570
764
|
const deviceLeft = Math.floor((canvasWidth - targetDeviceWidth) / 2);
|
|
571
765
|
if (verbose) {
|
|
@@ -640,6 +834,12 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
640
834
|
// Get caption box config
|
|
641
835
|
const captionBoxConfig = deviceConfig.captionBox || captionConfig.box || {};
|
|
642
836
|
const lineHeight = captionBoxConfig.lineHeight || 1.4;
|
|
837
|
+
const borderWidthForGap = (deviceConfig.captionBorder || captionConfig.border)?.width || 0;
|
|
838
|
+
const defaultBelowGap = Math.max(12, Math.round(borderWidthForGap / 2));
|
|
839
|
+
const marginTop = (captionBoxConfig.marginTop !== undefined)
|
|
840
|
+
? captionBoxConfig.marginTop
|
|
841
|
+
: defaultBelowGap;
|
|
842
|
+
const bottomMargin = captionBoxConfig.marginBottom || 0;
|
|
643
843
|
if (captionLines.length === 0) {
|
|
644
844
|
// Fallback if no lines were calculated
|
|
645
845
|
captionLines = [caption];
|
|
@@ -658,17 +858,40 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
658
858
|
console.log(pc.dim(` Style: ${fontStyle}, Weight: ${fontWeight}`));
|
|
659
859
|
}
|
|
660
860
|
}
|
|
661
|
-
svgText = generateCaptionSVG(captionLines, canvasWidth, captionHeight, fontSize, fontFamily, fontStyle, fontWeight, captionConfig.color, lineHeight, captionConfig);
|
|
861
|
+
svgText = generateCaptionSVG(captionLines, canvasWidth, captionHeight, fontSize, fontFamily, fontStyle, fontWeight, captionConfig.color, lineHeight, captionConfig, deviceConfig);
|
|
662
862
|
const captionImage = await sharp(Buffer.from(svgText))
|
|
663
863
|
.png()
|
|
664
864
|
.toBuffer();
|
|
665
|
-
//
|
|
666
|
-
|
|
865
|
+
// Caption positioning for 'below' mode
|
|
866
|
+
// Compute two anchors:
|
|
867
|
+
// 1) bottom-anchored top of the reserved caption area
|
|
868
|
+
// 2) just-below-device top (device bottom + marginTop)
|
|
869
|
+
// Use whichever is lower-bound safe (max), so the caption stays at the
|
|
870
|
+
// bottom under normal conditions, and moves down only if the device
|
|
871
|
+
// intrudes into the reserved area.
|
|
872
|
+
const deviceBottom = deviceTop + targetDeviceHeight; // includes watch bands
|
|
873
|
+
const bottomAnchorTop = canvasHeight - captionHeight - bottomMargin;
|
|
874
|
+
const justBelowDeviceTop = deviceBottom + marginTop;
|
|
875
|
+
const captionTop = Math.max(bottomAnchorTop, justBelowDeviceTop);
|
|
667
876
|
composites.push({
|
|
668
877
|
input: captionImage,
|
|
669
878
|
top: captionTop,
|
|
670
879
|
left: 0
|
|
671
880
|
});
|
|
881
|
+
if (options.onDebug) {
|
|
882
|
+
options.onDebug({
|
|
883
|
+
mode: 'below',
|
|
884
|
+
framePosition,
|
|
885
|
+
frameScale: deviceFrameScale,
|
|
886
|
+
deviceTop,
|
|
887
|
+
deviceBottom,
|
|
888
|
+
deviceHeight: targetDeviceHeight,
|
|
889
|
+
captionTop,
|
|
890
|
+
captionHeight,
|
|
891
|
+
marginTop,
|
|
892
|
+
marginBottom
|
|
893
|
+
});
|
|
894
|
+
}
|
|
672
895
|
}
|
|
673
896
|
catch {
|
|
674
897
|
// If text rendering fails, just add transparent area at bottom
|
|
@@ -707,16 +930,13 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
707
930
|
}
|
|
708
931
|
else {
|
|
709
932
|
// Simple fallback wrapping for overlay
|
|
710
|
-
|
|
711
|
-
if (caption.length > maxCharsPerLine) {
|
|
712
|
-
overlayLines = wrapText(caption, outputWidth, fontSize, 2); // Max 2 lines for overlay
|
|
713
|
-
}
|
|
714
|
-
else {
|
|
715
|
-
overlayLines = [caption];
|
|
716
|
-
}
|
|
933
|
+
overlayLines = wrapText(caption, wrapWidth, fontSize, 2); // Max 2 lines for overlay
|
|
717
934
|
}
|
|
718
|
-
// Position overlay text
|
|
719
|
-
|
|
935
|
+
// Position overlay text at bottom of canvas for overlay mode
|
|
936
|
+
// Use marginBottom from captionBox if available, otherwise use paddingBottom
|
|
937
|
+
const captionBoxConfig2 = deviceConfig.captionBox || captionConfig.box || {};
|
|
938
|
+
const marginBottom = captionBoxConfig2.marginBottom;
|
|
939
|
+
const totalTextHeight = overlayLines.length * fontSize * lineHeight;
|
|
720
940
|
// Use device-specific font if available, otherwise use global caption font
|
|
721
941
|
const fontToUse = deviceConfig.captionFont || captionConfig.font;
|
|
722
942
|
// Parse font name for style and weight
|
|
@@ -724,54 +944,78 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
724
944
|
const fontFamily = getFontStack(parsedFont.family);
|
|
725
945
|
const fontStyle = parsedFont.style || 'normal';
|
|
726
946
|
const fontWeight = parsedFont.weight === 'bold' ? '700' : '400';
|
|
727
|
-
|
|
728
|
-
console.log(pc.dim(' Overlay caption:'));
|
|
729
|
-
console.log(pc.dim(` Font: ${fontToUse} → ${fontFamily}`));
|
|
730
|
-
console.log(pc.dim(` Position: ${textY}px from top`));
|
|
731
|
-
}
|
|
947
|
+
// verbose logging deferred until after positioning is computed
|
|
732
948
|
// For overlay, we need custom SVG generation with background/border support
|
|
733
949
|
const svgElements = [];
|
|
734
|
-
//
|
|
735
|
-
const backgroundConfig = captionConfig.background;
|
|
736
|
-
const borderConfig = captionConfig.border;
|
|
950
|
+
// Use device-specific background/border settings if available, otherwise use global
|
|
951
|
+
const backgroundConfig = deviceConfig.captionBackground || captionConfig.background;
|
|
952
|
+
const borderConfig = deviceConfig.captionBorder || captionConfig.border;
|
|
953
|
+
// Anchor overlay by the OUTER BOX bottom (including padding and border stroke)
|
|
954
|
+
const bgPadding = backgroundConfig?.padding ?? 20;
|
|
955
|
+
const strokeWidth = borderConfig?.width ?? 0; // stroke is centered on rect; half extends outward
|
|
956
|
+
const bottomSpacing = (marginBottom ?? captionConfig.paddingBottom ?? 60); // respect explicit 0
|
|
957
|
+
const boxHeight = totalTextHeight + (bgPadding * 2);
|
|
958
|
+
const rectBottom = canvasHeight - bottomSpacing - (strokeWidth > 0 ? strokeWidth / 2 : 0);
|
|
959
|
+
const rectY = rectBottom - boxHeight; // top of the fill/border rects
|
|
960
|
+
const textY = rectY + bgPadding + fontSize; // first line baseline
|
|
737
961
|
if (backgroundConfig?.color) {
|
|
738
|
-
const
|
|
739
|
-
const bgOpacity = backgroundConfig.opacity || 0.8;
|
|
962
|
+
const bgOpacity = backgroundConfig.opacity !== undefined ? backgroundConfig.opacity : 0.8;
|
|
740
963
|
// Use full width minus margins for uniform appearance
|
|
741
|
-
const
|
|
742
|
-
const sideMargin = 30; // Margin from edges
|
|
964
|
+
const sideMargin = backgroundConfig.sideMargin ?? 30; // Margin from edges
|
|
743
965
|
const bgWidth = canvasWidth - (sideMargin * 2);
|
|
744
|
-
const bgHeight =
|
|
966
|
+
const bgHeight = boxHeight;
|
|
745
967
|
const bgX = sideMargin;
|
|
746
|
-
const bgY =
|
|
968
|
+
const bgY = rectY;
|
|
747
969
|
const bgRadius = borderConfig?.radius || 12; // Default to 12px for better visibility
|
|
748
970
|
svgElements.push(`<rect x="${bgX}" y="${bgY}" width="${bgWidth}" height="${bgHeight}" ` +
|
|
749
971
|
`fill="${backgroundConfig.color}" opacity="${bgOpacity}" rx="${bgRadius}"/>`);
|
|
750
972
|
}
|
|
751
973
|
// Add border rectangle if configured
|
|
752
974
|
if (borderConfig?.color && borderConfig?.width) {
|
|
753
|
-
const bgPadding = backgroundConfig?.padding || 20;
|
|
754
975
|
const borderWidth = borderConfig.width;
|
|
755
976
|
const borderRadius = borderConfig.radius || 12;
|
|
756
|
-
const
|
|
757
|
-
const sideMargin = 30; // Margin from edges
|
|
977
|
+
const sideMargin = backgroundConfig?.sideMargin ?? 30; // Margin from edges
|
|
758
978
|
const rectWidth = canvasWidth - (sideMargin * 2);
|
|
759
|
-
const rectHeight =
|
|
979
|
+
const rectHeight = boxHeight;
|
|
760
980
|
const rectX = sideMargin;
|
|
761
|
-
const
|
|
762
|
-
svgElements.push(`<rect x="${rectX}" y="${
|
|
981
|
+
const rectY2 = rectY;
|
|
982
|
+
svgElements.push(`<rect x="${rectX}" y="${rectY2}" width="${rectWidth}" height="${rectHeight}" ` +
|
|
763
983
|
`fill="none" stroke="${borderConfig.color}" stroke-width="${borderWidth}" rx="${borderRadius}"/>`);
|
|
764
984
|
}
|
|
985
|
+
// Now that we know final positions, emit verbose details
|
|
986
|
+
if (verbose) {
|
|
987
|
+
console.log(pc.dim(' Overlay caption:'));
|
|
988
|
+
console.log(pc.dim(` Font: ${fontToUse} → ${fontFamily}`));
|
|
989
|
+
console.log(pc.dim(` Rect: y=${rectY}, h=${boxHeight}, bottom gap=${bottomSpacing}`));
|
|
990
|
+
console.log(pc.dim(` Baseline Y: ${textY}`));
|
|
991
|
+
}
|
|
992
|
+
// Horizontal alignment for overlay
|
|
993
|
+
const align = captionConfig.align || 'center';
|
|
994
|
+
const sideMargin = backgroundConfig?.sideMargin ?? 30;
|
|
995
|
+
const pad = bgPadding;
|
|
996
|
+
const leftX = sideMargin + pad;
|
|
997
|
+
const rightX = canvasWidth - (sideMargin + pad);
|
|
998
|
+
const centerX = Math.floor(canvasWidth / 2);
|
|
999
|
+
let textAnchor = 'middle';
|
|
1000
|
+
let textX = centerX;
|
|
1001
|
+
if (align === 'left') {
|
|
1002
|
+
textAnchor = 'start';
|
|
1003
|
+
textX = leftX;
|
|
1004
|
+
}
|
|
1005
|
+
else if (align === 'right') {
|
|
1006
|
+
textAnchor = 'end';
|
|
1007
|
+
textX = rightX;
|
|
1008
|
+
}
|
|
765
1009
|
// Add text elements
|
|
766
1010
|
const textElements = overlayLines.map((line, index) => {
|
|
767
1011
|
const y = textY + (index * fontSize * lineHeight);
|
|
768
|
-
return `<text x="${
|
|
1012
|
+
return `<text x="${textX}" y="${y}" ` +
|
|
769
1013
|
`font-family="${fontFamily}" ` +
|
|
770
1014
|
`font-size="${fontSize}" ` +
|
|
771
1015
|
`font-style="${fontStyle}" ` +
|
|
772
1016
|
`font-weight="${fontWeight}" ` +
|
|
773
1017
|
`fill="${captionConfig.color}" ` +
|
|
774
|
-
`text-anchor="
|
|
1018
|
+
`text-anchor="${textAnchor}">${escapeXml(line)}</text>`;
|
|
775
1019
|
});
|
|
776
1020
|
svgElements.push(...textElements);
|
|
777
1021
|
const svgText = `<svg width="${canvasWidth}" height="${canvasHeight}" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -787,6 +1031,21 @@ export async function composeAppStoreScreenshot(options) {
|
|
|
787
1031
|
left: 0,
|
|
788
1032
|
blend: 'over'
|
|
789
1033
|
});
|
|
1034
|
+
if (options.onDebug) {
|
|
1035
|
+
options.onDebug({
|
|
1036
|
+
mode: 'overlay',
|
|
1037
|
+
framePosition,
|
|
1038
|
+
frameScale: deviceFrameScale,
|
|
1039
|
+
deviceTop,
|
|
1040
|
+
deviceBottom: deviceTop + targetDeviceHeight,
|
|
1041
|
+
deviceHeight: targetDeviceHeight,
|
|
1042
|
+
captionTop: rectY,
|
|
1043
|
+
captionHeight: boxHeight,
|
|
1044
|
+
bottomSpacing,
|
|
1045
|
+
rectY,
|
|
1046
|
+
rectBottom
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
790
1049
|
}
|
|
791
1050
|
catch {
|
|
792
1051
|
console.log('[INFO] Overlay caption rendering failed, skipping caption');
|