MrBeast text overlay & bake (HTML/CSS + canvas)

How to lay a MrBeast-style headline over the image and "bake" it into a flat PNG. Two layers: (1) a live HTML/CSS preview to tune the text in a browser, (2) a canvas bake — the same look re-rendered to pixels at the image's native resolution.

This is the implementation of the 5 overlay styles named in the skill's Text policy. The overlay is the DEFAULT delivery for headline text (zero generation credits, always legible); baking text INTO the generation is the fallback, only on an explicit ask.

What makes text "MrBeast-y"

Not one trick but a stack of six — remove any one and it falls apart.

Trait Value Why
Font Anton (or Anton SC), one weight, reads 900-heavy fat condensed grotesk — the signature
Case ALL CAPS maximum density and aggression
Stroke manual, thick (8–14% of cap size), drawn UNDER the fill separates letters from any background
Shadow hard, dark, offset down, blur shifted "sticker" depth
Color white / yellow→orange→red gradient / acid lime #D4FF3F punchy contrast, reads in the feed
Tracking tight (-0.01…-0.02em), line-height 0.9 letters lock together like a logo

Placement rules: - 2–4 words max. A headline, not a sentence. - Not on the face. Text in a free quarter (bottom / corner / side). - Large. Cap height 12–18% of frame height. When in doubt, bigger. - Margins: keep text within the frame with comfortable padding, off the extreme edges.

Layer 1 — live HTML/CSS preview

Feed the background into --bg, put the text in <h1>, tune it in a browser.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css2?family=Anton&display=swap" rel="stylesheet">
<style>
  :root {
    --bg: url('photo.jpg');   /* local path or image URL */
    --W: 1280px;              /* frame size (16:9 = 1280×720) */
    --H: 720px;
  }
  * { margin: 0; box-sizing: border-box; }
  #poster {
    position: relative;
    width: var(--W); height: var(--H);
    background: var(--bg) center/cover no-repeat;
    overflow: hidden;
    font-family: 'Anton', sans-serif;
  }
  /* text block — bottom-center, comfortable margin */
  #headline {
    position: absolute;
    left: 50%; bottom: 6%;
    transform: translateX(-50%);
    text-align: center;
    line-height: 0.9;
    letter-spacing: -0.01em;
    text-transform: uppercase;
    white-space: pre-line;         /* line breaks via \n in the text */
  }
  #headline .line {
    font-size: 120px;              /* ≈16% of 720 */
    color: #fff;
    /* CRITICAL: paint-order draws the stroke UNDER the fill — otherwise it eats the letters */
    -webkit-text-stroke: 14px #000;
    paint-order: stroke fill;
    /* hard "sticker" shadow */
    text-shadow: 0 8px 0 rgba(0,0,0,.35), 0 14px 24px rgba(0,0,0,.55);
  }
</style>
</head>
<body>
  <div id="poster">
    <div id="headline">
      <div class="line">I SPENT</div>
      <div class="line" style="color:#D4FF3F">100 DAYS</div>
    </div>
  </div>
</body>
</html>

paint-order: stroke fill is critical. Without it the stroke paints ON TOP and "eats" half the letter — the #1 bug in 90% of home-made MrBeast text.

5 proven presets (swap the .line block) — same 5 as the skill's Text policy

Beast — white + thick black stroke (default)

.line { color:#fff; -webkit-text-stroke:14px #000; paint-order:stroke fill;
        text-shadow:0 8px 0 rgba(0,0,0,.35), 0 14px 24px rgba(0,0,0,.55); }

Fire — yellow→orange→red gradient

