150 lines
4.9 KiB
JavaScript
150 lines
4.9 KiB
JavaScript
import '../../styles/TShirtSVG.css';
|
||
|
||
/**
|
||
* Pure-SVG t-shirt mockup. The path is a stylized tee silhouette (collar +
|
||
* sleeves + body). Color is the user-selected garment color from the Shirt
|
||
* Options panel. The print-zone rectangle in the chest area indicates where
|
||
* the design overlay will land — it's visual-only (the actual design overlay
|
||
* sits in the Konva stage on top of this).
|
||
*
|
||
* Sized via `viewBox` so the SVG scales fluidly with its container.
|
||
*/
|
||
export function TShirtSVG({ size = 360, color = '#ffffff', showPrintZone = true }) {
|
||
// Compute a complementary stroke shade so the silhouette stays visible on
|
||
// light AND dark fills. We darken the fill ~12% in HSL space — close enough
|
||
// for our small palette without pulling in a color library.
|
||
const stroke = darken(color, 0.12);
|
||
const isDark = perceptualLuminance(color) < 0.45;
|
||
|
||
return (
|
||
<svg
|
||
className="tshirt-svg"
|
||
viewBox="40 50 400 470"
|
||
role="img"
|
||
aria-label="T-shirt preview"
|
||
preserveAspectRatio="xMidYMid meet"
|
||
>
|
||
{/* Soft drop shadow under the shirt — kept subtle so the canvas remains the focus. */}
|
||
<defs>
|
||
<filter id="tshirt-shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||
<feGaussianBlur in="SourceAlpha" stdDeviation="6" />
|
||
<feOffset dx="0" dy="6" result="offset" />
|
||
<feComponentTransfer>
|
||
<feFuncA type="linear" slope="0.18" />
|
||
</feComponentTransfer>
|
||
<feMerge>
|
||
<feMergeNode />
|
||
<feMergeNode in="SourceGraphic" />
|
||
</feMerge>
|
||
</filter>
|
||
</defs>
|
||
|
||
{/* Shirt silhouette ---------------------------------------------------- */}
|
||
<g filter="url(#tshirt-shadow)">
|
||
<path
|
||
d="
|
||
M 110 90
|
||
L 170 60
|
||
Q 192 84 240 84
|
||
Q 288 84 310 60
|
||
L 370 90
|
||
L 430 130
|
||
L 400 200
|
||
L 360 180
|
||
L 360 480
|
||
Q 360 500 340 500
|
||
L 140 500
|
||
Q 120 500 120 480
|
||
L 120 180
|
||
L 80 200
|
||
L 50 130
|
||
Z
|
||
"
|
||
fill={color}
|
||
stroke={stroke}
|
||
strokeWidth="2"
|
||
strokeLinejoin="round"
|
||
/>
|
||
{/* Collar */}
|
||
<path
|
||
d="
|
||
M 200 78
|
||
Q 240 110 280 78
|
||
"
|
||
fill="none"
|
||
stroke={stroke}
|
||
strokeWidth="2.5"
|
||
strokeLinecap="round"
|
||
/>
|
||
</g>
|
||
|
||
{/* Print zone — corner brackets, not a closed rectangle, so the design
|
||
overlay isn't visually competing with a hard frame. */}
|
||
{showPrintZone && (
|
||
<g
|
||
className="tshirt-svg__print-zone"
|
||
stroke={isDark ? 'rgba(255,255,255,0.55)' : 'rgba(31, 29, 35, 0.18)'}
|
||
strokeWidth="1.5"
|
||
strokeLinecap="round"
|
||
fill="none"
|
||
>
|
||
{/* Corners of a 220×220 print zone centered in the chest */}
|
||
{(() => {
|
||
const x = 130;
|
||
const y = 180;
|
||
const w = 220;
|
||
const h = 220;
|
||
const c = 14;
|
||
return (
|
||
<>
|
||
<path d={`M ${x} ${y + c} L ${x} ${y} L ${x + c} ${y}`} />
|
||
<path d={`M ${x + w - c} ${y} L ${x + w} ${y} L ${x + w} ${y + c}`} />
|
||
<path d={`M ${x + w} ${y + h - c} L ${x + w} ${y + h} L ${x + w - c} ${y + h}`} />
|
||
<path d={`M ${x + c} ${y + h} L ${x} ${y + h} L ${x} ${y + h - c}`} />
|
||
</>
|
||
);
|
||
})()}
|
||
</g>
|
||
)}
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
/* ────────────────────────────────────────────────────────────────────────── */
|
||
/* Tiny color helpers — local to this file to avoid pulling in a dependency. */
|
||
/* ────────────────────────────────────────────────────────────────────────── */
|
||
|
||
function parseHex(hex) {
|
||
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex);
|
||
if (!m) return [255, 255, 255];
|
||
const v = m[1];
|
||
if (v.length === 3) {
|
||
return [
|
||
parseInt(v[0] + v[0], 16),
|
||
parseInt(v[1] + v[1], 16),
|
||
parseInt(v[2] + v[2], 16),
|
||
];
|
||
}
|
||
return [
|
||
parseInt(v.slice(0, 2), 16),
|
||
parseInt(v.slice(2, 4), 16),
|
||
parseInt(v.slice(4, 6), 16),
|
||
];
|
||
}
|
||
|
||
function toHex(r, g, b) {
|
||
const clamp = (v) => Math.max(0, Math.min(255, Math.round(v)));
|
||
return `#${[r, g, b].map((v) => clamp(v).toString(16).padStart(2, '0')).join('')}`;
|
||
}
|
||
|
||
function darken(hex, amount = 0.1) {
|
||
const [r, g, b] = parseHex(hex);
|
||
return toHex(r * (1 - amount), g * (1 - amount), b * (1 - amount));
|
||
}
|
||
|
||
function perceptualLuminance(hex) {
|
||
const [r, g, b] = parseHex(hex);
|
||
// Rec. 601 luma — perceptually weighted, fast.
|
||
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||
}
|