<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: YanBess</title>
    <description>The latest articles on DEV Community by YanBess (@yanbess).</description>
    <link>https://dev.to/yanbess</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4044331%2Fd48e3401-2452-4052-8f59-5aee828ecc0d.jpg</url>
      <title>DEV Community: YanBess</title>
      <link>https://dev.to/yanbess</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yanbess"/>
    <language>en</language>
    <item>
      <title>I built a browser-based pixel-art &amp; animation editor with Vue, Laravel and AI</title>
      <dc:creator>YanBess</dc:creator>
      <pubDate>Thu, 23 Jul 2026 18:30:22 +0000</pubDate>
      <link>https://dev.to/yanbess/i-built-a-browser-based-pixel-art-animation-editor-with-vue-laravel-and-ai-2l58</link>
      <guid>https://dev.to/yanbess/i-built-a-browser-based-pixel-art-animation-editor-with-vue-laravel-and-ai-2l58</guid>
      <description>&lt;p&gt;I'm a solo dev, and for the past few months I've been building &lt;strong&gt;&lt;a href="https://pixanima.app" rel="noopener noreferrer"&gt;Pixanima&lt;/a&gt;&lt;/strong&gt; — a pixel-art and animation editor that runs entirely in the browser, with an optional AI assistant baked in. It just launched, and I wanted to share the parts that were technically interesting: making a general image model output &lt;em&gt;clean&lt;/em&gt; pixel art, an atomic credit system, AI inbetweening for animation, and why the whole business model falls out of one architectural fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Draw pixel art with layers, groups and effects, animate it on a frame timeline with onion-skin, and export to GIF / sprite sheets / PNG — all client-side, no install. On top of that, an AI assistant turns a text prompt into sprites, seamless tiles and palettes, re-poses characters, and generates in-between animation frames.&lt;/p&gt;

&lt;p&gt;The frontend is &lt;strong&gt;Vue 3&lt;/strong&gt; driving an HTML canvas; the backend is &lt;strong&gt;Laravel 12 (PHP 8.4)&lt;/strong&gt;. Here's what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything runs in the browser — and that decided the business model
&lt;/h2&gt;

&lt;p&gt;The entire editor is client-side. Projects live in &lt;strong&gt;IndexedDB&lt;/strong&gt;; nothing is uploaded. That's great for privacy and speed, but it has a consequence a lot of people miss: &lt;strong&gt;you cannot meaningfully gate a client-side feature.&lt;/strong&gt; If drawing, layers and export all run in the user's browser, any "pro" paywall around them is both unenforceable and, honestly, hostile to a price-sensitive hobbyist community.&lt;/p&gt;

&lt;p&gt;So I flipped it: the &lt;strong&gt;editor is 100% free, forever&lt;/strong&gt;. The only paid thing is AI — because AI is the only part with a real marginal cost, and it &lt;em&gt;requires&lt;/em&gt; a backend (which conveniently also protects the code that costs money to run). More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making a general model output clean pixel art
&lt;/h2&gt;

&lt;p&gt;The naive approach — prompt a diffusion model with "pixel art, 16 colors" — gives you &lt;em&gt;pixel-art-ish&lt;/em&gt; mush: anti-aliased edges, hundreds of colors, no real grid. Useless as an actual sprite.&lt;/p&gt;

&lt;p&gt;The fix is a post-processing pipeline. The model just produces raw input; the "pixel art" is made deterministically afterward with PHP's GD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Generate a normal image from the prompt (flux-schnell on Replicate)
2. Area-downscale to the target grid (e.g. 32×32 cells)
3. Quantize to N colors (imagetruecolortopalette) + optional ordered dithering
4. Key out the background with a tolerant corner flood-fill → transparency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the difference between "AI slop" and something you can actually drop onto a canvas and edit. The model is swappable behind an interface, so the same pipeline backs sprites, tiles and palette extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI inbetweening: the animation trick
&lt;/h2&gt;