.line { -webkit-text-stroke:14px #1a0a00; paint-order:stroke fill;
        background:linear-gradient(#FFE24B 0%, #FF9A1F 45%, #FF2E2E 100%);
        -webkit-background-clip:text; background-clip:text; color:transparent;
        filter:drop-shadow(0 0 22px rgba(255,120,0,.55)) drop-shadow(0 10px 0 rgba(0,0,0,.4)); }

Neon Lime — acid lime + glow

.line { color:#D4FF3F; -webkit-text-stroke:12px #0a1400; paint-order:stroke fill;
        text-shadow:0 0 26px rgba(180,255,40,.7), 0 10px 0 rgba(0,0,0,.4); }

Clean Glass — Inter 800 on a frosted pill

#headline { font-family:'Inter',sans-serif; }
.line { font-weight:800; color:#fff; letter-spacing:-0.02em;
        background:rgba(20,20,25,.45); backdrop-filter:blur(16px);
        padding:.12em .5em; border-radius:.2em;
        box-shadow:0 20px 60px rgba(0,0,0,.5); }

Marker — black Anton on lime line-boxes

.line { color:#0a0a0a; background:#D4FF3F; box-decoration-break:clone;
        padding:0 .18em; box-shadow:0 8px 0 rgba(0,0,0,.5); }

Layer 2 — bake to PNG (canvas)

The same look, re-rendered in canvas at native resolution — a clean PNG, zero dependencies. Important: wait for the font to load (document.fonts.load) BEFORE the first draw, or it bakes the system font.

<canvas id="c"></canvas>
<script>
async function bake({ src, lines, W, H, out='baked.png' }) {
  // 1) load Anton BEFORE drawing
  const fontLink = document.createElement('link');
  fontLink.rel = 'stylesheet';
  fontLink.href = 'https://fonts.googleapis.com/css2?family=Anton&display=swap';
  document.head.appendChild(fontLink);
  await document.fonts.load('120px "Anton"');
  await document.fonts.ready;

  // 2) background
  const img = new Image();
  img.crossOrigin = 'anonymous';        // else toDataURL fails on CORS (for URLs)
  await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = src; });

  const cv = document.getElementById('c');
  cv.width = W; cv.height = H;
  const ctx = cv.getContext('2d');
  // cover-fit the image
  const s = Math.max(W / img.width, H / img.height);
  const dw = img.width * s, dh = img.height * s;
  ctx.drawImage(img, (W - dw) / 2, (H - dh) / 2, dw, dh);

  // 3) text
  const fontPx = Math.round(H * 0.16);   // ≈16% of frame height
  const lineH  = fontPx * 0.9;
  const stroke = Math.round(fontPx * 0.11);
  ctx.font = `${fontPx}px "Anton"`;
  ctx.textAlign = 'center';
  ctx.textBaseline = 'alphabetic';
  ctx.letterSpacing = '-2px';            // tight tracking (Chrome/Edge)

  const cx = W / 2;
  const totalH = lines.length * lineH;
  let y = H * 0.94 - totalH + lineH;     // block pinned to the bottom, comfortable margin

  for (const ln of lines) {
    // hard shadow
    ctx.save();
    ctx.shadowColor = 'rgba(0,0,0,.55)';
    ctx.shadowBlur = 24; ctx.shadowOffsetY = 14;
    // paint-order: stroke FIRST
    ctx.lineWidth = stroke;
    ctx.strokeStyle = '#000';
    ctx.lineJoin = 'round';               // no sharp spikes on thick-stroke corners
    ctx.miterLimit = 2;
    ctx.strokeText(ln.text, cx, y);
    ctx.restore();

    // fill on top (white / gradient)
    if (ln.gradient) {
      const g = ctx.createLinearGradient(0, y - fontPx, 0, y);
      ln.gradient.forEach(([stop, col]) => g.addColorStop(stop, col));
      ctx.fillStyle = g;
    } else {
      ctx.fillStyle = ln.color || '#fff';
    }
    ctx.fillText(ln.text, cx, y);
    y += lineH;
  }

  // 4) export
  cv.toBlob(b => {
    const a = document.createElement('a');
    a.download = out; a.href = URL.createObjectURL(b); a.click();
  }, 'image/png');
}

// example call
bake({
  src: 'photo.jpg',
  W: 1280, H: 720,
  lines: [
    { text: 'I SPENT', color: '#fff' },
    { text: '100 DAYS', gradient: [[0,'#FFE24B'],[0.5,'#FF9A1F'],[1,'#FF2E2E']] },
  ],
});
</script>

Mechanics that are easy to forget: - strokeText before fillText — stroke under fill (the equivalent of paint-order: stroke fill). - document.fonts.load(...) + await document.fonts.ready before the first draw — else the first frame bakes the system font. - lineJoin='round' — removes sharp artifacts on the corners of a thick stroke. - img.crossOrigin='anonymous' only when the background is a URL; for a local file (<input type=file>URL.createObjectURL) it isn't needed and CORS doesn't block. - Draw the shadow inside save()/restore(), else shadowBlur bleeds onto the fill and smears the color.

4K / retina export

For a sharp PNG larger than the preview, compute everything from the image's NATIVE size, not from the preview viewport:

bake({ src: '4k_render.png', W: 3840, H: 2160, lines: [...] });

Cap size, stroke and margins are all tied to H (in percent), so at 2160px everything scales itself.

Font menu (Anton is the default — these are the overrides)

If the user asks for a different font, pick from this menu (all on Google Fonts, so they load through the same mechanism as Anton). Named font → use it; "something else / not Anton" → pick the closest fit below and say which you chose.

A — Top 3 on YouTube (punchy display, the Anton alternates)

Font Google Fonts weight Vibe / use
Bebas Neue 400 (reads bold) tall narrow ALL-CAPS — the #1 Anton alternative, maximum cap height
Oswald 600–700 condensed grotesk, a touch softer than Anton
Archivo Black 400 (ultra-bold) blocky, wide, very loud headline

B — 5 most-used bold workhorses

Font Weight Vibe / use
Montserrat 900 (Black) clean geometric sans, modern headline
Poppins 800 (ExtraBold) rounded geometric, friendly
Roboto Condensed 700 (Bold) neutral condensed workhorse
Inter 800 modern UI sans (same face as the Clean Glass preset)
Barlow Condensed 800 (ExtraBold) condensed, energetic

C — 5 aesthetic (girls' vlogs / soft-elegant)

Font Weight Vibe / use
Playfair Display 700–900 elegant high-contrast serif — classic "aesthetic" title
Cormorant Garamond 600–700 delicate refined serif, editorial feel
DM Serif Display 400 high-end display serif
Fraunces 600–900 soft "old-style" serif, trendy
Sacramento 400 (script) handwritten script — ACCENT / secondary line only, never the hero word (a script fails the ~120px legibility test as the main headline)

How to swap (keep everything else): - HTML/CSS: #headline { font-family: '<Family>', sans-serif; } and set .line { font-weight: <w>; }; update the Google-Fonts <link> to that family. - Canvas: await document.fonts.load('120px "<Family>"') and ctx.font = \ ${fontPx}px ""`. - Stroke / shadow /paint-order` / tracking stay as in the presets — only the family (and weight) change.

Stroke caveat by font class: heavy condensed faces (Anton, Bebas Neue, Oswald, Archivo Black) take the full 8–14% stroke. The delicate serifs and the script (Playfair, Cormorant, DM Serif, Fraunces, Sacramento) CLOG with a thick stroke — drop it to 3–6% (or none) and lean on a soft drop-shadow for separation instead. Sacramento (and any script) is an accent line, not the punch word.

Quick check before baking