Files
craft2prints/src/components/ProductMockup.tsx
T

277 lines
11 KiB
TypeScript

'use client';
import { useId } from 'react';
type Props = {
mockup: string;
color: string;
};
const STROKE = '#17181C';
/**
* Shading is done as color-independent overlays (white/black gradients with
* blend modes) on top of the flat garment fill, so any hex color still reads
* as "lit from one side" without us having to compute per-color shades.
*/
function Shading({ clipId, variant }: { clipId: string; variant: 'garment' | 'round' }) {
const hi = `${clipId}-hi`;
const sh = `${clipId}-sh`;
return (
<>
<defs>
<linearGradient id={hi} x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#FFFFFF" stopOpacity={variant === 'round' ? 0.55 : 0.35} />
<stop offset="45%" stopColor="#FFFFFF" stopOpacity="0" />
</linearGradient>
<linearGradient id={sh} x1="100%" y1="100%" x2="0%" y2="0%">
<stop offset="0%" stopColor="#000000" stopOpacity={variant === 'round' ? 0.28 : 0.16} />
<stop offset="45%" stopColor="#000000" stopOpacity="0" />
</linearGradient>
</defs>
<g clipPath={`url(#${clipId})`}>
<rect x="0" y="0" width="500" height="500" fill={`url(#${hi})`} style={{ mixBlendMode: 'screen' }} />
<rect x="0" y="0" width="500" height="500" fill={`url(#${sh})`} style={{ mixBlendMode: 'multiply' }} />
</g>
</>
);
}
function TShirt({ color }: { color: string }) {
const id = useId();
const clipId = `tee-${id}`;
const body =
'M170 92 L112 128 L68 192 L118 232 L150 204 L150 424 C150 434 158 442 168 442 L332 442 C342 442 350 434 350 424 L350 204 L382 232 L432 192 L388 128 L330 92 C330 92 316 122 250 122 C184 122 170 92 170 92 Z';
return (
<svg viewBox="0 0 500 500" className="h-full w-full">
<defs>
<clipPath id={clipId}>
<path d={body} />
</clipPath>
</defs>
<path d={body} fill={color} stroke={STROKE} strokeWidth="2.5" strokeLinejoin="round" />
{/* fold shadows */}
<g clipPath={`url(#${clipId})`} opacity="0.5">
<path d="M195 210 C 200 300, 195 380, 205 435" fill="none" stroke="#000" strokeOpacity="0.08" strokeWidth="10" />
<path d="M310 210 C 305 300, 312 380, 300 435" fill="none" stroke="#000" strokeOpacity="0.08" strokeWidth="10" />
<path d="M150 220 L150 340" stroke="#000" strokeOpacity="0.12" strokeWidth="4" />
<path d="M350 220 L350 340" stroke="#000" strokeOpacity="0.12" strokeWidth="4" />
</g>
<Shading clipId={clipId} variant="garment" />
{/* ribbed crew neck */}
<path
d="M330 92 C 330 92 316 122 250 122 C 184 122 170 92 170 92"
fill="none"
stroke={STROKE}
strokeWidth="2.5"
/>
<path
d="M320 98 C 300 118 300 128 250 128 C 200 128 200 118 180 98"
fill="none"
stroke={STROKE}
strokeOpacity="0.45"
strokeWidth="1.5"
/>
{/* sleeve hem topstitch */}
<path d="M112 128 L68 192 L118 232" fill="none" stroke={STROKE} strokeOpacity="0.4" strokeWidth="1.2" strokeDasharray="3 3" />
<path d="M330 92 L388 128 L432 192 L382 232" fill="none" stroke={STROKE} strokeOpacity="0.4" strokeWidth="1.2" strokeDasharray="3 3" />
</svg>
);
}
function Hoodie({ color }: { color: string }) {
const id = useId();
const clipId = `hoodie-${id}`;
const body =
'M178 100 L118 138 L78 198 L128 238 L158 212 L158 250 C 132 262 120 288 120 322 L120 432 C120 440 126 446 134 446 L366 446 C374 446 380 440 380 432 L380 322 C380 288 368 262 342 250 L342 212 L372 238 L422 198 L382 138 L322 100 C322 100 304 124 250 124 C196 124 178 100 178 100 Z';
return (
<svg viewBox="0 0 500 500" className="h-full w-full">
<defs>
<clipPath id={clipId}>
<path d={body} />
</clipPath>
</defs>
<path d={body} fill={color} stroke={STROKE} strokeWidth="2.5" strokeLinejoin="round" />
<g clipPath={`url(#${clipId})`} opacity="0.5">
<path d="M200 260 C 205 330, 200 390, 208 440" fill="none" stroke="#000" strokeOpacity="0.08" strokeWidth="10" />
<path d="M300 260 C 295 330, 300 390, 292 440" fill="none" stroke="#000" strokeOpacity="0.08" strokeWidth="10" />
</g>
<Shading clipId={clipId} variant="garment" />
{/* hood */}
<path
d="M195 252 C 195 288 216 304 250 304 C 284 304 305 288 305 252"
fill="none"
stroke={STROKE}
strokeWidth="2.5"
/>
<path
d="M205 250 C 205 278 222 292 250 292 C 278 292 295 278 295 250"
fill="none"
stroke={STROKE}
strokeOpacity="0.4"
strokeWidth="1.3"
/>
{/* drawstrings */}
<circle cx="228" cy="262" r="3.2" fill="none" stroke={STROKE} strokeWidth="1.3" />
<circle cx="272" cy="262" r="3.2" fill="none" stroke={STROKE} strokeWidth="1.3" />
<path d="M228 265 C 226 292 232 305 226 320" fill="none" stroke={STROKE} strokeWidth="1.3" />
<path d="M272 265 C 274 292 268 305 274 320" fill="none" stroke={STROKE} strokeWidth="1.3" />
{/* kangaroo pocket */}
<path
d="M172 345 C 172 380 200 395 250 395 C 300 395 328 380 328 345"
fill="none"
stroke={STROKE}
strokeWidth="2"
/>
<path d="M250 345 L250 365" fill="none" stroke={STROKE} strokeOpacity="0.35" strokeWidth="1.3" />
{/* cuffs + hem ribbing */}
<path d="M158 340 L158 400 M342 340 L342 400" stroke={STROKE} strokeWidth="1.6" fill="none" />
<path d="M124 428 L376 428" stroke={STROKE} strokeOpacity="0.35" strokeWidth="1.3" strokeDasharray="2 4" />
</svg>
);
}
function Mug({ color }: { color: string }) {
const id = useId();
const clipId = `mug-${id}`;
const body = 'M142 158 L358 158 L347 402 C347 418 332 428 316 428 L184 428 C168 428 153 418 153 402 Z';
return (
<svg viewBox="0 0 500 500" className="h-full w-full">
<defs>
<clipPath id={clipId}>
<path d={body} />
</clipPath>
<radialGradient id={`${clipId}-rim`} cx="50%" cy="50%" r="50%">
<stop offset="60%" stopColor="#000" stopOpacity="0.28" />
<stop offset="100%" stopColor="#000" stopOpacity="0" />
</radialGradient>
<linearGradient id={`${clipId}-handle`} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#FFF" stopOpacity="0.5" />
<stop offset="100%" stopColor="#000" stopOpacity="0.15" />
</linearGradient>
</defs>
<path d={body} fill={color} stroke={STROKE} strokeWidth="2.5" strokeLinejoin="round" />
<Shading clipId={clipId} variant="round" />
{/* handle — double curve for thickness + inner shading */}
<path
d="M352 196 C 415 196 440 228 440 262 C 440 296 415 328 352 328"
fill="none"
stroke={STROKE}
strokeWidth="14"
strokeLinecap="round"
/>
<path
d="M352 196 C 415 196 440 228 440 262 C 440 296 415 328 352 328"
fill="none"
stroke={`url(#${clipId}-handle)`}
strokeWidth="10"
strokeLinecap="round"
/>
{/* rim / opening */}
<ellipse cx="250" cy="158" rx="108" ry="16" fill={color} stroke={STROKE} strokeWidth="2.5" />
<ellipse cx="250" cy="158" rx="108" ry="16" fill={`url(#${clipId}-rim)`} />
<ellipse cx="250" cy="155" rx="92" ry="11" fill="none" stroke={STROKE} strokeOpacity="0.3" strokeWidth="1.2" />
</svg>
);
}
function Tumbler({ color }: { color: string }) {
const id = useId();
const clipId = `tumbler-${id}`;
const body = 'M188 148 L312 148 L296 424 C296 438 282 448 267 448 L233 448 C218 448 204 438 204 424 Z';
return (
<svg viewBox="0 0 500 500" className="h-full w-full">
<defs>
<clipPath id={clipId}>
<path d={body} />
</clipPath>
<linearGradient id={`${clipId}-lid`} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#F2F2F2" />
<stop offset="45%" stopColor="#CBCBCB" />
<stop offset="100%" stopColor="#9A9A9A" />
</linearGradient>
</defs>
{/* lid */}
<path
d="M198 100 L302 100 L308 140 L192 140 Z"
fill={`url(#${clipId}-lid)`}
stroke={STROKE}
strokeWidth="2.2"
strokeLinejoin="round"
/>
<rect x="228" y="86" width="44" height="18" rx="6" fill={`url(#${clipId}-lid)`} stroke={STROKE} strokeWidth="1.8" />
{/* body */}
<path d={body} fill={color} stroke={STROKE} strokeWidth="2.5" strokeLinejoin="round" />
<Shading clipId={clipId} variant="round" />
{/* seam ring near lid + base */}
<path d="M192 168 L308 168" stroke={STROKE} strokeOpacity="0.3" strokeWidth="1.3" />
<path d="M206 420 L294 420" stroke={STROKE} strokeOpacity="0.3" strokeWidth="1.3" />
</svg>
);
}
function PhoneCase({ color }: { color: string }) {
const id = useId();
const clipId = `case-${id}`;
return (
<svg viewBox="0 0 500 500" className="h-full w-full">
<defs>
<clipPath id={clipId}>
<rect x="150" y="70" width="200" height="400" rx="34" />
</clipPath>
<radialGradient id={`${clipId}-lens`} cx="40%" cy="35%" r="65%">
<stop offset="0%" stopColor="#5a5a5a" />
<stop offset="60%" stopColor="#1a1a1a" />
<stop offset="100%" stopColor="#000" />
</radialGradient>
</defs>
<rect x="150" y="70" width="200" height="400" rx="34" fill={color} stroke={STROKE} strokeWidth="2.5" />
<Shading clipId={clipId} variant="garment" />
{/* camera bump */}
<rect x="180" y="100" width="66" height="66" rx="16" fill="#00000022" stroke={STROKE} strokeWidth="1.6" />
<circle cx="201" cy="121" r="11" fill={`url(#${clipId}-lens)`} stroke={STROKE} strokeWidth="1.2" />
<circle cx="197" cy="117" r="2.4" fill="#fff" fillOpacity="0.6" />
<circle cx="227" cy="143" r="11" fill={`url(#${clipId}-lens)`} stroke={STROKE} strokeWidth="1.2" />
<circle cx="223" cy="139" r="2.4" fill="#fff" fillOpacity="0.6" />
<circle cx="201" cy="148" r="7" fill={`url(#${clipId}-lens)`} stroke={STROKE} strokeWidth="1" />
{/* side buttons */}
<rect x="147" y="150" width="4" height="30" rx="2" fill={STROKE} opacity="0.6" />
<rect x="147" y="195" width="4" height="46" rx="2" fill={STROKE} opacity="0.6" />
<rect x="349" y="170" width="4" height="40" rx="2" fill={STROKE} opacity="0.6" />
</svg>
);
}
export default function ProductMockup({ mockup, color }: Props) {
switch (mockup) {
case 'hoodie':
return <Hoodie color={color} />;
case 'mug':
return <Mug color={color} />;
case 'tumbler':
return <Tumbler color={color} />;
case 'phonecase':
return <PhoneCase color={color} />;
case 'tshirt':
default:
return <TShirt color={color} />;
}
}