explainx.ainewsletter3.5k
TrendingNewsPathwaysSkills
Pricing
explainx.ai

Upskill in AI — 16 free pathways, live workshops & bootcamps, and 50+ courses from practitioners. Plus the skills, tools, and MCP servers to practice on.

follow us

corporate training

support@explainx.ai

get started

Find your pathTake Free Evaluation

learn

pathways — start freeworkshopsbootcampscoursescertificationsmock testsexplainx universitycorporate traininglearn skills & mcp

discover

skillsmcp serversexplainx mcptoolsagentsllmsdesignsdictionaryagi trackerranks

company

aboutvisionmissionteaminstructorscommunityhackathonscareers

content

daily AI newsstate of AI — live resultsblogreleasespromptsgeneratorsresource libraryfor LLMsexplainx.ai kids

solutions

all solutionsdeveloper upskillingmarketing upskillingproduct manager upskillingleadership upskilling

newsletter · weekly

Get AI news, tools, and insights in your inbox.

supportcontactprivacytermsdata rightshow we create contentsubmission guidelines

© 2026 AISOLO Technologies Pvt Ltd

On this page

  • TL;DR: two ways to build it
  • What the effect actually is
  • The accurate version: canvas line-halftone
  • The fast approximation: SVG filter, no pixel reading
  • Getting an AI coding agent to build it for you
  • When you'd actually reach for a GPU shader instead
  • Related reading
← Back to blog

explainx / blog

How to Build the Viral "Ink Bleed" Portrait Effect (Canvas + CSS)

Tibo posted a scanline "ink bleed" portrait made with Codex and it went viral. Here's how to build the same line-halftone effect yourself with a canvas script, plus a lighter CSS/SVG-filter version.

Aug 17, 2026·6 min read·Yash Thakker
Creative CodingCSSCanvasCodexGenerative Art
go deep
How to Build the Viral "Ink Bleed" Portrait Effect (Canvas + CSS)

Tibo, who leads Codex and ChatGPT at OpenAI, posted a portrait rendered entirely as broken horizontal ink lines on August 17, 2026 — captioned simply "courtesy of Codex." It's a striking effect: from a distance it reads as a photograph, up close it's just ragged black bars of varying length, like a fax machine trying to print a face. Designer Jacob Miller (@pwnies, the developer behind diffui.ai) replied asking whether anyone had "figured out a good way to do this paper ink bleed effect with shaders / CSS" — then posted his own version of the effect applied to a colleague's photo a few hours later.

That's a genuinely fun creative-coding problem, and a good one to actually solve rather than just admire. Here's a working implementation.

Weekly digest3.5k readers

Catch up on AI

Curated AI updates on agents, skills, and MCP — delivered to your inbox. Unsubscribe anytime.

TL;DR: two ways to build it

