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 y is less than the minimum, move to the minimum. This is the top margin case.
  • If its y is greater than the maximum, move to the maximum. This is the bottom margin case.
  • If its y is 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
  • hook starts 100 pixels above the band, so it moves +100.
  • price is already inside, so its move is 0.
  • legal ends exactly at 1500 and counts as safe because both smallest_nudge and fits allow a bottom equal to SAFE_BOTTOM.
  • cta ends 120 pixels past the band, so it moves -120.
  • poster is 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_nudge must match brute_force_nudge, which tries every whole-pixel move from -1920 to +1920 and keeps the valid one closest to zero.
  • Every box with a move other than None must pass fits at 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, cta occupies 1300..1500 and legal occupies 1340..1500. Those vertical ranges overlap, and the code has no x values 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: poster is 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

Popular posts from this blog

Catch Overlapping Labels Before Rendering With a Sweep Over X

Emit an isometric cube as three SVG polygons and test its shared edges

Shading an isometric box in plain JavaScript: three face tones from one light direction