@appshoteditor/shot-dsl 0.5.1 → 0.5.2

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 CHANGED
@@ -64,7 +64,9 @@ validateTemplate(template); // { valid: true, errors: [] }
64
64
  bleeds decisively: it grows toward the 90%-of-W cap, then shifts down. It clears with the
65
65
  margin only when every ≥ 12% bleed would crop its focus band.
66
66
  - `none`: always fully visible.
67
- - `deep`: bleed ≥ 25%.
67
+ - `deep`: grows the subject toward a 25% bleed (still capped at 90% W). When growth alone can't
68
+ reach 25% (the size cap bites first), it shifts down too — but never further than a 12% bleed
69
+ needs, even though the target is 25%: it never shifts deeper just to force that target.
68
70
  3. **No side tangents.** No subject is wider than 90% of W (`NO_TANGENT.maxWidth`). The part that is
69
71
  actually on the canvas keeps ≥ 5% of W from both side edges. The only side bleed is a deliberate
70
72
  panorama straddle across a seam.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appshoteditor/shot-dsl",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "App Shot Editor layout DSL + device-frame geometry — framework-free building blocks for composing editable App Store screenshot layouts. Intended for use via a bundler (Vite, esbuild, etc.).",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/compose.ts CHANGED
@@ -295,6 +295,12 @@ export interface ComposeScreenMetrics {
295
295
  headlineSize: number;
296
296
  /** Headline box top ÷ H (identical across a set by construction). */
297
297
  headlineTop: number;
298
+ /**
299
+ * Bottom of the reserved text block (headline + subheadline + badge row) ÷ H. For `text-bottom`
300
+ * layouts the text block sits near the canvas bottom (not the top), so `top − textBottom` (the
301
+ * subject's top minus this) isn't a meaningful "gap under the text" for that layout.
302
+ */
303
+ textBottom: number;
298
304
  tilt: number;
299
305
  /** Horizontal centre ÷ W (≠ 0.5 for a straddling panorama device). */
300
306
  centerX: number;
@@ -2023,6 +2029,7 @@ export function composeSet(plan: ComposePlan): { template: Template; report: Com
2023
2029
  tangent: inTangentZone(overshoot, p.boxHeight, H),
2024
2030
  headlineSize: typo.headlineSize / W,
2025
2031
  headlineTop: g.headlineTop / H,
2032
+ textBottom: (g.areaTop + typo.textArea) / H,
2026
2033
  tilt: p.angle,
2027
2034
  centerX: cx / W,
2028
2035
  role: r.role,
@@ -17,7 +17,11 @@ export const NO_TANGENT = {
17
17
  clearGap: 0.04,
18
18
  /** A subject that bleeds must lose at least this × its own (rotated) height. */
19
19
  minBleed: 0.12,
20
- /** `bleed: "deep"` target, × the subject's height. */
20
+ /**
21
+ * `bleed: "deep"` target, × the subject's height. Reached by growing the subject (up to
22
+ * `maxWidth`·W) — never by shifting it down past a decisive (`minBleed`) overshoot, since a
23
+ * further down-shift would just be empty space between the text and the subject.
24
+ */
21
25
  deepBleed: 0.25,
22
26
  /** The focus band must end at least this × H inside the canvas edge. */
23
27
  focusSafe: 0.02,
@@ -207,10 +211,15 @@ export interface VerticalResult {
207
211
  * ≥ `clearGap`·H or bleeds by ≥ `minBleed` of the subject's height — never in between — and never
208
212
  * pushes the focus band off-canvas. Options:
209
213
  * - CLEAR: keep the near edge, shrink until the far edge clears with the margin.
210
- * - BLEED: grow (up to `maxWidth`·W), then shift toward the far edge, until the overshoot reaches
211
- * the target; if that hides the focus band, the largest focus-safe bleed ≥ `minBleed` is used,
212
- * else the bleed option is unavailable.
213
- * `none` → always clear. `deep` → bleed ≥ `deepBleed` (or the most the focus allows), else clear.
214
+ * - BLEED: grow (up to its size cap) toward the target overshoot; only when growth alone can't get
215
+ * there (the size cap bites first) does the subject shift toward the far edge, and then only far
216
+ * enough for a decisive (`minBleed`) overshoot — never further just to reach a deeper target, which
217
+ * would otherwise leave empty space between the text and the subject. If the result hides the focus
218
+ * band, the placement instead targets the smallest decisive bleed (`minBleed`) that stays focus-safe
219
+ * — or near0 itself, when near0 already sits further down than that — else the bleed option is
220
+ * unavailable.
221
+ * `none` → always clear. `deep` → grow toward `deepBleed` (≥ `minBleed` at the width cap, ≥ `deepBleed`
222
+ * whenever growth alone reaches it), or the most the focus allows, else clear.
214
223
  * `auto` → keep the natural placement when it already clears by the margin or is a focus-safe
215
224
  * decisive bleed; otherwise (the tangent zone) PREFER the bleed option, and fall back to clear only
216
225
  * when every ≥ `minBleed` bleed would crop the focus band. `preferBleed` (tilt) also turns a natural
@@ -240,24 +249,36 @@ export function solveVertical(inp: VerticalInput): VerticalResult {
240
249
  return make(s, near0, s < s0 - EPS ? 'shrunk to clear the edge with a margin' : 'clears the edge');
241
250
  };
242
251
 
243
- /** Smallest move reaching overshoot ≥ t·height, then made focus-safe (null if impossible). */
252
+ /**
253
+ * Smallest move reaching overshoot ≥ t·height, then made focus-safe (null if impossible). Growing
254
+ * the subject (up to its size cap — the width cap, or a tighter `scaleCap` when one binds) is
255
+ * always tried first. Only when that growth is capped short of t does the subject shift toward the
256
+ * far edge — and even then, only far enough for a decisive (`minBleed`) overshoot, never all the
257
+ * way to t: a deeper target like `deepBleed` is something to grow toward, not something worth
258
+ * shifting the subject away from the text to force.
259
+ */
244
260
  const bleedOption = (t: number): VerticalResult | null => {
245
261
  let s = s0;
246
262
  let a = near0;
247
263
  if (overshoot(s, a) < t * s * eh - EPS) {
248
264
  const need = (H - a) / ((1 - t) * eh);
249
265
  s = Math.max(s0, Math.min(need, Math.max(sCap, s0)));
250
- if (overshoot(s, a) < t * s * eh - EPS) a = H - s * eh * (1 - t);
266
+ if (overshoot(s, a) < NO_TANGENT.minBleed * s * eh - EPS) a = H - s * eh * (1 - NO_TANGENT.minBleed);
251
267
  }
252
- if (focusOk(s, a)) return make(s, a, `bleeds ${Math.round(t * 100)}%+`);
268
+ const reason =
269
+ overshoot(s, a) >= t * s * eh - EPS
270
+ ? `bleeds ${Math.round(t * 100)}%+`
271
+ : `grows to its size cap (no gap forced to reach ${Math.round(t * 100)}%+)`;
272
+ if (focusOk(s, a)) return make(s, a, reason);
253
273
  // Focus-constrained: keep the scale (≤ what the focus allows at near0) and bleed only as deep
254
- // as the focus band allows — accepted if that is still a decisive (≥ minBleed) bleed.
274
+ // as the focus band allows — accepted if that is still a decisive (≥ minBleed) bleed. The shift
275
+ // itself only ever targets minBleed (never t), for the same no-gap reason as above.
255
276
  if (inp.focusReach == null) return null;
256
277
  const fr = inp.focusReach;
257
278
  const sF = Math.min(s, Math.max(sCap, s0), (focusLimit - near0) / fr);
258
279
  const denom = eh * (1 - NO_TANGENT.minBleed) - fr;
259
280
  if (!(sF > 0) || denom <= 0 || sF < (H - focusLimit) / denom - EPS) return null;
260
- const aF = Math.max(near0, Math.min(focusLimit - sF * fr, H - sF * eh * (1 - t)));
281
+ const aF = Math.max(near0, Math.min(focusLimit - sF * fr, H - sF * eh * (1 - NO_TANGENT.minBleed)));
261
282
  if (overshoot(sF, aF) < NO_TANGENT.minBleed * sF * eh - EPS || !focusOk(sF, aF)) return null;
262
283
  return make(sF, aF, 'bleed reduced to keep the focus band visible');
263
284
  };