You are browsing as a guest. Sign up (or log in) to start making projects!

Ziyaad

@Ziyaad

Joined June 8th, 2026

  • 15Devlogs
  • 4Projects
  • 2Ships
  • 30Votes
Yo.
Open comments for this post

1h 34m 39s logged

victorylap Devlog #6 (now it has taste and a backup plan)

two things done:

paper theme

the first look was amber terminal, mono type, sharp slides. paper is the opposite: warm cream background, serif ink, a thin red line down the left margin, everything fading in soft. same slots, same wiring, completely different film. –theme paper gets you one, no flag gets you the other. thats the whole point of themes proving out.


model fallback.

the video script is written by a free model on OpenRouter, and free models get rate limited constantly. it kept returning nothing and the whole thing crashed.

so now it doesnt lean on one model. it runs down a list. if the first one returns no answer, it logs a line, saves the reason, and tries the next. first model that responds wins. it only gives up if every single one refuses, and then it prints the actual reason instead of a raw crash.

went from “run it again and hope” to the tool doing the hoping for me.

next up: making the text resize itself so long project names dont spill off the edge and a lot more :)

0
0
4
Open comments for this post

6h 22m 36s logged

victorylap Devlog #5 (one quote in the wrong place cost me a full render)

its been longer than i realized since my last devlog. a lot has happened.

TL;DR

  • tore the video out of one giant html string and split it into separate scene files
  • one misplaced quote in a script tag stopped gsap from loading. every animation silently died
  • built a loading bar so you dont watch hyperframes scroll a wall of text anymore
  • started a second theme, paper, so –theme actually changes the whole film

The Refactor

the whole video used to be one massive html string. every scene, every style, every timeline stuffed into one file. it worked but it was getting hard to read and harder to change.

so i split it. the host file holds the layout and the four slots. each scene is its own composition file: hook, name, stats, closer. hyperframes loads each one, seeks it on its own, and drops it into the slot at the right time.

adding a scene is now its own file instead of a needle in a haystack.


The Loading Bar

hyperframes prints a scrolling wall of render logs. it works but it looks like nothing you’d want a user to see.

so i wrapped the render in my own bar. a spinner, a set of rotating words, and a real percent bar that reads the actual progress out of the render output. all of hyperframes’ output gets buffered quietly in the background, and if the render fails i print the last chunk of it so i can actually see what broke. it’s clean on success, honest on failure.


Paper

i’m mid build on the second theme. the first look is amber terminal, mono, sharp slides. paper is the opposite: warm cream, serif ink, soft fades, a thin red margin rule down the side. same slots, same wiring, totally different film. thats the point of –theme. once this one renders clean i can add a third by copying the shape.


0
0
3
Open comments for this post

1h 39m logged


victorylap Devlog #4

victorylap Devlog #4 (it’s a real command now)

TL;DR

  • victorylap is npx-able. one command, any repo, out comes an mp4
  • added –out and –theme flags, friendly errors for bad input, and it runs the render itself now
  • you can paste a github url. it clones to a temp folder, makes the video, cleans up after itself
  • tested on three repos i did not write: slugify, requests, is-odd. all three rendered

What I Did

a bin field in package.json and a shebang line. that’s the whole trick behind every cli tool i’ve ever npx’d, and now mine is one of them.

the flags are not magic either. process.argv is just an array of what the user typed, and parsing –out is an if statement in a loop.

most of the session was making it fail nicely. bad folder, fake theme, empty repo, missing api key, broken llm reply. each one now gets one plain sentence instead of a stack trace.

my own typos fought back the whole way. i typed the new code and kept the old line twice, wrote studio instead of stdio, THEMS instead of THEMES. when a snippet replaces a line, the delete is half the edit.


What I Learned

the github url feature had a security lesson in it. my first instinct was to glue the url into a shell command string. bad idea. If someone passes a “url” with && and a delete command in it, the shell runs it. that’s command injection. the fix is execFileSync, which hands git the arguments as an array so no shell ever reads them. user input never gets glued into a command string.

three repos i have never read, three videos.

next up: multiple scenes, so the video stops being one card.

0
0
3
Open comments for this post

1h 6m 23s logged

victorylap Devlog #3 (the ai wrote a video about a repo it has never seen)

