DEV Community

Cover image for How TikTok’s Safety Bot Forced Me to Optimize My JS Canvas Engine
Jason Bann
Jason Bann

Posted on

How TikTok’s Safety Bot Forced Me to Optimize My JS Canvas Engine

If you’ve ever tried marketing an indie game on short-form video platforms, you already know the golden rule: Make it flashy, make it fast, and make it pop. I’m currently building an in-browser sci-fi defense game called Divine Orbit. You play as an AI defending a planet from cosmic threats using orbital superweapons. Because it’s a web game, I wanted to prove that an HTML5 could produce chaotic, massive, cinematic explosions that rival desktop games.

So, I cranked the visual juice to the absolute max. I wanted every nuke and meteor impact to feel devastating.

The result? My gameplay clips looked awesome.
The problem? TikTok’s algorithm shadowbanned my account to exactly 0 views.

Here is how an automated safety bot forced me to rethink my rendering path, ditch my DOM animations, and build a high-performance, zero-allocation volumetric particle engine.

The Problem: Accidentally Coding a Flashbang
In my pursuit of "flashy," I made a classic visual effects mistake. I was relying heavily on two things to make my explosions look powerful:

CSS/DOM-based Shockwaves: Every time a meteor hit, I was pushing a new object into a React state array (setMeteorExplosions) that rendered a rapidly expanding, stroked HTML

ring over the canvas.

Screen Blending: Inside the actual canvas particle system, I was rendering hundreds of particles using ctx.globalCompositeOperation = 'screen'.

If you know how screen blending works, you already know where this is going. When 50 red and gold particles overlap in the exact same coordinate space, the colors compound. It doesn’t look red anymore; it blows out into pure, blinding #FFFFFF white.

Add in the rapidly expanding CSS rings on a pitch-black space background, and my game wasn’t just flashy—it was a literal strobe light. TikTok’s automated trust and safety bots flagged the high-frequency contrast shifts as an epilepsy hazard, and silently dropped my reach to zero.

The Fix: Volumetric Plasma & Zero-Allocation
I couldn't just turn down the opacity; it made the game look weak. I needed the explosions to feel heavy and cinematic without triggering the strobe filters.

I completely gutted the visual pipeline and made three major architectural changes:

  1. Killing the React DOM Rings
    I was making React do layout recalculations in the middle of heavy combat just to render CSS circles. I completely deleted the setMeteorExplosions state and moved 100% of the explosion logic directly into the engine. No more sharp, high-contrast geometric outlines—just soft pixels.

  2. Switching to 'Source-Over' Volumetrics
    I stripped out the screen blending. Instead of letting colors compound into pure white light, I locked the context to source-over and hard-coded warm, capped-opacity gradients.

  3. The Zero-Allocation Pool
    To handle the increased load of drawing all these soft-body particles on the canvas, I used a pre-allocated object pool. Instead of creating and garbage-collecting hundreds of objects per frame, I recycle them:

JavaScript
// The TikTok-Safe Volumetric Render Loop
ctx.globalCompositeOperation = 'source-over'; // No more blinding screen blends!

const partLen = particlePool.length;
for (let i = 0; i < partLen; i++) {
const p = particlePool[i];
if (!p.active) continue;

p.life -= delta * 5.0; 
if (p.life <= 0) { p.active = false; continue; }

p.size += ((p.maxSize) - p.size) * (delta * 25);
const currentRadius = Math.max(1, p.size);

// THE GLOWING GAS CLOUD (Soft Filled, Capped Opacity)
const cachedSprite = spriteCacheRef.current[p.color];
if (cachedSprite) {
    ctx.globalAlpha = Math.max(0, p.life * 0.7); // Never hits 100%
    ctx.drawImage(cachedSprite, p.x - currentRadius, p.y - currentRadius, currentRadius * 2, currentRadius * 2);
} else {
    ctx.globalAlpha = Math.max(0, p.life * 0.6);
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x, p.y, currentRadius, 0, Math.PI * 2);
    ctx.fill();
}

// THE SEARING CORE (Warm & Muted)
// Removed pure white. Uses a warm orange/yellow that fades out instantly.
ctx.globalAlpha = Math.max(0, (p.life - 0.4) * 0.5); 
ctx.beginPath();
ctx.arc(p.x, p.y, currentRadius * 0.35, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(250, 200, 100, 0.8)'; 
ctx.fill();

}
ctx.globalAlpha = 1.0;
The Result
The irony of this whole nightmare is that the TikTok algorithm was actually right.

By forcing me to remove the cheap strobe effects and screen blending, the game looks objectively better. The explosions now look like heavy, billowing cinematic plasma instead of a neon geometry rave.

More importantly, by migrating everything away from React state updates and strictly into a zero-allocation canvas pool, my frame times stabilized drastically during late-game waves when the screen is covered in orbital strikes.

If you are building browser games, be very careful with screen or lighter composite operations on dark backgrounds. What looks like a cool laser blast to you might look like a liability to a social media algorithm.

If you want to see the new optimized engine in action, you can play the live web build right now on VibeAxis.

And if you hate the idea of Earth being destroyed by giant rocks, consider dropping a Wishlist for Divine Orbit on Steam!

Top comments (0)