table · 3 cols
ApproachBest forEffort
Canvas line-halftone scriptAccurate reproduction — bar length/thickness actually tracks image luminance~40 lines of JavaScript, runs once per image
CSS/SVG filter approximationFast, no pixel reading, works as a live filter on any elementA few lines of SVG <filter>, applied via filter: url(#id)
Does it need a GPU shader?Only for real-time video/streamsOptional — see the WebGL note at the end
Can an AI coding agent build this from a prompt?Yes — it's exactly the kind of visual, self-checkable task Codex, Claude Code, or Cursor handle wellOne well-scoped prompt, described below

What the effect actually is

Look closely at the viral image: it isn't dots (a classic halftone), it's horizontal bars. Each scanline of the source image gets converted into a row of black bars — longer and thicker where the source pixels are darker, shorter and thinner where they're lighter — and the bar edges are jittered randomly so they look torn rather than crisp. That jitter is what makes it read as "ink bleed" instead of a clean technical scan.

That's three ingredients:

  1. Row sampling — walk the source image row by row (or every N pixels for a coarser, chunkier look).
  2. Luminance-to-length mapping — convert each sampled pixel's brightness into a bar length.
  3. Edge jitter — randomize bar length and spacing slightly so edges look bled rather than measured.

The accurate version: canvas line-halftone

This runs in any browser, reads the source image via getImageData, and draws the result to a second canvas:

javascript
async function inkBleedPortrait(imageUrl, canvas, options = {}) {
  const {
    rowHeight = 4,      // vertical spacing between scanlines
    maxBarLength = 26,  // longest possible bar, in pixels
    jitter = 0.5,       // 0 = perfectly clean, 1 = maximally torn
    threshold = 0.12,   // ignore near-white pixels entirely
  } = options;

  const img = await new Promise((resolve, reject) => {
    const el = new Image();
    el.crossOrigin = 'anonymous';
    el.onload = () => resolve(el);
    el.onerror = reject;
    el.src = imageUrl;
  });

  // Read the source into an offscreen canvas so we can sample pixels.
  const source = document.createElement('canvas');
  source.width = img.width;
  source.height = img.height;
  const sctx = source.getContext('2d');
  sctx.drawImage(img, 0, 0);
  const { data } = sctx.getImageData(0, 0, img.width, img.height);

  canvas.width = img.width;
  canvas.height = img.height;
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = '#111111';

  for (let y = 0; y < img.height; y += rowHeight) {
    let x = 0;
    while (x < img.width) {
      const i = (y * img.width + x) * 4;
      const luminance = (0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]) / 255;
      const darkness = 1 - luminance;

      if (darkness > threshold) {
        const jitterFactor = 1 - jitter + Math.random() * jitter;
        const barLength = Math.max(2, darkness * maxBarLength * jitterFactor);
        const barHeight = rowHeight * (0.35 + darkness * 0.65);
        ctx.fillRect(x, y + (rowHeight - barHeight) / 2, barLength, barHeight);
        x += barLength + Math.random() * 3; // gap before the next segment
      } else {
        x += 4; // skip light areas faster — no bar drawn
      }
    }
  }
}

// Usage:
// const canvas = document.querySelector('#output');
// inkBleedPortrait('/portrait.jpg', canvas, { rowHeight: 3, maxBarLength: 20 });

Tune it from there: lower rowHeight (down to 2px) for finer detail closer to the viral example; raise jitter toward 1 for a rougher, more distressed look; raise threshold if midtones are producing too much noise in flat background areas.

The fast approximation: SVG filter, no pixel reading

If you don't need the bar lengths to track the actual image content — just the torn, ink-bled texture applied over an existing image or shape — an SVG filter gets most of the visual character for free, and it works as a live CSS filter on any element:

html
<svg style="position: absolute; width: 0; height: 0;">
  <filter id="ink-bleed">
    <feTurbulence type="fractalNoise" baseFrequency="0.01 0.9" numOctaves="2" result="noise" />
    <feDisplacementMap in="SourceGraphic" in2="noise" scale="8" />
    <feColorMatrix type="matrix" values="0 0 0 0 0.07  0 0 0 0 0.07  0 0 0 0 0.07  0 0 0 1 0" />
  </filter>
</svg>

<img src="/portrait.jpg" style="filter: url(#ink-bleed);" />

The baseFrequency="0.01 0.9" is doing the work here — a low horizontal frequency and a high vertical one stretches the turbulence noise into the same horizontal-streak character as the canvas version, without ever reading a single pixel value. It's a good starting point; for a closer match to a genuine line-halftone, layer it over horizontal repeating-linear-gradient stripes with mix-blend-mode: multiply. This general SVG-filter technique for ink-style distortion is documented in more depth in Andy Jakubowski's ink bleed tutorial and Carmen Ansio's browser halftone writeup, both worth reading if you want to go deeper than this guide.

Getting an AI coding agent to build it for you

