Major update for v1 and tests

This commit is contained in:
khalid@traclabs.com
2026-05-23 03:28:58 -05:00
parent 628a6765f4
commit b1fb6fa3aa
1748 changed files with 27723 additions and 1854 deletions

View File

@@ -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;
}