TL;DR

  • week 3 done, two weeks early. analyzer json goes to a free llm, the llm writes the script, the script renders to mp4
  • the hardcoded demo frame is gone. video/index.html is generated fresh for every repo
  • tested on site-slap, a repo victorylap had never seen. the video came out correct without me typing a word of it

The Brain

script.js sends the analyzer’s json to a free model on openrouter with three things: the facts, the exact json shape i want back, and rules. the rules do the heavy lifting. only use facts from the json. never invent numbers. no generic startup language.

for site-slap it wrote “Paste URL. Get roasted by AI.” as the hook and picked 15 commits, 11 ts files and 7 days built as the stats.

the 7 days one surprised me. i never gave it a duration. it read the first and last commit dates from the git data and did the math itself. that’s the whole idea of this project working on the first real run: give the model true facts and strict rules, and it stays honest.


The Template Became a Function

the frame i built by hand in week 1 was a dead end on purpose. now the whole page lives inside one javascript function as a template string, with slots where the hardcoded text used to be. the llm’s script fills the slots and the file gets written fresh each run.

the stats section is three lines of .map(). the ai sends an array, the template turns each entrinto a div, join glues them together. if it ever sends four stats instead of three, the template does not care.
.


What I Learned

there is a closing script tag inside my template string, because the template contains a full html page. anything parsing that file as html would see the tag and end the script early, right in the middle of the string. the fix is writing it as </script>. Inside a javascript string a backslash slash is just a slash, so the file that gets written out is normal html, but nothing scanning the template can mistake it for a real tag anymore. two characters of insurance against a genuinely confusing class of bug.

—.
next up: week 4. make it npx-able, so anyone can run this without cloning anything.

see y’all soon


0
0
1
Open comments for this post

1h 31m 12s logged

victorylap Devlog #2 (my analyzer thought the repo was 90% virtual environment)

TL;DR

  • wrote analyze.js, the first real victorylap code. point it at any repo, get structured json back
  • it reads package.json, pulls the first real sentence out of the readme, detects languages by walking the file tree, and reads git
    history
  • zero dependencies. just node builtins: fs, path, child_process
  • tested on two repos, one TypeScript and one Python. both came back clean

The Analyzer

this is the machine that replaces “ai looks at your repo and vibes.” victorylap needs facts about a project before it can make a video about it, so analyze.js collects them into one json report: name, description, first readme sentence, language breakdown, commit count, project dates, last 10 commit messages.

every source is its own function that returns null if the file isn’t there. repos are hostile territory. some have no package.json, some have no readme, some aren’t even git repos. the analyzer assumes nothing exists and survives all of it.

the readme part was more parsing than reading. readmes open with badges, logos and headings before any actual sentence, so the code skips every line that starts with #, ![ or < until it hits real prose. on site-slap that landed on “paste a url. get a score out of 100. get roasted.” which is exactly the tagline a video wants.


The venv Bug

first run on free-claude-code reported 1045 python files.

the repo has about 125.

the language detector walks the whole file tree and counts extensions, with a skip list for folders that aren’t your code: node_modules, .git, dist. my list had “.venv” in it. the folder in the repo was named “venv”. no dot, different string, the walker went right in and counted 928 files of installed packages as project code.

one word added to the skip list and python dropped to 125. the numbers always look right until you check them against reality.

git history had its own surprise. my hardcoded demo frame has said 107 COMMITS since day 1, a number i eyeballed. git rev-list says 494. the analyzer’s first real act was fact-checking my own video.


What I Learned

the file tree walk was my first recursive function, a function that calls itself.

walk() reads one folder. every file gets its extension counted. every subfolder triggers walk() on that subfolder, and here’s the part that made it click: every call receives the same counts object, passed down as an argument. there aren’t 50 tallies getting merged at the end, there is one shared tally that every level of the tree writes into. when the outermost call finishes, the whole repo is already counted.

recursion sounds like a trick until you need to handle “a folder can contain folders which contain folders.” then it’s just the honest shape of the problem.


Tip of the Day

try/catch will eat your typos, not just your expected errors. i wrapped the git code in try/catch so non-git folders return null
instead of crashing. then i typed “utf8 “ with a trailing space as the encoding. node rejected it, the catch swallowed it, and git
came back null with zero explanation. if a section of your output is mysteriously null, the first suspect is a typo hiding inside
your own safety net.

