Add gray-scaling when guessing
All checks were successful
Deploy Feature / deploy-feature (push) Successful in 25s

This commit is contained in:
Johnny322
2026-03-02 21:03:01 +01:00
parent f440118fef
commit 551b5a76d8
2 changed files with 112 additions and 35 deletions

View File

@@ -1,5 +1,5 @@
<template>
<div class="app">
<div class="app" :class="{ suspended: isGuessSuspended }">
<header class="app-header">
<div>
<p class="eyebrow">Music Jeopardy</p>
@@ -263,12 +263,14 @@
@pause="handlePlayerPause"
@ended="handlePlayerPause"
></audio>
<button v-if="showEnableAudio" class="primary enable-audio" @click="enableViewerAudio">
Tap To Enable Audio
</button>
<button v-if="canViewerGuess" class="primary viewer-guess" @click="requestGuessStop">
Guess Now
</button>
<div v-if="viewerGuessVisible || showEnableAudio" class="viewer-actions">
<button v-if="showEnableAudio" class="primary enable-audio" @click="enableViewerAudio">
Tap To Enable Audio
</button>
<button class="primary viewer-guess" :disabled="!canViewerGuess" @click="requestGuessStop">
Stop Song And Guess
</button>
</div>
<div v-if="!currentClipUrl" class="player-empty"></div>
</div>
</div>
@@ -300,6 +302,9 @@
</div>
</section>
</main>
<div v-if="isGuessSuspended" class="guess-overlay">
<p>{{ guessingTeamLabel }} is guessing</p>
</div>
</div>
</template>
@@ -334,6 +339,7 @@ type RealtimeState = {
currentSelectorId: string | null
lastAwardedTeamId: string | null
isAnswerClip: boolean
guessingTeamId: string | null
}
type RealtimeMessage = {
@@ -399,7 +405,8 @@ export default {
syncTimer: 0,
audioUnlocked: true,
latestRemoteState: null as RealtimeState | null,
viewerTeamId: ''
viewerTeamId: '',
guessingTeamId: null as string | null
}
},
async mounted() {
@@ -437,14 +444,26 @@ export default {
canViewerGuess() {
return (
!this.canControlGame &&
this.audioUnlocked &&
!!this.viewerTeamId &&
this.getCurrentTileStatus() === 'playing'
)
},
viewerGuessVisible() {
return !this.canControlGame && this.getCurrentTileStatus() === 'playing'
},
canControlGame() {
return !this.gameId || this.isHost
},
isGuessSuspended() {
return this.step === 'game' && this.getCurrentTileStatus() === 'paused'
},
guessingTeamLabel() {
if (this.guessingTeamId) {
const team = this.teams.find((candidate) => candidate.id === this.guessingTeamId)
if (team?.name?.trim()) return team.name.trim()
}
return 'A team'
},
currentSelector() {
return this.teams.find((team) => team.id === this.currentSelectorId) || null
},
@@ -571,6 +590,7 @@ export default {
this.audioUnlocked = true
this.latestRemoteState = null
this.viewerTeamId = ''
this.guessingTeamId = null
this.setGameInUrl('')
},
async connectSession() {
@@ -632,7 +652,8 @@ export default {
currentClipUrl: this.currentClipUrl,
currentSelectorId: this.currentSelectorId,
lastAwardedTeamId: this.lastAwardedTeamId,
isAnswerClip: this.isAnswerClip
isAnswerClip: this.isAnswerClip,
guessingTeamId: this.guessingTeamId
}
},
publishState() {
@@ -675,6 +696,7 @@ export default {
this.currentSelectorId = state.currentSelectorId
this.lastAwardedTeamId = state.lastAwardedTeamId
this.isAnswerClip = state.isAnswerClip
this.guessingTeamId = state.guessingTeamId || null
if (this.viewerTeamId && !this.teams.some((team) => team.id === this.viewerTeamId)) {
this.viewerTeamId = ''
}
@@ -710,27 +732,12 @@ export default {
const status = this.getCurrentTileStatus()
if (status !== 'playing') return
const key = this.currentTileKey
const [cIndex, qIndex] = key.split('-').map(Number)
const clue = this.selectedGame.categories[cIndex].clues[qIndex]
this.tiles[key].status = 'guessed'
this.tiles[key].status = 'paused'
this.lastAwardedTeamId = null
if (clue.answer) {
this.currentClipUrl = encodeURI(clue.answer)
this.isAnswerClip = true
await nextTick()
const player = this.getPlayer()
if (player) {
this.ensureAudioContext()
player.currentTime = 0
player.load()
player.play().catch(() => {})
}
} else {
const player = this.getPlayer()
player?.pause()
this.currentClipUrl = ''
this.isAnswerClip = false
}
this.guessingTeamId = teamId
this.isAnswerClip = false
const player = this.getPlayer()
player?.pause()
this.queueStateSync()
},
getCurrentTileStatus() {
@@ -956,6 +963,7 @@ export default {
this.currentTileKey = null
this.currentClipUrl = ''
this.lastAwardedTeamId = null
this.guessingTeamId = null
this.teams = this.teams.map((team) => ({ ...team, score: 0 }))
const randomTeam = this.teams[Math.floor(Math.random() * this.teams.length)]
this.currentSelectorId = randomTeam?.id || null
@@ -971,6 +979,7 @@ export default {
this.currentClipUrl = ''
this.lastAwardedTeamId = null
this.isAnswerClip = false
this.guessingTeamId = null
this.teardownAudio()
this.queueStateSync()
},
@@ -1018,6 +1027,7 @@ export default {
this.currentTileKey = key
this.currentClipUrl = encodeURI(clue.song)
this.isAnswerClip = false
this.guessingTeamId = null
await nextTick()
const player = this.getPlayer()
if (player) {
@@ -1032,6 +1042,7 @@ export default {
if (status === 'playing') {
this.tiles[key].status = 'paused'
this.guessingTeamId = this.currentSelectorId
const player = this.getPlayer()
player?.pause()
this.queueStateSync()
@@ -1065,6 +1076,7 @@ export default {
this.currentTileKey = null
this.currentClipUrl = ''
this.lastAwardedTeamId = null
this.guessingTeamId = null
this.checkEnd()
this.queueStateSync()
}
@@ -1077,6 +1089,7 @@ export default {
if (status === 'paused') {
this.tiles[key].status = 'playing'
this.guessingTeamId = null
const player = this.getPlayer()
player?.play().catch(() => {})
this.queueStateSync()
@@ -1088,6 +1101,7 @@ export default {
this.currentTileKey = null
this.currentClipUrl = ''
this.isAnswerClip = false
this.guessingTeamId = null
const player = this.getPlayer()
player?.pause()
this.teardownAudio()
@@ -1122,6 +1136,7 @@ export default {
const finished = allTiles.every((status) => status === 'won' || status === 'void')
if (finished) {
this.step = 'end'
this.guessingTeamId = null
}
}
},

View File

@@ -29,6 +29,42 @@ code {
gap: 24px;
}
.app-header,
.app-main {
transition: filter 0.25s ease, opacity 0.25s ease;
}
.app.suspended .app-header,
.app.suspended .app-main {
filter: grayscale(1) brightness(0.55);
opacity: 0.75;
}
.guess-overlay {
position: fixed;
inset: 0;
display: grid;
place-items: center;
background: rgba(8, 12, 26, 0.35);
backdrop-filter: blur(2px) grayscale(0.15);
pointer-events: none;
z-index: 12;
}
.guess-overlay p {
margin: 0;
padding: 16px 24px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(13, 17, 35, 0.88);
color: #ffffff;
font-family: 'Bebas Neue', sans-serif;
font-size: clamp(2.1rem, 4vw, 3.3rem);
letter-spacing: 0.07em;
text-transform: uppercase;
text-align: center;
}
.app-header {
display: flex;
justify-content: space-between;
@@ -554,16 +590,33 @@ audio.hidden-audio {
color: rgba(255, 255, 255, 0.6);
}
.enable-audio {
.viewer-actions {
position: absolute;
left: 50%;
bottom: 20px;
z-index: 3;
transform: translateX(-50%);
z-index: 4;
display: flex;
align-items: center;
gap: 12px;
}
.enable-audio {
border: 1px solid rgba(255, 255, 255, 0.4);
}
.viewer-guess {
position: absolute;
bottom: 20px;
z-index: 3;
font-size: 1rem;
font-weight: 800;
letter-spacing: 0.03em;
padding: 14px 28px;
background: linear-gradient(135deg, #ffce3a, #ff6a3a);
box-shadow: 0 12px 30px rgba(255, 106, 58, 0.35);
}
.viewer-guess:disabled {
background: linear-gradient(135deg, #8f8f8f, #666666);
box-shadow: none;
}
.end-panel {
@@ -606,4 +659,13 @@ audio.hidden-audio {
.session-row {
flex-wrap: wrap;
}
.viewer-actions {
flex-direction: column;
width: calc(100% - 24px);
}
.viewer-actions .primary {
width: 100%;
}
}