[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 {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
@@ -31,6 +29,7 @@ import {
|
|||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { ObjectPositionControl } from "./object-position-control";
|
||||||
|
|
||||||
export function ImageConverter() {
|
export function ImageConverter() {
|
||||||
const [images, setImages] = useState<File[]>([]);
|
const [images, setImages] = useState<File[]>([]);
|
||||||
@@ -47,6 +46,9 @@ export function ImageConverter() {
|
|||||||
const [counterStart, setCounterStart] = useState<number>(1);
|
const [counterStart, setCounterStart] = useState<number>(1);
|
||||||
const [counterDigits, setCounterDigits] = useState<number>(3);
|
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 [isConverting, setIsConverting] = useState(false);
|
||||||
const [convertingIndex, setConvertingIndex] = useState<number | null>(null);
|
const [convertingIndex, setConvertingIndex] = useState<number | null>(null);
|
||||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||||
@@ -167,7 +169,49 @@ export function ImageConverter() {
|
|||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
if (ctx) {
|
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 mimeType = `image/${format}`;
|
||||||
const dataUrl = canvas.toDataURL(mimeType, format === 'png' ? undefined : quality / 100);
|
const dataUrl = canvas.toDataURL(mimeType, format === 'png' ? undefined : quality / 100);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
@@ -250,7 +294,7 @@ export function ImageConverter() {
|
|||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="text-lg font-medium leading-none">Image Settings</h3>
|
<h3 className="text-lg font-medium leading-none">Image Settings</h3>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
Adjust resolution for all uploaded images.
|
Adjust resolution and scaling for all images.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
@@ -265,6 +309,23 @@ export function ImageConverter() {
|
|||||||
<Input id="height" type="number" placeholder="Original" value={height} onChange={(e) => setHeight(e.target.value)} />
|
<Input id="height" type="number" placeholder="Original" value={height} onChange={(e) => setHeight(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</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