next up: feed this json to a free llm and get a video script back.


0
0
1
Open comments for this post

1h 44m 38s logged

🏁 victorylap Devlog #1 (two days in and i already made like 200 mistakes)

TL;DR

  • started victorylap, my main summer project. one command, npx victorylap, turns any git repo into a brag video
  • built the first video frame by hand in html, 1920x1080, amber on black
  • promoted it to a real Hyperframes composition and rendered my first mp4. the pipeline works
  • test data for the demo frame comes from the free-claude-code repo

What This Actually Is

every dev has repos that die quietly on github. you build something real, push it, and nobody ever sees it. victorylap turns the
repo itself into a short video you can post. it reads your readme, file tree and git history, an ai writes a script about what you
built, and the whole thing renders to mp4 on your own machine.

that last part is the point. no render server, no paid api, no upload. your computer does the work so it stays free forever.

credit where it’s due: the idea comes from the /brag skill for claude code, and the render engine is Hyperframes. i’m not porting
brag, i’m remaking it as a standalone product. same engine, my own analyzer, my own templates, free llm for the script. the
original creators get named in every version of this.


Day 1, the Frame

wrote one hardcoded html file. project name huge, a tagline, three stats. the data is real, it’s from free-claude-code, which i’m
using as the test repo all summer.

it broke immediately. the name rendered tiny and almost invisible. three separate causes: i never typed the .project-name css
block, forgot color on body so the text defaulted to black, and typed .tagLine with a capital L so the selector never matched. two
different bugs producing the same symptom taught me more about css inheritance than any tutorial has.


Day 2, the Render

Hyperframes doesn’t screenshot a plain page. it needs a composition, which is html with a contract: a root div with data-duration,
clips with data-start and data-track-index, and one paused gsap timeline registered on window.__timelines.

lint caught a missing data-start on the root. validate ran it in headless chrome, clean. render gave me an mp4.

then it gave me a black one.

turns out i had typed all my fixes into template.html, the day 1 file, while the renderer reads video/index.html. did that twice.
closed the old tab in vscode and the problem stopped existing. also found height: 180px where 1080px should have been, which would
have rendered the whole video as a squished strip.

second render worked. five seconds, text staggering in, my first programmatically rendered video.


What I Learned

the paused timeline is the core trick of the whole engine. a normal browser animation plays on the browser’s clock, so recording it
means racing it in real time and hoping no frame drops. Hyperframes refuses to race. the timeline is created paused, and the
renderer takes ownership of time itself. it seeks to 0.000s, screenshots, seeks to 0.033s, screenshots, three hundred times for a
ten second video. then ffmpeg stitches the stills into video. every frame is exact because nothing was ever actually moving.

deterministic time is why the same file renders identically on any machine.


0
0
1
Ship

Shipped: Site Slap

Paste a URL. Get a score out of 100. Get roasted.

Site Slap is an AI design critic. It screenshots any live website, shows it to a vision model, and judges the visual design the way a person with taste
would. No SEO metrics, no performance audit. You get a score, a one-sentence verdict, three specific roasts, and three fixes that would actually move the
number.

Then the part I'm proudest of: hit View Upgraded and a draggable, mac-style popup opens with your site rebuilt, fixes applied, rendered live inside a
sandboxed iframe. Both versions start generating in the background while you're still reading your roast, so the popup usually opens already loaded. Minor
upgrade or full redesign, your choice.

The whole app ships in three skins: basic, decent, and fire. Same tool, three levels of design effort. Fire is the one worth seeing, with an aurora hero
built in pure CSS, snap scrolling, and a score that counts up. The skin switcher is a live demonstration of what the scoring scale actually means.

The technicals, briefly: Next.js and TypeScript on Vercel. Microlink handles screenshots, so there is no headless browser to babysit. Groq's Llama 4 Scout
does the judging for free, and Claude Haiku writes the upgraded site as a single self-contained HTML file, about three cents per render with a daily cap.
The generated pages contain no JavaScript, and the iframe's empty sandbox attribute makes sure none could run regardless.

The scoring is deliberately harsh. Minimal is not the same as designed, and blank is not the same as clean.

Go slap your own site before someone else does: https://site-slap-topaz.vercel.app

  • 5 devlogs
  • 6h
  • 17.93x multiplier
  • 116 Stardust
Try project → See source code →
Open comments for this post

