<?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: Jacko (Nightlit)</title>
    <description>The latest articles on DEV Community by Jacko (Nightlit) (@playnightlit).</description>
    <link>https://dev.to/playnightlit</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%2F4197095%2F8e35eeef-ddfd-4baa-9b36-7643c375452b.png</url>
      <title>DEV Community: Jacko (Nightlit)</title>
      <link>https://dev.to/playnightlit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/playnightlit"/>
    <language>en</language>
    <item>
      <title>I built a 3D free-kick game in three.js: Magnus curl in 10 lines, floodlights and a keeper that reads your shot</title>
      <dc:creator>Jacko (Nightlit)</dc:creator>
      <pubDate>Thu, 22 Oct 2026 07:46:23 +0000</pubDate>
      <link>https://dev.to/playnightlit/i-built-a-3d-free-kick-game-in-threejs-magnus-curl-in-10-lines-floodlights-and-a-keeper-that-24ao</link>
      <guid>https://dev.to/playnightlit/i-built-a-3d-free-kick-game-in-threejs-magnus-curl-in-10-lines-floodlights-and-a-keeper-that-24ao</guid>
      <description>&lt;p&gt;I'm a solo dev and I just shipped &lt;strong&gt;Efectazo&lt;/strong&gt;, a free free-kick football game that runs in the browser, on phones and desktop: &lt;a href="https://efectazo.pages.dev" rel="noopener noreferrer"&gt;https://efectazo.pages.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You swipe to shoot, and the shape of your swipe becomes curl on the ball. Here's how the main pieces work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ball: gravity, drag and a Magnus force
&lt;/h2&gt;

&lt;p&gt;The physics module is plain JS with no three.js in it, so I can run it headless in tests. Each substep adds three forces to the ball:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Magnus: CM * (w x v)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;accel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wind&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vz&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;wind&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;rz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vz&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;wind&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rz&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wz&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rz&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;wz&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;my&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wz&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;wx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;vy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;wy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;KD&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CM&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;mx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;G&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;KD&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;vy&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CM&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;my&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;KD&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rz&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CM&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;mz&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quadratic drag slows the ball, and the cross product of spin and velocity pushes it sideways (side spin) or down (topspin dip). Spin decays exponentially, so the bend gets stronger late in the flight, which is what makes a good free kick look right.&lt;/p&gt;

&lt;h2&gt;
  
  
  From swipe to shot
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Direction comes from the first 40% of the swipe&lt;/li&gt;
&lt;li&gt;Power from swipe speed, height from swipe length&lt;/li&gt;
&lt;li&gt;Side spin from how much the swipe curves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if you draw a banana, the ball bends like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goalkeeper
&lt;/h2&gt;

&lt;p&gt;The keeper has a reaction delay, predicts where the ball will cross the line (better keepers read the curl better), and dives with limited speed and reach. A save only happens when the ball actually hits his limbs, so it never feels scripted. The wall (0 to 6 players) jumps when you kick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;three.js r160, MeshStandard/Physical materials, PMREM image-based lighting&lt;/li&gt;
&lt;li&gt;Two shadow-casting "floodlight" directional lights with PCF soft shadows&lt;/li&gt;
&lt;li&gt;A procedural grass shader (mowing stripes plus detail noise) instead of a big texture&lt;/li&gt;
&lt;li&gt;ACES filmic tone mapping, bloom, and a slow-motion replay with depth of field after goals&lt;/li&gt;
&lt;li&gt;Quality tiers with an Auto mode that drops a tier if the frame rate stays under ~47 fps for 2 seconds, which matters a lot on phones&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The game around it
&lt;/h2&gt;

&lt;p&gt;40 career levels, a seeded Daily Challenge (5 shots, with an emoji grid you can share), an endless Arcade mode and Cups. Coins unlock balls, boots, kits, stadiums (night, day, sunset, rain, snow) and celebrations. English and Spanish. It's free, with an optional one-time Pro upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Play it here: &lt;a href="https://efectazo.pages.dev" rel="noopener noreferrer"&gt;https://efectazo.pages.dev&lt;/a&gt;&lt;/strong&gt; (also on itch: &lt;a href="https://nightlit.itch.io/efectazo" rel="noopener noreferrer"&gt;https://nightlit.itch.io/efectazo&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;I'd love to hear how it runs on your device and how the curl feels. Happy to go deeper on any part in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>threejs</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built a no-sign-up glow-up challenge tracker as a single-file PWA</title>
      <dc:creator>Jacko (Nightlit)</dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:17:43 +0000</pubDate>
      <link>https://dev.to/playnightlit/i-built-a-no-sign-up-glow-up-challenge-tracker-as-a-single-file-pwa-26ec</link>
      <guid>https://dev.to/playnightlit/i-built-a-no-sign-up-glow-up-challenge-tracker-as-a-single-file-pwa-26ec</guid>
      <description>&lt;p&gt;"Glow-up challenges" (30, 60, 75 or 90 days of small daily habits) are all over TikTok, but most trackers for them want an account, a subscription and your progress photos on their servers before you've checked off day one.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://glowstreak.pages.dev" rel="noopener noreferrer"&gt;GlowStreak&lt;/a&gt;&lt;/strong&gt;: pick a challenge, tap your habits each day, keep your streak, share a "Day X" story card. No sign-up, English and Spanish, installs to your home screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints I set
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No backend.&lt;/strong&gt; Everything lives in &lt;code&gt;localStorage&lt;/code&gt; on the device. No accounts means no password resets, no GDPR export requests, and nothing to breach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Photos never leave the phone.&lt;/strong&gt; Progress photos are stored locally and only ever rendered into a share card when &lt;em&gt;you&lt;/em&gt; hit share.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One file.&lt;/strong&gt; The app is a single &lt;code&gt;index.html&lt;/code&gt; with inline CSS and JS, plus a manifest and a tiny service worker. It deploys to Cloudflare Pages in seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bilingual from day one.&lt;/strong&gt; All strings sit in one EN/ES dictionary; language defaults from &lt;code&gt;navigator.language&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bits that were fun
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Streaks without a server.&lt;/strong&gt; A day key is just &lt;code&gt;YYYY-MM-DD&lt;/code&gt; in local time, and a streak is "walk back from today while every habit is checked". Doing it in local time (not UTC) matters a lot when someone checks in at 11:50 pm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The share card.&lt;/strong&gt; It's drawn on a 1080×1920 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; (Stories size), turned into a &lt;code&gt;File&lt;/code&gt;, and handed to the Web Share API with &lt;code&gt;navigator.canShare({files})&lt;/code&gt;. On browsers that can't share files it falls back to a download.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feeling native.&lt;/strong&gt; Spring-y &lt;code&gt;cubic-bezier&lt;/code&gt; transitions, a progress ring with &lt;code&gt;stroke-dashoffset&lt;/code&gt;, small &lt;code&gt;navigator.vibrate&lt;/code&gt; taps on Android, and a confetti burst when the last habit of the day is checked. Small things, but they're what make people come back on day 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Presets
&lt;/h2&gt;

&lt;p&gt;Gentle 75 (miss a day, log it, keep going), Glow-Up 60, Balanced 75, Monk Mode 30, a 30-Day Walk, and Lock-In 90, or your own habits.&lt;/p&gt;

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

&lt;p&gt;👉 &lt;a href="https://glowstreak.pages.dev" rel="noopener noreferrer"&gt;https://glowstreak.pages.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love feedback on the onboarding flow and the share card. What would make you actually open a habit app on day 14?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>pwa</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built a colour-season analysis app as one HTML file (with a live camera "drape" mirror)</title>
      <dc:creator>Jacko (Nightlit)</dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:00:43 +0000</pubDate>
      <link>https://dev.to/playnightlit/i-built-a-colour-season-analysis-app-as-one-html-file-with-a-live-camera-drape-mirror-n2g</link>
      <guid>https://dev.to/playnightlit/i-built-a-colour-season-analysis-app-as-one-html-file-with-a-live-camera-drape-mirror-n2g</guid>
      <description>&lt;p&gt;Colour analysis is huge on TikTok right now: people hold coloured fabric ("drapes") under their face to see whether they're a Soft Summer, a Deep Winter, and so on. The paid apps in this space do very well, but most of them want an account, an app install, or a selfie uploaded to a server before you see anything.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Paleta&lt;/strong&gt; (&lt;a href="https://paletacolor.com):" rel="noopener noreferrer"&gt;https://paletacolor.com):&lt;/a&gt; a free colour-season quiz plus a live camera drape mirror, in English and Spanish, that runs entirely in the browser.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A short quiz maps you to one of the 12 seasons (rule-based scoring, not AI).&lt;/li&gt;
