A web app to help you design things, local, offline, on device. In your browser.
TypeScript
53.1%
JSON
45.6%
CSS
0.9%
Markdown
0.3%
JavaScript
0.1%
'use client';
import React, { useRef, useEffect } from 'react';
import { Group, Image as KonvaImage, Rect } from 'react-konva';
import useImage from 'use-image';
import Konva from 'konva';
import type { Layer, ImageLayerData, ScreenshotLayerData, CornerRadius, ImageFilters } from '@/types';
interface ImageLayerProps {
id: string;
layer: Layer;
isSelected: boolean;
onClick: (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => void;
onTransformEnd: (node: Konva.Node) => void;
onDragStart?: () => void;
onDragMove?: (node: Konva.Node) => void;
onDragEnd: (node: Konva.Node) => void;
}
export function ImageLayer({
id,
layer,
isSelected,
onClick,
onTransformEnd,
onDragStart,
onDragMove,
onDragEnd,
}: ImageLayerProps) {
const groupRef = useRef<Konva.Group>(null);
const imageRef = useRef<Konva.Image>(null);
const data = layer.data as ImageLayerData | ScreenshotLayerData;
const [image, status] = useImage(data.src, 'anonymous');
// Calculate image dimensions based on fit mode
const getImageDimensions = () => {
if (!image) return { width: layer.size.width, height: layer.size.height, x: 0, y: 0 };
const containerRatio = layer.size.width / layer.size.height;
const imageRatio = image.width / image.height;
const fit = 'fit' in data ? data.fit : 'cover';
if (fit === 'fill') {
return {
width: layer.size.width,
height: layer.size.height,
x: 0,
y: 0,
};
}
if (fit === 'contain') {
if (containerRatio > imageRatio) {
const width = layer.size.height * imageRatio;
return {
width,
height: layer.size.height,
x: (layer.size.width - width) / 2,
y: 0,
};
} else {
const height = layer.size.width / imageRatio;
return {
width: layer.size.width,
height,
x: 0,
y: (layer.size.height - height) / 2,
};
}
}
// Cover (default)
if (containerRatio > imageRatio) {
const height = layer.size.width / imageRatio;
return {
width: layer.size.width,
height,
x: 0,
y: (layer.size.height - height) / 2,
};
} else {
const width = layer.size.height * imageRatio;
return {
width,
height: layer.size.height,
x: (layer.size.width - width) / 2,
y: 0,
};
}
};
const imageDims = getImageDimensions();
// Get corner radius values
const getCornerRadius = (): { tl: number; tr: number; br: number; bl: number } => {
const imageData = data as ImageLayerData;
if (!imageData.cornerRadius) {
return { tl: 0, tr: 0, br: 0, bl: 0 };
}
if (typeof imageData.cornerRadius === 'number') {
return {
tl: imageData.cornerRadius,
tr: imageData.cornerRadius,
br: imageData.cornerRadius,
bl: imageData.cornerRadius,
};
}
const cr = imageData.cornerRadius as CornerRadius;
return {
tl: cr.topLeft,
tr: cr.topRight,
br: cr.bottomRight,
bl: cr.bottomLeft,
};
};
const corners = getCornerRadius();
const hasRoundedCorners = corners.tl > 0 || corners.tr > 0 || corners.br > 0 || corners.bl > 0;
// Get image filters
const getFilters = (): ImageFilters => {
const imageData = data as ImageLayerData;
return imageData.filters || { brightness: 0, contrast: 0, saturation: 0, blur: 0 };
};
const filters = getFilters();
const hasFilters = filters.brightness !== 0 || filters.contrast !== 0 ||
filters.saturation !== 0 || filters.blur !== 0;
// Apply Konva filters when image loads or filters change
useEffect(() => {
if (!imageRef.current || !image) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const konvaFilters: any[] = [];
if (filters.brightness !== 0) {
konvaFilters.push(Konva.Filters.Brighten);
imageRef.current.brightness(filters.brightness / 100);
}
if (filters.contrast !== 0) {
konvaFilters.push(Konva.Filters.Contrast);
imageRef.current.contrast(filters.contrast);
}
if (filters.saturation !== 0) {
konvaFilters.push(Konva.Filters.HSL);
imageRef.current.saturation(filters.saturation / 50);
}
if (filters.blur !== 0) {
konvaFilters.push(Konva.Filters.Blur);
imageRef.current.blurRadius(filters.blur);
}
if (konvaFilters.length > 0) {
imageRef.current.cache();
imageRef.current.filters(konvaFilters);
} else {
imageRef.current.clearCache();
imageRef.current.filters([]);
}
}, [image, filters.brightness, filters.contrast, filters.saturation, filters.blur]);
// Create rounded rectangle clip path
const createRoundedRectPath = (
ctx: CanvasRenderingContext2D,
width: number,
height: number,
{ tl, tr, br, bl }: { tl: number; tr: number; br: number; bl: number }
) => {
ctx.beginPath();
ctx.moveTo(tl, 0);
ctx.lineTo(width - tr, 0);
ctx.quadraticCurveTo(width, 0, width, tr);
ctx.lineTo(width, height - br);
ctx.quadraticCurveTo(width, height, width - br, height);
ctx.lineTo(bl, height);
ctx.quadraticCurveTo(0, height, 0, height - bl);
ctx.lineTo(0, tl);
ctx.quadraticCurveTo(0, 0, tl, 0);
ctx.closePath();
};
return (
<Group
ref={groupRef}
id={id}
x={layer.position.x}
y={layer.position.y}
width={layer.size.width}
height={layer.size.height}
rotation={layer.rotation}
opacity={layer.opacity}
draggable={!layer.locked}
onClick={onClick}
onTap={onClick}
onDragStart={onDragStart}
onDragMove={(e) => onDragMove?.(e.target)}
onDragEnd={(e) => onDragEnd(e.target)}
onTransformEnd={(e) => onTransformEnd(e.target)}
clipFunc={(ctx) => {
if (hasRoundedCorners) {
createRoundedRectPath(ctx as unknown as CanvasRenderingContext2D, layer.size.width, layer.size.height, corners);
} else {
ctx.rect(0, 0, layer.size.width, layer.size.height);
}
}}
>
{/* Loading state */}
{status === 'loading' && (
<Rect
width={layer.size.width}
height={layer.size.height}
fill="rgba(255, 255, 255, 0.05)"
/>
)}
{/* Error state */}
{status === 'failed' && (
<Group>
<Rect
width={layer.size.width}
height={layer.size.height}
fill="rgba(239, 68, 68, 0.1)"
stroke="rgba(239, 68, 68, 0.5)"
strokeWidth={2}
dash={[8, 4]}
/>
</Group>
)}
{/* Image */}
{image && (
<KonvaImage
ref={imageRef}
image={image}
x={imageDims.x}
y={imageDims.y}
width={imageDims.width}
height={imageDims.height}
/>
)}
{/* Selection border */}
{isSelected && (
<Rect
width={layer.size.width}
height={layer.size.height}
stroke="#4f46e5"
strokeWidth={2}
listening={false}
/>
)}
</Group>
);
}
About
A web app to help you design things, local, offline, on device. In your browser.
0 stars
0 forks