[dyad] Added image scaling and position controls - wrote 2 file(s)
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
46
src/components/object-position-control.tsx
Normal file
46
src/components/object-position-control.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user