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, { useEffect, useRef } from 'react';
import { Group, Rect, Image as KonvaImage } from 'react-konva';
import useImage from 'use-image';
import type Konva from 'konva';
import type { Layer, DeviceLayerData, DeviceColor } from '@/types';
import { DEVICE_SPECS, DEVICE_COLORS } from '@/lib/devices/device-specs';
interface DeviceLayerProps {
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 DeviceLayer({
id,
layer,
isSelected,
onClick,
onTransformEnd,
onDragStart,
onDragMove,
onDragEnd,
}: DeviceLayerProps) {
const groupRef = useRef<Konva.Group>(null);
const data = layer.data as DeviceLayerData;
const spec = DEVICE_SPECS[data.deviceId];
if (!spec) return null;
const frameColor = DEVICE_COLORS[data.color] || DEVICE_COLORS.black;
// Calculate scale to fit layer size
const scaleX = layer.size.width / spec.frameSize.width;
const scaleY = layer.size.height / spec.frameSize.height;
const scale = Math.min(scaleX, scaleY);
const scaledFrameWidth = spec.frameSize.width * scale;
const scaledFrameHeight = spec.frameSize.height * scale;
const scaledScreenWidth = spec.screenSize.width * scale;
const scaledScreenHeight = spec.screenSize.height * scale;
const scaledOffsetX = spec.screenOffset.x * scale;
const scaledOffsetY = spec.screenOffset.y * scale;
return (
<Group
ref={groupRef}
id={id}
x={layer.position.x}
y={layer.position.y}
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)}
>
{/* Device Frame */}
{data.showFrame && (
<>
{/* Outer frame with shadow */}
<Rect
x={0}
y={0}
width={scaledFrameWidth}
height={scaledFrameHeight}
fill={frameColor}
cornerRadius={spec.cornerRadius * scale}
shadowColor="rgba(0, 0, 0, 0.4)"
shadowBlur={20 * scale}
shadowOffsetY={10 * scale}
/>
{/* Inner bezel highlight */}
<Rect
x={2 * scale}
y={2 * scale}
width={scaledFrameWidth - 4 * scale}
height={scaledFrameHeight - 4 * scale}
stroke="rgba(255, 255, 255, 0.1)"
strokeWidth={1}
cornerRadius={(spec.cornerRadius - 2) * scale}
listening={false}
/>
{/* Side buttons for iPhone */}
{spec.category === 'iphone' && (
<>
{/* Volume buttons */}
<Rect
x={-3 * scale}
y={120 * scale}
width={3 * scale}
height={30 * scale}
fill={frameColor}
cornerRadius={2 * scale}
/>
<Rect
x={-3 * scale}
y={160 * scale}
width={3 * scale}
height={60 * scale}
fill={frameColor}
cornerRadius={2 * scale}
/>
{/* Power button */}
<Rect
x={scaledFrameWidth}
y={140 * scale}
width={3 * scale}
height={70 * scale}
fill={frameColor}
cornerRadius={2 * scale}
/>
</>
)}
{/* Dynamic Island / Notch */}
{spec.category === 'iphone' && spec.id !== 'iphone-se' && (
<Rect
x={scaledFrameWidth / 2 - 60 * scale}
y={12 * scale}
width={120 * scale}
height={35 * scale}
fill="#000"
cornerRadius={18 * scale}
/>
)}
</>
)}
{/* Screen area */}
<Rect
x={data.showFrame ? scaledOffsetX : 0}
y={data.showFrame ? scaledOffsetY : 0}
width={data.showFrame ? scaledScreenWidth : layer.size.width}
height={data.showFrame ? scaledScreenHeight : layer.size.height}
fill="#000"
cornerRadius={data.showFrame ? (spec.cornerRadius - 5) * scale : 0}
/>
{/* Screenshot placeholder */}
{!data.screenshotId && (
<Group
x={data.showFrame ? scaledOffsetX : 0}
y={data.showFrame ? scaledOffsetY : 0}
>
<Rect
width={data.showFrame ? scaledScreenWidth : layer.size.width}
height={data.showFrame ? scaledScreenHeight : layer.size.height}
fill="rgba(255, 255, 255, 0.05)"
cornerRadius={data.showFrame ? (spec.cornerRadius - 5) * scale : 0}
/>
{/* Placeholder icon */}
<Rect
x={(data.showFrame ? scaledScreenWidth : layer.size.width) / 2 - 30 * scale}
y={(data.showFrame ? scaledScreenHeight : layer.size.height) / 2 - 30 * scale}
width={60 * scale}
height={60 * scale}
fill="rgba(255, 255, 255, 0.1)"
cornerRadius={10 * scale}
/>
</Group>
)}
{/* Screen reflection overlay */}
{data.showFrame && (
<Rect
x={scaledOffsetX}
y={scaledOffsetY}
width={scaledScreenWidth}
height={scaledScreenHeight / 3}
fillLinearGradientStartPoint={{ x: 0, y: 0 }}
fillLinearGradientEndPoint={{ x: 0, y: scaledScreenHeight / 3 }}
fillLinearGradientColorStops={[
0,
'rgba(255, 255, 255, 0.1)',
1,
'rgba(255, 255, 255, 0)',
]}
cornerRadius={[(spec.cornerRadius - 5) * scale, (spec.cornerRadius - 5) * scale, 0, 0]}
listening={false}
/>
)}
</Group>
);
}
// Screenshot component for device
interface DeviceScreenshotProps {
src: string;
width: number;
height: number;
cornerRadius: number;
}
export function DeviceScreenshot({ src, width, height, cornerRadius }: DeviceScreenshotProps) {
const [image] = useImage(src, 'anonymous');
if (!image) return null;
return (
<Group
clipFunc={(ctx) => {
ctx.beginPath();
ctx.roundRect(0, 0, width, height, cornerRadius);
ctx.closePath();
}}
>
<KonvaImage
image={image}
width={width}
height={height}
listening={false}
/>
</Group>
);
}
About
A web app to help you design things, local, offline, on device. In your browser.
0 stars
0 forks