Refactor dice components for true 3D rendering.

Replaced the 2D dice logic with fully 3D-rendered cubes, including animations and proper face-to-value mappings. Updated the design to use CSS transformations and added visual enhancements for a more immersive user experience.
This commit is contained in:
2026-02-08 11:02:43 +01:00
parent eec4597fda
commit 56944bc8d7
2 changed files with 369 additions and 250 deletions

View File

@@ -541,7 +541,6 @@ export default function App() {
); );
}; };
// App.jsx (füge diese Komponente irgendwo unter deinen anderen Komponenten ein)
// ✅ 3 Würfel: 2x d6 + 1x Spezial (Häuser + Hilfkarte + Dunkles Deck) // ✅ 3 Würfel: 2x d6 + 1x Spezial (Häuser + Hilfkarte + Dunkles Deck)
const DicePanel = ({ onRoll }) => { const DicePanel = ({ onRoll }) => {
@@ -551,36 +550,24 @@ const DicePanel = ({ onRoll }) => {
const [rolling, setRolling] = useState(false); const [rolling, setRolling] = useState(false);
const specialFaces = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff", "help", "dark"]; const specialFaces = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff", "help", "dark"];
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const rollAll = () => { const rollAll = () => {
if (rolling) return; if (rolling) return;
setRolling(true); setRolling(true);
// Während der Animation kurz "flackern" lassen (optional, wirkt lebendiger) // wir picken final direkt (kein Sound, kein Flicker nötig Animation macht den Job)
const flickerId = setInterval(() => {
setD1(randInt(1, 6));
setD2(randInt(1, 6));
setSpecial(specialFaces[randInt(0, specialFaces.length - 1)]);
}, 90);
// Finales Ergebnis nach Animation
setTimeout(() => {
clearInterval(flickerId);
const nd1 = randInt(1, 6); const nd1 = randInt(1, 6);
const nd2 = randInt(1, 6); const nd2 = randInt(1, 6);
const ns = specialFaces[randInt(0, specialFaces.length - 1)]; const ns = specialFaces[randInt(0, specialFaces.length - 1)];
setTimeout(() => {
setD1(nd1); setD1(nd1);
setD2(nd2); setD2(nd2);
setSpecial(ns); setSpecial(ns);
setRolling(false); setRolling(false);
onRoll?.({ d1: nd1, d2: nd2, special: ns }); onRoll?.({ d1: nd1, d2: nd2, special: ns });
}, 820); // muss zur CSS Animation passen }, 820);
}; };
return ( return (
@@ -653,7 +640,7 @@ const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)", rolling = fa
<button <button
type="button" type="button"
onClick={onClick} onClick={onClick}
className={`die3d ${rolling ? "die3dRolling" : ""}`} className="die3d"
style={{ style={{
width: 64, width: 64,
height: 64, height: 64,
@@ -716,38 +703,71 @@ const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)", rolling = fa
); );
}; };
const DieD6 = ({ value = 1, rolling = false, onClick }) => { /* map value->cube rotation so that value face is FRONT
const P = ({ x, y }) => ( Face layout:
<div front=1, back=6, top=2, bottom=5, right=3, left=4
style={{ */
position: "absolute", const cubeRotationForD6 = (value) => {
left: `${x * 50}%`, switch (value) {
top: `${y * 50}%`, case 1: return { rx: "0deg", ry: "0deg" };
transform: "translate(-50%, -50%)", case 2: return { rx: "-90deg", ry: "0deg" };
width: 7.5, case 3: return { rx: "0deg", ry: "-90deg" };
height: 7.5, case 4: return { rx: "0deg", ry: "90deg" };
borderRadius: 999, case 5: return { rx: "90deg", ry: "0deg" };
background: "rgba(245,245,245,0.92)", case 6: return { rx: "0deg", ry: "180deg" };
boxShadow: "0 3px 10px rgba(0,0,0,0.35), inset 0 1px 2px rgba(0,0,0,0.20)", default: return { rx: "0deg", ry: "0deg" };
}} }
/> };
);
const PipFace = ({ value }) => {
// pip positions: 0..2 grid
const pos = (gx, gy) => ({ left: `${gx * 50}%`, top: `${gy * 50}%` });
const faces = { const faces = {
1: [{ x: 1, y: 1 }], 1: [[1, 1]],
2: [{ x: 0, y: 0 }, { x: 2, y: 2 }], 2: [[0, 0], [2, 2]],
3: [{ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }], 3: [[0, 0], [1, 1], [2, 2]],
4: [{ x: 0, y: 0 }, { x: 2, y: 0 }, { x: 0, y: 2 }, { x: 2, y: 2 }], 4: [[0, 0], [2, 0], [0, 2], [2, 2]],
5: [{ x: 0, y: 0 }, { x: 2, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 2 }, { x: 2, y: 2 }], 5: [[0, 0], [2, 0], [1, 1], [0, 2], [2, 2]],
6: [{ x: 0, y: 0 }, { x: 0, y: 1 }, { x: 0, y: 2 }, { x: 2, y: 0 }, { x: 2, y: 1 }, { x: 2, y: 2 }], 6: [[0, 0], [0, 1], [0, 2], [2, 0], [2, 1], [2, 2]],
}; };
const arr = faces[value] || faces[1];
return (
<div className="pipGrid">
{arr.map(([x, y], idx) => (
<div
key={idx}
className="pip"
style={{
...pos(x, y),
transform: "translate(-50%, -50%)",
}}
/>
))}
</div>
);
};
const DieD6 = ({ value = 1, rolling = false, onClick }) => {
const rot = cubeRotationForD6(value);
return ( return (
<DieShell ringColor="rgba(242,210,122,0.18)" rolling={rolling} onClick={onClick}> <DieShell ringColor="rgba(242,210,122,0.18)" rolling={rolling} onClick={onClick}>
<div style={{ width: 46, height: 46, position: "relative", transform: "translateZ(12px)" }}> <div className="dieCubeWrap">
{(faces[value] || faces[1]).map((p, idx) => ( <div
<P key={idx} x={p.x} y={p.y} /> className={`dieCube ${rolling ? "rolling" : ""}`}
))} style={{ "--rx": rot.rx, "--ry": rot.ry }}
>
{/* faces show correct pips like a real die */}
<div className="dieFace front"><PipFace value={1} /></div>
<div className="dieFace back"><PipFace value={6} /></div>
<div className="dieFace top"><PipFace value={2} /></div>
<div className="dieFace bottom"><PipFace value={5} /></div>
<div className="dieFace right"><PipFace value={3} /></div>
<div className="dieFace left"><PipFace value={4} /></div>
</div>
</div> </div>
<div <div
@@ -759,7 +779,6 @@ const DieD6 = ({ value = 1, rolling = false, onClick }) => {
fontWeight: 900, fontWeight: 900,
color: "rgba(255,255,255,0.55)", color: "rgba(255,255,255,0.55)",
letterSpacing: 0.6, letterSpacing: 0.6,
transform: "translateZ(10px)",
}} }}
> >
d6 d6
@@ -769,44 +788,60 @@ const DieD6 = ({ value = 1, rolling = false, onClick }) => {
}; };
const HouseDie = ({ face = "gryffindor", rolling = false, onClick }) => { const HouseDie = ({ face = "gryffindor", rolling = false, onClick }) => {
// icon instead of letters
const faces = { const faces = {
gryffindor: { label: "G", color: "#ef4444", sub: "Gryff." }, gryffindor: { icon: "🦁", color: "#ef4444", sub: "Gryff." },
slytherin: { label: "S", color: "#22c55e", sub: "Slyth." }, slytherin: { icon: "🐍", color: "#22c55e", sub: "Slyth." },
ravenclaw: { label: "R", color: "#3b82f6", sub: "Raven." }, ravenclaw: { icon: "🦅", color: "#3b82f6", sub: "Raven." },
hufflepuff: { label: "H", color: "#facc15", sub: "Huff." }, hufflepuff: { icon: "🦡", color: "#facc15", sub: "Huff." },
help: { label: "?", color: "#f2d27a", sub: "Hilf" }, help: { icon: "🃏", color: "#f2d27a", sub: "Hilf" },
dark: { label: "", color: "rgba(255,255,255,0.78)", sub: "Dark" }, dark: { icon: "🌙", color: "rgba(255,255,255,0.85)", sub: "Dark" },
}; };
const order = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff", "help", "dark"];
const idx = Math.max(0, order.indexOf(face));
const value = idx + 1; // 1..6
const rot = cubeRotationForD6(value);
const f = faces[face] || faces.gryffindor; const f = faces[face] || faces.gryffindor;
const ring = `${f.color}55`;
return ( return (
<DieShell ringColor={`${f.color}55`} rolling={rolling} onClick={onClick}> <DieShell ringColor={ring} rolling={rolling} onClick={onClick}>
<div className="dieCubeWrap">
<div <div
className={`dieCube ${rolling ? "rolling" : ""}`}
style={{ style={{
width: 46, "--rx": rot.rx,
height: 46, "--ry": rot.ry,
borderRadius: 14, "--ring": `${f.color}22`,
display: "grid",
placeItems: "center",
background:
"radial-gradient(120% 120% at 30% 20%, rgba(255,255,255,0.10), rgba(0,0,0,0.35) 70%, rgba(0,0,0,0.60))",
border: "1px solid rgba(255,255,255,0.08)",
boxShadow: `inset 0 0 0 1px ${f.color}22`,
position: "relative",
transform: "translateZ(12px)",
}} }}
> >
{/* order mapped to 6 faces */}
{order.map((key, i) => {
const item = faces[key];
const faceClass =
i === 0 ? "front" :
i === 1 ? "top" :
i === 2 ? "right" :
i === 3 ? "left" :
i === 4 ? "bottom" :
"back";
return (
<div key={key} className={`dieFace ${faceClass}`} style={{ "--ring": `${item.color}22` }}>
<div <div
className="specialIcon"
style={{ style={{
fontWeight: 1000, color: item.color,
fontSize: 22, textShadow: `0 0 18px ${item.color}55, 0 10px 22px rgba(0,0,0,0.55)`,
color: f.color,
textShadow: `0 0 16px ${f.color}55, 0 8px 24px rgba(0,0,0,0.55)`,
letterSpacing: 0.5,
}} }}
> >
{f.label} {item.icon}
</div>
</div>
);
})}
</div> </div>
</div> </div>
@@ -819,7 +854,6 @@ const HouseDie = ({ face = "gryffindor", rolling = false, onClick }) => {
fontWeight: 900, fontWeight: 900,
color: "rgba(255,255,255,0.55)", color: "rgba(255,255,255,0.55)",
letterSpacing: 0.6, letterSpacing: 0.6,
transform: "translateZ(10px)",
}} }}
> >
Spezial Spezial
@@ -829,6 +863,7 @@ const HouseDie = ({ face = "gryffindor", rolling = false, onClick }) => {
}; };
const PlayerIdentityCard = ({ const PlayerIdentityCard = ({
name = "Harry Potter", name = "Harry Potter",
houseLabel = "Gryffindor", houseLabel = "Gryffindor",

View File

@@ -176,11 +176,7 @@ body {
opacity: 0.98; opacity: 0.98;
} }
/* --- Dice 3D feel --- */ /* ===== True 3D Cube Dice ===== */
.diceRow3d {
perspective: 900px;
}
.die3d { .die3d {
transform-style: preserve-3d; transform-style: preserve-3d;
will-change: transform; will-change: transform;
@@ -190,6 +186,94 @@ body {
transform: translateY(-3px) rotateX(8deg) rotateY(-10deg); transform: translateY(-3px) rotateX(8deg) rotateY(-10deg);
} }
/* Cube container inside the button */
.dieCubeWrap {
width: 44px;
height: 44px;
position: relative;
transform-style: preserve-3d;
}
/* actual cube */
.dieCube {
width: 44px;
height: 44px;
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
transition: transform 260ms cubic-bezier(0.22, 1.0, 0.25, 1);
}
/* rolling animation (spins, then we snap by updating --rx/--ry) */
@keyframes cubeRoll {
0% { transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)); }
100% {
transform:
rotateX(calc(var(--rx, 0deg) + 720deg))
rotateY(calc(var(--ry, 0deg) + 540deg));
}
}
.dieCube.rolling {
animation: cubeRoll 820ms cubic-bezier(0.18, 0.88, 0.22, 1) both;
}
/* one face */
.dieFace {
position: absolute;
inset: 0;
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.10);
background:
radial-gradient(120% 120% at 25% 18%, rgba(255,255,255,0.14), rgba(0,0,0,0.40) 60%, rgba(0,0,0,0.72));
box-shadow:
inset 0 0 0 1px rgba(255,255,255,0.06),
inset 0 12px 20px rgba(255,255,255,0.05);
display: grid;
place-items: center;
backface-visibility: hidden;
}
/* face positions */
.dieFace.front { transform: rotateY( 0deg) translateZ(22px); }
.dieFace.back { transform: rotateY(180deg) translateZ(22px); }
.dieFace.right { transform: rotateY( 90deg) translateZ(22px); }
.dieFace.left { transform: rotateY(-90deg) translateZ(22px); }
.dieFace.top { transform: rotateX( 90deg) translateZ(22px); }
.dieFace.bottom { transform: rotateX(-90deg) translateZ(22px); }
/* pips */
.pipGrid {
width: 70%;
height: 70%;
position: relative;
}
.pip {
position: absolute;
width: 7px;
height: 7px;
border-radius: 999px;
background: rgba(245,245,245,0.92);
box-shadow: 0 3px 10px rgba(0,0,0,0.35), inset 0 1px 2px rgba(0,0,0,0.20);
}
/* special icon */
.specialIcon {
font-size: 20px;
line-height: 1;
filter: drop-shadow(0 10px 18px rgba(0,0,0,0.55));
}
/* optional: subtle face ring tint via CSS var */
.dieFace {
box-shadow:
inset 0 0 0 1px rgba(255,255,255,0.06),
inset 0 0 0 2px var(--ring, rgba(255,255,255,0.00)),
inset 0 12px 20px rgba(255,255,255,0.05);
}
/* Roll animation */ /* Roll animation */
@keyframes dieRoll { @keyframes dieRoll {
0% { 0% {