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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Polaroid
# Rewind

Arrange photos polaroid-style with **EXIF auto-captions** (city + date) and tile them onto **A4 sheets** for print-at-home. Private, client-side, free.
Arrange photos as Rewind Cards with **EXIF auto-captions** (city + date) and tile them onto **A4 sheets** for print-at-home. Private, client-side, free.

No upload, no account, no watermark — everything runs in your browser.

Expand All @@ -9,7 +9,7 @@ No upload, no account, no watermark — everything runs in your browser.
Existing tools each do a piece, but none combine all four:

- **EXIF auto-captions** — city + date filled in automatically (Canva makes you type every caption)
- **Smart A4 nesting** — maximize polaroids per sheet, with cut/crop marks for clean trimming
- **Smart A4 nesting** — maximize Rewind Cards per sheet, with cut/crop marks for clean trimming
- **Private & client-side** — photos never leave your device
- **Free & open source**

Expand Down Expand Up @@ -46,12 +46,12 @@ pnpm typecheck
The web app alone:

```bash
pnpm --filter @polaroid/web dev
pnpm --filter @rewind/web dev
```

## Roadmap

Tracked in [GitHub Issues](https://github.com/opencore-x/polaroid/issues) across two milestones: **Phase 1 — MVP** (upload → polaroid + EXIF captions → smart A4 tiling → sRGB PDF) and **Phase 2 — Polish** (multipage, drag-reorder, responsive).
Tracked in [GitHub Issues](https://github.com/opencore-x/rewind/issues) across two milestones: **Phase 1 — MVP** (upload → Rewind Card + EXIF captions → smart A4 tiling → sRGB PDF) and **Phase 2 — Polish** (multipage, drag-reorder, responsive).

## License

Expand Down
2 changes: 1 addition & 1 deletion apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta name="theme-color" content="#0a0a0a" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/icon-192.png" />
<title>Polaroid — print-at-home photo frames</title>
<title>Rewind — print-at-home photo frames</title>
<script>
// Apply the saved (or system) theme before paint to avoid a flash.
try {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@polaroid/web",
"name": "@rewind/web",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/a4-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function A4Preview() {
const setPageShape = useSettingsStore((state) => state.setPageShape);
const borderColor = useSettingsStore((state) => state.borderColor);
const borderWidth = useSettingsStore((state) => state.borderWidth);
const perRow = useSettingsStore((state) => state.polaroidsPerRow);
const perRow = useSettingsStore((state) => state.cardsPerRow);
const rows = useSettingsStore((state) => state.rowsPerPage);
const showCutMarks = useSettingsStore((state) => state.showCutMarks);
const showCaptions = useSettingsStore((state) => state.showCaptions);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/empty-hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
import { useAddPhotos } from "@/hooks/use-add-photos";
import { PHOTO_ACCEPT } from "@/lib/upload";

/** A few illustrative polaroids — pure CSS, no image assets, so it loads instantly. */
/** A few illustrative Rewind Cards — pure CSS, no image assets, so it loads instantly. */
const SAMPLES = [
{ tint: "linear-gradient(150deg, #f7c59f, #ee6c4d)", caption: "Lisbon · '24", rotate: -6 },
{ tint: "linear-gradient(150deg, #bcd4e6, #3d5a80)", caption: "Sunday market", rotate: 3 },
Expand Down Expand Up @@ -40,7 +40,7 @@ export function EmptyHero() {

<div className="flex max-w-md flex-col items-center gap-3">
<h2 className="font-display text-3xl leading-tight tracking-tight [font-variation-settings:'opsz'_144] sm:text-4xl">
Turn your photos into polaroids
Turn your photos into Rewind Cards
</h2>
<p className="text-muted-foreground text-sm leading-relaxed sm:text-base">
Drop in a few shots — captions fill themselves in from each photo's
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/options-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ export function OptionsPanel({ bare = false }: { bare?: boolean } = {}) {
const setBorderWidth = useSettingsStore((state) => state.setBorderWidth);
const sheetFormat = useSettingsStore((state) => state.sheetFormat);
const setSheetFormat = useSettingsStore((state) => state.setSheetFormat);
const perRow = useSettingsStore((state) => state.polaroidsPerRow);
const setPerRow = useSettingsStore((state) => state.setPolaroidsPerRow);
const perRow = useSettingsStore((state) => state.cardsPerRow);
const setPerRow = useSettingsStore((state) => state.setCardsPerRow);
const rows = useSettingsStore((state) => state.rowsPerPage);
const setRows = useSettingsStore((state) => state.setRowsPerPage);
const stripsPerRow = useSettingsStore((state) => state.stripsPerRow);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import { type CSSProperties } from 'react'
import { CroppedImage } from '@/components/cropped-image'
import { captionColors } from '@/lib/color'
import { type Orientation, orientationAspect } from '@/lib/crop'
import { POLAROID, cardAspect } from '@/lib/layout'
import { CARD, cardAspect } from '@/lib/layout'
import { type Photo } from '@/lib/photos'
import { cn } from '@/lib/utils'
import { usePhotoStore } from '@/stores/photo-store'

/**
* A polaroid sized proportionally to `width` (px) so it matches the exported
* A card sized proportionally to `width` (px) so it matches the exported
* PDF. On the editor sheet it's interactive: click to select, then drag/zoom the
* photo and edit captions right on the frame.
*/
export function SheetPolaroid({
export function SheetCard({
photo,
width,
shape,
Expand Down Expand Up @@ -96,7 +96,7 @@ export function SheetPolaroid({
placeholder="Add caption"
onChange={(value) => setCaption(photo.id, 'captionTop', value)}
style={{
fontSize: width * POLAROID.captionTopSize,
fontSize: width * CARD.captionTopSize,
lineHeight: 1.1,
color: ink.top,
}}
Expand All @@ -107,7 +107,7 @@ export function SheetPolaroid({
placeholder="Date"
onChange={(value) => setCaption(photo.id, 'captionBottom', value)}
style={{
fontSize: width * POLAROID.captionBottomSize,
fontSize: width * CARD.captionBottomSize,
lineHeight: 1.1,
color: ink.bottom,
}}
Expand Down Expand Up @@ -146,7 +146,7 @@ function CaptionLine({
type="text"
value={value}
placeholder={placeholder}
aria-label="Polaroid caption"
aria-label="Card caption"
onClick={(event) => event.stopPropagation()}
onChange={(event) => onChange(event.target.value)}
className="w-full bg-transparent text-center placeholder:text-neutral-300 focus:outline-none"
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/sheet-page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { SheetPolaroid } from '@/components/sheet-polaroid'
import { SheetCard } from '@/components/sheet-card'
import { type Orientation } from '@/lib/crop'
import { type PaperSize, cropMarks, sheetLayout } from '@/lib/layout'
import { type Photo } from '@/lib/photos'
import { useEditorStore } from '@/stores/editor-store'

/** One print sheet: margin guide, positioned polaroids, and crop marks. */
/** One print sheet: margin guide, positioned cards, and crop marks. */
export function SheetPage({
photos,
width,
Expand Down Expand Up @@ -68,7 +68,7 @@ export function SheetPage({
zIndex: editable && selectedId === photo.id ? 10 : undefined,
}}
>
<SheetPolaroid
<SheetCard
photo={photo}
width={rect.width * mmToPx}
shape={shape}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/hooks/use-export-pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function useExportPdf() {
const photos = usePhotoStore((state) => state.photos);
const sheetFormat = useSettingsStore((state) => state.sheetFormat);
const paperSizeId = useSettingsStore((state) => state.paperSizeId);
const perRow = useSettingsStore((state) => state.polaroidsPerRow);
const perRow = useSettingsStore((state) => state.cardsPerRow);
const rows = useSettingsStore((state) => state.rowsPerPage);
const stripsPerRow = useSettingsStore((state) => state.stripsPerRow);
const borderWidth = useSettingsStore((state) => state.borderWidth);
Expand Down Expand Up @@ -49,7 +49,7 @@ export function useExportPdf() {
} else {
await downloadSheetPdf(
photos,
s.polaroidsPerRow,
s.cardsPerRow,
s.rowsPerPage,
s.showCutMarks,
s.showCaptions,
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/crop.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// How a photo is framed inside its polaroid window. The same math drives the
// How a photo is framed inside its card window. The same math drives the
// on-screen preview (as CSS percentages) and the PDF rasterizer (as source
// pixels), so what you position is exactly what prints.

Expand All @@ -15,7 +15,7 @@ export const DEFAULT_CROP: Crop = { x: 0.5, y: 0.5, scale: 1 }
export const MIN_CROP_SCALE = 1
export const MAX_CROP_SCALE = 4

/** Shape of the photo window inside the (uniform) polaroid card. */
/** Shape of the photo window inside the (uniform) card card. */
export type Orientation = 'square' | 'portrait' | 'landscape'
export const ORIENTATIONS: Orientation[] = ['square', 'portrait', 'landscape']
export const DEFAULT_ORIENTATION: Orientation = 'square'
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const formatters: Record<DateFormat, Intl.DateTimeFormat> = {
year: new Intl.DateTimeFormat(undefined, { year: 'numeric' }),
}

/** Formats a capture date for a polaroid caption, e.g. "October 2008". */
/** Formats a capture date for a card caption, e.g. "October 2008". */
export function formatCaptionDate(
date: Date,
format: DateFormat = DEFAULT_DATE_FORMAT,
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/lib/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export function paperSize(id: string): PaperSize {
return PAPER_SIZES.find((paper) => paper.id === id) ?? PAPER_SIZES[0]
}

// Polaroid proportions, expressed as ratios of the polaroid's width so the same
// Card proportions, expressed as ratios of the card's width so the same
// numbers drive the on-screen preview (px) and the exported PDF (pt).
export const POLAROID = {
export const CARD = {
captionBand: 0.25, // thicker bottom band that holds the captions
captionTopSize: 0.1, // city line font size
captionBottomSize: 0.075, // date line font size
Expand Down Expand Up @@ -71,7 +71,7 @@ export function cardAspect(
shape: Orientation,
border = DEFAULT_BORDER_WIDTH,
): number {
return border + (1 - border * 2) / orientationAspect(shape) + POLAROID.captionBand
return border + (1 - border * 2) / orientationAspect(shape) + CARD.captionBand
}

export interface Rect {
Expand All @@ -89,7 +89,7 @@ export interface Segment {
}

/**
* L-shaped trim ticks at each corner of a polaroid, offset outward by `gap` so
* L-shaped trim ticks at each corner of a card, offset outward by `gap` so
* the marks point at the cut corners without inking the trim line itself.
*/
export function cropMarks(r: Rect, len = 2.5, gap = 1): Segment[] {
Expand All @@ -112,13 +112,13 @@ export function cropMarks(r: Rect, len = 2.5, gap = 1): Segment[] {
export interface SheetLayout {
perRow: number
rows: number
/** Max polaroids that fit on one sheet. */
/** Max cards that fit on one sheet. */
capacity: number
cellWidthMm: number
cellHeightMm: number
marginMm: number
gapMm: number
/** Top-left position + size (mm) of the polaroid at `index` on the sheet. */
/** Top-left position + size (mm) of the card at `index` on the sheet. */
rectFor: (index: number) => Rect
}

Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/lib/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { captionColors, hexToRgb01 } from '@/lib/color'
import { type Crop, type Orientation, cropSource, orientationAspect } from '@/lib/crop'
import {
type PaperSize,
POLAROID,
CARD,
PT_PER_MM,
cropMarks,
sheetLayout,
Expand Down Expand Up @@ -167,8 +167,8 @@ export async function buildSheetPdf(
}

const cx = x + w / 2
const topSize = w * POLAROID.captionTopSize
const botSize = w * POLAROID.captionBottomSize
const topSize = w * CARD.captionTopSize
const botSize = w * CARD.captionBottomSize
const capY = pageH - yTop - pad - imgH - pad * 0.6
if (showCaptions && photo.captionTop) {
page.drawText(photo.captionTop, {
Expand Down Expand Up @@ -311,7 +311,7 @@ export async function downloadSheetPdf(
borderColor: string,
borderWidth: number,
captionFontId: string,
filename = 'polaroids.pdf',
filename = 'rewind-cards.pdf',
): Promise<void> {
const bytes = await buildSheetPdf(
photos,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/photos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface Photo {
url: string
name: string
size: number
/** Polaroid caption lines (auto-filled from EXIF later; editable). */
/** Card caption lines (auto-filled from EXIF later; editable). */
captionTop: string
captionBottom: string
/** Place resolved from EXIF GPS — kept so the city/country toggle can switch. */
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/lib/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const SHEET_PRESETS: SheetPreset[] = [
frameShape: "square",
borderColor: "#ffffff",
borderWidth: 0.05,
polaroidsPerRow: 3,
cardsPerRow: 3,
rowsPerPage: 3,
},
},
Expand All @@ -28,7 +28,7 @@ export const SHEET_PRESETS: SheetPreset[] = [
frameShape: "square",
borderColor: "#ffffff",
borderWidth: 0.035,
polaroidsPerRow: 4,
cardsPerRow: 4,
rowsPerPage: 5,
},
},
Expand All @@ -50,7 +50,7 @@ export const SHEET_PRESETS: SheetPreset[] = [
frameShape: "portrait",
borderColor: "#111111",
borderWidth: 0.08,
polaroidsPerRow: 2,
cardsPerRow: 2,
rowsPerPage: 3,
},
},
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export async function buildProjectFile(
export async function downloadProject(
photos: Photo[],
settings: SettingsSnapshot,
filename = 'polaroid-project.json',
filename = 'rewind-project.json',
): Promise<void> {
const project = await buildProjectFile(photos, settings)
const blob = new Blob([JSON.stringify(project)], { type: 'application/json' })
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { type SettingsSnapshot } from '@/stores/settings-store'
// settings live in IndexedDB so a refresh or accidental tab close doesn't wipe
// the working set. Nothing ever leaves the device.

const DB_NAME = 'polaroid'
const DB_NAME = 'rewind'
const DB_VERSION = 1
const BLOBS = 'blobs' // key: photo id -> File
const META = 'meta' // key: photo id -> PhotoMeta
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/strip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type Photo } from '@/lib/photos'
// Photo-booth strip geometry. A strip is a tall card holding four square photos
// stacked in a column with a thin border between them and a wider footer band
// to write on. The same crop math drives the on-screen preview and the PDF, so
// what you frame is what prints — exactly like the polaroid grid.
// what you frame is what prints — exactly like the card grid.

export const PHOTOS_PER_STRIP = 4

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function Home() {
<header className="flex flex-col gap-1">
<div className="flex items-start justify-between gap-3">
<h1 className="font-display text-3xl font-semibold tracking-tight [font-variation-settings:'opsz'_144]">
Polaroid<span className="text-primary">.</span>
Rewind<span className="text-primary">.</span>
</h1>
<div className="flex items-start gap-2">
<ThemeToggle />
Expand Down
Loading
Loading