42m 21s logged

Site Slap Devlog #5 (it ships today)

TL;DR

  • the popup got its brain: it renders your actual upgraded site now, written by Claude Haiku, shown in a sandboxed iframe
  • went through three ai models to kill the slop. the free ones couldn’t write html
  • final polish landed: tab name, hand favicon, pulsing dots pulling people to the better skins
  • found out the site was invisible to everyone except me. fixed an hour before shipping
  • site slap is live. link at the bottom. go get roasted.

the upgrade renderer

the moment your slap result lands, two requests fire in the background, one per tier. by the time you scroll down and hit view
upgraded, the redesign is already sitting there.

Claude Haiku gets the screenshot plus the fixes and writes your site back as one self-contained html file. no javascript allowed,
and the iframe’s empty sandbox attribute makes sure it wouldn’t matter anyway. prompts are requests. sandboxes are enforcement.

the roasts still run free on groq. scoring doesn’t need the muscle, writing html does. about three cents per render, daily capped
so a viral day costs me a dollar and not my whole balance.


three models and a thumbnail trick

groq’s scout wrote the first upgrades. overlapping headlines, floating color blocks. i gave it a page of design rules. still slop.
maverick was better and slower. haiku ended the argument.

the other half of “broken” renders was my fault: the model writes desktop css and i was rendering it in a 670px frame. now the
iframe is actually 1280px wide and a css transform scales it to 52%, like a thumbnail. layout at real size, display at small size.

best bug of the week: one render had perfect nav links next to three giant ugly buttons. turns out buttons don’t inherit fonts.
browsers give buttons their own default font unless you write button { font: inherit }. that reset is now a mandatory line the
model must paste, not a rule it might remember.


the launch blocker nobody saw

an hour before shipping, i checked the live url from outside my account.

login page.

vercel’s deployment protection was walling everything, production included. i’d been looking at my site logged in for days, seeing
it fine, while every public url served a vercel login screen. the site worked perfectly and nobody on earth could see it.

one setting flipped, one clean domain added, and then a real test from the outside: curl the live api, no cookies, no login.
example.com came back with a 40 and “barely functional, utterly forgettable.” the renderer returned real html with the viewport
meta exactly where the prompt demands it. green across the board.

you and the public do not see the same internet. test logged out.


thank you

this project went from “paste a url, get roasted” scribbled in a chat to a three-skin app with an ai design critic and a popup that
redesigns your site in front of you. five devlogs, three dead heroes, two ai providers, one rem/em bug i will never make again.

thanks to Hack Club and Stardance for the reason to ship instead of endlessly polish. thanks to everyone who read these devlogs and
watched the thing grow. and thanks to arngren.net for being the calibration anchor no prompt could do without.

it ships.


Tip of the Day

open your deployed site in an incognito window before you share it anywhere. logged-in you and the public are two different
visitors, and only one of them matters on launch day.


🖐️ go slap your site

https://site-slap-topaz.vercel.app

0
0
1
Open comments for this post

1h 22m 53s logged

Site Slap Devlog #4 (pasted it twice, rendered it never)

TL;DR

  • fire skin got its upgrade wave: cinematic slow timing, snap scrolling between full-height panels, three tabs replaced by one
    pulsing button, and a draggable popup window
  • the popup drags at 60fps because React never hears about it. position lives in a ref and writes straight to the dom
  • my paste of the popup duplicated 12 lines of jsx, and the line that actually renders it never existed. the button flipped state
    into the void
  • next: the popup stops showing a fix list and starts showing your actual upgraded site

the upgrade wave

four changes in one session.

the window expands first, then the text rises in. everything slowed to about half its old speed, long delays between each element,
so switching to fire feels like a scene opening instead of ui popping in.

the page now snap-scrolls between full-height panels: hero, score, screenshot, roast, fixes. one flick per section.

the og / quick fix / full fix tabs are gone from fire. in their place, one button at the end of the fixes that pulses a sonar ring
and says view upgraded.


the floating window

clicking it opens what looks like another mac window. traffic lights, a title bar, and the red dot actually closes it. you can grab
the title bar and drag it anywhere on screen.

inside: your current score for contrast, the upgraded score in the big serif, and a minor / full toggle that swaps both the number
and the fix list.

it works.


what i learned

