feature/image-reveal #10
4 changed files with 60 additions and 35 deletions
|
|
@ -6,8 +6,9 @@ formats supportés `*.gif`, `*.webp`, `*.apng`, `*.png`, `*.avif`.
|
||||||
Rangement par mode (recommandé pour des transitions personnalisées) :
|
Rangement par mode (recommandé pour des transitions personnalisées) :
|
||||||
|
|
||||||
- `quiz/` → jouées avant une manche de **quiz**
|
- `quiz/` → jouées avant une manche de **quiz**
|
||||||
|
- `image/` → jouées avant une manche du mode **images**
|
||||||
- `blindtest/` → jouées avant une manche de **blindtest**
|
- `blindtest/` → jouées avant une manche de **blindtest**
|
||||||
- racine (`./`) → **communs**, fallback pour les deux modes
|
- racine (`./`) → **communs**, fallback pour tous les modes
|
||||||
|
|
||||||
Règles :
|
Règles :
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { createPortal } from "react-dom"
|
import { createPortal } from "react-dom"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
import type { RoundType } from "@nerdware/shared"
|
|
||||||
|
/** Catégorie visuelle de la transition (image_reveal est distinct du quiz). */
|
||||||
|
export type TransitionKind = "quiz" | "image" | "blindtest"
|
||||||
|
|
||||||
const VISIBLE_MS = 1300 // durée d'affichage avant le wipe de sortie (≈ leadMs serveur)
|
const VISIBLE_MS = 1300 // durée d'affichage avant le wipe de sortie (≈ leadMs serveur)
|
||||||
|
|
||||||
// Médias custom déposés par l'utilisateur, rangés par mode :
|
// Médias custom déposés par l'utilisateur, rangés par mode :
|
||||||
// src/assets/transitions/quiz/* → transitions de quiz
|
// src/assets/transitions/quiz/* → transitions de quiz
|
||||||
|
// src/assets/transitions/image/* → transitions du mode images
|
||||||
// src/assets/transitions/blindtest/* → transitions de blindtest
|
// src/assets/transitions/blindtest/* → transitions de blindtest
|
||||||
// src/assets/transitions/* → communs (fallback)
|
// src/assets/transitions/* → communs (fallback)
|
||||||
// Détectés au build ; sinon, animation Framer par défaut.
|
// Détectés au build ; sinon, animation Framer par défaut.
|
||||||
|
|
@ -15,17 +18,17 @@ const ALL_MEDIA = import.meta.glob(
|
||||||
{ eager: true, import: "default" }
|
{ eager: true, import: "default" }
|
||||||
) as Record<string, string>
|
) as Record<string, string>
|
||||||
|
|
||||||
const QUIZ_MEDIA: string[] = []
|
const MEDIA: Record<TransitionKind, string[]> = { quiz: [], image: [], blindtest: [] }
|
||||||
const BLINDTEST_MEDIA: string[] = []
|
|
||||||
const SHARED_MEDIA: string[] = []
|
const SHARED_MEDIA: string[] = []
|
||||||
for (const [path, url] of Object.entries(ALL_MEDIA)) {
|
for (const [path, url] of Object.entries(ALL_MEDIA)) {
|
||||||
if (path.includes("/transitions/quiz/")) QUIZ_MEDIA.push(url)
|
if (path.includes("/transitions/quiz/")) MEDIA.quiz.push(url)
|
||||||
else if (path.includes("/transitions/blindtest/")) BLINDTEST_MEDIA.push(url)
|
else if (path.includes("/transitions/image/")) MEDIA.image.push(url)
|
||||||
|
else if (path.includes("/transitions/blindtest/")) MEDIA.blindtest.push(url)
|
||||||
else SHARED_MEDIA.push(url)
|
else SHARED_MEDIA.push(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEME: Record<
|
const THEME: Record<
|
||||||
RoundType,
|
TransitionKind,
|
||||||
{ gradient: string; accent: string; label: string; kind: string; emoji: string }
|
{ gradient: string; accent: string; label: string; kind: string; emoji: string }
|
||||||
> = {
|
> = {
|
||||||
quiz: {
|
quiz: {
|
||||||
|
|
@ -35,6 +38,13 @@ const THEME: Record<
|
||||||
kind: "Question",
|
kind: "Question",
|
||||||
emoji: "🧠",
|
emoji: "🧠",
|
||||||
},
|
},
|
||||||
|
image: {
|
||||||
|
gradient: "from-emerald-500 via-teal-600 to-cyan-700",
|
||||||
|
accent: "text-lime-300",
|
||||||
|
label: "IMAGE",
|
||||||
|
kind: "Image",
|
||||||
|
emoji: "🖼️",
|
||||||
|
},
|
||||||
blindtest: {
|
blindtest: {
|
||||||
gradient: "from-fuchsia-500 via-purple-600 to-indigo-700",
|
gradient: "from-fuchsia-500 via-purple-600 to-indigo-700",
|
||||||
accent: "text-yellow-300",
|
accent: "text-yellow-300",
|
||||||
|
|
@ -44,29 +54,28 @@ const THEME: Record<
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function mediaFor(type: RoundType): string[] {
|
function mediaFor(kind: TransitionKind): string[] {
|
||||||
const own = type === "quiz" ? QUIZ_MEDIA : BLINDTEST_MEDIA
|
return [...MEDIA[kind], ...SHARED_MEDIA]
|
||||||
return [...own, ...SHARED_MEDIA]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transition "WarioWare" entre manches, thémée par mode. Annonce le mode quand
|
* Transition "WarioWare" entre manches, thémée par mode (quiz / image / blindtest).
|
||||||
* il change (quiz ↔ blindtest), sinon affiche un teaser léger de la manche.
|
* Annonce le mode quand il change, sinon affiche un teaser léger de la manche.
|
||||||
* Montée avec une `key` qui change → rejoue à chaque manche.
|
* Montée avec une `key` qui change → rejoue à chaque manche.
|
||||||
*/
|
*/
|
||||||
export function RoundTransition({
|
export function RoundTransition({
|
||||||
type,
|
kind,
|
||||||
index,
|
index,
|
||||||
modeChanged,
|
modeChanged,
|
||||||
}: {
|
}: {
|
||||||
type: RoundType
|
kind: TransitionKind
|
||||||
index: number
|
index: number
|
||||||
modeChanged: boolean
|
modeChanged: boolean
|
||||||
}) {
|
}) {
|
||||||
const [show, setShow] = useState(true)
|
const [show, setShow] = useState(true)
|
||||||
const theme = THEME[type]
|
const theme = THEME[kind]
|
||||||
const [media] = useState(() => {
|
const [media] = useState(() => {
|
||||||
const pool = mediaFor(type)
|
const pool = mediaFor(kind)
|
||||||
return pool.length ? pool[Math.floor(Math.random() * pool.length)] : null
|
return pool.length ? pool[Math.floor(Math.random() * pool.length)] : null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export function RoomPage({ code }: { code: string }) {
|
||||||
const roundKey = useRoomStore((s) => s.roundKey)
|
const roundKey = useRoomStore((s) => s.roundKey)
|
||||||
const voteProgress = useRoomStore((s) => s.voteProgress)
|
const voteProgress = useRoomStore((s) => s.voteProgress)
|
||||||
const round = useRoomStore((s) => s.round)
|
const round = useRoomStore((s) => s.round)
|
||||||
|
const roundKind = useRoomStore((s) => s.roundKind)
|
||||||
const roundModeChanged = useRoomStore((s) => s.roundModeChanged)
|
const roundModeChanged = useRoomStore((s) => s.roundModeChanged)
|
||||||
|
|
||||||
// Pas dans cette room (accès direct sans rejoindre / refresh) : retour accueil.
|
// Pas dans cette room (accès direct sans rejoindre / refresh) : retour accueil.
|
||||||
|
|
@ -106,7 +107,7 @@ export function RoomPage({ code }: { code: string }) {
|
||||||
{roundKey > 0 && round && (
|
{roundKey > 0 && round && (
|
||||||
<RoundTransition
|
<RoundTransition
|
||||||
key={roundKey}
|
key={roundKey}
|
||||||
type={round.type}
|
kind={roundKind}
|
||||||
index={snapshot.currentRound}
|
index={snapshot.currentRound}
|
||||||
modeChanged={roundModeChanged}
|
modeChanged={roundModeChanged}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,9 @@ interface RoomState {
|
||||||
mediaSync: MediaSyncPayload | null
|
mediaSync: MediaSyncPayload | null
|
||||||
boomKey: number
|
boomKey: number
|
||||||
roundKey: number
|
roundKey: number
|
||||||
/** Le type d'épreuve a-t-il changé par rapport à la manche précédente ? */
|
/** Catégorie visuelle de la manche (image_reveal distinct du quiz). */
|
||||||
|
roundKind: "quiz" | "image" | "blindtest"
|
||||||
|
/** Le mode a-t-il changé par rapport à la manche précédente ? */
|
||||||
roundModeChanged: boolean
|
roundModeChanged: boolean
|
||||||
|
|
||||||
createRoom: () => Promise<RoomCreatedPayload>
|
createRoom: () => Promise<RoomCreatedPayload>
|
||||||
|
|
@ -91,6 +93,7 @@ export const useRoomStore = create<RoomState>((set, get) => ({
|
||||||
mediaSync: null,
|
mediaSync: null,
|
||||||
boomKey: 0,
|
boomKey: 0,
|
||||||
roundKey: 0,
|
roundKey: 0,
|
||||||
|
roundKind: "quiz",
|
||||||
roundModeChanged: false,
|
roundModeChanged: false,
|
||||||
|
|
||||||
createRoom: () =>
|
createRoom: () =>
|
||||||
|
|
@ -221,6 +224,7 @@ export const useRoomStore = create<RoomState>((set, get) => ({
|
||||||
mediaSync: null,
|
mediaSync: null,
|
||||||
boomKey: 0,
|
boomKey: 0,
|
||||||
roundKey: 0,
|
roundKey: 0,
|
||||||
|
roundKind: "quiz",
|
||||||
roundModeChanged: false,
|
roundModeChanged: false,
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
@ -248,7 +252,15 @@ socket.on("room:state", (snapshot) =>
|
||||||
socket.on("error", (error) => useRoomStore.setState({ lastError: error }))
|
socket.on("error", (error) => useRoomStore.setState({ lastError: error }))
|
||||||
|
|
||||||
socket.on("round:start", (payload) =>
|
socket.on("round:start", (payload) =>
|
||||||
useRoomStore.setState((s) => ({
|
useRoomStore.setState((s) => {
|
||||||
|
const fmt = (payload.payload as { format?: string } | null)?.format
|
||||||
|
const kind =
|
||||||
|
payload.type === "blindtest"
|
||||||
|
? "blindtest"
|
||||||
|
: fmt === "image_reveal"
|
||||||
|
? "image"
|
||||||
|
: "quiz"
|
||||||
|
return {
|
||||||
round: {
|
round: {
|
||||||
type: payload.type,
|
type: payload.type,
|
||||||
djId: payload.djId,
|
djId: payload.djId,
|
||||||
|
|
@ -263,8 +275,10 @@ socket.on("round:start", (payload) =>
|
||||||
hasVoted: false,
|
hasVoted: false,
|
||||||
mediaSync: null,
|
mediaSync: null,
|
||||||
roundKey: s.roundKey + 1,
|
roundKey: s.roundKey + 1,
|
||||||
roundModeChanged: (s.round?.type ?? null) !== payload.type,
|
roundKind: kind,
|
||||||
}))
|
roundModeChanged: s.roundKind !== kind,
|
||||||
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
socket.on("round:voteAck", (voteProgress) =>
|
socket.on("round:voteAck", (voteProgress) =>
|
||||||
useRoomStore.setState({ voteProgress })
|
useRoomStore.setState({ voteProgress })
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue