feat(footer): implement responsive 3D isometric SVG footer scene with theme tokens
This commit is contained in:
497
src/components/IsometricFooter/IsoElements.tsx
Normal file
497
src/components/IsometricFooter/IsoElements.tsx
Normal file
@@ -0,0 +1,497 @@
|
||||
import React from 'react';
|
||||
import type {
|
||||
IsoBoxProps,
|
||||
MountainClusterProps,
|
||||
PineTreeProps,
|
||||
IsoPencilProps,
|
||||
IsoMonitorProps,
|
||||
IsoTowerProps,
|
||||
IsoSignpostProps,
|
||||
} from './IsoTypes';
|
||||
|
||||
/**
|
||||
* Single isometric box: top diamond + two shaded side faces.
|
||||
* (x, y) is the TOP vertex of the diamond. s = half-width, h = height.
|
||||
*/
|
||||
export function IsoBox({ x, y, s, h, top, left, right, stroke = 'none' }: IsoBoxProps) {
|
||||
const topPts = `${x},${y} ${x + s},${y + s / 2} ${x},${y + s} ${x - s},${y + s / 2}`;
|
||||
const leftPts = `${x - s},${y + s / 2} ${x},${y + s} ${x},${y + s + h} ${x - s},${y + s / 2 + h}`;
|
||||
const rightPts = `${x + s},${y + s / 2} ${x},${y + s} ${x},${y + s + h} ${x + s},${y + s / 2 + h}`;
|
||||
return (
|
||||
<g>
|
||||
<polygon points={leftPts} fill={left} stroke={stroke} strokeWidth={stroke === 'none' ? '0' : '1'} />
|
||||
<polygon points={rightPts} fill={right} stroke={stroke} strokeWidth={stroke === 'none' ? '0' : '1'} />
|
||||
<polygon points={topPts} fill={top} stroke={stroke} strokeWidth={stroke === 'none' ? '0' : '1'} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mountain Cluster
|
||||
* Four stacked isometric boxes forming the mountain peak and foothills
|
||||
*/
|
||||
export function MountainCluster({ x, y, flip = false, scale = 1 }: MountainClusterProps) {
|
||||
const f = flip ? -1 : 1;
|
||||
const boxes = [
|
||||
{ dx: 15 * f, dy: -45, s: 95, h: 130 },
|
||||
{ dx: -60 * f, dy: 52, s: 58, h: 70 },
|
||||
{ dx: 85 * f, dy: 65, s: 55, h: 60 },
|
||||
{ dx: 0, dy: 98, s: 46, h: 46 },
|
||||
];
|
||||
|
||||
return (
|
||||
<g transform={`translate(${x} ${y}) scale(${scale})`}>
|
||||
{boxes.map((b, i) => (
|
||||
<IsoBox
|
||||
key={i}
|
||||
x={b.dx}
|
||||
y={b.dy}
|
||||
s={b.s}
|
||||
h={b.h}
|
||||
top="var(--hero-illus-cube-top, #9DCDFD)"
|
||||
left="var(--hero-illus-cube-front, #2066AB)"
|
||||
right="var(--hero-illus-cube-side, #0E4073)"
|
||||
stroke="none"
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pine tree
|
||||
* Features signature smooth teardrop/conical foliage and rounded capsule trunk
|
||||
*/
|
||||
export function PineTree({ x, y, scale = 1, dark = false }: PineTreeProps) {
|
||||
const foliageColor = dark
|
||||
? 'var(--hero-illus-tree-dark, #104179)'
|
||||
: 'var(--hero-illus-tree-light, #2166A9)';
|
||||
const trunkColor = 'var(--hero-illus-tree-trunk, #08234B)';
|
||||
|
||||
return (
|
||||
<g transform={`translate(${x} ${y}) scale(${scale})`}>
|
||||
{/* Capsule Trunk */}
|
||||
<path
|
||||
d="M-4.044 -8.583C-4.044 -10.544 -2.455 -12.133 -0.494 -12.133H0.494C2.455 -12.133 4.044 -10.544 4.044 -8.583V-3.551C4.044 -1.59 2.455 0 0.494 0H-0.494C-2.455 0 -4.044 -1.59 -4.044 -3.551Z"
|
||||
fill={trunkColor}
|
||||
/>
|
||||
{/* Foliage */}
|
||||
<path
|
||||
d="M12.551 -18.365L0 -56.958L-12.551 -18.365C-13.154 -16.509 -13.163 -14.402 -11.681 -13.133C-9.714 -11.447 -6.067 -9.774 0 -9.774C6.067 -9.774 9.714 -11.447 11.681 -13.133C13.163 -14.402 13.154 -16.509 12.551 -18.365Z"
|
||||
fill={foliageColor}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern SVG Icons for Top Contact Bar and Signposts
|
||||
*/
|
||||
export const PhoneIcon: React.FC<{ size?: number; color?: string }> = ({
|
||||
size = 20,
|
||||
color = 'currentColor',
|
||||
}) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill={color} aria-hidden="true">
|
||||
<path d="M6.62 10.79a15.46 15.46 0 0 0 6.59 6.59l2.2-2.2a1.02 1.02 0 0 1 1.02-.24c1.12.37 2.33.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1C10.61 21 3 13.39 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LinkedInIcon: React.FC<{ size?: number; color?: string }> = ({
|
||||
size = 20,
|
||||
color = 'currentColor',
|
||||
}) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill={color} aria-hidden="true">
|
||||
<path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.28 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93h2.75M6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37h2.77z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const GitHubIcon: React.FC<{ size?: number; color?: string }> = ({
|
||||
size = 20,
|
||||
color = 'currentColor',
|
||||
}) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill={color} aria-hidden="true">
|
||||
<path d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33.85 0 1.71.11 2.5.33 1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const TelegramIcon: React.FC<{ size?: number; color?: string }> = ({
|
||||
size = 20,
|
||||
color = 'currentColor',
|
||||
}) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill={color} aria-hidden="true">
|
||||
<path d="M9.8 15.6l-.4 5.3c.6 0 .8-.2 1.1-.5l2.7-2.6 5.6 4.1c1 .6 1.7.3 2-.9l3.7-17.3c.4-1.5-.5-2.2-1.5-1.8L1.6 10.3c-1.5.6-1.5 1.5-.2 1.9l5.5 1.7 12.8-8.1c.6-.4 1.1-.2.7.2z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MailIcon: React.FC<{ size?: number; color?: string }> = ({
|
||||
size = 18,
|
||||
color = 'currentColor',
|
||||
}) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill={color} aria-hidden="true">
|
||||
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Upright Majestic Faceted Pencil matching reference
|
||||
* Blue faceted hexagonal shaft, cool white wood collar, and royal blue conical tip.
|
||||
*/
|
||||
export function IsoUprightPencil({ x, y, height = 84, width = 18 }: IsoPencilProps) {
|
||||
const hw = width / 2;
|
||||
const bodyH = height - 26;
|
||||
const collarH = 14;
|
||||
const tipH = 12;
|
||||
|
||||
const baseY = y;
|
||||
const collarBaseY = baseY - bodyH;
|
||||
const tipBaseY = collarBaseY - collarH;
|
||||
const tipY = tipBaseY - tipH;
|
||||
|
||||
const p1 = `${x - hw},${baseY - 2} ${x - hw * 0.25},${baseY + 1} ${x - hw * 0.25},${collarBaseY} ${x - hw},${collarBaseY - 2}`;
|
||||
const p2 = `${x - hw * 0.25},${baseY + 1} ${x + hw * 0.25},${baseY + 1} ${x + hw * 0.25},${collarBaseY} ${x - hw * 0.25},${collarBaseY}`;
|
||||
const p3 = `${x + hw * 0.25},${baseY + 1} ${x + hw},${baseY - 2} ${x + hw},${collarBaseY - 2} ${x + hw * 0.25},${collarBaseY}`;
|
||||
|
||||
const woodLeft = `${x - hw},${collarBaseY - 2} ${x - hw * 0.25},${collarBaseY} ${x},${tipBaseY}`;
|
||||
const woodMid = `${x - hw * 0.25},${collarBaseY} ${x + hw * 0.25},${collarBaseY} ${x},${tipBaseY}`;
|
||||
const woodRight = `${x + hw * 0.25},${collarBaseY} ${x + hw},${collarBaseY - 2} ${x},${tipBaseY}`;
|
||||
|
||||
const leadLeft = `${x - hw * 0.32},${tipBaseY} ${x},${tipBaseY} ${x},${tipY}`;
|
||||
const leadRight = `${x},${tipBaseY} ${x + hw * 0.32},${tipBaseY} ${x},${tipY}`;
|
||||
|
||||
return (
|
||||
<g className="iso-pencil">
|
||||
{/* Ground Contact Shadow */}
|
||||
<polygon
|
||||
points={`${x - hw * 1.2},${baseY - 1} ${x},${baseY + 3} ${x + hw * 1.2},${baseY - 1} ${x},${baseY - 4}`}
|
||||
fill="var(--hero-illus-cube-shadow, #0C234F)"
|
||||
opacity={0.18}
|
||||
/>
|
||||
|
||||
{/* Shaft Facets (Light / Front / Dark matching 3D isometric lighting) */}
|
||||
<polygon points={p1} fill="var(--hero-illus-cube-top, #9DCDFD)" stroke="var(--hero-illus-cube-front, #2066AB)" strokeWidth={0.4} />
|
||||
<polygon points={p2} fill="var(--hero-illus-cube-front, #2066AB)" stroke="var(--hero-illus-cube-side, #0E4073)" strokeWidth={0.4} />
|
||||
<polygon points={p3} fill="var(--hero-illus-cube-side, #0E4073)" stroke="var(--hero-illus-cube-shadow, #0C234F)" strokeWidth={0.4} />
|
||||
|
||||
{/* Scalloped Wood Facets */}
|
||||
<polygon points={woodLeft} fill="var(--bg-subtle, #E6F0FE)" stroke="var(--border-default, #D9E2EF)" strokeWidth={0.3} />
|
||||
<polygon points={woodMid} fill="var(--bg-raised, #FCFDFF)" stroke="var(--border-default, #D9E2EF)" strokeWidth={0.3} />
|
||||
<polygon points={woodRight} fill="var(--color-border-light, #D9E2EF)" stroke="var(--border-strong, #A9BEDA)" strokeWidth={0.3} />
|
||||
|
||||
{/* Lead Tip */}
|
||||
<polygon points={leadLeft} fill="var(--hero-illus-cube-front, #2066AB)" stroke="var(--hero-illus-cube-side, #0E4073)" strokeWidth={0.3} />
|
||||
<polygon points={leadRight} fill="var(--hero-illus-cube-side, #0E4073)" stroke="var(--hero-illus-cube-shadow, #0C234F)" strokeWidth={0.3} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Isometric Workstation Monitor matching reference
|
||||
* Sizable flat panel screen angled in 3D, standing proudly on Tile 1
|
||||
*/
|
||||
export function IsoMonitorDisplay({ x, y, scale = 1 }: IsoMonitorProps) {
|
||||
const baseW = 20;
|
||||
const baseH = 10;
|
||||
|
||||
// Stand Base diamond
|
||||
const sbFront = `${x},${y + baseH / 2}`;
|
||||
const sbLeft = `${x - baseW / 2},${y}`;
|
||||
const sbRight = `${x + baseW / 2},${y}`;
|
||||
const sbBack = `${x},${y - baseH / 2}`;
|
||||
const sbTop = `${sbBack} ${sbRight} ${sbFront} ${sbLeft}`;
|
||||
const sbLeftDrop = `${sbLeft} ${sbFront} ${x},${y + baseH / 2 + 2} ${x - baseW / 2},${y + 2}`;
|
||||
const sbRightDrop = `${sbFront} ${sbRight} ${x + baseW / 2},${y + 2} ${x},${y + baseH / 2 + 2}`;
|
||||
|
||||
// Monitor Display Panel (sleek, well-proportioned, clear of pencil)
|
||||
const panCx = x;
|
||||
const panCy = y - 24;
|
||||
|
||||
const hw = 21;
|
||||
const hh = 15;
|
||||
const slopeDy = 10.5; // 21 * 0.5 = 10.5
|
||||
const depth = 3.5;
|
||||
|
||||
// Stand Neck column (extends from base plate up solidly behind the opaque screen panel)
|
||||
const neckTop = panCy + 4;
|
||||
const neckFront = `${x - 2},${neckTop} ${x + 2},${neckTop + 2} ${x + 2},${y} ${x - 2},${y - 2}`;
|
||||
const neckSide = `${x + 2},${neckTop + 2} ${x + 4},${neckTop + 1} ${x + 4},${y - 1} ${x + 2},${y}`;
|
||||
|
||||
const fTopLeft = `${panCx - hw},${panCy - hh - slopeDy}`;
|
||||
const fTopRight = `${panCx + hw},${panCy - hh + slopeDy}`;
|
||||
const fBottomRight = `${panCx + hw},${panCy + hh + slopeDy}`;
|
||||
const fBottomLeft = `${panCx - hw},${panCy + hh - slopeDy}`;
|
||||
const pFront = `${fTopLeft} ${fTopRight} ${fBottomRight} ${fBottomLeft}`;
|
||||
|
||||
const bTopLeft = `${panCx - hw + depth},${panCy - hh - slopeDy - depth * 0.5}`;
|
||||
const bTopRight = `${panCx + hw + depth},${panCy - hh + slopeDy - depth * 0.5}`;
|
||||
const pTop = `${fTopLeft} ${bTopLeft} ${bTopRight} ${fTopRight}`;
|
||||
|
||||
const bBottomRight = `${panCx + hw + depth},${panCy + hh + slopeDy - depth * 0.5}`;
|
||||
const pRight = `${fTopRight} ${bTopRight} ${bBottomRight} ${fBottomRight}`;
|
||||
|
||||
return (
|
||||
<g
|
||||
className="iso-monitor"
|
||||
transform={scale !== 1 ? `translate(${x}, ${y}) scale(${scale}) translate(${-x}, ${-y})` : undefined}
|
||||
>
|
||||
{/* Ground Contact Shadow */}
|
||||
<polygon
|
||||
points={`${x - baseW * 0.7},${y} ${x},${y + baseH * 0.7} ${x + baseW * 0.7},${y} ${x},${y - baseH * 0.7}`}
|
||||
fill="var(--hero-illus-cube-shadow, #0C234F)"
|
||||
opacity={0.16}
|
||||
/>
|
||||
|
||||
{/* Stand Base Drop Faces */}
|
||||
<polygon points={sbLeftDrop} fill="var(--hero-illus-cube-front, #2066AB)" stroke="var(--hero-illus-cube-side, #0E4073)" strokeWidth={0.4} />
|
||||
<polygon points={sbRightDrop} fill="var(--hero-illus-cube-side, #0E4073)" stroke="var(--hero-illus-cube-shadow, #0C234F)" strokeWidth={0.4} />
|
||||
<polygon points={sbTop} fill="var(--hero-illus-cube-top, #9DCDFD)" stroke="var(--color-blue-300, #82B5FF)" strokeWidth={0.6} />
|
||||
|
||||
{/* Stand Neck Column */}
|
||||
<polygon points={neckFront} fill="var(--hero-illus-cube-top, #9DCDFD)" stroke="var(--hero-illus-cube-front, #2066AB)" strokeWidth={0.4} />
|
||||
<polygon points={neckSide} fill="var(--hero-illus-cube-side, #0E4073)" stroke="var(--hero-illus-cube-shadow, #0C234F)" strokeWidth={0.4} />
|
||||
|
||||
{/* Monitor Display Bezel & Screen Face */}
|
||||
<polygon points={pTop} fill="var(--hero-illus-cube-top, #9DCDFD)" stroke="var(--color-blue-300, #82B5FF)" strokeWidth={0.6} />
|
||||
<polygon points={pRight} fill="var(--hero-illus-cube-side, #0E4073)" stroke="var(--hero-illus-cube-shadow, #0C234F)" strokeWidth={0.6} />
|
||||
<polygon points={pFront} fill="var(--accent-default, #3D8AFA)" stroke="var(--color-blue-500, #1557A6)" strokeWidth={0.8} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Isometric PC Tower Unit matching reference
|
||||
* Vertical rectangular prism sitting on a clean white pedestal plate
|
||||
*/
|
||||
export function IsoPCTower({ x, y, size = 20, height = 40 }: IsoTowerProps) {
|
||||
const hs = size / 2;
|
||||
const plateW = size + 10;
|
||||
const plateH = (size + 10) * 0.5;
|
||||
|
||||
// Base Pedestal Plate
|
||||
const plFront = `${x},${y + plateH / 2}`;
|
||||
const plLeft = `${x - plateW / 2},${y}`;
|
||||
const plRight = `${x + plateW / 2},${y}`;
|
||||
const plBack = `${x},${y - plateH / 2}`;
|
||||
const plTop = `${plBack} ${plRight} ${plFront} ${plLeft}`;
|
||||
const plLeftDrop = `${plLeft} ${plFront} ${x},${y + plateH / 2 + 2.5} ${x - plateW / 2},${y + 2.5}`;
|
||||
const plRightDrop = `${plFront} ${plRight} ${x + plateW / 2},${y + 2.5} ${x},${y + plateH / 2 + 2.5}`;
|
||||
|
||||
// Cube Base corners
|
||||
const baseY = y - 1;
|
||||
const cFront = `${x},${baseY + hs * 0.5}`;
|
||||
const cLeft = `${x - hs},${baseY}`;
|
||||
const cRight = `${x + hs},${baseY}`;
|
||||
const cBack = `${x},${baseY - hs * 0.5}`;
|
||||
|
||||
// Cube Top corners
|
||||
const topY = baseY - height;
|
||||
const tFront = `${x},${topY + hs * 0.5}`;
|
||||
const tLeft = `${x - hs},${topY}`;
|
||||
const tRight = `${x + hs},${topY}`;
|
||||
const tBack = `${x},${topY - hs * 0.5}`;
|
||||
|
||||
const faceTop = `${tBack} ${tRight} ${tFront} ${tLeft}`;
|
||||
const faceLeft = `${cLeft} ${cFront} ${tFront} ${tLeft}`;
|
||||
const faceRight = `${cFront} ${cRight} ${tRight} ${tFront}`;
|
||||
|
||||
return (
|
||||
<g className="iso-pc-tower">
|
||||
{/* Pedestal Plate */}
|
||||
<polygon points={plLeftDrop} fill="var(--hero-illus-cube-front, #2066AB)" stroke="var(--hero-illus-cube-side, #0E4073)" strokeWidth={0.4} />
|
||||
<polygon points={plRightDrop} fill="var(--hero-illus-cube-side, #0E4073)" stroke="var(--hero-illus-cube-shadow, #0C234F)" strokeWidth={0.4} />
|
||||
<polygon points={plTop} fill="var(--bg-raised, #FCFDFF)" stroke="var(--border-strong, #A9BEDA)" strokeWidth={0.7} />
|
||||
|
||||
{/* Main Vertical Tower Body */}
|
||||
<polygon points={faceLeft} fill="var(--hero-illus-cube-front, #2066AB)" stroke="var(--hero-illus-cube-side, #0E4073)" strokeWidth={0.6} />
|
||||
<polygon points={faceRight} fill="var(--hero-illus-cube-side, #0E4073)" stroke="var(--hero-illus-cube-shadow, #0C234F)" strokeWidth={0.6} />
|
||||
<polygon points={faceTop} fill="var(--hero-illus-cube-top, #9DCDFD)" stroke="var(--hero-illus-cube-front, #2066AB)" strokeWidth={0.7} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Isometric Signpost
|
||||
* True 3D item with elevated 3D cylindrical base plate and shaded 3D cylindrical metallic pole
|
||||
*/
|
||||
export function IsoSignpost({
|
||||
id: _id,
|
||||
x,
|
||||
y,
|
||||
postHeight = 48,
|
||||
facing = 'right',
|
||||
backdropDirection,
|
||||
icon,
|
||||
label: _label,
|
||||
isActive = false,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onClick,
|
||||
}: IsoSignpostProps) {
|
||||
const isRight = facing === 'right';
|
||||
const postTopY = y - postHeight;
|
||||
const cardSize = 32;
|
||||
const radius = 6;
|
||||
|
||||
// 3D Isometric Shear Matrix:
|
||||
// facing="right" (SE): slope +0.5 -> matrix(1, 0.5, 0, 1, 0, 0)
|
||||
// facing="left" (SW): slope -0.5 -> matrix(1, -0.5, 0, 1, 0, 0)
|
||||
const shearSlope = isRight ? 0.5 : -0.5;
|
||||
const gradId = `standPoleGrad-${_id}`;
|
||||
|
||||
// Directional Backdrop Offset:
|
||||
// Rightside items (GitHub, Telegram): 'left-top' (backShiftX = -3.5, backShiftY = -3.2)
|
||||
// Leftside items (Phone, LinkedIn): 'right-top' (backShiftX = +3.5, backShiftY = -3.2)
|
||||
const dir = backdropDirection ?? (isRight ? 'right-top' : 'left-top');
|
||||
const isRightTop = dir === 'right-top';
|
||||
const backShiftX = isRightTop ? 3.5 : -3.5;
|
||||
const backShiftY = -3.2;
|
||||
|
||||
return (
|
||||
<g
|
||||
className={`iso-signpost ${isActive ? 'active' : ''}`}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={onClick}
|
||||
style={{ cursor: 'pointer', touchAction: 'manipulation' }}
|
||||
>
|
||||
<defs>
|
||||
{/* 3D Cylindrical Metallic Gradient for Upright Pole */}
|
||||
<linearGradient id={gradId} x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="var(--hero-illus-cube-top, #9DCDFD)" />
|
||||
<stop offset="35%" stopColor="var(--accent-default, #3D8AFA)" />
|
||||
<stop offset="70%" stopColor="var(--hero-illus-cube-front, #2066AB)" />
|
||||
<stop offset="100%" stopColor="var(--hero-illus-cube-side, #0E4073)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* Expanded invisible touch hit box */}
|
||||
<rect
|
||||
x={x - 28}
|
||||
y={postTopY - cardSize - 10}
|
||||
width={56}
|
||||
height={cardSize + postHeight + 16}
|
||||
fill="transparent"
|
||||
/>
|
||||
|
||||
{/* 3D Ground Base Assembly */}
|
||||
{/* 1. 3D Cylindrical Disc Drop Apron (Thickness = 4px) */}
|
||||
<path
|
||||
d={`M ${x - 11.5},${y} A 11.5 5.75 0 0 0 ${x + 11.5},${y} L ${x + 11.5},${y + 4} A 11.5 5.75 0 0 1 ${x - 11.5},${y + 4} Z`}
|
||||
fill={isActive ? 'var(--hero-illus-cube-side, #0E4073)' : 'var(--hero-illus-cube-front, #2066AB)'}
|
||||
stroke="var(--hero-illus-cube-side, #0E4073)"
|
||||
strokeWidth={0.6}
|
||||
/>
|
||||
{/* Center ridge divider on the cylinder drop */}
|
||||
<line x1={x} y1={y + 5.75} x2={x} y2={y + 9.75} stroke="var(--hero-illus-cube-shadow, #0C234F)" strokeWidth={0.8} />
|
||||
|
||||
{/* 3. 3D Disc Top Surface */}
|
||||
<ellipse
|
||||
cx={x}
|
||||
cy={y}
|
||||
rx={11.5}
|
||||
ry={5.75}
|
||||
fill={isActive ? 'var(--color-blue-300, #82B5FF)' : 'var(--bg-subtle, #E6F0FE)'}
|
||||
stroke={isActive ? 'var(--hero-illus-cube-front, #2066AB)' : 'var(--border-strong, #A9BEDA)'}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
|
||||
{/* 4. Concentric Socket Collar / Ring where pole enters */}
|
||||
<path
|
||||
d={`M ${x - 4.5},${y - 0.5} A 4.5 2.25 0 0 0 ${x + 4.5},${y - 0.5} L ${x + 4.5},${y + 1.5} A 4.5 2.25 0 0 1 ${x - 4.5},${y + 1.5} Z`}
|
||||
fill="var(--hero-illus-cube-shadow, #0C234F)"
|
||||
/>
|
||||
<ellipse
|
||||
cx={x}
|
||||
cy={y - 0.5}
|
||||
rx={4.5}
|
||||
ry={2.25}
|
||||
fill="var(--hero-illus-cube-front, #2066AB)"
|
||||
stroke="var(--hero-illus-cube-side, #0E4073)"
|
||||
strokeWidth={0.6}
|
||||
/>
|
||||
<ellipse
|
||||
cx={x}
|
||||
cy={y - 0.5}
|
||||
rx={2}
|
||||
ry={1}
|
||||
fill="var(--hero-illus-cube-shadow, #0C234F)"
|
||||
/>
|
||||
|
||||
{/* 3D Cylindrical Metallic Pole (extends seamlessly up behind the card body) */}
|
||||
<rect
|
||||
x={x - 1.4}
|
||||
y={postTopY - 16}
|
||||
width={2.8}
|
||||
height={y - (postTopY - 16)}
|
||||
rx={1.4}
|
||||
fill={`url(#${gradId})`}
|
||||
/>
|
||||
{/* Left-edge specular highlight glint line */}
|
||||
<line
|
||||
x1={x - 0.7}
|
||||
y1={y - 2}
|
||||
x2={x - 0.7}
|
||||
y2={postTopY - 14}
|
||||
stroke="var(--color-blue-white, #F8FAFF)"
|
||||
strokeWidth={0.6}
|
||||
opacity={0.65}
|
||||
/>
|
||||
|
||||
{/* 3D Rotated Isometric Signboard */}
|
||||
<g
|
||||
transform={isActive ? 'translate(0, -3)' : 'translate(0, 0)'}
|
||||
style={{ transition: 'transform 0.25s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
||||
>
|
||||
{/* 1. Backdrop Layer (Positioned to left-top for rightside items, right-top for leftside items) */}
|
||||
<g transform={`translate(${x + backShiftX}, ${postTopY - 18 + backShiftY}) matrix(1, ${shearSlope}, 0, 1, 0, 0)`}>
|
||||
{/* Subtle Ambient Shadow behind the backdrop plate */}
|
||||
<rect
|
||||
x={-cardSize / 2 + (isRightTop ? 1.2 : -1.2)}
|
||||
y={-cardSize / 2 + 1}
|
||||
width={cardSize}
|
||||
height={cardSize}
|
||||
rx={radius}
|
||||
fill="var(--hero-illus-cube-shadow, #0C234F)"
|
||||
opacity={isActive ? 0.35 : 0.18}
|
||||
/>
|
||||
|
||||
{/* 3D Backdrop Plate with signature royal blue fill and darker border */}
|
||||
<rect
|
||||
x={-cardSize / 2}
|
||||
y={-cardSize / 2}
|
||||
width={cardSize}
|
||||
height={cardSize}
|
||||
rx={radius}
|
||||
fill={isActive ? 'var(--hero-illus-cube-side, #0E4073)' : 'var(--hero-illus-cube-front, #2066AB)'}
|
||||
stroke={isActive ? 'var(--hero-illus-cube-shadow, #0C234F)' : 'var(--hero-illus-cube-side, #0E4073)'}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* 2. Front Card Layer (Centered flush on the pole axis at x, postTopY - 18) */}
|
||||
<g transform={`translate(${x}, ${postTopY - 18}) matrix(1, ${shearSlope}, 0, 1, 0, 0)`}>
|
||||
{/* Crisp Opaque Card Body with Blue Border */}
|
||||
<rect
|
||||
x={-cardSize / 2}
|
||||
y={-cardSize / 2}
|
||||
width={cardSize}
|
||||
height={cardSize}
|
||||
rx={radius}
|
||||
fill="var(--bg-raised, #FCFDFF)"
|
||||
stroke={isActive ? 'var(--accent-default, #3D8AFA)' : 'var(--border-strong, #A9BEDA)'}
|
||||
strokeWidth={isActive ? 2 : 1.6}
|
||||
/>
|
||||
|
||||
{/* 3D Rotated Icon inside Card */}
|
||||
<g
|
||||
transform="translate(0, 0)"
|
||||
color={isActive ? 'var(--accent-default, #3D8AFA)' : 'var(--hero-illus-cube-front, #2066AB)'}
|
||||
>
|
||||
<g transform="translate(-8.5, -8.5) scale(0.85)">
|
||||
{icon}
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user