Major update for v1 and tests
This commit is contained in:
@@ -1,16 +1,149 @@
|
||||
import '../../styles/TShirtSVG.css';
|
||||
|
||||
export function TShirtSVG({ size = 300 }) {
|
||||
const padding = size * 0.1;
|
||||
const innerSize = size - padding * 2;
|
||||
/**
|
||||
* 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 width={size} height={size} viewBox={`0 0 ${size} ${size}`}
|
||||
className="tshirt-svg">
|
||||
<path d={`M ${padding} ${padding + innerSize * 0.15} L ${padding + innerSize * 0.15} ${padding} L ${size - padding - innerSize * 0.15} ${padding} L ${size - padding} ${padding + innerSize * 0.15} L ${size - padding} ${size - padding} L ${padding} ${size - padding} Z`}
|
||||
fill="none" stroke="var(--border)" strokeWidth="2" strokeDasharray="4,4" />
|
||||
<rect x={size * 0.3} y={size * 0.25} width={size * 0.4} height={size * 0.35} fill="none" stroke="var(--accent)" strokeWidth="1.5" opacity="0.5" />
|
||||
<text x={size / 2} y={size * 0.45} textAnchor="middle" fill="var(--text-muted)" fontSize="10" fontFamily="var(--font-mono)">Print Zone</text>
|
||||
<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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user