mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add per-cell fit/fill toggle to collage tool
Adds objectFit property to CellTransform (cover/contain). When set to "contain", the entire image is shown within the cell with background color fill. Toggle button in the cell controls toolbar switches between modes. Server-side rendering handles both modes via Sharp.
This commit is contained in:
@@ -11,7 +11,17 @@ import {
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { useDrag, usePinch } from "@use-gesture/react";
|
||||
import { Download, GripVertical, ImagePlus, Loader2, RotateCcw, Upload, X } from "lucide-react";
|
||||
import {
|
||||
Download,
|
||||
Expand,
|
||||
GripVertical,
|
||||
ImagePlus,
|
||||
Loader2,
|
||||
Maximize,
|
||||
RotateCcw,
|
||||
Upload,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { type DragEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { type CollageTemplate, getTemplateById } from "@/lib/collage-templates";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -373,6 +383,15 @@ function CollageCell({
|
||||
[cellIndex, store],
|
||||
);
|
||||
|
||||
const handleToggleFit = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const next = transform.objectFit === "cover" ? "contain" : "cover";
|
||||
store.setCellTransform(cellIndex, { objectFit: next });
|
||||
},
|
||||
[cellIndex, store, transform.objectFit],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
@@ -431,7 +450,10 @@ function CollageCell({
|
||||
src={displayUrl(image)}
|
||||
alt=""
|
||||
draggable={false}
|
||||
className="w-full h-full object-cover select-none pointer-events-none"
|
||||
className={cn(
|
||||
"w-full h-full select-none pointer-events-none",
|
||||
transform.objectFit === "contain" ? "object-contain" : "object-cover",
|
||||
)}
|
||||
style={{
|
||||
transform: `translate(${transform.panX}%, ${transform.panY}%) scale(${transform.zoom})`,
|
||||
}}
|
||||
@@ -484,6 +506,23 @@ function CollageCell({
|
||||
<span className="text-white text-[10px] font-mono w-7 text-right shrink-0">
|
||||
{transform.zoom.toFixed(1)}x
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleFit}
|
||||
className={cn(
|
||||
"transition-colors shrink-0",
|
||||
transform.objectFit === "contain"
|
||||
? "text-blue-300 hover:text-blue-200"
|
||||
: "text-white hover:text-white/80",
|
||||
)}
|
||||
title={transform.objectFit === "cover" ? "Fit entire image" : "Fill cell"}
|
||||
>
|
||||
{transform.objectFit === "contain" ? (
|
||||
<Maximize className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Expand className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleReset}
|
||||
|
||||
@@ -76,8 +76,8 @@ export function CollageSettings() {
|
||||
}
|
||||
|
||||
const cells = template.cells.map((_, i) => {
|
||||
const t = cellTransforms[i] ?? { panX: 0, panY: 0, zoom: 1 };
|
||||
return { imageIndex: i, panX: t.panX, panY: t.panY, zoom: t.zoom };
|
||||
const t = cellTransforms[i] ?? { panX: 0, panY: 0, zoom: 1, objectFit: "cover" };
|
||||
return { imageIndex: i, panX: t.panX, panY: t.panY, zoom: t.zoom, objectFit: t.objectFit };
|
||||
});
|
||||
|
||||
formData.append(
|
||||
|
||||
@@ -14,10 +14,13 @@ export interface CollageImage {
|
||||
previewLoading: boolean;
|
||||
}
|
||||
|
||||
export type ObjectFit = "cover" | "contain";
|
||||
|
||||
export interface CellTransform {
|
||||
panX: number; // percentage -100..100
|
||||
panY: number; // percentage -100..100
|
||||
zoom: number; // 1.0 to 3.0
|
||||
objectFit: ObjectFit;
|
||||
}
|
||||
|
||||
interface CollageState {
|
||||
@@ -76,7 +79,7 @@ interface CollageState {
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_TRANSFORM: CellTransform = { panX: 0, panY: 0, zoom: 1 };
|
||||
const DEFAULT_TRANSFORM: CellTransform = { panX: 0, panY: 0, zoom: 1, objectFit: "cover" };
|
||||
|
||||
let nextImageId = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user