Tibo's post credits Codex directly, and that's not surprising — this is exactly the kind of task coding agents are good at: a self-contained visual algorithm you can verify by looking at the output, with no ambiguity about whether it "worked." A prompt that gets you most of the way there in one pass:

Write a JavaScript function that loads an image onto a canvas, reads its pixel data, and re-renders it as horizontal black bars: for each scanline, sample luminance across the row and draw bar segments whose length and thickness scale with darkness. Add randomized jitter to bar length and spacing so edges look torn/ink-bled rather than clean. Skip near-white pixels entirely. Expose rowHeight, maxBarLength, jitter, and threshold as tunable parameters.

That's close to the exact prompt structure behind the "three ingredients" breakdown earlier in this post — row sampling, luminance mapping, edge jitter — because naming the algorithm's actual steps, not just describing the desired look, is what keeps a coding agent from guessing at implementation details you'd have to fix by hand anyway. explainx.ai's loop engineering guide covers this pattern in more depth: give the agent a tight, checkable spec and let it iterate against the visual result rather than trying to describe the destination and hoping it fills in the mechanism correctly.

When you'd actually reach for a GPU shader instead

Everything above runs once per static image, which is all the viral post needed. If you want this effect live on video, a webcam feed, or applied per-frame in a generative art piece, move the luminance-to-bar logic into a GLSL fragment shader: sample the source texture at each scanline's y-coordinate, compute darkness the same way, and use a step() function against a per-row noise value (from a hash or simplex noise function) to decide bar presence — the same three ingredients, just running per-pixel on the GPU instead of per-row on the CPU. That's overkill for a single portrait, but it's the natural next step if this effect becomes part of a live tool rather than a one-off render.

Related reading

  • Loop Engineering for Coding Agents: The Complete Guide
  • Anatomy Atelier: A Solo Dev Built 3D Anatomy With Codex and TripoAI
  • Figma Config 2026 Recap: Motion, Code, and Shaders in Design Tools
  • AI Aesthetic Design Patterns
  • Top AI Prompts for Design
  • Best AI Coding Subscription Under $20 (ChatGPT Plus vs. Claude Pro)

Further reading on the underlying technique: Ink bleed effect with SVG filters — Andy Jakubowski · Halftone effect in the browser — Carmen Ansio

Code samples in this post are an original implementation inspired by the publicly posted effect, not a reproduction of Codex's or diffui's actual source — both remain unpublished as of August 17, 2026.

Spotted something out of date? Let us know.
Yash Thakker

Written by

Yash Thakker

Yash is an AI expert with over 300K learners. Join his workshops →

Related posts

Aug 22, 2026

He Couldn't Upload Music to X, So He Vibe-Coded a Music App

Andrew Ambrosino wanted to share his album on X. Instead of fighting Twitter's audio-upload limits, he prompted a hosted music-player app into existence on ChatGPT Sites in minutes. explainx.ai breaks down why that choice — build over workaround — is the real story, plus what ChatGPT Sites requires to try it yourself.

Aug 22, 2026

OpenAI Cuts GPT-5.6 Sol API Pricing Over 20% for 3 Months

On August 22, 2026, @OpenAI announced it is dropping GPT-5.6 Sol's API and credit pricing over 20% for the next three months — a real, official cut, not the OpenRouter promo covered here days earlier. It applies to the API and ChatGPT Work/Codex credits; Pro, Plus, and Business subscription usage is unchanged. Here's the exact new pricing and the rate-limit skepticism already pushing back on it.

Aug 21, 2026

ChatGPT Can Read and Send Apple Messages on Mac — What to Know Before You Enable It

OpenAI's August 20, 2026 Apple Messages plugin lets ChatGPT Work and Codex on Apple silicon Macs search, summarize, draft, and send iMessage, SMS, and RCS texts — after you grant Full Disk Access and approve each send by default. The dystopian framing on social media overshoots the opt-in mechanics, but the underlying risk is real: an agent with read access to your entire message history and standing permission to text as you.