Keep Reel Callouts in the Safe Area by Nudging Them the Smallest Distance
A callout that starts at y=1420 and is 200 pixels tall ends at y=1620, which puts it 120 pixels inside the bottom margin used in this example. You could drag it upward until it looks fine, but that can move it further than needed. Instead, you can move it only as far as needed to clear the margin.
This tutorial covers vertical placement in a 1080x1920 reel. The margins are 220 pixels at the top and 420 at the bottom, leaving a safe band from y=220 to y=1500. These are example values, so check them against the current overlays in your target app.
Why the smallest move is simple
A box no taller than the band can start at any y from SAFE_TOP to SAFE_BOTTOM - height. For the 200-pixel cta box, that range is 220 to 1300. Its current y of 1420 is greater than the maximum, so the nearest valid start is 1300, a move of -120.
Valid positions form one continuous range, so a box that fits has only three cases:
- If its
yis less than the minimum, move to the minimum. This is the top margin case. - If its
yis greater than the maximum, move to the maximum. This is the bottom margin case. - If its
yis inside the range, don't move.
A box can only cross both margins if it is taller than the band. The code rejects boxes that are too tall first, so each later branch only has to deal with one edge.
The program
from dataclasses import dataclass
from typing import Optional
REEL_HEIGHT = 1920
# Example margins; check them against the target app's current overlays.
TOP_MARGIN = 220
BOTTOM_MARGIN = 420
SAFE_TOP = TOP_MARGIN
SAFE_BOTTOM = REEL_HEIGHT - BOTTOM_MARGIN
@dataclass(frozen=True)
class Callout:
name: str
y: int
height: int
@property
def bottom(self) -> int:
return self.y + self.height
def fits(y: int, height: int) -> bool:
return y >= SAFE_TOP and y + height <= SAFE_BOTTOM
def smallest_nudge(box: Callout) -> Optional[int]:
"""Return the vertical move in pixels, or None if the box cannot fit."""
if box.height > SAFE_BOTTOM - SAFE_TOP:
return None
if box.y < SAFE_TOP:
return SAFE_TOP - box.y
if box.bottom > SAFE_BOTTOM:
return SAFE_BOTTOM - box.bottom
return 0
def brute_force_nudge(box: Callout) -> Optional[int]:
moves = range(-REEL_HEIGHT, REEL_HEIGHT + 1)
valid = (move for move in moves if fits(box.y + move, box.height))
return min(valid, key=abs, default=None)
callouts = [
Callout("hook", 120, 180),
Callout("price", 900, 160),
Callout("legal", 1340, 160),
Callout("cta", 1420, 200),
Callout("poster", 100, 1400),
]
print(f"Safe band: y={SAFE_TOP}..{SAFE_BOTTOM} of {REEL_HEIGHT}")
print(f"{'callout':<8}{'before':<12}{'after':<12}result")
moved = unchanged = unfit = 0
for box in callouts:
move = smallest_nudge(box)
assert move == brute_force_nudge(box), box.name
before = f"{box.y}..{box.bottom}"
if move is None:
overflow = box.height - (SAFE_BOTTOM - SAFE_TOP)
unfit += 1
print(f"{box.name:<8}{before:<12}{'-':<12}too tall by {overflow}px")
continue
assert fits(box.y + move, box.height), box.name
after = f"{box.y + move}..{box.bottom + move}"
if move == 0:
unchanged += 1
result = "already safe"
else:
moved += 1
result = f"moved {move:+d}px"
print(f"{box.name:<8}{before:<12}{after:<12}{result}")
print(f"Summary: {moved} moved, {unchanged} unchanged, {unfit} need resizing")
smallest_nudge returns a signed move, where a positive value moves the box down the frame because y increases downward. It returns None when no move can work.
What it printed
Safe band: y=220..1500 of 1920
callout before after result
hook 120..300 220..400 moved +100px
price 900..1060 900..1060 already safe
legal 1340..1500 1340..1500 already safe
cta 1420..1620 1300..1500 moved -120px
poster 100..1500 - too tall by 120px
Summary: 2 moved, 2 unchanged, 1 need resizing
hookstarts 100 pixels above the band, so it moves +100.priceis already inside, so its move is 0.legalends exactly at 1500 and counts as safe because bothsmallest_nudgeandfitsallow a bottom equal toSAFE_BOTTOM.ctaends 120 pixels past the band, so it moves -120.posteris 1400 pixels tall and the band is 1280, so it is reported as too tall by 120 pixels.
Where it bites
The height check has to come before the margin checks. Without it, poster would hit the y < SAFE_TOP branch and move +120 to 220..1620. That clears the top margin but pushes the box 120 pixels past the bottom of the band, which looks like a successful fix unless something checks the result.
Touching a margin also counts as safe. If you want visible padding around a nudged box, build it into the margins, because a nonzero nudge always leaves the box flush against the margin it crossed.
What I checked
The program makes two assertions:
smallest_nudgemust matchbrute_force_nudge, which tries every whole-pixel move from-1920to+1920and keeps the valid one closest to zero.- Every box with a move other than
Nonemust passfitsat its new position.
Both held in this run, since a failed assertion would have stopped the program before it printed the summary. This covers only these five boxes, so it is a check on the examples and not a proof for all inputs. The reasoning about ranges is what supports the general rule.
The brute-force search also has a limit. A box that needs a move larger than 1920 pixels would make it return None, tripping the assertion even though smallest_nudge could still return the right answer.
Where this stops being enough
- Collisions: after the nudge,
ctaoccupies1300..1500andlegaloccupies1340..1500. Those vertical ranges overlap, and the code has noxvalues or timing data, so it can't tell whether the boxes actually collide on screen. - Horizontal space: the 1080 width never enters the code, so side overlays and left or right overflow go unchecked.
- Oversized boxes:
posteris only reported. Someone still has to decide how to make it fit, such as by shrinking or splitting it. - Meaning: a callout that points at something can lose its connection to that target when it moves, even by the smallest distance.
Disclosure: I'm the founder of Homingo; Homingo sells this skill, and this article was written with AI assistance, with the code run before publishing. This is one small idea behind what we package in Isometric Explainers, our Claude Code skill: Claude Code draws how-it-works machines for your docs, blogs and landing pages, with live readouts and built-in layout, motion and geometry checks, and one command turns the same figure into a 1080x1920 explainer reel. Skill Rs 1,499 (about US$17), Complete Kit Rs 2,499 (about US$28; adds 3 more machine reel projects, the finished demo reel, 4 video lessons and a written tutorial), one-time, prices in INR, international cards accepted; it needs Claude access with Claude Code and Node.js 20.10+, Claude plans not included: Isometric Explainers. Everything above works without it.
Comments
Post a Comment