dragging with react state is a trap. pointermove fires around 60 times a second, and putting the position in state would re-render
the whole component on every single event.

instead the position lives in a ref. pointerdown records where you grabbed and where the window already was. pointermove computes
the offset and writes style.transform directly onto the dom node. React never re-renders during the drag, so it stays smooth.

the other half is setPointerCapture. without it, dragging fast outruns the title bar, the cursor leaves the element, and the
window gets dropped mid-drag. capture pins every pointer event to the bar until you let go.

also learned the dumb way that pasting a component in is not the same as shipping it. my paste duplicated a 12 line block of jsx,
and i never added the line that renders the window when the state flips. state going true means nothing if nothing reads it.


what’s next

  • the popup body swaps the fix list for the upgraded site itself. Groq’s vision model looks at the screenshot plus the fixes and
    writes a new version of the page as one self-contained html file, rendered inside a sandboxed iframe
  • both upgrade tiers start generating in the background the moment the slap result lands, so the popup opens already loaded
  • push it, then check the Vercel deploy

Tip of the Day

if a button does nothing, check whether the thing it opens is ever rendered. flipping state to true is only half the job. something
has to read that state and put pixels on screen.

0
0
3
Open comments for this post

1h 52m 44s logged

Site Slap Devlog #3 (the hero that died twice before it shipped)

TL;DR

  • fire skin is done. the third and final tier, dark cinematic, animated aurora hero in pure css
  • the original plan was an ai-generated image hero, then an ai video. both died. the final version uses zero assets
  • three motion moments: staggered word reveal on load, score count-up, roast lines fading in on scroll
  • one bug in the results css: rem where em should be. the “/100” rendered at 4 pixels tall

the plan kept dying

fire was supposed to have a generated image hero. i made the image, it came out painterly instead of real, i liked it anyway. then i tried animating it into a looping video. the videos came out static, then the download button stopped working, and my credits were almost gone.

so i dropped the whole thing.

the replacement is an aurora effect built entirely in css. no image, no video, no library, nothing to download. rose, amber and teal glows drifting over near-black. it loads instantly and it can’t break.


how the aurora works

two repeating linear gradients stacked on one div. the first paints thin diagonal stripes of rose, amber and teal. the second paints stripes of the background color on top, so it cuts gaps into the first one.

then filter: blur(42px) melts the stripes into soft bands of light.

the movement is one keyframe. the background is sized at 300% width, and background-position slides from 50% to 350% over 26 seconds on a loop. the stripes drift sideways like slow northern lights. that’s the entire trick, one animated property, gpu-composited, no jank.


the motion

the headline is five words, each wrapped in its own span with a staggered animation-delay. they rise 28px and fade in one after another on load.

when results land, the page auto-scrolls down and the score counts up from 0 with a requestAnimationFrame loop and a cubic ease-out, so it sprints early and settles slow.

the roast and fix lines start invisible. an IntersectionObserver adds a class when each one enters the viewport, staggered 80ms apart.

it works.


what i learned

em and rem are not interchangeable, and the difference is exactly one letter.

the giant score is clamped up to 168px. next to it, “/100” is sized at 0.28em, which means 28% of the parent’s font size, so it scales with the score. i typed 0.28rem instead. rem means 28% of the root font size, 16px. so “/100” rendered at about 4.5 pixels. invisible.

em looks at the parent. rem looks at the page. one letter, 40 pixel difference.

also, prefers-reduced-motion has a trap. if you just set animation: none on elements that start at opacity 0, they stay at opacity 0 forever. the media query has to force opacity: 1 back on, or reduced-motion users get a blank page.


Tip of the Day

if your css animation “isn’t working”, check whether it already finished. a mount animation runs once in the first second. if you looked away, you missed it, and the end state looks identical to no animation at all.


0
0
1
Open comments for this post

1h 15m 37s logged

Site Slap Devlog #2 (it looks like a real app now)

TL;DR

  • wrapped the whole thing in a mac-style browser window with traffic lights and a skin switcher
  • screenshot buffer so microlink waits for the site to actually finish loading
  • skeleton loaders so the ui doesn’t just sit there looking dead

the window

the app had no personality before. it was just a black page with a text input. it worked but it felt like a prototype.

now the whole thing lives inside a browser-window shell. dark title bar, red yellow green traffic lights top-left, three tabs across the top, basic, decent, fire. those tabs are the skin switcher. click one and the content inside the window changes completely.

only basic is wired up right now. decent and fire both say “coming soon.” but the chrome is done, it’s consistent, and it holds everything together.


the screenshot buffer

microlink was taking the screenshot too early. some sites load fast enough, but anything with javascript rendering would come out half-built. spinners, blank sections, layouts that hadn’t collapsed yet.

added waitFor=2500 to the microlink request. it waits 2.5 seconds after the page loads before grabbing the screenshot. costs a bit of latency but the results are actually what the site looks like.


skeleton loaders

while the slap is processing there’s nothing to show. before this it was just a blank area and a button that said “slapping…” you had no idea if it was working.

now there are ghost blocks that pulse while it loads. one tall block for where the screenshot will be, then smaller ones for the score, verdict, roast, and fixes. the layout mirrors the real result so when it drops in it doesn’t feel like a jump cut.

it works.


what i learned

the shimmer animation is simpler than it looks. it’s one div with a gradient that’s twice the width of the element. background-size: 200% 100% stretches the gradient out. then an animation shifts background-position from 200% 0 to -200% 0, which sweeps the lighter middle band across the div left to right.

no javascript. no opacity toggle. just a moving gradient. the whole thing is one css keyframe and four lines of inline style.


what’s next

decent skin and fire skin. the chrome is ready.

decent is a clean modern redesign of the same tool, same data, different presentation. fire is the stardance premium version with motion. that one needs real thought before any code gets written.


Tip of the Day

screenshot your site at 375px width before you ship anything. most people will see it on a phone first. if it breaks there, nothing else matters.

0
0
2
Open comments for this post

1h 16m 3s logged

Site Slap Devlog #1 V0 (the ai gave arngren.net a 42)

TL;DR

  • built the full backend: screenshot, vision model, score + roast + fixes
  • wired it to a live ui and deployed on Vercel
  • tuned the prompt

the idea

paste a url. get a score. get roasted.

that’s the whole thing. no seo metrics, no performance audit. just a vision model looking at your site the way a person would, and being honest about it.


how the backend works

three steps, one route.

first, microlink takes a screenshot of the live site. it’s a simple GET request, no chromium to manage, no headless browser on the server. just a url in, a screenshot url back.

then that screenshot url goes straight to Groq’s vision model, llama 4 scout. it sees what the site actually looks like, not the html. the prompt tells it to score on visual vibe only, and return raw JSON with a score, a verdict, a roast, and three fixes.

then /api/slap hands that JSON back to the client.

it works.


the markdown fence bug

linear.app broke it on the first test. Groq decided to wrap the JSON in a code block even though the prompt said not to. one line fix: strip the fences before parsing. linear scored fine after that.


what i learned

the model is too generous by default.

arngren.net is one of the most visually chaotic sites on the internet. it got a 42. that’s “meh” territory. the prompt said “0-30 is painful” but the model hedged toward the middle anyway.

turns out you have to anchor the scale with specific examples and tell it to actually use the low end. added “arngren.net level” next to the 0-15 range and “be harsh when it’s bad” as a direct instruction. scores tightened up.

the prompt is the model’s personality. treat it like code.


Tip of the Day

go slap your own site before someone else does. you already know what’s wrong with it.

0
0
1
Ship

built a web app that tells you which photo of your face to use as your profile pic.

you upload a bunch of photos, it runs face detection in your browser (no uploads, no server, no account) and scores each face on centering, size, brightness, and sharpness. the highest score wins. it tells you why in plain words.

the challenging part was getting the scoring to feel honest instead of random. sharpness especially, I had to measure contrast between neighboring pixels to get it right.

proud of the fact that nothing leaves your browser. your photos never touch a server. close the tab, it's gone.

to test it: just drag in 3-5 photos of your face. good luck and thank you

-Ziyaad :)

  • 4 devlogs
  • 6h
  • 12.10x multiplier
  • 72 Stardust
Try project → See source code →
Open comments for this post

2h 46m 2s logged

PFP Studio Devlog #3 (idk if anyone will ever read this)

Live link: https://pfp-studio-fawn.vercel.app/

TL;DR

  • the winner explanation sounds like a person now, not a form letter
  • photos with no face get a card instead of silently vanishing
  • rebuilt the whole upload screen into orbiting rings of your photos
  • shipped to vercel, after one line of css put up a fight

