[dyad] Added image scaling and position controls - wrote 2 file(s)

This commit is contained in:
[dyad]
2026-01-18 11:43:31 +01:00
parent d3b704135e
commit 272f0ebc31
2 changed files with 111 additions and 4 deletions

View File

@@ -4,8 +4,6 @@ import { useState, useRef, ChangeEvent, useEffect } from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
@@ -31,6 +29,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Slider } from "@/components/ui/slider";
import { ObjectPositionControl } from "./object-position-control";
export function ImageConverter() {
const [images, setImages] = useState<File[]>([]);
@@ -47,6 +46,9 @@ export function ImageConverter() {
const [counterStart, setCounterStart] = useState<number>(1);
const [counterDigits, setCounterDigits] = useState<number>(3);
const [scaleMode, setScaleMode] = useState<'fill' | 'cover' | 'contain'>('fill');
const [objectPosition, setObjectPosition] = useState<string>('center center');
const [isConverting, setIsConverting] = useState(false);
const [convertingIndex, setConvertingIndex] = useState<number | null>(null);
const [isDraggingOver, setIsDraggingOver] = useState(false);
@@ -167,7 +169,49 @@ export function ImageConverter() {
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
const sWidth = img.naturalWidth;
const sHeight = img.naturalHeight;
const dWidth = targetWidth;
const dHeight = targetHeight;
if (scaleMode === 'fill' || !width || !height) {
ctx.drawImage(img, 0, 0, dWidth, dHeight);
} else {
const sourceRatio = sWidth / sHeight;
const targetRatio = dWidth / dHeight;
let sx = 0, sy = 0, sRenderWidth = sWidth, sRenderHeight = sHeight;
let dx = 0, dy = 0, dRenderWidth = dWidth, dRenderHeight = dHeight;
const [hPos, vPos] = objectPosition.split(' ');
if (scaleMode === 'cover') {
if (sourceRatio > targetRatio) {
sRenderHeight = sHeight;
sRenderWidth = sHeight * targetRatio;
if (hPos === 'center') sx = (sWidth - sRenderWidth) / 2;
if (hPos === 'right') sx = sWidth - sRenderWidth;
} else {
sRenderWidth = sWidth;
sRenderHeight = sWidth / targetRatio;
if (vPos === 'center') sy = (sHeight - sRenderHeight) / 2;
if (vPos === 'bottom') sy = sHeight - sRenderHeight;
}
ctx.drawImage(img, sx, sy, sRenderWidth, sRenderHeight, 0, 0, dWidth, dHeight);
} else if (scaleMode === 'contain') {
if (sourceRatio > targetRatio) {
dRenderWidth = dWidth;
dRenderHeight = dWidth / sourceRatio;
if (vPos === 'center') dy = (dHeight - dRenderHeight) / 2;
if (vPos === 'bottom') dy = dHeight - dRenderHeight;
} else {
dRenderHeight = dHeight;
dRenderWidth = dHeight * sourceRatio;
if (hPos === 'center') dx = (dWidth - dRenderWidth) / 2;
if (hPos === 'right') dx = dWidth - dRenderWidth;
}
ctx.drawImage(img, 0, 0, sWidth, sHeight, dx, dy, dRenderWidth, dRenderHeight);
}
}
const mimeType = `image/${format}`;
const dataUrl = canvas.toDataURL(mimeType, format === 'png' ? undefined : quality / 100);
const link = document.createElement("a");
@@ -250,7 +294,7 @@ export function ImageConverter() {
<div className="text-left">
<h3 className="text-lg font-medium leading-none">Image Settings</h3>
<p className="text-sm text-muted-foreground mt-1">
Adjust resolution for all uploaded images.
Adjust resolution and scaling for all images.
</p>
</div>
</AccordionTrigger>
@@ -265,6 +309,23 @@ export function ImageConverter() {
<Input id="height" type="number" placeholder="Original" value={height} onChange={(e) => setHeight(e.target.value)} />
</div>
</div>
<div className="mt-4 space-y-2">
<Label htmlFor="scale-mode">Scaling</Label>
<Select value={scaleMode} onValueChange={(value: 'fill' | 'cover' | 'contain') => setScaleMode(value)}>
<SelectTrigger id="scale-mode"><SelectValue placeholder="Select scaling mode" /></SelectTrigger>
<SelectContent>
<SelectItem value="fill">Fill (stretch to fit)</SelectItem>
<SelectItem value="cover">Cover (crop to fit)</SelectItem>
<SelectItem value="contain">Contain (letterbox)</SelectItem>
</SelectContent>
</Select>
</div>
{scaleMode !== 'fill' && (
<div className="mt-4 space-y-2">
<Label>Position</Label>
<ObjectPositionControl value={objectPosition} onChange={(pos) => setObjectPosition(pos)} />
</div>
)}
</AccordionContent>
</AccordionItem>

View File

@@ -0,0 +1,46 @@
"use client";
import { cn } from "@/lib/utils";
type Position =
| "left top"
| "center top"
| "right top"
| "left center"
| "center center"
| "right center"
| "left bottom"
| "center bottom"
| "right bottom";
const positions: Position[] = [
"left top", "center top", "right top",
"left center", "center center", "right center",
"left bottom", "center bottom", "right bottom",
];
interface ObjectPositionControlProps {
value: string;
onChange: (value: Position) => void;
}
export function ObjectPositionControl({ value, onChange }: ObjectPositionControlProps) {
return (
<div className="grid grid-cols-3 gap-1 w-24 h-24 p-1 rounded-md bg-muted">
{positions.map((pos) => (
<button
key={pos}
type="button"
onClick={() => onChange(pos)}
className={cn(
"rounded-sm transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
value === pos
? "bg-primary"
: "bg-muted-foreground/20 hover:bg-muted-foreground/40"
)}
aria-label={`Set object position to ${pos.replace(' ', ' ')}`}
/>
))}
</div>
);
}