From eec4597fdabc50202755c21c1c71464506e463b7 Mon Sep 17 00:00:00 2001 From: nessi Date: Sat, 7 Feb 2026 19:01:37 +0100 Subject: [PATCH] Add rolling animation and interactive functionality to dice. Enhanced the DicePanel component with interactive rolling logic and animations for a more dynamic experience. Updated styles and functionality, including hover effects, roll results, and a 3D feel for dice components. --- frontend/src/App.jsx | 221 ++++++++++++++++++++++--------------- frontend/src/AppLayout.css | 34 ++++++ 2 files changed, 164 insertions(+), 91 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b0ab987..b9e6611 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -544,16 +544,47 @@ 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) -const DicePanel = () => { +const DicePanel = ({ onRoll }) => { + const [d1, setD1] = useState(4); + const [d2, setD2] = useState(2); + const [special, setSpecial] = useState("gryffindor"); + const [rolling, setRolling] = useState(false); + + const specialFaces = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff", "help", "dark"]; + + const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; + + const rollAll = () => { + if (rolling) return; + setRolling(true); + + // Während der Animation kurz "flackern" lassen (optional, wirkt lebendiger) + 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 nd2 = randInt(1, 6); + const ns = specialFaces[randInt(0, specialFaces.length - 1)]; + + setD1(nd1); + setD2(nd2); + setSpecial(ns); + + setRolling(false); + + onRoll?.({ d1: nd1, d2: nd2, special: ns }); + }, 820); // muss zur CSS Animation passen + }; + return ( -
+
{ minWidth: 0, }} > -
- Würfel -
-
- 2× d6 + 1× Spezial +
+
+
+ Würfel +
+
+ Klicken zum Rollen +
+
+ +
+ {rolling ? "ROLL…" : "READY"} +
- - - + + +
); }; -const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)" }) => { +const DieShell = ({ children, ringColor = "rgba(255,255,255,0.10)", rolling = false, onClick }) => { return ( -
{ - e.currentTarget.style.transform = "translateY(-3px) rotate(-1deg)"; - e.currentTarget.style.boxShadow = - "0 26px 70px rgba(0,0,0,0.65), inset 0 0 0 1px rgba(255,255,255,0.08)"; - }} - onMouseLeave={(e) => { - e.currentTarget.style.transform = "translateY(0px) rotate(0deg)"; - e.currentTarget.style.boxShadow = - "0 18px 50px rgba(0,0,0,0.55), inset 0 0 0 1px rgba(255,255,255,0.06)"; - }} - title="Würfel (placeholder)" + disabled={rolling} + title={rolling ? "Rolling…" : "Roll"} > - {/* subtle inner ring */} + {/* inner ring */}
{ }} /> - {/* gloss */} + {/* bevel light */} +
+ + {/* gloss stripe */}
{children} -
+ ); }; -const DieD6 = ({ value = 1 }) => { - // pip layout positions on 3x3 grid +const DieD6 = ({ value = 1, rolling = false, onClick }) => { const P = ({ x, y }) => (
{ height: 7.5, borderRadius: 999, background: "rgba(245,245,245,0.92)", - boxShadow: "0 3px 10px rgba(0,0,0,0.35)", + boxShadow: "0 3px 10px rgba(0,0,0,0.35), inset 0 1px 2px rgba(0,0,0,0.20)", }} /> ); const faces = { 1: [{ x: 1, y: 1 }], - 2: [ - { x: 0, y: 0 }, - { x: 2, y: 2 }, - ], - 3: [ - { x: 0, y: 0 }, - { x: 1, y: 1 }, - { x: 2, y: 2 }, - ], - 4: [ - { x: 0, y: 0 }, - { x: 2, y: 0 }, - { x: 0, y: 2 }, - { x: 2, y: 2 }, - ], - 5: [ - { x: 0, y: 0 }, - { x: 2, y: 0 }, - { x: 1, y: 1 }, - { x: 0, y: 2 }, - { x: 2, y: 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 }, - ], + 2: [{ x: 0, y: 0 }, { x: 2, y: 2 }], + 3: [{ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }], + 4: [{ x: 0, y: 0 }, { x: 2, y: 0 }, { x: 0, y: 2 }, { x: 2, y: 2 }], + 5: [{ x: 0, y: 0 }, { x: 2, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 2 }, { x: 2, y: 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 }], }; return ( - -
+ +
{(faces[value] || faces[1]).map((p, idx) => (

))}

- {/* tiny label */}
{ fontWeight: 900, color: "rgba(255,255,255,0.55)", letterSpacing: 0.6, + transform: "translateZ(10px)", }} > d6 @@ -736,33 +768,33 @@ const DieD6 = ({ value = 1 }) => { ); }; -const HouseDie = ({ face = "gryffindor" }) => { - // 4 Häuser + Hilfkarte + Dunkles Deck +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.70)", sub: "Dark" }, + dark: { label: "☾", color: "rgba(255,255,255,0.78)", sub: "Dark" }, }; const f = faces[face] || faces.gryffindor; return ( - +
{ fontWeight: 900, color: "rgba(255,255,255,0.55)", letterSpacing: 0.6, + transform: "translateZ(10px)", }} > Spezial @@ -1016,7 +1049,13 @@ const HouseDie = ({ face = "gryffindor" }) => {
- + { + console.log("Rolled:", { d1, d2, special }); + // später: API call / game action + // api(`/games/${gameId}/roll`, { method:"POST", body: JSON.stringify({ d1,d2,special }) }) + }} + />
diff --git a/frontend/src/AppLayout.css b/frontend/src/AppLayout.css index b2efc87..172d751 100644 --- a/frontend/src/AppLayout.css +++ b/frontend/src/AppLayout.css @@ -176,6 +176,40 @@ body { opacity: 0.98; } +/* --- Dice 3D feel --- */ +.diceRow3d { + perspective: 900px; +} + +.die3d { + transform-style: preserve-3d; + will-change: transform; +} + +.die3d:hover:not(:disabled) { + transform: translateY(-3px) rotateX(8deg) rotateY(-10deg); +} + +/* Roll animation */ +@keyframes dieRoll { + 0% { + transform: translateY(0) rotateX(0deg) rotateY(0deg) rotateZ(0deg); + } + 25% { + transform: translateY(-6px) rotateX(220deg) rotateY(160deg) rotateZ(90deg); + } + 55% { + transform: translateY(0px) rotateX(520deg) rotateY(460deg) rotateZ(240deg); + } + 100% { + transform: translateY(-2px) rotateX(720deg) rotateY(720deg) rotateZ(360deg); + } +} + +.die3dRolling { + animation: dieRoll 820ms cubic-bezier(0.2, 0.9, 0.25, 1) both; +} + /* --- Responsive: shrink gracefully (no scroll outside notes) --- */ @media (max-height: 860px) { .leftPane {