Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ pnpm-debug.log*
*.psd
.source
.claude/

# Gumroad pack — large binaries, regen via scripts/generate-gumroad-*.mjs
/pixabots-gumroad/pixabots-pack/
/pixabots-gumroad/upload/zips/
/pixabots-gumroad/upload/gallery/
/pixabots-gumroad/upload/thumbnail.gif
/pixabots-gumroad/campaign/social/
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ When reviewing code, use the Before/After markdown table format from the skill.
- [x] **Cache header revalidation.** `Cache-Control: immutable` was wrong once PR #156 made same-ID sprite art mutable. Swapped for 1-day fresh + 7-day stale-while-revalidate — art updates propagate within ~1 day without a manual dashboard purge. Manual purge still works for urgent rollout. (PR #166, #168, #169)
- [x] **Animated `cheeky-terminal` (16-frame sequence) + `terminal-round` + `glasses` (blink).** Upgraded three previously-static eye parts to animated. `cheeky-terminal` is the first 16-frame sequence (fills one full super-loop). `ANIM_VERSION` 1→2 to bust CDN caches. Parts.ts docstring updated to include N=16 as valid sequence length.
- [x] **Client PNG download baked full sprite sheet.** Creator `download()` used 5-arg `drawImage(img, 0, 0, size, size)` which stretched whole sheet — animated faces showed both blink frames side-by-side. Switched to 9-arg form with source rect `(0, 0, NATIVE, NATIVE)` so only frame 0 is sampled. Works for every layer (eyes/heads/body/top), every kind (static/blink/sequence). (PR #175)
- [x] **Perf — lazy-load Inspector + CommandPalette, swap `setInterval` for `requestAnimationFrame`.** Inspector pulled `react-colorful` (~16KB chunk) into main bundle even though only used when FX panel opens — now `next/dynamic`. CommandPalette was mounted in root layout for every page — also dynamic. Creator animation loop ran `setInterval(FRAME_MS)` regardless of tab visibility (battery drain on hidden tabs); swapped for rAF with FRAME_MS throttle. Build chunks now show Inspector + react-colorful as a separate split.
- [x] **Perf — lazy-load Inspector + CommandPalette, swap `setInterval` for `requestAnimationFrame`.** Inspector pulled `react-colorful` (~16KB chunk) into main bundle even though only used when FX panel opens — now `next/dynamic`. CommandPalette was mounted in root layout for every page — also dynamic. Creator animation loop ran `setInterval(FRAME_MS)` regardless of tab visibility (battery drain on hidden tabs); swapped for rAF with FRAME_MS throttle. Build chunks now show Inspector + react-colorful as a separate split. (PR #176)
- [x] **Gumroad free pack launch.** Shipped a 2,000-pixabot Gumroad pack ([pablostanley.gumroad.com/l/pixabots](https://pablostanley.gumroad.com/l/pixabots)) — 4 zip variants (all formats / PNG / GIF / WebP). New scripts in `scripts/` regenerate the pack + 6 animated 16:9 banner GIFs + 5 social-size variants + animated square thumbnail. Campaign HTML dashboard at `pixabots-gumroad/campaign/index.html` with click-to-copy titles, captions, tags, and variant Name/Description pairs. Site nav gets a new "pack" entry — opens an in-app `PackDialog` modal (hero image + CTA to Gumroad) rather than navigating away.

## Improve GIFs / Animated API

Expand Down
2 changes: 2 additions & 0 deletions app/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SITE_URL, TOTAL_COMBOS_LABEL } from "@/lib/constants";
import "./globals.css";

const CommandPalette = dynamic(() => import("@/components/command-palette").then((m) => m.CommandPalette));
const PackDialog = dynamic(() => import("@/components/pack-dialog").then((m) => m.PackDialog));

const pixelify = Pixelify_Sans({
subsets: ["latin"],
Expand Down Expand Up @@ -105,6 +106,7 @@ export default function RootLayout({
<SiteFooter />
<ShortcutsOverlay />
<CommandPalette />
<PackDialog />
</RootProvider>
<Analytics />
<SpeedInsights />
Expand Down
38 changes: 29 additions & 9 deletions app/src/components/header-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@ import {
import { PixelIcon } from "@/components/ui/pixel-icon";
import { useSfx } from "@/lib/use-sfx";
import { useTheme } from "@/lib/use-theme";
import { openPackDialog } from "@/components/pack-dialog";

type Item = { href: string; label: string; external?: boolean };
type Item =
| { kind: "internal"; href: string; label: string }
| { kind: "external"; href: string; label: string }
| { kind: "pack"; label: string };

const ITEMS: readonly Item[] = [
{ href: "/", label: "Create" },
{ href: "/browse", label: "Browse" },
{ href: "/favorites", label: "Stars" },
{ href: "/docs", label: "Docs" },
{ href: "/docs/api", label: "API" },
{ href: "https://github.com/pablostanley/pixabots", label: "GitHub", external: true },
{ kind: "internal", href: "/", label: "Create" },
{ kind: "internal", href: "/browse", label: "Browse" },
{ kind: "internal", href: "/favorites", label: "Stars" },
{ kind: "internal", href: "/docs", label: "Docs" },
{ kind: "internal", href: "/docs/api", label: "API" },
{ kind: "pack", label: "Pack" },
{ kind: "external", href: "https://github.com/pablostanley/pixabots", label: "GitHub" },
];

/**
Expand Down Expand Up @@ -49,13 +54,28 @@ export function HeaderMenu() {
<DialogTitle className="text-sm uppercase tracking-wide text-muted-foreground">Menu</DialogTitle>
<nav className="flex flex-col mt-3">
{ITEMS.map((item) => {
const active = !item.external && pathname === item.href;
const active = item.kind === "internal" && pathname === item.href;
const base =
"flex items-center py-3 px-2 border-b border-border text-base transition-colors";
const color = active
? "text-foreground font-medium"
: "text-muted-foreground hover:text-foreground";
if (item.external) {
if (item.kind === "pack") {
return (
<button
key="pack"
type="button"
className={`${base} ${color} text-left cursor-pointer`}
onClick={() => {
setOpen(false);
openPackDialog();
}}
>
{item.label}
</button>
);
}
if (item.kind === "external") {
return (
<a
key={item.href}
Expand Down
93 changes: 93 additions & 0 deletions app/src/components/pack-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use client";

import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";

const EVENT = "open-pack-dialog";

export function openPackDialog() {
window.dispatchEvent(new Event(EVENT));
}

/**
* Mounted once in the root layout. Any header button dispatches the
* `open-pack-dialog` event via `openPackDialog()` and the modal opens
* in place — no prop drilling, no global store.
*/
export function PackDialog() {
const [open, setOpen] = useState(false);

useEffect(() => {
const handler = () => setOpen(true);
window.addEventListener(EVENT, handler);
return () => window.removeEventListener(EVENT, handler);
}, []);

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogTitle className="text-base uppercase tracking-wide text-muted-foreground">
Free pack
</DialogTitle>
<DialogDescription className="sr-only">
2,000 pixel characters as PNG, GIF, and WebP.
</DialogDescription>

<div className="mt-2 flex flex-col gap-4">
<a
href="https://pablostanley.gumroad.com/l/pixabots"
target="_blank"
rel="noopener noreferrer"
className="block overflow-hidden border border-border bg-muted"
>
<img

Check warning on line 48 in app/src/components/pack-dialog.tsx

View workflow job for this annotation

GitHub Actions / check

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src="https://public-files.gumroad.com/gxs3g6uqux6013yvj6qs7v37jie3"
alt="Pixabots free pack"
className="block w-full h-auto"
style={{ imageRendering: "pixelated" }}
/>
</a>

<div className="text-sm leading-relaxed text-foreground">
<p className="text-lg font-semibold mb-2">2,000 pixel characters. Free.</p>
<p className="text-muted-foreground">
A pack of 2,000 curated pixabots ready to drop into anything &mdash; PNGs at
four sizes (240/480/960/1920), animated GIFs, and animated WebPs.
Personal and commercial use.
</p>
</div>

<ul className="text-sm text-muted-foreground flex flex-col gap-1">
<li>2,000 unique characters</li>
<li>12,000 files total</li>
<li>PNG · GIF · WebP</li>
<li>Free for personal + commercial use</li>
</ul>

<div className="flex flex-col sm:flex-row gap-2 mt-2">
<a
href="https://pablostanley.gumroad.com/l/pixabots"
target="_blank"
rel="noopener noreferrer"
className="flex-1 inline-flex items-center justify-center h-11 px-4 bg-foreground text-background font-medium hover:opacity-90 transition-opacity"
>
Get the pack on Gumroad &rarr;
</a>
<button
type="button"
onClick={() => setOpen(false)}
className="sm:flex-none inline-flex items-center justify-center h-11 px-4 border border-border hover:bg-muted transition-colors cursor-pointer text-sm"
>
Maybe later
</button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
8 changes: 8 additions & 0 deletions app/src/components/site-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { usePathname } from "next/navigation";
import { Button } from "@/components/ui/button";
import { PixelIcon } from "@/components/ui/pixel-icon";
import { HeaderMenu } from "@/components/header-menu";
import { openPackDialog } from "@/components/pack-dialog";
import { hasModifier, isTypingTarget, useKeydown } from "@/lib/use-keydown";
import { useScrollDirection } from "@/lib/use-scroll-direction";
import { useTheme } from "@/lib/use-theme";
Expand Down Expand Up @@ -77,6 +78,13 @@ export function SiteHeader() {
{navLink("/favorites", "stars")}
{navLink("/docs", "docs")}
{navLink("/docs/api", "api")}
<button
type="button"
onClick={() => openPackDialog()}
className="text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
>
pack
</button>
<a href="https://github.com/pablostanley/pixabots" target="_blank" rel="noopener noreferrer" className="text-muted-foreground hover:text-foreground transition-colors">
github
</a>
Expand Down
71 changes: 71 additions & 0 deletions pixabots-gumroad/campaign/email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!-- Subscriber email. Gumroad email composer accepts HTML; you can also
copy the rendered text into any other tool (Mailchimp, Beehiiv, etc.). -->

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pixabots Free Pack — 2,000 little pixel characters</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif; line-height: 1.55; max-width: 560px; margin: 0 auto; padding: 32px 20px; color: #1a1a1a;">

<p><strong>Subject line options (pick one):</strong></p>
<ul style="color:#666; font-size:13px;">
<li>2,000 free pixel characters — just dropped</li>
<li>I made you 2,000 little pixel friends</li>
<li>Free: 2,000 pixel avatars (PNG + GIF + WebP)</li>
<li>Pixabots Free Pack is live</li>
</ul>

<hr style="border:none; border-top:1px solid #eee; margin: 28px 0;">

<p style="text-align:center; margin: 0 0 28px;">
<a href="https://pablostanley.gumroad.com/l/pixabots">
<img src="https://public-files.gumroad.com/gxs3g6uqux6013yvj6qs7v37jie3" alt="Pixabots — 2,000 pixel characters" style="max-width:100%; height:auto; border-radius:8px; display:block; margin:0 auto;">
</a>
</p>

<p>Hey &mdash;</p>

<p>Short one: I just dropped a free pack of <strong>2,000 unique pixel characters</strong>. PNGs in four sizes. Animated GIFs. Animated WebPs. 12,000 files in total. Yours, no strings.</p>

<p style="text-align:center; margin: 28px 0;">
<a href="https://pablostanley.gumroad.com/l/pixabots" style="display:inline-block; background:#1a1a1a; color:#fff; text-decoration:none; padding:14px 22px; border-radius:6px; font-weight:600;">Grab the free pack &rarr;</a>
</p>

<p>They&rsquo;re from <a href="https://pixabots.com">Pixabots</a>, my little pixel character generator. It can spin up 10,752 unique combos &mdash; this pack is 2,000 of them, ready to drop into anything.</p>

<p style="text-align:center; margin: 28px 0;">
<img src="https://public-files.gumroad.com/0jbdothyr662nyhojdmrcrhn8dn8" alt="Pixabots grid" style="max-width:100%; height:auto; border-radius:8px; display:block; margin:0 auto;">
</p>

<p><strong>Some ideas:</strong></p>

<ul>
<li>Default avatars for your SaaS / app</li>
<li>Placeholder characters in mockups</li>
<li>NPCs in a game jam</li>
<li>Stickers / merch</li>
<li>Twitch / YouTube channel art</li>
</ul>

<p>Free for personal <em>and</em> commercial use. Modify, recolor, composite, ship.</p>

<p>If you want more than 2,000, the full set lives at <a href="https://pixabots.com">pixabots.com</a> with a free API and a live editor. No signup.</p>

<p>That&rsquo;s it. Go make something.</p>

<p>&mdash; Pablo</p>

<hr style="border:none; border-top:1px solid #eee; margin: 28px 0;">

<p style="font-size:12px; color:#888;">
P.S. If you&rsquo;d rather just see the source: <a href="https://github.com/pablostanley/pixabots">github.com/pablostanley/pixabots</a>. MIT licensed, fork away.
</p>

<p style="font-size:12px; color:#888;">
Direct link: <a href="https://pablostanley.gumroad.com/l/pixabots">pablostanley.gumroad.com/l/pixabots</a>
</p>

</body>
</html>
52 changes: 52 additions & 0 deletions pixabots-gumroad/campaign/gumroad-body.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!-- Paste this whole thing into the Gumroad product description editor.
Gumroad's editor preserves: <p>, <h2>, <h3>, <ul>, <ol>, <li>,
<strong>, <em>, <a>, line breaks. Strip anything else if it complains. -->

<p>I made a tiny pixel character creator called <strong>Pixabots</strong>. It generates 10,752 unique combinations from four categories &mdash; eyes, heads, bodies, and tops &mdash; all bouncing on a little idle loop.</p>

<p>This pack is a curated 2,000 of them, ready to use. Free.</p>

<h2>What's inside</h2>

<ul>
<li><strong>2,000 PNGs at 240px</strong> &mdash; chat avatars, UI, favicons</li>
<li><strong>2,000 PNGs at 480px</strong> &mdash; cards, hero spots</li>
<li><strong>2,000 PNGs at 960px</strong> &mdash; retina, print</li>
<li><strong>2,000 PNGs at 1920px</strong> &mdash; full HD, posters</li>
<li><strong>2,000 animated GIFs at 480px</strong> &mdash; the signature bounce</li>
<li><strong>2,000 animated WebPs at 480px</strong> &mdash; modern format, smaller than GIF</li>
</ul>

<p>That&rsquo;s 12,000 files. Around 550 MB unzipped.</p>

<p>Every file is named by its 4-character ID (like <code>7k3a.png</code>). Paste any ID into <a href="https://pixabots.com">pixabots.com</a> and you can keep editing it in the browser &mdash; change colors, swap parts, export at any size.</p>

<h2>Use them for</h2>

<ul>
<li>Avatars in your SaaS, app, or Slack workspace</li>
<li>Placeholder characters in design files</li>
<li>Game NPCs, stickers, merch</li>
<li>Twitch and YouTube channel art</li>
<li>Anything where &ldquo;I need a character and I don&rsquo;t want to draw one&rdquo; is the problem</li>
</ul>

<h2>License</h2>

<p>Free for <strong>personal and commercial use</strong>. Modify, recolor, composite, redistribute as part of your work. The one thing you can&rsquo;t do is resell the images as a standalone pack &mdash; but if you build something with them, ship it.</p>

<h2>Want more?</h2>

<p>The full set of 10,752 is browsable at <a href="https://pixabots.com">pixabots.com</a> with a live editor, palette controls, animated GIFs, and a free API. No signup. No rate limit on the UI.</p>

<p>There&rsquo;s also an SDK on npm (<code>@pixabots/core</code>, <code>@pixabots/react</code>) and a CLI (<code>npx pixabots</code>).</p>

<p>Everything is open source: <a href="https://github.com/pablostanley/pixabots">github.com/pablostanley/pixabots</a>.</p>

<h2>Why free?</h2>

<p>I make stuff. Sometimes the stuff is useful to other people. This is one of those times.</p>

<p>If you want to send me a few bucks via pay-what-you-want, lovely. If you don&rsquo;t, also lovely &mdash; just go make something cool with them.</p>

<p>&mdash; Pablo</p>
Loading
Loading