update navber wings

This commit is contained in:
2026-07-09 15:20:34 +05:30
parent d83b709245
commit ae881da02b
3 changed files with 252 additions and 9 deletions

View File

@@ -460,7 +460,7 @@ export default function Header() {
<Link href="/#home" onClick={(event) => handleNavClick(event, "/", "home")}>Home</Link> <Link href="/#home" onClick={(event) => handleNavClick(event, "/", "home")}>Home</Link>
</li> </li>
<li id="menu-item-10509" className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10509${dmHeaderActive("doormile-wings")}`}> <li id="menu-item-10509" className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10509${dmHeaderActive("doormile-wings")}`}>
<Link href="/doormile-wings" prefetch>Doormile Wings</Link> <Link href="/doormile-wings" prefetch>Wings</Link>
</li> </li>
<li id="menu-item-10510" className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10510${dmHeaderActive("miletruth")}`}> <li id="menu-item-10510" className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10510${dmHeaderActive("miletruth")}`}>
<Link href="/miletruth#miletruth" onClick={(event) => handleNavClick(event, "/miletruth", "miletruth")}>MileTruth&trade; AI</Link> <Link href="/miletruth#miletruth" onClick={(event) => handleNavClick(event, "/miletruth", "miletruth")}>MileTruth&trade; AI</Link>
@@ -558,7 +558,7 @@ export default function Header() {
<Link href="/#home" onClick={(event) => handleNavClick(event, "/", "home", true)}>Home</Link> <Link href="/#home" onClick={(event) => handleNavClick(event, "/", "home", true)}>Home</Link>
</li> </li>
<li className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10509${dmHeaderActive("doormile-wings")}`}> <li className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10509${dmHeaderActive("doormile-wings")}`}>
<Link href="/doormile-wings" prefetch onClick={closeMobileMenu}>Doormile Wings</Link> <Link href="/doormile-wings" prefetch onClick={closeMobileMenu}>Wings</Link>
</li> </li>
<li className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10510${dmHeaderActive("miletruth")}`}> <li className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10510${dmHeaderActive("miletruth")}`}>
<Link href="/miletruth#miletruth" onClick={(event) => handleNavClick(event, "/miletruth", "miletruth", true)}>MileTruth&trade; AI</Link> <Link href="/miletruth#miletruth" onClick={(event) => handleNavClick(event, "/miletruth", "miletruth", true)}>MileTruth&trade; AI</Link>

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollReveal, StaggerChildren } from "@/animations/Reveal"; import { ScrollReveal, StaggerChildren } from "@/animations/Reveal";
const CAPABILITIES = [ const CAPABILITIES = [
@@ -64,6 +65,114 @@ const CAPABILITIES = [
}, },
]; ];
function TiltCard({ children, className }: { children: React.ReactNode, className: string }) {
const cardRef = useRef<HTMLDivElement>(null);
const glareRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const card = cardRef.current;
const glare = glareRef.current;
if (!card || !glare) return;
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (reduced) return;
const handleMouseMove = (e: MouseEvent) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
// Softer, more premium 3D tilt
const rotateX = ((y - centerY) / centerY) * -3;
const rotateY = ((x - centerX) / centerX) * 3;
gsap.to(card, {
duration: 0.4,
rotateX,
rotateY,
y: -4,
transformPerspective: 1000,
ease: "power2.out"
});
// Dynamic glare tracking
gsap.set(glare, {
"--mx": `${x}px`,
"--my": `${y}px`,
});
gsap.to(glare, {
duration: 0.3,
opacity: 1,
ease: "power2.out"
});
// Z-axis pop
gsap.to(card.querySelector('.wmt-card-icon'), {
duration: 0.4,
z: 25,
ease: "power2.out"
});
gsap.to(card.querySelector('.wmt-card-title'), {
duration: 0.4,
z: 15,
ease: "power2.out"
});
};
const handleMouseLeave = () => {
gsap.to(card, {
duration: 0.8,
rotateX: 0,
rotateY: 0,
y: 0,
ease: "power3.out"
});
gsap.to(glare, {
duration: 0.5,
opacity: 0,
ease: "power3.out"
});
gsap.to(card.querySelectorAll('.wmt-card-icon, .wmt-card-title'), {
duration: 0.8,
z: 0,
ease: "power3.out"
});
};
card.addEventListener("mousemove", handleMouseMove);
card.addEventListener("mouseleave", handleMouseLeave);
return () => {
card.removeEventListener("mousemove", handleMouseMove);
card.removeEventListener("mouseleave", handleMouseLeave);
};
}, []);
return (
<div ref={cardRef} className={className}>
<div
ref={glareRef}
style={{
position: "absolute",
inset: 0,
borderRadius: "inherit",
background: "radial-gradient(circle 220px at var(--mx, 50%) var(--my, 50%), rgba(255, 255, 255, 0.08) 0%, rgba(200, 16, 46, 0.03) 40%, transparent 100%)",
opacity: 0,
pointerEvents: "none",
zIndex: 0,
transform: "translateZ(1px)",
mixBlendMode: "screen"
}}
/>
<div style={{ position: "relative", zIndex: 1, transformStyle: "preserve-3d" }}>
{children}
</div>
</div>
);
}
export default function WingsMileTruth() { export default function WingsMileTruth() {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -238,11 +347,12 @@ export default function WingsMileTruth() {
border: 1px solid rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 18px; border-radius: 18px;
padding: 26px 22px; padding: 26px 22px;
transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.5s cubic-bezier(0.16, 1, 0.3, 1), background-color 0.5s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.5s cubic-bezier(0.16, 1, 0.3, 1); transition: border-color 0.5s cubic-bezier(0.16, 1, 0.3, 1), background-color 0.5s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.5s cubic-bezier(0.16, 1, 0.3, 1);
transform-style: preserve-3d;
will-change: transform;
} }
.wmt-card:hover { .wmt-card:hover {
transform: translateY(-6px);
border-color: rgba(255, 255, 255, 0.16); border-color: rgba(255, 255, 255, 0.16);
background-color: rgba(255, 255, 255, 0.055); background-color: rgba(255, 255, 255, 0.055);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35);
@@ -370,11 +480,11 @@ export default function WingsMileTruth() {
<StaggerChildren stagger={0.08} duration={0.65} yOffset={25} className="wmt-grid"> <StaggerChildren stagger={0.08} duration={0.65} yOffset={25} className="wmt-grid">
{CAPABILITIES.map((c) => ( {CAPABILITIES.map((c) => (
<div key={c.title} className="wmt-card"> <TiltCard key={c.title} className="wmt-card">
<div className="wmt-card-icon" aria-hidden="true">{c.icon}</div> <div className="wmt-card-icon" aria-hidden="true">{c.icon}</div>
<h3 className="wmt-card-title">{c.title}</h3> <h3 className="wmt-card-title">{c.title}</h3>
<p className="wmt-card-desc">{c.desc}</p> <p className="wmt-card-desc">{c.desc}</p>
</div> </TiltCard>
))} ))}
</StaggerChildren> </StaggerChildren>

View File

@@ -25,6 +25,14 @@ const MOBILE_BQ = 600;
const AIRCRAFT_ROTATION_OFFSET = 90; // glyph's rest pose points "up" const AIRCRAFT_ROTATION_OFFSET = 90; // glyph's rest pose points "up"
const WAVE_AMPLITUDE_MIN = 20; const WAVE_AMPLITUDE_MIN = 20;
const WAVE_AMPLITUDE_MAX = 38; const WAVE_AMPLITUDE_MAX = 38;
// Row-wrapped grid layouts only (e.g. the 601-1024px 2-column tablet
// breakpoint) — single-row layouts (desktop, mobile) never use these.
const ROW_Y_TOLERANCE = 12; // px tolerance for clustering card anchors into the same row
const ROW_CLEARANCE_PADDING = 6; // safety margin between a crest and the row above's card bottom
// Exact max of buildWavePoints' ampFactor (1 + 0.28 + 0.15), used to clamp a
// row's wave amplitude so no crest can ever reach into the row above it,
// regardless of segment index/phase.
const MAX_WAVE_AMP_FACTOR = 1 + 0.28 + 0.15;
const FEATURES = [ const FEATURES = [
{ {
@@ -79,6 +87,20 @@ interface Point {
y: number; y: number;
} }
// A non-mobile card anchor, carrying its source card's full box (wrap-
// relative) alongside the {x,y} route point — top/bottom are needed to
// group anchors into rows and clamp a row's wave amplitude against the row
// above it; left/right are needed to find the empty column gutter (the
// icon's own x sits near the left edge of its card, not the card's true
// horizontal center, so card edges — not icon x — must drive that
// calculation). Never leaves measure(); only plain Point[] is returned.
interface CardAnchor extends Point {
top: number;
bottom: number;
left: number;
right: number;
}
function buildPathD(points: Point[]): string { function buildPathD(points: Point[]): string {
if (points.length < 2) return ""; if (points.length < 2) return "";
let d = `M ${points[0].x} ${points[0].y}`; let d = `M ${points[0].x} ${points[0].y}`;
@@ -126,6 +148,87 @@ function buildWavePoints(points: Point[], amplitude: number): Point[] {
return waved; return waved;
} }
// Clusters card anchors into visual rows by their shared top edge. CSS grid
// auto-flow is row-major and `align-items: stretch` (the grid default) makes
// same-row cards share cardRect.top exactly, so a single-pass walk against
// the current row's reference y (with a small tolerance for sub-pixel
// getBoundingClientRect noise) is sufficient — no need for full clustering.
function groupIntoRows(anchors: CardAnchor[]): CardAnchor[][] {
const rows: CardAnchor[][] = [];
for (const a of anchors) {
const row = rows[rows.length - 1];
if (row && Math.abs(row[0].top - a.top) <= ROW_Y_TOLERANCE) {
row.push(a);
} else {
rows.push([a]);
}
}
return rows;
}
// The x midpoint of the empty strip between two adjacent columns in a row —
// i.e. a real, always-card-free gutter, derived from actual card edges (not
// icon x, which sits near each card's left padding rather than its true
// horizontal center). `side` picks which internal gutter: "right" is
// between the two rightmost columns, "left" between the two leftmost — for
// the current 2-column layout these are the same single gutter, but this
// stays correct if a wider grid (3+ columns) ever wraps into multiple rows.
function computeColumnGutterX(row: CardAnchor[], side: "left" | "right"): number {
const sorted = [...row].sort((a, b) => a.left - b.left);
if (sorted.length < 2) return (sorted[0].left + sorted[0].right) / 2;
return side === "right"
? (sorted[sorted.length - 2].right + sorted[sorted.length - 1].left) / 2
: (sorted[0].right + sorted[1].left) / 2;
}
// Builds a safe serpentine (boustrophedon) route across a row-wrapped grid:
// each row is traversed in alternating direction so consecutive rows' ends
// line up on the same side, and the vertical drop between rows is routed
// through the always-empty column gutter on that side rather than through
// either column's own x — the only way to guarantee the connector never
// crosses a card, since a card anchor sits only ~10px below the card above
// it in the same column. Each row's own wave amplitude is clamped to the
// measured clearance above it so no crest can reach into the row above.
function buildMultiRowRoute(rows: CardAnchor[][], baseAmplitude: number): Point[] {
const out: Point[] = [];
let prevRowBottom: number | null = null;
rows.forEach((row, ri) => {
const ordered = ri % 2 === 0 ? row : [...row].reverse();
const rowAnchorY = ordered[0].y;
const amplitude =
prevRowBottom === null
? baseAmplitude
: Math.max(
0,
Math.min(baseAmplitude, (rowAnchorY - prevRowBottom - ROW_CLEARANCE_PADDING) / MAX_WAVE_AMP_FACTOR)
);
const waved = buildWavePoints(
ordered.map(({ x, y }) => ({ x, y })),
amplitude
);
if (out.length > 0) {
const prevPoint = out[out.length - 1];
const nextPoint = waved[0];
// Row (ri-1) ends at its right extreme when it traveled left-to-right
// (even index, unreversed), left extreme when it traveled
// right-to-left (odd index, reversed) — the gutter must sit on that
// same side so the dogleg never has to cross back over a card.
const side: "left" | "right" = (ri - 1) % 2 === 0 ? "right" : "left";
const gutterX = computeColumnGutterX(rows[ri - 1], side);
out.push({ x: gutterX, y: prevPoint.y }, { x: gutterX, y: nextPoint.y });
}
out.push(...waved);
prevRowBottom = Math.max(...row.map((a) => a.bottom));
});
return out;
}
export default function WingsWhyGrid() { export default function WingsWhyGrid() {
const wrapRef = useRef<HTMLDivElement>(null); const wrapRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement>(null); const svgRef = useRef<SVGSVGElement>(null);
@@ -198,24 +301,43 @@ export default function WingsWhyGrid() {
const wrapRect = wrap.getBoundingClientRect(); const wrapRect = wrap.getBoundingClientRect();
const isMobile = isMobileLayout(); const isMobile = isMobileLayout();
const points = cardRefs.current // Card box (wrap-relative) is captured alongside x/y for every card,
// mobile included, so the shape is uniform — only the non-mobile
// branch below actually reads top/bottom/left/right (to group into
// rows, clamp wave amplitude against the row above, and find the
// empty column gutter for inter-row transitions).
const anchors = cardRefs.current
.map((card, i) => { .map((card, i) => {
const icon = iconRefs.current[i]; const icon = iconRefs.current[i];
if (!card || !icon) return null; if (!card || !icon) return null;
const cardRect = card.getBoundingClientRect(); const cardRect = card.getBoundingClientRect();
const iconRect = icon.getBoundingClientRect(); const iconRect = icon.getBoundingClientRect();
const top = cardRect.top - wrapRect.top;
const bottom = cardRect.bottom - wrapRect.top;
const left = cardRect.left - wrapRect.left;
const right = cardRect.right - wrapRect.left;
if (isMobile) { if (isMobile) {
return { return {
x: 12, x: 12,
y: iconRect.top + iconRect.height / 2 - wrapRect.top, y: iconRect.top + iconRect.height / 2 - wrapRect.top,
top,
bottom,
left,
right,
}; };
} }
return { return {
x: iconRect.left + iconRect.width / 2 - wrapRect.left, x: iconRect.left + iconRect.width / 2 - wrapRect.left,
y: cardRect.top - wrapRect.top - 14, y: cardRect.top - wrapRect.top - 14,
top,
bottom,
left,
right,
}; };
}) })
.filter((p): p is Point => p !== null); .filter((p): p is CardAnchor => p !== null);
const points: Point[] = anchors.map(({ x, y }) => ({ x, y }));
svg.setAttribute("width", String(wrapRect.width)); svg.setAttribute("width", String(wrapRect.width));
svg.setAttribute("height", String(wrapRect.height)); svg.setAttribute("height", String(wrapRect.height));
@@ -228,7 +350,18 @@ export default function WingsWhyGrid() {
const amplitude = isMobile const amplitude = isMobile
? 0 ? 0
: Math.max(WAVE_AMPLITUDE_MIN, Math.min(WAVE_AMPLITUDE_MAX, wrapRect.width / 20)); : Math.max(WAVE_AMPLITUDE_MIN, Math.min(WAVE_AMPLITUDE_MAX, wrapRect.width / 20));
const routePoints = isMobile ? points : buildWavePoints(points, amplitude);
let routePoints: Point[];
if (isMobile) {
routePoints = points;
} else {
const rows = groupIntoRows(anchors);
// A single row (desktop, always 4-up) takes the exact original
// path — no behavior change there. Only a row-wrapped layout (the
// tablet/small-laptop 2-column breakpoint) uses the row-aware
// serpentine route.
routePoints = rows.length <= 1 ? buildWavePoints(points, amplitude) : buildMultiRowRoute(rows, amplitude);
}
const d = buildPathD(routePoints); const d = buildPathD(routePoints);
path.setAttribute("d", d); path.setAttribute("d", d);
illum.setAttribute("d", d); illum.setAttribute("d", d);