The Justification

before, the winner card said the exact same thing every single time. “this photo is the best because the face is well-centered in the frame.” every photo. every run. it read like a robot filling out a form.

so i gave each check its own pool of phrasings, picked at random, and made it call out two strengths instead of one. now it says stuff like “easy top pick. face sits right in the middle, plus crisp, every detail holds up.”

reads like a person wrote it. which was the whole point.


No Face? Still Get a Card

what’s the point of ranking photos if half of them disappear? exactly, there’s none.

face-api returns nothing when it can’t find a face, and i was quietly dropping those photos. drop a landscape or a logo and it just vanished from the results. now every photo gets a card. no face means it says “no face detected” and sinks to the bottom. nothing disappears anymore.


The Ring Thing

the old upload screen was a plain button. boring. the new one is your photos, cropped into circles, orbiting in concentric rings on a tilted plane with a citrus glow behind them, the rank button sitting dead center.


What I Learned

here’s the problem nobody warns you about. if you spin a ring, everything pinned to it spins too. so your face would slowly tumble upside down as it goes around. not a good look.

the fix is a counter-spin. the photo sits inside a wrapper that rotates the opposite direction at the exact same speed as its ring. the two cancel out. the ring carries the photo around the circle, the wrapper holds the face perfectly level the entire way.

i also learned the hard way that your local dev server lies to you. i had a busted line of css, var(var(–accent-soft)), sitting in the code for days. local never said a word. the second Vercel ran the real production build, it rejected it instantly and killed the whole deploy. one line. fixed it, pushed again, live.


Tip of the Day

shoot at eye level, not from below.

a low angle points the lens up your nose and widens your jaw. eye level flatters basically everyone. bonus, the app rewards it too, since a centered well-framed face scores higher until next time, -ziyaad :) (please lmk any feedback)

0
0
1
Open comments for this post

1h 23m 59s logged


PFP Studio Devlog #2

the scoring system is done.

four checks per photo: centering, size, brightness, sharpness. each one reads the actual pixel data from the face region and turns it into a number. combine them with weights, sort the results, and the best photo rises to the top.

the sharpness one was the most interesting to build. it loops through every pixel and compares it to the pixels directly to its right and below. blurry photos have smooth transitions. sharp photos have high contrast between neighbors. the bigger the average difference, the sharper the photo.


the ui got a full revamp too.

went from raw unstyled html to something that actually looks like a product. warm cream background, Syne for the headings, orange accent for the winner card. each result staggered in with a fade. the #1 card gets an orange ring and shows the “why it won” explanation underneath.


it works.

uploaded 5 photos, hit rank, and the results actually make sense. the best-lit, well-centered shot came out on top. the blurry one ranked last.


what’s next: loading states so it doesn’t feel frozen while processing, and filling out the empty state so the page doesn’t look bare on load.

then it ships.


0
0
15
Open comments for this post

1h 10m 40s logged

PFP Studio Devlog #1.5

ok so here’s the thing.

everyone has that moment where they’re picking a profile pic and they just… can’t decide. you’re staring at 12 photos that all look basically the same and you pick one based on vibes.

i wanted to fix that.

the idea is simple. upload your photos, the app checks each face, scores them on actual stuff (is it centered, is the face big enough, is it well lit, is it sharp or blurry), then ranks them. #1 wins. done.


getting started was rougher than expected

i’d used html/css/js before but this was my first real React project. spent an embarrassing amount of time confused about why my component wasn’t showing up before realizing i named it uploadZone instead of UploadZone. React treats lowercase as HTML tags. learned that one the hard way.

also had a white screen moment that lasted way longer than i want to admit. turned out export default App was just… missing. one line.


the model files situation

face-api.js needs ML model files to detect faces. downloaded them through the browser. seemed fine. then got this error: “tensor should have 864 values but has 617.”

turns out the browser saved them as HTML files instead of the actual binary. re-downloaded them properly, everything worked.


where it’s at now

the core scoring works. it reads pixel data from each face region, runs four checks, combines them into a total score, and sorts the photos. tested it and the ranking actually makes sense.

next: working on the logic and reasoning behind the best pfp and then later making the app look nice :)

we’ll see how that goes.

-Ziyaad

0
0
2

Followers

Loading…