update navber wings
This commit is contained in:
@@ -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™ AI</Link>
|
<Link href="/miletruth#miletruth" onClick={(event) => handleNavClick(event, "/miletruth", "miletruth")}>MileTruth™ 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™ AI</Link>
|
<Link href="/miletruth#miletruth" onClick={(event) => handleNavClick(event, "/miletruth", "miletruth", true)}>MileTruth™ AI</Link>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user