Vibecode Shots.so
track this build6 phases, 14 steps, beginner friendly0%Gradient background, padding, rounded corners, export. Pure client-side canvas work; there is no server to even build.
You are building a lean indie version of Shots.so.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Shots.so · indie build
A screenshot beautifier as one static page: drop, paste or pick a screenshot, adjust padding, corners, shadow, background and canvas ratio with live preview, export a crisp PNG at 1x or 2x or copy it to the clipboard. Everything happens in the browser; nothing is uploaded; it works offline after the first load.
Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Everything | One HTML file, vanilla JS, Canvas 2D | no framework, no build step, no server, no dependency to break |
| Hosting | Any static host, or open the file | there is nothing to run |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **A Chromium browser and Safari** · free
- Why: Clipboard image copying behaves differently in Safari; Phase 5 must be tested in both.
- Get it: Chrome or Edge, plus Safari if you have a Mac. Without Safari, note the clipboard code path is untested there.
- [ ] **A few screenshots to test with** · free
- Why: Including one very tall one and one wider than 4000px, to exercise ratios and performance.
- Get it: Take three: a normal window, a full-page tall capture, a large retina one.
- [ ] **A static host (optional)** (optional) · free
- Why: To share it with others. Locally, opening the file is enough.
- Get it: Cloudflare Pages, Netlify or GitHub Pages: connect the repo, done.
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
## Quick start
Follow Phase 1 in `BUILD_PLAN.md`; it creates the project.
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- A template and device-frame library. Keeping frames current is real ongoing work and a real reason to pay.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here.
- their template/mockup library
- device frames kept up to date
- batch processing
If one of those is essential to you, that is the reason to keep paying for Shots.so, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Shots.so
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a screenshot beautifier like Shots or Xnapper, as a single static page.
Build it in phases, in the order below. Do not write the whole tool in one pass.
Finish a phase, run its "Done when" check, fix what fails, and only then start
the next phase.
### Stack (fixed, do not substitute)
- One HTML file, vanilla JS, Canvas 2D. No React, no framework, no build step,
no dependencies, no server.
- Nothing ever leaves the browser · no uploads, no analytics, no fonts fetched at
runtime. The page must work fully offline after first load.
### Data model (decide this before Phase 1)
A single `state` object drives every render: `{ image, padding, radius, shadow,
background, ratio, scale }`. Every control writes to `state` and calls one
`render()`. There is exactly one render function and one source of truth · the
moment two code paths draw to the canvas, the controls start disagreeing with
the export, which is the classic bug in this tool.
### Phase 1 · Image input and render loop
Build: three input paths · drag-and-drop onto the page, paste from clipboard
(`paste` event, read `clipboardData.items`), and a file picker. Then `render()`
drawing the image centered on the canvas at correct aspect ratio. Handle the
device pixel ratio from the start: the canvas backing store is
`cssSize * devicePixelRatio` and the context is scaled once. Retrofitting this
later means re-tuning every offset.
Done when: all three input paths load the same image identically, the render is
crisp on a HiDPI display, and a 5000px-wide screenshot loads without freezing the
page.
Do not build yet: any control, any background, export.
### Phase 2 · Geometry controls
Build: padding, corner radius and shadow, each a live slider in a sidebar with no
Apply button. Clip the rounded corners with a real path (`roundRect`), not a CSS
overlay, or the export will not match the preview. Draw the shadow with canvas
shadow properties on the clipped shape.
Done when: every slider updates the preview within one frame, the corners are
genuinely transparent at the edges, and the shadow follows the rounded shape
rather than the bounding box.
### Phase 3 · Backgrounds
Build: a picker with about ten presets · a mix of linear gradients, mesh-ish
multi-stop gradients and solid colors, plus a custom color input and a
transparent option. Make the defaults genuinely beautiful; the first export
should look premium before the user touches anything, because most users will
never touch anything.
Done when: every preset renders correctly at every canvas ratio, and the
transparent option exports a PNG with a real alpha channel rather than white.
### Phase 4 · Canvas ratios
Build: a ratio selector · original, 16:9, 4:3, 1:1, and 2:1 for social cards. The
image scales to fit within the padding without ever cropping or distorting.
Done when: switching ratios never crops the screenshot, a very tall screenshot
still fits inside a 16:9 canvas, and the image stays centered in both axes.
### Phase 5 · Export
Build: PNG export at 1x and 2x, and copy-to-clipboard. Export by rendering to an
offscreen canvas at the target scale, never by upscaling the preview bitmap. For
the clipboard, always pass a `Promise<Blob>` into `ClipboardItem` rather than a
resolved Blob · Safari requires the promise form and Chromium accepts it, so one
code path works in both. Call `navigator.clipboard.write` synchronously inside
the click handler or the user gesture is lost and the write is rejected.
Done when: the 2x export is exactly twice the pixel dimensions and visibly
sharper (not a blurry upscale), the exported file matches the preview pixel for
pixel in layout, and paste works in both Safari and a Chromium browser.
### Phase 6 · Polish
Build: keyboard shortcuts (paste, copy, export), an empty state that explains the
three input methods, a reset button, remembering the last settings in
`localStorage` wrapped in try/catch, and a visible "nothing is uploaded" note.
Done when: the page loads and works with the network disabled, and a first-time
user knows what to do without instructions.
### Out of scope (and why)
- A template and device-frame library. Keeping device frames current as hardware
ships is real ongoing work and it is a genuine reason to pay for the app.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here, which is the point.
### README must contain
- One line: everything happens in the browser, no image is ever uploaded.
- How to run it · open the file, or serve the folder from any static host.
===== AGENTS.md =====
# Agent instructions · Shots.so indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: One HTML file, vanilla JS, Canvas 2D, Any static host, or open the file. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
===== BUILD_PLAN.md =====
# Build plan · Shots.so
A screenshot beautifier as one static page: drop, paste or pick a screenshot, adjust padding, corners, shadow, background and canvas ratio with live preview, export a crisp PNG at 1x or 2x or copy it to the clipboard. Everything happens in the browser; nothing is uploaded; it works offline after the first load.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Image input and render loop
Three ways in, one render function, correct on HiDPI from the start.
### Steps
1. Create index.html with a canvas, a sidebar and one state object
state = { image, padding, radius, shadow, background, ratio, scale }. Every control writes to state and calls render(). Exactly one render function; two code paths drawing to the canvas is the classic bug here.
Files: `index.html`
2. Wire drag-and-drop, paste and a file picker
paste: read clipboardData.items for an image type. All three create an Image from a Blob URL and set state.image.
3. Handle devicePixelRatio in render()
Backing store = css size times devicePixelRatio; ctx.scale once. Retrofitting this later means re-tuning every offset.
### Done when
- [ ] All three input paths load the same image identically
- [ ] The render is crisp on a HiDPI display
- [ ] A 5000px-wide screenshot loads without freezing the page
## Phase 2 · Geometry controls
Padding, radius and shadow as live sliders whose export matches the preview.
### Steps
1. Add the three sliders
Range inputs bound to state; render on input.
2. Clip rounded corners with a real path
ctx.roundRect and clip, not a CSS overlay, or the export will not match the preview. Draw the shadow with canvas shadow properties on the clipped shape.
### Done when
- [ ] Every slider updates the preview within one frame
- [ ] Corners are genuinely transparent at the edges
- [ ] The shadow follows the rounded shape, not the bounding box
## Phase 3 · Backgrounds
Ten presets that look premium before anyone touches anything.
### Steps
1. Add a preset picker with gradients, multi-stop meshes and solids
Plus a custom colour input and a transparent option.
2. Make the default the best one
Most users never change it.
### Done when
- [ ] Every preset renders correctly at every ratio
- [ ] The transparent option exports a PNG with a real alpha channel
## Phase 4 · Canvas ratios
Fit without cropping or distortion.
### Steps
1. Add the ratio selector: original, 16:9, 4:3, 1:1 and 2:1
Store it in state; a change triggers one render().
2. Fit the image inside the padding without cropping and centre it on both axes
Compute the scale as the minimum of available width over image width and available height over image height. Never crop, never distort.
### Done when
- [ ] Switching ratios never crops the screenshot
- [ ] A very tall screenshot fits inside a 16:9 canvas
- [ ] The image stays centred
## Phase 5 · Export
Sharp PNG at 1x and 2x, and clipboard that works in both engines.
### Steps
1. Export by rendering to an offscreen canvas at the target scale
Never upscale the preview bitmap.
2. Copy to clipboard the portable way
Pass a Promise resolving to a Blob into ClipboardItem; Safari requires the promise form and Chromium accepts it. Call navigator.clipboard.write synchronously inside the click handler.
### Done when
- [ ] The 2x export is exactly twice the pixels and visibly sharper
- [ ] The exported file matches the preview pixel for pixel in layout
- [ ] Paste works in both Safari and a Chromium browser
## Phase 6 · Polish
Shortcuts, an empty state, memory, offline.
### Steps
1. Add keyboard shortcuts, reset, and remembered settings in localStorage wrapped in try/catch
2. Write the empty state and a visible nothing-is-uploaded note
3. Confirm offline: no external requests at all
Open DevTools > Network, reload with Offline checked.
### Done when
- [ ] The page works with the network disabled
- [ ] A first-time user knows what to do without instructions
## Not in this build
- A template and device-frame library. Keeping frames current is real ongoing work and a real reason to pay.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here.
## After v1, if you want it
- Device frames as SVG overlays you maintain yourself
- A share-target so phones can send screenshots straight in
===== .env.example =====
# This build needs no environment variables. Add one here the moment a
# phase introduces it, with a comment saying where the value comes from.
You are building a lean indie version of Shots.so.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Shots.so · indie build
A screenshot beautifier as one static page: drop, paste or pick a screenshot, adjust padding, corners, shadow, background and canvas ratio with live preview, export a crisp PNG at 1x or 2x or copy it to the clipboard. Everything happens in the browser; nothing is uploaded; it works offline after the first load.
Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Everything | One HTML file, vanilla JS, Canvas 2D | no framework, no build step, no server, no dependency to break |
| Hosting | Any static host, or open the file | there is nothing to run |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **A Chromium browser and Safari** · free
- Why: Clipboard image copying behaves differently in Safari; Phase 5 must be tested in both.
- Get it: Chrome or Edge, plus Safari if you have a Mac. Without Safari, note the clipboard code path is untested there.
- [ ] **A few screenshots to test with** · free
- Why: Including one very tall one and one wider than 4000px, to exercise ratios and performance.
- Get it: Take three: a normal window, a full-page tall capture, a large retina one.
- [ ] **A static host (optional)** (optional) · free
- Why: To share it with others. Locally, opening the file is enough.
- Get it: Cloudflare Pages, Netlify or GitHub Pages: connect the repo, done.
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
## Quick start
Follow Phase 1 in `BUILD_PLAN.md`; it creates the project.
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- A template and device-frame library. Keeping frames current is real ongoing work and a real reason to pay.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here.
- their template/mockup library
- device frames kept up to date
- batch processing
If one of those is essential to you, that is the reason to keep paying for Shots.so, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Shots.so
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a screenshot beautifier like Shots or Xnapper, as a single static page.
Build it in phases, in the order below. Do not write the whole tool in one pass.
Finish a phase, run its "Done when" check, fix what fails, and only then start
the next phase.
### Stack (fixed, do not substitute)
- One HTML file, vanilla JS, Canvas 2D. No React, no framework, no build step,
no dependencies, no server.
- Nothing ever leaves the browser · no uploads, no analytics, no fonts fetched at
runtime. The page must work fully offline after first load.
### Data model (decide this before Phase 1)
A single `state` object drives every render: `{ image, padding, radius, shadow,
background, ratio, scale }`. Every control writes to `state` and calls one
`render()`. There is exactly one render function and one source of truth · the
moment two code paths draw to the canvas, the controls start disagreeing with
the export, which is the classic bug in this tool.
### Phase 1 · Image input and render loop
Build: three input paths · drag-and-drop onto the page, paste from clipboard
(`paste` event, read `clipboardData.items`), and a file picker. Then `render()`
drawing the image centered on the canvas at correct aspect ratio. Handle the
device pixel ratio from the start: the canvas backing store is
`cssSize * devicePixelRatio` and the context is scaled once. Retrofitting this
later means re-tuning every offset.
Done when: all three input paths load the same image identically, the render is
crisp on a HiDPI display, and a 5000px-wide screenshot loads without freezing the
page.
Do not build yet: any control, any background, export.
### Phase 2 · Geometry controls
Build: padding, corner radius and shadow, each a live slider in a sidebar with no
Apply button. Clip the rounded corners with a real path (`roundRect`), not a CSS
overlay, or the export will not match the preview. Draw the shadow with canvas
shadow properties on the clipped shape.
Done when: every slider updates the preview within one frame, the corners are
genuinely transparent at the edges, and the shadow follows the rounded shape
rather than the bounding box.
### Phase 3 · Backgrounds
Build: a picker with about ten presets · a mix of linear gradients, mesh-ish
multi-stop gradients and solid colors, plus a custom color input and a
transparent option. Make the defaults genuinely beautiful; the first export
should look premium before the user touches anything, because most users will
never touch anything.
Done when: every preset renders correctly at every canvas ratio, and the
transparent option exports a PNG with a real alpha channel rather than white.
### Phase 4 · Canvas ratios
Build: a ratio selector · original, 16:9, 4:3, 1:1, and 2:1 for social cards. The
image scales to fit within the padding without ever cropping or distorting.
Done when: switching ratios never crops the screenshot, a very tall screenshot
still fits inside a 16:9 canvas, and the image stays centered in both axes.
### Phase 5 · Export
Build: PNG export at 1x and 2x, and copy-to-clipboard. Export by rendering to an
offscreen canvas at the target scale, never by upscaling the preview bitmap. For
the clipboard, always pass a `Promise<Blob>` into `ClipboardItem` rather than a
resolved Blob · Safari requires the promise form and Chromium accepts it, so one
code path works in both. Call `navigator.clipboard.write` synchronously inside
the click handler or the user gesture is lost and the write is rejected.
Done when: the 2x export is exactly twice the pixel dimensions and visibly
sharper (not a blurry upscale), the exported file matches the preview pixel for
pixel in layout, and paste works in both Safari and a Chromium browser.
### Phase 6 · Polish
Build: keyboard shortcuts (paste, copy, export), an empty state that explains the
three input methods, a reset button, remembering the last settings in
`localStorage` wrapped in try/catch, and a visible "nothing is uploaded" note.
Done when: the page loads and works with the network disabled, and a first-time
user knows what to do without instructions.
### Out of scope (and why)
- A template and device-frame library. Keeping device frames current as hardware
ships is real ongoing work and it is a genuine reason to pay for the app.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here, which is the point.
### README must contain
- One line: everything happens in the browser, no image is ever uploaded.
- How to run it · open the file, or serve the folder from any static host.
===== AGENTS.md =====
# Agent instructions · Shots.so indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: One HTML file, vanilla JS, Canvas 2D, Any static host, or open the file. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
===== BUILD_PLAN.md =====
# Build plan · Shots.so
A screenshot beautifier as one static page: drop, paste or pick a screenshot, adjust padding, corners, shadow, background and canvas ratio with live preview, export a crisp PNG at 1x or 2x or copy it to the clipboard. Everything happens in the browser; nothing is uploaded; it works offline after the first load.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Image input and render loop
Three ways in, one render function, correct on HiDPI from the start.
### Steps
1. Create index.html with a canvas, a sidebar and one state object
state = { image, padding, radius, shadow, background, ratio, scale }. Every control writes to state and calls render(). Exactly one render function; two code paths drawing to the canvas is the classic bug here.
Files: `index.html`
2. Wire drag-and-drop, paste and a file picker
paste: read clipboardData.items for an image type. All three create an Image from a Blob URL and set state.image.
3. Handle devicePixelRatio in render()
Backing store = css size times devicePixelRatio; ctx.scale once. Retrofitting this later means re-tuning every offset.
### Done when
- [ ] All three input paths load the same image identically
- [ ] The render is crisp on a HiDPI display
- [ ] A 5000px-wide screenshot loads without freezing the page
## Phase 2 · Geometry controls
Padding, radius and shadow as live sliders whose export matches the preview.
### Steps
1. Add the three sliders
Range inputs bound to state; render on input.
2. Clip rounded corners with a real path
ctx.roundRect and clip, not a CSS overlay, or the export will not match the preview. Draw the shadow with canvas shadow properties on the clipped shape.
### Done when
- [ ] Every slider updates the preview within one frame
- [ ] Corners are genuinely transparent at the edges
- [ ] The shadow follows the rounded shape, not the bounding box
## Phase 3 · Backgrounds
Ten presets that look premium before anyone touches anything.
### Steps
1. Add a preset picker with gradients, multi-stop meshes and solids
Plus a custom colour input and a transparent option.
2. Make the default the best one
Most users never change it.
### Done when
- [ ] Every preset renders correctly at every ratio
- [ ] The transparent option exports a PNG with a real alpha channel
## Phase 4 · Canvas ratios
Fit without cropping or distortion.
### Steps
1. Add the ratio selector: original, 16:9, 4:3, 1:1 and 2:1
Store it in state; a change triggers one render().
2. Fit the image inside the padding without cropping and centre it on both axes
Compute the scale as the minimum of available width over image width and available height over image height. Never crop, never distort.
### Done when
- [ ] Switching ratios never crops the screenshot
- [ ] A very tall screenshot fits inside a 16:9 canvas
- [ ] The image stays centred
## Phase 5 · Export
Sharp PNG at 1x and 2x, and clipboard that works in both engines.
### Steps
1. Export by rendering to an offscreen canvas at the target scale
Never upscale the preview bitmap.
2. Copy to clipboard the portable way
Pass a Promise resolving to a Blob into ClipboardItem; Safari requires the promise form and Chromium accepts it. Call navigator.clipboard.write synchronously inside the click handler.
### Done when
- [ ] The 2x export is exactly twice the pixels and visibly sharper
- [ ] The exported file matches the preview pixel for pixel in layout
- [ ] Paste works in both Safari and a Chromium browser
## Phase 6 · Polish
Shortcuts, an empty state, memory, offline.
### Steps
1. Add keyboard shortcuts, reset, and remembered settings in localStorage wrapped in try/catch
2. Write the empty state and a visible nothing-is-uploaded note
3. Confirm offline: no external requests at all
Open DevTools > Network, reload with Offline checked.
### Done when
- [ ] The page works with the network disabled
- [ ] A first-time user knows what to do without instructions
## Not in this build
- A template and device-frame library. Keeping frames current is real ongoing work and a real reason to pay.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here.
## After v1, if you want it
- Device frames as SVG overlays you maintain yourself
- A share-target so phones can send screenshots straight in
===== .env.example =====
# This build needs no environment variables. Add one here the moment a
# phase introduces it, with a comment saying where the value comes from.
You are building a production product version of Shots.so.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== PRODUCT.md =====
# Shots.so · product brief
## Problem
Gradient background, padding, rounded corners, export. Pure client-side canvas work; there is no server to even build.
## Product outcome
An installable, offline screenshot tool with a defensible privacy statement: the code is the policy.
## Target user
A builder who needs a maintainable product foundation, not a one-off demo.
## Required capabilities
- Implement the core workflow described in ARCHITECTURE.md
## Explicit non-goals for v1
- A template and device-frame library. Keeping frames current is real ongoing work and a real reason to pay.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here.
- their template/mockup library
- device frames kept up to date
- batch processing
## Success criteria
- Zero network requests after first load
- Clipboard copy verified in Safari and Chromium
- Installable as a PWA
===== BRIEF.md =====
# Build brief · Shots.so
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a screenshot beautifier like Shots or Xnapper, as a single static page.
Build it in phases, in the order below. Do not write the whole tool in one pass.
Finish a phase, run its "Done when" check, fix what fails, and only then start
the next phase.
### Stack (fixed, do not substitute)
- One HTML file, vanilla JS, Canvas 2D. No React, no framework, no build step,
no dependencies, no server.
- Nothing ever leaves the browser · no uploads, no analytics, no fonts fetched at
runtime. The page must work fully offline after first load.
### Data model (decide this before Phase 1)
A single `state` object drives every render: `{ image, padding, radius, shadow,
background, ratio, scale }`. Every control writes to `state` and calls one
`render()`. There is exactly one render function and one source of truth · the
moment two code paths draw to the canvas, the controls start disagreeing with
the export, which is the classic bug in this tool.
### Phase 1 · Image input and render loop
Build: three input paths · drag-and-drop onto the page, paste from clipboard
(`paste` event, read `clipboardData.items`), and a file picker. Then `render()`
drawing the image centered on the canvas at correct aspect ratio. Handle the
device pixel ratio from the start: the canvas backing store is
`cssSize * devicePixelRatio` and the context is scaled once. Retrofitting this
later means re-tuning every offset.
Done when: all three input paths load the same image identically, the render is
crisp on a HiDPI display, and a 5000px-wide screenshot loads without freezing the
page.
Do not build yet: any control, any background, export.
### Phase 2 · Geometry controls
Build: padding, corner radius and shadow, each a live slider in a sidebar with no
Apply button. Clip the rounded corners with a real path (`roundRect`), not a CSS
overlay, or the export will not match the preview. Draw the shadow with canvas
shadow properties on the clipped shape.
Done when: every slider updates the preview within one frame, the corners are
genuinely transparent at the edges, and the shadow follows the rounded shape
rather than the bounding box.
### Phase 3 · Backgrounds
Build: a picker with about ten presets · a mix of linear gradients, mesh-ish
multi-stop gradients and solid colors, plus a custom color input and a
transparent option. Make the defaults genuinely beautiful; the first export
should look premium before the user touches anything, because most users will
never touch anything.
Done when: every preset renders correctly at every canvas ratio, and the
transparent option exports a PNG with a real alpha channel rather than white.
### Phase 4 · Canvas ratios
Build: a ratio selector · original, 16:9, 4:3, 1:1, and 2:1 for social cards. The
image scales to fit within the padding without ever cropping or distorting.
Done when: switching ratios never crops the screenshot, a very tall screenshot
still fits inside a 16:9 canvas, and the image stays centered in both axes.
### Phase 5 · Export
Build: PNG export at 1x and 2x, and copy-to-clipboard. Export by rendering to an
offscreen canvas at the target scale, never by upscaling the preview bitmap. For
the clipboard, always pass a `Promise<Blob>` into `ClipboardItem` rather than a
resolved Blob · Safari requires the promise form and Chromium accepts it, so one
code path works in both. Call `navigator.clipboard.write` synchronously inside
the click handler or the user gesture is lost and the write is rejected.
Done when: the 2x export is exactly twice the pixel dimensions and visibly
sharper (not a blurry upscale), the exported file matches the preview pixel for
pixel in layout, and paste works in both Safari and a Chromium browser.
### Phase 6 · Polish
Build: keyboard shortcuts (paste, copy, export), an empty state that explains the
three input methods, a reset button, remembering the last settings in
`localStorage` wrapped in try/catch, and a visible "nothing is uploaded" note.
Done when: the page loads and works with the network disabled, and a first-time
user knows what to do without instructions.
### Out of scope (and why)
- A template and device-frame library. Keeping device frames current as hardware
ships is real ongoing work and it is a genuine reason to pay for the app.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here, which is the point.
### README must contain
- One line: everything happens in the browser, no image is ever uploaded.
- How to run it · open the file, or serve the folder from any static host.
===== ARCHITECTURE.md =====
# Architecture · Shots.so
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Everything | One HTML file, vanilla JS, Canvas 2D | no framework, no build step, no server, no dependency to break |
| Hosting | Any static host, or open the file | there is nothing to run |
## Modules
Each module has one owner concern and a documented way to replace it.
| Module | Owns | How to replace it |
| --- | --- | --- |
| State | the single state object and render() | The whole app is this; keep it one file |
| Inputs | drop, paste, picker | Add a share-target in the manifest later |
| Export | offscreen rendering and clipboard | Add WebP or JPEG output here only |
## Configuration
Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists.
- No environment variables are needed for this build.
## Production baseline
- Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives.
- Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised.
- Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts.
- Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists.
- Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path.
## Decision records
For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
===== AGENTS.md =====
# Agent instructions · Shots.so product build
- Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: One HTML file, vanilla JS, Canvas 2D, Any static host, or open the file.
- Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit.
- Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present.
- Never invent cryptography or silently weaken a requirement to make a check pass.
- Put every external service behind an interface with a deterministic fake for tests.
- Add migrations and rollback or recovery notes for every persistent data change.
- Log useful operational context without credentials, tokens, passwords or personal data.
- Update documentation and run every check before completing a milestone.
===== MILESTONES.md =====
# Delivery milestones · Shots.so
Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer.
## M1 · Image input and render loop
Three ways in, one render function, correct on HiDPI from the start.
### Steps
1. Create index.html with a canvas, a sidebar and one state object
state = { image, padding, radius, shadow, background, ratio, scale }. Every control writes to state and calls render(). Exactly one render function; two code paths drawing to the canvas is the classic bug here.
Files: `index.html`
2. Wire drag-and-drop, paste and a file picker
paste: read clipboardData.items for an image type. All three create an Image from a Blob URL and set state.image.
3. Handle devicePixelRatio in render()
Backing store = css size times devicePixelRatio; ctx.scale once. Retrofitting this later means re-tuning every offset.
### Done when
- [ ] All three input paths load the same image identically
- [ ] The render is crisp on a HiDPI display
- [ ] A 5000px-wide screenshot loads without freezing the page
## M2 · Geometry controls
Padding, radius and shadow as live sliders whose export matches the preview.
### Steps
1. Add the three sliders
Range inputs bound to state; render on input.
2. Clip rounded corners with a real path
ctx.roundRect and clip, not a CSS overlay, or the export will not match the preview. Draw the shadow with canvas shadow properties on the clipped shape.
### Done when
- [ ] Every slider updates the preview within one frame
- [ ] Corners are genuinely transparent at the edges
- [ ] The shadow follows the rounded shape, not the bounding box
## M3 · Backgrounds
Ten presets that look premium before anyone touches anything.
### Steps
1. Add a preset picker with gradients, multi-stop meshes and solids
Plus a custom colour input and a transparent option.
2. Make the default the best one
Most users never change it.
### Done when
- [ ] Every preset renders correctly at every ratio
- [ ] The transparent option exports a PNG with a real alpha channel
## M4 · Canvas ratios
Fit without cropping or distortion.
### Steps
1. Add the ratio selector: original, 16:9, 4:3, 1:1 and 2:1
Store it in state; a change triggers one render().
2. Fit the image inside the padding without cropping and centre it on both axes
Compute the scale as the minimum of available width over image width and available height over image height. Never crop, never distort.
### Done when
- [ ] Switching ratios never crops the screenshot
- [ ] A very tall screenshot fits inside a 16:9 canvas
- [ ] The image stays centred
## M5 · Export
Sharp PNG at 1x and 2x, and clipboard that works in both engines.
### Steps
1. Export by rendering to an offscreen canvas at the target scale
Never upscale the preview bitmap.
2. Copy to clipboard the portable way
Pass a Promise resolving to a Blob into ClipboardItem; Safari requires the promise form and Chromium accepts it. Call navigator.clipboard.write synchronously inside the click handler.
### Done when
- [ ] The 2x export is exactly twice the pixels and visibly sharper
- [ ] The exported file matches the preview pixel for pixel in layout
- [ ] Paste works in both Safari and a Chromium browser
## M6 · Polish
Shortcuts, an empty state, memory, offline.
### Steps
1. Add keyboard shortcuts, reset, and remembered settings in localStorage wrapped in try/catch
2. Write the empty state and a visible nothing-is-uploaded note
3. Confirm offline: no external requests at all
Open DevTools > Network, reload with Offline checked.
### Done when
- [ ] The page works with the network disabled
- [ ] A first-time user knows what to do without instructions
## M7 · Ship it as an app (production only)
Only for the product path: installable, offline-first, with a privacy statement.
### Steps
1. Add a web app manifest and a service worker that caches the one file
Files: `manifest.webmanifest`, `sw.js`
2. Deploy to a static host with a custom domain and a one-line privacy note
Nothing is uploaded; there is no analytics.
### Done when
- [ ] The site installs as an app and opens offline
- [ ] The deployed page makes zero network requests after first load
===== OPERATIONS.md =====
# Operations · Shots.so
## Backup
The repo is the backup. There is no state anywhere else.
## Restore
Redeploy from git.
Do a restore drill before the first real user, and write the date here when it passes.
## Monitoring
An uptime check on the URL. Nothing else can fail.
## Incident checklist
A broken deploy is reverted with git; there is no data to lose.
1. Contain the issue without destroying evidence or user data.
2. Record the timeline and affected scope.
3. Rotate exposed secrets and revoke compromised sessions or credentials.
4. Restore from a verified backup when needed.
5. Document the root cause, the remediation and the regression test.
## Release gate
- [ ] Zero network requests after first load
- [ ] Clipboard copy verified in Safari and Chromium
- [ ] Installable as a PWA
## Launch constraint
Do not market omitted Shots.so capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
===== .env.example =====
# This build needs no environment variables. Add one here the moment a
# phase introduces it, with a comment saying where the value comes from.
# Shots.so · indie build A screenshot beautifier as one static page: drop, paste or pick a screenshot, adjust padding, corners, shadow, background and canvas ratio with live preview, export a crisp PNG at 1x or 2x or copy it to the clipboard. Everything happens in the browser; nothing is uploaded; it works offline after the first load. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Everything | One HTML file, vanilla JS, Canvas 2D | no framework, no build step, no server, no dependency to break | | Hosting | Any static host, or open the file | there is nothing to run | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **A Chromium browser and Safari** · free - Why: Clipboard image copying behaves differently in Safari; Phase 5 must be tested in both. - Get it: Chrome or Edge, plus Safari if you have a Mac. Without Safari, note the clipboard code path is untested there. - [ ] **A few screenshots to test with** · free - Why: Including one very tall one and one wider than 4000px, to exercise ratios and performance. - Get it: Take three: a normal window, a full-page tall capture, a large retina one. - [ ] **A static host (optional)** (optional) · free - Why: To share it with others. Locally, opening the file is enough. - Get it: Cloudflare Pages, Netlify or GitHub Pages: connect the repo, done. - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A template and device-frame library. Keeping frames current is real ongoing work and a real reason to pay. - Batch processing and automatic text redaction. - Cloud sync of presets. There is no cloud here. - their template/mockup library - device frames kept up to date - batch processing If one of those is essential to you, that is the reason to keep paying for Shots.so, and the README should say so rather than pretend.
# Build brief · Shots.so
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a screenshot beautifier like Shots or Xnapper, as a single static page.
Build it in phases, in the order below. Do not write the whole tool in one pass.
Finish a phase, run its "Done when" check, fix what fails, and only then start
the next phase.
### Stack (fixed, do not substitute)
- One HTML file, vanilla JS, Canvas 2D. No React, no framework, no build step,
no dependencies, no server.
- Nothing ever leaves the browser · no uploads, no analytics, no fonts fetched at
runtime. The page must work fully offline after first load.
### Data model (decide this before Phase 1)
A single `state` object drives every render: `{ image, padding, radius, shadow,
background, ratio, scale }`. Every control writes to `state` and calls one
`render()`. There is exactly one render function and one source of truth · the
moment two code paths draw to the canvas, the controls start disagreeing with
the export, which is the classic bug in this tool.
### Phase 1 · Image input and render loop
Build: three input paths · drag-and-drop onto the page, paste from clipboard
(`paste` event, read `clipboardData.items`), and a file picker. Then `render()`
drawing the image centered on the canvas at correct aspect ratio. Handle the
device pixel ratio from the start: the canvas backing store is
`cssSize * devicePixelRatio` and the context is scaled once. Retrofitting this
later means re-tuning every offset.
Done when: all three input paths load the same image identically, the render is
crisp on a HiDPI display, and a 5000px-wide screenshot loads without freezing the
page.
Do not build yet: any control, any background, export.
### Phase 2 · Geometry controls
Build: padding, corner radius and shadow, each a live slider in a sidebar with no
Apply button. Clip the rounded corners with a real path (`roundRect`), not a CSS
overlay, or the export will not match the preview. Draw the shadow with canvas
shadow properties on the clipped shape.
Done when: every slider updates the preview within one frame, the corners are
genuinely transparent at the edges, and the shadow follows the rounded shape
rather than the bounding box.
### Phase 3 · Backgrounds
Build: a picker with about ten presets · a mix of linear gradients, mesh-ish
multi-stop gradients and solid colors, plus a custom color input and a
transparent option. Make the defaults genuinely beautiful; the first export
should look premium before the user touches anything, because most users will
never touch anything.
Done when: every preset renders correctly at every canvas ratio, and the
transparent option exports a PNG with a real alpha channel rather than white.
### Phase 4 · Canvas ratios
Build: a ratio selector · original, 16:9, 4:3, 1:1, and 2:1 for social cards. The
image scales to fit within the padding without ever cropping or distorting.
Done when: switching ratios never crops the screenshot, a very tall screenshot
still fits inside a 16:9 canvas, and the image stays centered in both axes.
### Phase 5 · Export
Build: PNG export at 1x and 2x, and copy-to-clipboard. Export by rendering to an
offscreen canvas at the target scale, never by upscaling the preview bitmap. For
the clipboard, always pass a `Promise<Blob>` into `ClipboardItem` rather than a
resolved Blob · Safari requires the promise form and Chromium accepts it, so one
code path works in both. Call `navigator.clipboard.write` synchronously inside
the click handler or the user gesture is lost and the write is rejected.
Done when: the 2x export is exactly twice the pixel dimensions and visibly
sharper (not a blurry upscale), the exported file matches the preview pixel for
pixel in layout, and paste works in both Safari and a Chromium browser.
### Phase 6 · Polish
Build: keyboard shortcuts (paste, copy, export), an empty state that explains the
three input methods, a reset button, remembering the last settings in
`localStorage` wrapped in try/catch, and a visible "nothing is uploaded" note.
Done when: the page loads and works with the network disabled, and a first-time
user knows what to do without instructions.
### Out of scope (and why)
- A template and device-frame library. Keeping device frames current as hardware
ships is real ongoing work and it is a genuine reason to pay for the app.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here, which is the point.
### README must contain
- One line: everything happens in the browser, no image is ever uploaded.
- How to run it · open the file, or serve the folder from any static host.# Agent instructions · Shots.so indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: One HTML file, vanilla JS, Canvas 2D, Any static host, or open the file. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
# Build plan · Shots.so
A screenshot beautifier as one static page: drop, paste or pick a screenshot, adjust padding, corners, shadow, background and canvas ratio with live preview, export a crisp PNG at 1x or 2x or copy it to the clipboard. Everything happens in the browser; nothing is uploaded; it works offline after the first load.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Image input and render loop
Three ways in, one render function, correct on HiDPI from the start.
### Steps
1. Create index.html with a canvas, a sidebar and one state object
state = { image, padding, radius, shadow, background, ratio, scale }. Every control writes to state and calls render(). Exactly one render function; two code paths drawing to the canvas is the classic bug here.
Files: `index.html`
2. Wire drag-and-drop, paste and a file picker
paste: read clipboardData.items for an image type. All three create an Image from a Blob URL and set state.image.
3. Handle devicePixelRatio in render()
Backing store = css size times devicePixelRatio; ctx.scale once. Retrofitting this later means re-tuning every offset.
### Done when
- [ ] All three input paths load the same image identically
- [ ] The render is crisp on a HiDPI display
- [ ] A 5000px-wide screenshot loads without freezing the page
## Phase 2 · Geometry controls
Padding, radius and shadow as live sliders whose export matches the preview.
### Steps
1. Add the three sliders
Range inputs bound to state; render on input.
2. Clip rounded corners with a real path
ctx.roundRect and clip, not a CSS overlay, or the export will not match the preview. Draw the shadow with canvas shadow properties on the clipped shape.
### Done when
- [ ] Every slider updates the preview within one frame
- [ ] Corners are genuinely transparent at the edges
- [ ] The shadow follows the rounded shape, not the bounding box
## Phase 3 · Backgrounds
Ten presets that look premium before anyone touches anything.
### Steps
1. Add a preset picker with gradients, multi-stop meshes and solids
Plus a custom colour input and a transparent option.
2. Make the default the best one
Most users never change it.
### Done when
- [ ] Every preset renders correctly at every ratio
- [ ] The transparent option exports a PNG with a real alpha channel
## Phase 4 · Canvas ratios
Fit without cropping or distortion.
### Steps
1. Add the ratio selector: original, 16:9, 4:3, 1:1 and 2:1
Store it in state; a change triggers one render().
2. Fit the image inside the padding without cropping and centre it on both axes
Compute the scale as the minimum of available width over image width and available height over image height. Never crop, never distort.
### Done when
- [ ] Switching ratios never crops the screenshot
- [ ] A very tall screenshot fits inside a 16:9 canvas
- [ ] The image stays centred
## Phase 5 · Export
Sharp PNG at 1x and 2x, and clipboard that works in both engines.
### Steps
1. Export by rendering to an offscreen canvas at the target scale
Never upscale the preview bitmap.
2. Copy to clipboard the portable way
Pass a Promise resolving to a Blob into ClipboardItem; Safari requires the promise form and Chromium accepts it. Call navigator.clipboard.write synchronously inside the click handler.
### Done when
- [ ] The 2x export is exactly twice the pixels and visibly sharper
- [ ] The exported file matches the preview pixel for pixel in layout
- [ ] Paste works in both Safari and a Chromium browser
## Phase 6 · Polish
Shortcuts, an empty state, memory, offline.
### Steps
1. Add keyboard shortcuts, reset, and remembered settings in localStorage wrapped in try/catch
2. Write the empty state and a visible nothing-is-uploaded note
3. Confirm offline: no external requests at all
Open DevTools > Network, reload with Offline checked.
### Done when
- [ ] The page works with the network disabled
- [ ] A first-time user knows what to do without instructions
## Not in this build
- A template and device-frame library. Keeping frames current is real ongoing work and a real reason to pay.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here.
## After v1, if you want it
- Device frames as SVG overlays you maintain yourself
- A share-target so phones can send screenshots straight in# This build needs no environment variables. Add one here the moment a # phase introduces it, with a comment saying where the value comes from.
# Shots.so · product brief ## Problem Gradient background, padding, rounded corners, export. Pure client-side canvas work; there is no server to even build. ## Product outcome An installable, offline screenshot tool with a defensible privacy statement: the code is the policy. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Implement the core workflow described in ARCHITECTURE.md ## Explicit non-goals for v1 - A template and device-frame library. Keeping frames current is real ongoing work and a real reason to pay. - Batch processing and automatic text redaction. - Cloud sync of presets. There is no cloud here. - their template/mockup library - device frames kept up to date - batch processing ## Success criteria - Zero network requests after first load - Clipboard copy verified in Safari and Chromium - Installable as a PWA
# Build brief · Shots.so
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a screenshot beautifier like Shots or Xnapper, as a single static page.
Build it in phases, in the order below. Do not write the whole tool in one pass.
Finish a phase, run its "Done when" check, fix what fails, and only then start
the next phase.
### Stack (fixed, do not substitute)
- One HTML file, vanilla JS, Canvas 2D. No React, no framework, no build step,
no dependencies, no server.
- Nothing ever leaves the browser · no uploads, no analytics, no fonts fetched at
runtime. The page must work fully offline after first load.
### Data model (decide this before Phase 1)
A single `state` object drives every render: `{ image, padding, radius, shadow,
background, ratio, scale }`. Every control writes to `state` and calls one
`render()`. There is exactly one render function and one source of truth · the
moment two code paths draw to the canvas, the controls start disagreeing with
the export, which is the classic bug in this tool.
### Phase 1 · Image input and render loop
Build: three input paths · drag-and-drop onto the page, paste from clipboard
(`paste` event, read `clipboardData.items`), and a file picker. Then `render()`
drawing the image centered on the canvas at correct aspect ratio. Handle the
device pixel ratio from the start: the canvas backing store is
`cssSize * devicePixelRatio` and the context is scaled once. Retrofitting this
later means re-tuning every offset.
Done when: all three input paths load the same image identically, the render is
crisp on a HiDPI display, and a 5000px-wide screenshot loads without freezing the
page.
Do not build yet: any control, any background, export.
### Phase 2 · Geometry controls
Build: padding, corner radius and shadow, each a live slider in a sidebar with no
Apply button. Clip the rounded corners with a real path (`roundRect`), not a CSS
overlay, or the export will not match the preview. Draw the shadow with canvas
shadow properties on the clipped shape.
Done when: every slider updates the preview within one frame, the corners are
genuinely transparent at the edges, and the shadow follows the rounded shape
rather than the bounding box.
### Phase 3 · Backgrounds
Build: a picker with about ten presets · a mix of linear gradients, mesh-ish
multi-stop gradients and solid colors, plus a custom color input and a
transparent option. Make the defaults genuinely beautiful; the first export
should look premium before the user touches anything, because most users will
never touch anything.
Done when: every preset renders correctly at every canvas ratio, and the
transparent option exports a PNG with a real alpha channel rather than white.
### Phase 4 · Canvas ratios
Build: a ratio selector · original, 16:9, 4:3, 1:1, and 2:1 for social cards. The
image scales to fit within the padding without ever cropping or distorting.
Done when: switching ratios never crops the screenshot, a very tall screenshot
still fits inside a 16:9 canvas, and the image stays centered in both axes.
### Phase 5 · Export
Build: PNG export at 1x and 2x, and copy-to-clipboard. Export by rendering to an
offscreen canvas at the target scale, never by upscaling the preview bitmap. For
the clipboard, always pass a `Promise<Blob>` into `ClipboardItem` rather than a
resolved Blob · Safari requires the promise form and Chromium accepts it, so one
code path works in both. Call `navigator.clipboard.write` synchronously inside
the click handler or the user gesture is lost and the write is rejected.
Done when: the 2x export is exactly twice the pixel dimensions and visibly
sharper (not a blurry upscale), the exported file matches the preview pixel for
pixel in layout, and paste works in both Safari and a Chromium browser.
### Phase 6 · Polish
Build: keyboard shortcuts (paste, copy, export), an empty state that explains the
three input methods, a reset button, remembering the last settings in
`localStorage` wrapped in try/catch, and a visible "nothing is uploaded" note.
Done when: the page loads and works with the network disabled, and a first-time
user knows what to do without instructions.
### Out of scope (and why)
- A template and device-frame library. Keeping device frames current as hardware
ships is real ongoing work and it is a genuine reason to pay for the app.
- Batch processing and automatic text redaction.
- Cloud sync of presets. There is no cloud here, which is the point.
### README must contain
- One line: everything happens in the browser, no image is ever uploaded.
- How to run it · open the file, or serve the folder from any static host.# Architecture · Shots.so ## Stack | Part | Choice | Why | | --- | --- | --- | | Everything | One HTML file, vanilla JS, Canvas 2D | no framework, no build step, no server, no dependency to break | | Hosting | Any static host, or open the file | there is nothing to run | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | State | the single state object and render() | The whole app is this; keep it one file | | Inputs | drop, paste, picker | Add a share-target in the manifest later | | Export | offscreen rendering and clipboard | Add WebP or JPEG output here only | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - No environment variables are needed for this build. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Shots.so product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: One HTML file, vanilla JS, Canvas 2D, Any static host, or open the file. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone.
# Delivery milestones · Shots.so
Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer.
## M1 · Image input and render loop
Three ways in, one render function, correct on HiDPI from the start.
### Steps
1. Create index.html with a canvas, a sidebar and one state object
state = { image, padding, radius, shadow, background, ratio, scale }. Every control writes to state and calls render(). Exactly one render function; two code paths drawing to the canvas is the classic bug here.
Files: `index.html`
2. Wire drag-and-drop, paste and a file picker
paste: read clipboardData.items for an image type. All three create an Image from a Blob URL and set state.image.
3. Handle devicePixelRatio in render()
Backing store = css size times devicePixelRatio; ctx.scale once. Retrofitting this later means re-tuning every offset.
### Done when
- [ ] All three input paths load the same image identically
- [ ] The render is crisp on a HiDPI display
- [ ] A 5000px-wide screenshot loads without freezing the page
## M2 · Geometry controls
Padding, radius and shadow as live sliders whose export matches the preview.
### Steps
1. Add the three sliders
Range inputs bound to state; render on input.
2. Clip rounded corners with a real path
ctx.roundRect and clip, not a CSS overlay, or the export will not match the preview. Draw the shadow with canvas shadow properties on the clipped shape.
### Done when
- [ ] Every slider updates the preview within one frame
- [ ] Corners are genuinely transparent at the edges
- [ ] The shadow follows the rounded shape, not the bounding box
## M3 · Backgrounds
Ten presets that look premium before anyone touches anything.
### Steps
1. Add a preset picker with gradients, multi-stop meshes and solids
Plus a custom colour input and a transparent option.
2. Make the default the best one
Most users never change it.
### Done when
- [ ] Every preset renders correctly at every ratio
- [ ] The transparent option exports a PNG with a real alpha channel
## M4 · Canvas ratios
Fit without cropping or distortion.
### Steps
1. Add the ratio selector: original, 16:9, 4:3, 1:1 and 2:1
Store it in state; a change triggers one render().
2. Fit the image inside the padding without cropping and centre it on both axes
Compute the scale as the minimum of available width over image width and available height over image height. Never crop, never distort.
### Done when
- [ ] Switching ratios never crops the screenshot
- [ ] A very tall screenshot fits inside a 16:9 canvas
- [ ] The image stays centred
## M5 · Export
Sharp PNG at 1x and 2x, and clipboard that works in both engines.
### Steps
1. Export by rendering to an offscreen canvas at the target scale
Never upscale the preview bitmap.
2. Copy to clipboard the portable way
Pass a Promise resolving to a Blob into ClipboardItem; Safari requires the promise form and Chromium accepts it. Call navigator.clipboard.write synchronously inside the click handler.
### Done when
- [ ] The 2x export is exactly twice the pixels and visibly sharper
- [ ] The exported file matches the preview pixel for pixel in layout
- [ ] Paste works in both Safari and a Chromium browser
## M6 · Polish
Shortcuts, an empty state, memory, offline.
### Steps
1. Add keyboard shortcuts, reset, and remembered settings in localStorage wrapped in try/catch
2. Write the empty state and a visible nothing-is-uploaded note
3. Confirm offline: no external requests at all
Open DevTools > Network, reload with Offline checked.
### Done when
- [ ] The page works with the network disabled
- [ ] A first-time user knows what to do without instructions
## M7 · Ship it as an app (production only)
Only for the product path: installable, offline-first, with a privacy statement.
### Steps
1. Add a web app manifest and a service worker that caches the one file
Files: `manifest.webmanifest`, `sw.js`
2. Deploy to a static host with a custom domain and a one-line privacy note
Nothing is uploaded; there is no analytics.
### Done when
- [ ] The site installs as an app and opens offline
- [ ] The deployed page makes zero network requests after first load# Operations · Shots.so ## Backup The repo is the backup. There is no state anywhere else. ## Restore Redeploy from git. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring An uptime check on the URL. Nothing else can fail. ## Incident checklist A broken deploy is reverted with git; there is no data to lose. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Zero network requests after first load - [ ] Clipboard copy verified in Safari and Chromium - [ ] Installable as a PWA ## Launch constraint Do not market omitted Shots.so capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# This build needs no environment variables. Add one here the moment a # phase introduces it, with a comment saying where the value comes from.
$ choose a build depth, inspect the files, then open the complete pack in your agent
xtheir template/mockup library
xdevice frames kept up to date
xbatch processing
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Shots.so →· no votes, no pay-to-list · just what's real
Shots.so pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| shots.so web app | $0 | $0 | Free web editor; no numeric export or project cap is publicly stated. |
| xnapper free | $0/user | $0/user | 1 Mac; watermark remains; no numeric screenshot cap is publicly stated. |
| xnapper basic | custom | — | 1 Mac; 1 year of updates. |
| xnapper personal | custom | — | 2 Macs; 1 year of updates. |
| xnapper standard | custom | — | 3 Macs; 1 year of updates. |
| xnapper team | — | $5/user | Priced per device; annual commitment; volume pricing above 100 devices is custom. |
free tierShots.so: no numeric usage cap published. Xnapper Free: 1 Mac, watermark, and no numeric screenshot cap published.
billingShots.so is free. Xnapper desktop personal licenses are one-time perpetual purchases with 1 year of updates; Xnapper Team is annual per device.
hidden costsXnapper keeps working after the included update year, but optional update renewal costs 60% of the original license price; the iOS app is a separate purchase; taxes can be added; teams above 100 devices require a quote.
verified 2026-08-12 · source ↗
Vibecode Shots.so / Xnapper
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Shots.so / Xnapper replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Shots.so / Xnapper cost?
Shots.so / Xnapper costs about $5/month (Xnapper Team, checked 2026-08-12), which is $60 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Shots.so / Xnapper?
Honestly: their template/mockup library; device frames kept up to date; batch processing. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Shots.so / Xnapper?
Yes: macshot (Gradients, padding, rounded corners and shadows in a native Mac app that also takes the screenshot.) BrandBird Screenshot Beautifier (Gradients, padding and rounded corners for free; batch work is where the bill starts.) ScreenCharm (The Shots.so recipe, free forever and done entirely in your browser.) All 3 curated free alternatives are at vibecodeit.com/shots/alternatives. The prompt is for when you want it exactly your way.