css-3d-card-transforms
<!-- Loaded by combat-effects-upgrade when task involves card tilt, backface-visibility, CSS perspective, or Framer Motion + CSS 3D integration -->
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
<!-- Loaded by combat-effects-upgrade when task involves card tilt, backface-visibility, CSS perspective, or Framer Motion + CSS 3D integration -->
Agent definition
css-3d-card-transforms.mdCSS 3D Card Transforms Reference
<!-- Loaded by combat-effects-upgrade when task involves card tilt, backface-visibility, CSS perspective, or Framer Motion + CSS 3D integration -->
CSS 3D transforms: tilt toward cursor on hover, `backface-visibility: hidden` for flip reveals. GPU-composited only (`transform`, `opacity`). Universal browser support (Chrome 36+, Firefox 16+, Safari 9+).
Tilt Formula
rotateY = (mouseX - cardCenterX) / cardWidth * MAX_TILT_DEG
rotateX = -(mouseY - cardCenterY) / cardHeight * MAX_TILT_DEG
- `MAX_TILT_DEG` = 15 (degrees) — beyond 20deg starts looking exaggerated
- Negate `rotateX` because positive mouseY (mouse below center) should tilt the top toward viewer (negative rotateX in CSS convention)
- `cardCenter` is measured once from `getBoundingClientRect()` on `mouseenter`, not recalculated on every `mousemove`
---
Complete Component: TiltCard
// src/components/FramedCard.tsx
import { motion, useMotionValue, useSpring, useTransform } from 'motion/react';
import { useRef, useCallback, useState } from 'react';
const MAX_TILT_DEG = 15;
const SPRING_CONFIG = { stiffness: 300, damping: 30 } as const;
interface FramedCardProps {
card: Card;
onClick?: () => void;
}
export function FramedCard({ card, onClick }: FramedCardProps) {
const cardRef = useRef<HTMLDivElement>(null);
const [isTouchDevice] = useState(
() => window.matchMedia('(hover: none)').matches
);
// Raw motion values — these update on every mousemove WITHOUT triggering re-render
const rawRotateX = useMotionValue(0);
const rawRotateY = useMotionValue(0);
// Springs smooth the raw values — tilt follows cursor with a slight lag
const rotateX = useSpring(rawRotateX, SPRING_CONFIG);
const rotateY = useSpring(rawRotateY, SPRING_CONFIG);
// Subtle glare effect: opacity follows rotateY
const glareOpacity = useTransform(rotateY, [-MAX_TILT_DEG, 0, MAX_TILT_DEG], [0.15, 0, 0.15]);
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
if (isTouchDevice || !cardRef.current) return;
const rect = cardRef.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
rawRotateY.set((e.clientX - centerX) / (rect.width / 2) * MAX_TILT_DEG);
rawRotateX.set(-((e.clientY - centerY) / (rect.height / 2)) * MAX_TILT_DEG);
}, [isTouchDevice, rawRotateX, rawRotateY]);
const handleMouseLeave = useCallback(() => {
// Spring back to flat — spring physics handles the animation
rawRotateX.set(0);
rawRotateY.set(0);
}, [rawRotateX, rawRotateY]);
return (
// Perspective on the container — children share one vanishing point
<div className="card-perspective-container" ref={cardRef}>
<motion.div
className="framed-card"
style={{
rotateX, // MotionValue — no re-renders, compositor-only updates
rotateY,
transformStyle: 'preserve-3d',
}}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={onClick}
// Existing hover scale
whileHover={isTouchDevice ? undefined : { scale: 1.06 }}
whileTap={{ scale: 0.97 }}
transition={{ type: 'spring', stiffness: 400, damping: 28 }}
>
{/* Card face */}
<div className="card-face card-face--front">
<CardArtwork card={card} />
<CardStats card={card} />
</div>
{/* Glare overlay — follows tilt direction */}
<motion.div
className="card-glare"
style={{ opacity: glareOpacity }}
aria-hidden="true"
/>
</motion.div>
</div>
);
}---
Required CSS
/* ─────────────────────────────────────────────
Card perspective container
perspective goes on the PARENT — gives children
a shared vanishing point
───────────────────────────────────────────── */
.card-perspective-container {
perspective: 1000px;
perspective-origin: center center;
/* Contain so tilt doesn't affect sibling layout */
isolation: isolate;
}
/* ─────────────────────────────────────────────
Card body
───────────────────────────────────────────── */
.framed-card {
position: relative;
width: 160px;
height: 240px;
border-radius: 12px;
transform-style: preserve-3d;
cursor: pointer;
/* GPU layer hint — apply dynamically via JS, or only in hover state */
}
.framed-card:hover {
will-change: transform;
}
/* Remove will-change when not hovering */
.framed-card:not(:hover) {
will-change: auto;
}
/* ─────────────────────────────────────────────
Card faces — used for flip reveal
───────────────────────────────────────────── */
.card-face {
position: absolute;
inset: 0;
border-radius: 12px;
backface-visibility: hidden; /* Hide when rotated 180deg */
-webkit-backface-visibility: hidden; /* Safari */
}
.card-face--front {
/* Front is naturally visible (0deg rotation) */
}
.card-face--back {
/* Back starts flipped — visible when card rotates 180deg */
transform: rotateY(180deg);
background: url('/card-back.png') center/cover;
}
/* ─────────────────────────────────────────────
Glare overlay
───────────────────────────────────────────── */
.card-glare {
position: absolute;
inset: 0;
border-radius: 12px;
background: linear-gradient(
105deg,
transparent 40%,
rgba(255, 255, 255, 0.15) 50%,
transparent 60%
);
pointer-events: none;
/* translateZ pushes it above the card face in 3D space */
transform: translateZ(1px);
}---
Card Flip Animation
Used when a face-down card is revealed (e.g. drawing from deck, enemy showing intent).
// src/components/FramedCard.tsx — flip variant
import { motion, AnimatePresence } from 'motion/react';
import { useState } from 'react';
interface FlippableCardProps {
card: Card;
isRevealed: boolean;
}
export function FlippableCard({ card, isRevealedRead more
CSS 3D Card Transforms Reference
<!-- Loaded by combat-effects-upgrade when task involves card tilt, backface-visibility, CSS perspective, or Framer Motion + CSS 3D integration -->
CSS 3D transforms: tilt toward cursor on hover, `backface-visibility: hidden` for flip reveals. GPU-composited only (`transform`, `opacity`). Universal browser support (Chrome 36+, Firefox 16+, Safari 9+).
Tilt Formula
rotateY = (mouseX - cardCenterX) / cardWidth * MAX_TILT_DEG rotateX = -(mouseY - cardCenterY) / cardHeight * MAX_TILT_DEG
- `MAX_TILT_DEG` = 15 (degrees) — beyond 20deg starts looking exaggerated
- Negate `rotateX` because positive mouseY (mouse below center) should tilt the top toward viewer (negative rotateX in CSS convention)
- `cardCenter` is measured once from `getBoundingClientRect()` on `mouseenter`, not recalculated on every `mousemove`
---
Complete Component: TiltCard
// src/components/FramedCard.tsx
import { motion, useMotionValue, useSpring, useTransform } from 'motion/react';
import { useRef, useCallback, useState } from 'react';
const MAX_TILT_DEG = 15;
const SPRING_CONFIG = { stiffness: 300, damping: 30 } as const;
interface FramedCardProps {
card: Card;
onClick?: () => void;
}
export function FramedCard({ card, onClick }: FramedCardProps) {
const cardRef = useRef<HTMLDivElement>(null);
const [isTouchDevice] = useState(
() => window.matchMedia('(hover: none)').matches
);
// Raw motion values — these update on every mousemove WITHOUT triggering re-render
const rawRotateX = useMotionValue(0);
const rawRotateY = useMotionValue(0);
// Springs smooth the raw values — tilt follows cursor with a slight lag
const rotateX = useSpring(rawRotateX, SPRING_CONFIG);
const rotateY = useSpring(rawRotateY, SPRING_CONFIG);
// Subtle glare effect: opacity follows rotateY
const glareOpacity = useTransform(rotateY, [-MAX_TILT_DEG, 0, MAX_TILT_DEG], [0.15, 0, 0.15]);
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
if (isTouchDevice || !cardRef.current) return;
const rect = cardRef.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
rawRotateY.set((e.clientX - centerX) / (rect.width / 2) * MAX_TILT_DEG);
rawRotateX.set(-((e.clientY - centerY) / (rect.height / 2)) * MAX_TILT_DEG);
}, [isTouchDevice, rawRotateX, rawRotateY]);
const handleMouseLeave = useCallback(() => {
// Spring back to flat — spring physics handles the animation
rawRotateX.set(0);
rawRotateY.set(0);
}, [rawRotateX, rawRotateY]);
return (
// Perspective on the container — children share one vanishing point
<div className="card-perspective-container" ref={cardRef}>
<motion.div
className="framed-card"
style={{
rotateX, // MotionValue — no re-renders, compositor-only updates
rotateY,
transformStyle: 'preserve-3d',
}}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={onClick}
// Existing hover scale
whileHover={isTouchDevice ? undefined : { scale: 1.06 }}
whileTap={{ scale: 0.97 }}
transition={{ type: 'spring', stiffness: 400, damping: 28 }}
>
{/* Card face */}
<div className="card-face card-face--front">
<CardArtwork card={card} />
<CardStats card={card} />
</div>
{/* Glare overlay — follows tilt direction */}
<motion.div
className="card-glare"
style={{ opacity: glareOpacity }}
aria-hidden="true"
/>
</motion.div>
</div>
);
}---
Required CSS
/* ─────────────────────────────────────────────
Card perspective container
perspective goes on the PARENT — gives children
a shared vanishing point
───────────────────────────────────────────── */
.card-perspective-container {
perspective: 1000px;
perspective-origin: center center;
/* Contain so tilt doesn't affect sibling layout */
isolation: isolate;
}
/* ─────────────────────────────────────────────
Card body
───────────────────────────────────────────── */
.framed-card {
position: relative;
width: 160px;
height: 240px;
border-radius: 12px;
transform-style: preserve-3d;
cursor: pointer;
/* GPU layer hint — apply dynamically via JS, or only in hover state */
}
.framed-card:hover {
will-change: transform;
}
/* Remove will-change when not hovering */
.framed-card:not(:hover) {
will-change: auto;
}
/* ─────────────────────────────────────────────
Card faces — used for flip reveal
───────────────────────────────────────────── */
.card-face {
position: absolute;
inset: 0;
border-radius: 12px;
backface-visibility: hidden; /* Hide when rotated 180deg */
-webkit-backface-visibility: hidden; /* Safari */
}
.card-face--front {
/* Front is naturally visible (0deg rotation) */
}
.card-face--back {
/* Back starts flipped — visible when card rotates 180deg */
transform: rotateY(180deg);
background: url('/card-back.png') center/cover;
}
/* ─────────────────────────────────────────────
Glare overlay
───────────────────────────────────────────── */
.card-glare {
position: absolute;
inset: 0;
border-radius: 12px;
background: linear-gradient(
105deg,
transparent 40%,
rgba(255, 255, 255, 0.15) 50%,
transparent 60%
);
pointer-events: none;
/* translateZ pushes it above the card face in 3D space */
transform: translateZ(1px);
}---
Card Flip Animation
Used when a face-down card is revealed (e.g. drawing from deck, enemy showing intent).
// src/components/FramedCard.tsx — flip variant
import { motion, AnimatePresence } from 'motion/react';
import { useState } from 'react';
interface FlippableCardProps {
card: Card;
isRevealed: boolean;
}
export function FlippableCard({ card, isRevealedEssays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

