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
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Pairframe

NativePHP desktop app to compose two screenshots into split or overlap thumbnails, with background patterns and PNG, JPG, or transition-video export.
NativePHP desktop app to compose two screenshots into split or overlap thumbnails, with background patterns, overlays, and PNG, JPG, GIF, or transition-video export.

![Pairframe desktop app with diagonal wavy split preview and video export selected](docs/pairframe.png)
![Pairframe desktop app with split preview, collapsible sidebars, and export controls](docs/pairframe.png)

**Heads up: this project is AI-assisted.** A lot of the code and docs were written with AI tools (reviewed and steered by me). Pairframe exists first for my own thumbnail workflow; I open-sourced it in case it helps someone else. Issues and pull requests are welcome, just know what you're looking at.

Expand Down Expand Up @@ -94,7 +94,10 @@ Local builds are unsigned unless you configure Apple or Windows code signing. Pr
- Layouts: vertical, horizontal, diagonal, and overlap (cards, side, stack)
- Split edge styles (vertical / horizontal / diagonal): straight, wavy, zigzag, scallop, soft, torn, pixel (with density)
- Split position, angle, padding, border radius, and optional shadow on overlap
- Theme (light / dark / auto), side labels, badge, and swap sides
- Background patterns with custom colors and density
- Theme (light / dark / auto), per-image labels (9-point presets + custom X/Y), badge, and swap sides
- Background patterns with custom colors and density (optional per-side A/B)
- Overlay pattern marks only (dots, grid, and more) with color, opacity, and density — no base fill; optional per-side A/B
- Collapsible sidebar groups with scrollable panels and per-group reset (shown when dirty)
- Undo / redo for settings (Cmd/Ctrl+Z, Cmd/Ctrl+Shift+Z) — images are not in the history
- Save / load settings presets (images are not stored)
- Export PNG or JPG at optional scale (1×, 1.5×, 2×), or transition video (wipe, dissolve, push, iris, reveal → MP4 / WebM)
- Export PNG or JPG at optional scale (1×, 1.5×, 2×), transition video (MP4 / WebM), or animated GIF — with easing and hold-start/hold-end
Binary file modified docs/pairframe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"vite": "^8.0.0"
},
"dependencies": {
"alpinejs": "^3.15.12"
"alpinejs": "^3.15.12",
"gifenc": "^1.0.3"
}
}
144 changes: 139 additions & 5 deletions resources/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@

@layer base {
html {
height: 100%;
overflow: hidden;
font-size: 110%;
color-scheme: light;
}
Expand Down Expand Up @@ -98,18 +100,26 @@
}

input.slider-value {
width: 3.25rem;
width: auto;
min-width: 1ch;
margin: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
padding: 0.1rem 0.3rem;
border: 1px solid transparent;
border-radius: var(--radius-subtle);
background: color-mix(in srgb, var(--color-lumis-ink) 5%, transparent);
color: #a1a1aa;
font-size: 0.75rem;
line-height: 1rem;
text-align: right;
outline: none;
field-sizing: content;
-moz-appearance: textfield;
cursor: text;
}

input.slider-value:hover {
color: var(--color-lumis-ink);
background: color-mix(in srgb, var(--color-lumis-ink) 8%, transparent);
}

input.preset-name-input,
Expand Down Expand Up @@ -140,6 +150,40 @@

input.slider-value:focus {
color: var(--color-lumis-ink);
border-color: var(--color-lumis-panel-line);
background: color-mix(in srgb, var(--color-lumis-ink) 8%, transparent);
}

/* Keep number + unit in one subtle chip when wrapped together */
.slider-value-wrap {
gap: 0.05rem;
padding: 0.1rem 0.3rem;
border: 1px solid transparent;
border-radius: var(--radius-subtle);
background: color-mix(in srgb, var(--color-lumis-ink) 5%, transparent);
}

.slider-value-wrap:hover {
background: color-mix(in srgb, var(--color-lumis-ink) 8%, transparent);
}

.slider-value-wrap:focus-within {
border-color: var(--color-lumis-panel-line);
background: color-mix(in srgb, var(--color-lumis-ink) 8%, transparent);
}

.slider-value-wrap input.slider-value {
width: auto;
min-width: 1ch;
padding: 0;
border: 0;
background: transparent;
field-sizing: content;
}

.slider-value-wrap input.slider-value:hover,
.slider-value-wrap input.slider-value:focus {
background: transparent;
}

input.slider-value::-webkit-inner-spin-button,
Expand Down Expand Up @@ -189,4 +233,94 @@
border: none;
border-radius: calc(var(--radius-subtle) - 1px);
}

.sidebar-scroll {
flex: 1 1 0%;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
display: flex;
flex-direction: column;
-webkit-overflow-scrolling: touch;
}

.sidebar-group {
flex-shrink: 0;
border-bottom: 1px solid var(--color-lumis-panel-line);
background: var(--color-lumis-panel-surface);
}

.sidebar-group.is-collapsed {
background: var(--color-lumis-segment-idle);
}

.sidebar-group.is-collapsed .sidebar-group-toggle {
color: color-mix(in srgb, var(--color-lumis-ink) 72%, transparent);
}

.sidebar-group-header {
display: flex;
align-items: stretch;
}

.sidebar-group-toggle {
display: flex;
min-width: 0;
flex: 1 1 auto;
cursor: pointer;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 0.5rem 0.625rem 0.75rem;
text-align: left;
border-radius: 0;
background: transparent;
}

.sidebar-group-toggle:hover,
.sidebar-group-reset:hover {
background: color-mix(in srgb, var(--color-lumis-ink) 4%, transparent);
}

.dark .sidebar-group-toggle:hover,
.dark .sidebar-group-reset:hover {
background: color-mix(in srgb, #ffffff 8%, transparent);
}

.sidebar-group-reset {
display: inline-flex;
width: 2rem;
flex-shrink: 0;
cursor: pointer;
align-items: center;
justify-content: center;
border-radius: 0;
color: #a3a3a3;
background: transparent;
}

.sidebar-group-reset:hover {
color: var(--color-lumis-ink);
}

.sidebar-group-body {
border-top: 1px solid var(--color-lumis-panel-line);
padding: 0.75rem;
background: var(--color-lumis-panel-surface);
}

.label-preset-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.25rem;
}

.label-preset-cell {
min-height: 1.75rem;
padding: 0.25rem 0.125rem;
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1;
}
}
Loading
Loading