&lt;p&gt;The feature I'm most happy with is &lt;strong&gt;AI inbetweening&lt;/strong&gt; — give it two keyframes and it generates the frames between them.&lt;/p&gt;

&lt;p&gt;Under the hood it runs Google's &lt;strong&gt;FILM&lt;/strong&gt; frame-interpolation model on the two frame PNGs, gets back an mp4, then &lt;strong&gt;ffmpeg&lt;/strong&gt; splits it into stills. Each interior frame goes through the same pixelization pass (keying out the video's black background), and the results are inserted into the timeline as real, editable frames. For an &lt;em&gt;animation&lt;/em&gt; tool, that's a genuine differentiator over "just another sprite editor."&lt;/p&gt;

&lt;h2&gt;
  
  
  The credit system: charge-then-generate, idempotent, refundable
&lt;/h2&gt;

&lt;p&gt;AI costs money per call, so credits need to be bulletproof. Two rules: never double-charge, and never charge for a failure.&lt;/p&gt;

&lt;p&gt;The ledger (&lt;code&gt;credit_transactions&lt;/code&gt;) is the source of truth; the balance on the user row is just a synced cache. Every charge is a locked, idempotent transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$reference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;whereKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;lockForUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;credit_balance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$cost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InsufficientCreditsException&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// → HTTP 402&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// idempotent on $reference: a retried webhook or AI call&lt;/span&gt;
    &lt;span class="c1"&gt;// with the same reference never charges twice&lt;/span&gt;
    &lt;span class="nc"&gt;CreditTransaction&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;firstOrCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'reference'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$reference&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'amount'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'spend'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... update the cached balance&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI endpoint does &lt;strong&gt;charge → generate → auto-refund on failure&lt;/strong&gt;. If Replicate errors or times out, the credits go straight back to the balance. The same idempotency key protects the Stripe webhook that &lt;em&gt;grants&lt;/em&gt; credits, so a replayed &lt;code&gt;checkout.session.completed&lt;/code&gt; can't credit an account twice.&lt;/p&gt;

&lt;p&gt;Writing the tests for this caught a real bug: &lt;code&gt;credit_balance&lt;/code&gt; isn't mass-assignable (correct, for security), so an &lt;code&gt;update([...])&lt;/code&gt; was silently doing nothing. Tests &amp;gt; vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monetization, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Editor:&lt;/strong&gt; free forever. Drawing/animation/export are all client-side; gating them is pointless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI:&lt;/strong&gt; prepaid credits, marked up over raw model cost to leave a real margin (payment fees, trial credits, refunds, infra, taxes and time are not free). New accounts get free credits to try it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Devs sometimes flinch at markup, but it's not greed — it's the only thing keeping the free editor sustainable. The "they earn → they pay" pressure is captured by a soft commercial-use note, not by crippling features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack, briefly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Vue 3 SPA, canvas rendering, IndexedDB persistence, Vite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Laravel 12 / PHP 8.4, Sanctum (Bearer tokens), Socialite (Google/GitHub).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI:&lt;/strong&gt; Replicate (FLUX for generation, FLUX Kontext for pose edits, FILM for interpolation), GD + ffmpeg for the pixelization/animation pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; Stripe Checkout + idempotent webhooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infra:&lt;/strong&gt; Docker stacks on a single Hetzner box behind system nginx + Cloudflare.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it / roast it
&lt;/h2&gt;

&lt;p&gt;It's live and free at &lt;strong&gt;&lt;a href="https://pixanima.app" rel="noopener noreferrer"&gt;pixanima.app&lt;/a&gt;&lt;/strong&gt; — no install, projects stay in your browser. I'd genuinely love feedback, especially on the editor feel and the AI features. What would make you actually use it over your current tool?&lt;/p&gt;

&lt;p&gt;Happy to go deeper on any part in the comments — the canvas rendering, the pixelization math, or the "free tool + paid AI" model.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>vue</category>
      <category>laravel</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
