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:
@@ -541,46 +541,33 @@ 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 }) => {
|
||||||
const [d1, setD1] = useState(4);
|
const [d1, setD1] = useState(4);
|
||||||
const [d2, setD2] = useState(2);
|
const [d2, setD2] = useState(2);
|
||||||
const [special, setSpecial] = useState("gryffindor");
|
const [special, setSpecial] = useState("gryffindor");
|
||||||
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 (
|
||||||
@@ -646,14 +633,14 @@ const DicePanel = ({ onRoll }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)", rolling = false, onClick }) => {
|
const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)", rolling = false, onClick }) => {
|
||||||
return (
|
return (
|
||||||
<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,
|
||||||
@@ -714,40 +701,73 @@ const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)", rolling = fa
|
|||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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,54 +779,69 @@ 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
|
||||||
</div>
|
</div>
|
||||||
</DieShell>
|
</DieShell>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const HouseDie = ({ face = "gryffindor", rolling = false, onClick }) => {
|
|
||||||
const faces = {
|
|
||||||
gryffindor: { label: "G", color: "#ef4444", sub: "Gryff." },
|
|
||||||
slytherin: { label: "S", color: "#22c55e", sub: "Slyth." },
|
|
||||||
ravenclaw: { label: "R", color: "#3b82f6", sub: "Raven." },
|
|
||||||
hufflepuff: { label: "H", color: "#facc15", sub: "Huff." },
|
|
||||||
help: { label: "?", color: "#f2d27a", sub: "Hilf" },
|
|
||||||
dark: { label: "☾", color: "rgba(255,255,255,0.78)", sub: "Dark" },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const HouseDie = ({ face = "gryffindor", rolling = false, onClick }) => {
|
||||||
|
// icon instead of letters
|
||||||
|
const faces = {
|
||||||
|
gryffindor: { icon: "🦁", color: "#ef4444", sub: "Gryff." },
|
||||||
|
slytherin: { icon: "🐍", color: "#22c55e", sub: "Slyth." },
|
||||||
|
ravenclaw: { icon: "🦅", color: "#3b82f6", sub: "Raven." },
|
||||||
|
hufflepuff: { icon: "🦡", color: "#facc15", sub: "Huff." },
|
||||||
|
help: { icon: "🃏", color: "#f2d27a", sub: "Hilf" },
|
||||||
|
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,14 +854,14 @@ 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
|
||||||
</div>
|
</div>
|
||||||
</DieShell>
|
</DieShell>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const PlayerIdentityCard = ({
|
const PlayerIdentityCard = ({
|
||||||
|
|||||||
@@ -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% {
|
||||||
|
|||||||
Reference in New Issue
Block a user