&lt;li&gt;You get your season, your 6 best colours and a shareable palette card.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;drape mirror&lt;/strong&gt; opens your front camera and frames your face with a band of colour, so you can flip between palettes and see which shades brighten your skin and which make you look tired.&lt;/li&gt;
&lt;li&gt;No sign-up. The camera stream never leaves the device.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One self-contained &lt;code&gt;index.html&lt;/code&gt;.&lt;/strong&gt; HTML, CSS, JS, fonts (subsetted woff2) and season data all inline. No framework, no build step for the core app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera mirror:&lt;/strong&gt; &lt;code&gt;getUserMedia({ video: { facingMode: 'user' } })&lt;/code&gt; feeds a hidden &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;, and each frame is centre-cropped, mirrored and drawn to a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; with the colour drape painted around the face. No ML, no uploads, so it stays smooth on cheap phones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share card:&lt;/strong&gt; drawn on a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; and exported with &lt;code&gt;toBlob()&lt;/code&gt;, then handed to the Web Share API (&lt;code&gt;navigator.share({ files })&lt;/code&gt;) with a download fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;i18n:&lt;/strong&gt; a tiny dictionary object with EN/ES strings, language picked from &lt;code&gt;navigator.language&lt;/code&gt; and switchable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PWA:&lt;/strong&gt; a manifest plus a small service worker that caches the single page, so it opens offline and installs to the home screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Cloudflare Pages, free tier, deployed through the Pages direct-upload API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; an optional $4.99 one-time "Plus" unlock uses a Stripe Payment Link that redirects back with a flag. No backend at all (yes, that's spoofable; for a $5 unlock I'm fine with it for now).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Mobile Safari needs &lt;code&gt;playsinline&lt;/code&gt; and &lt;code&gt;muted&lt;/code&gt; on the &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; or the camera preview goes fullscreen.&lt;/li&gt;
&lt;li&gt;Lighting matters more than any algorithm. The app tells you to face a window in daylight, which improves results more than any code could.&lt;/li&gt;
&lt;li&gt;Bilingual from day one is cheap when the whole app is one file, and the Spanish-speaking colour-analysis community is underserved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try it here: &lt;a href="https://paletacolor.com" rel="noopener noreferrer"&gt;https://paletacolor.com&lt;/a&gt;. I'd love feedback, especially on the camera mirror on different phones.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>pwa</category>
    </item>
    <item>
      <title>I built a free Splitwise alternative that needs no sign-up</title>
      <dc:creator>Jacko (Nightlit)</dc:creator>
      <pubDate>Wed, 21 Oct 2026 00:49:27 +0000</pubDate>
      <link>https://dev.to/playnightlit/i-built-a-free-splitwise-alternative-that-needs-no-sign-up-bp3</link>
      <guid>https://dev.to/playnightlit/i-built-a-free-splitwise-alternative-that-needs-no-sign-up-bp3</guid>
      <description>&lt;p&gt;Every group trip I've been on ends the same way: someone opens a spreadsheet, someone else says "I'll pay you back later", and two weeks later nobody remembers who paid for the Airbnb. Splitwise solves this, but its free tier now caps how many expenses you can add per day, and everyone in the group has to make an account.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Parejo&lt;/strong&gt;: &lt;a href="https://getparejo.com" rel="noopener noreferrer"&gt;https://getparejo.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a small web app (works on any phone, installable as a PWA) for splitting trip and group costs. I'm Jacko, an indie maker from Mexico City, and this is one of a few small free apps I've shipped this month.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No sign-up.&lt;/strong&gt; Open the page, create a group, add people, add expenses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited expenses&lt;/strong&gt; on the free plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any currency.&lt;/strong&gt; Pay dinner in euros and the hotel in dollars; it converts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fewest payments to settle up.&lt;/strong&gt; Instead of everyone paying everyone, it works out a short list of "A pays B" transfers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share by link.&lt;/strong&gt; Friends open the link and see the same balances, and can add their own expenses and merge them back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp-friendly summary image&lt;/strong&gt; of who owes whom.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;English and Spanish.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A few technical choices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No backend, no accounts.&lt;/strong&gt; Groups live in your browser's localStorage, and receipt photos (a Pro feature) stay in IndexedDB on your device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The whole group lives in the share link.&lt;/strong&gt; The group is packed to compact JSON, compressed with &lt;code&gt;deflate-raw&lt;/code&gt; (the browser's &lt;code&gt;CompressionStream&lt;/code&gt;), base64url-encoded and put in the URL &lt;em&gt;fragment&lt;/em&gt; (&lt;code&gt;#g=...&lt;/code&gt;). Fragments are never sent to the server, so sharing a group doesn't upload anything. When a friend adds expenses and sends their link back, the two copies merge as a union by member/expense id, with tombstones for deletions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Money in integer cents.&lt;/strong&gt; Every amount is stored in cents, and splits allocate the leftover cent deterministically so balances always sum to exactly zero. The core money logic is a separate pure module with tests, including a fuzz test that generates 400 random groups and checks that every one settles to zero in at most n−1 transfers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settling up.&lt;/strong&gt; It's a greedy match: sort debtors and creditors by amount and repeatedly pair the largest debtor with the largest creditor. Not provably minimal in every case (that's a hard problem), but it never needs more than n−1 payments and in practice it's what you'd do by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing (honest version)
&lt;/h2&gt;

&lt;p&gt;Splitting is free with no limits. There's an optional one-time &lt;strong&gt;Trip Pass ($4.99)&lt;/strong&gt; for one group or &lt;strong&gt;Lifetime ($14.99)&lt;/strong&gt; that adds receipt photos, CSV/PDF export, custom categories and removes the "Made with Parejo" watermark on the summary image. No subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it / tell me what's broken
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://getparejo.com" rel="noopener noreferrer"&gt;https://getparejo.com&lt;/a&gt; (there's a "See an example" button if you just want to poke around).&lt;/p&gt;

&lt;p&gt;I'd love feedback, especially on the settle-up screen and the share-link flow. What would make you switch from whatever your group uses now?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
