diff options
Diffstat (limited to 'web/src/screens')
| -rw-r--r-- | web/src/screens/accountScreen.js | 4 | ||||
| -rw-r--r-- | web/src/screens/endScreen.js | 6 | ||||
| -rw-r--r-- | web/src/screens/friendsScreen.js | 6 | ||||
| -rw-r--r-- | web/src/screens/icons.js | 35 | ||||
| -rw-r--r-- | web/src/screens/leaderboardScreen.js | 3 | ||||
| -rw-r--r-- | web/src/screens/mainMenu.js | 4 | ||||
| -rw-r--r-- | web/src/screens/multiplayerScreen.js | 29 | ||||
| -rw-r--r-- | web/src/screens/storeScreen.js | 16 |
8 files changed, 81 insertions, 22 deletions
diff --git a/web/src/screens/accountScreen.js b/web/src/screens/accountScreen.js index 5ae2bd0..5f3bdfe 100644 --- a/web/src/screens/accountScreen.js +++ b/web/src/screens/accountScreen.js @@ -44,8 +44,8 @@ export function renderAccountScreen(root, { onBack, onShowAccount, onShowStats, } return ` <div class="account-tabs"> - <button class="menu-button small${mode === 'login' ? '' : ' ghost'}" data-action="tab-login">Log In</button> - <button class="menu-button small${mode === 'register' ? '' : ' ghost'}" data-action="tab-register">Register</button> + <button class="menu-button small quiet${mode === 'login' ? ' active' : ''}" data-action="tab-login">Log In</button> + <button class="menu-button small quiet${mode === 'register' ? ' active' : ''}" data-action="tab-register">Register</button> </div> <form class="account-form"> <input class="account-input" type="text" name="username" placeholder="Username" autocomplete="username" required> diff --git a/web/src/screens/endScreen.js b/web/src/screens/endScreen.js index 2787f76..aebd62b 100644 --- a/web/src/screens/endScreen.js +++ b/web/src/screens/endScreen.js @@ -3,6 +3,7 @@ import { escapeHtml } from '../util.js'; import { attachTooltips } from '../tooltip.js'; import { onThemeChange } from '../theme.js'; import { renderSiteFooter } from '../siteFooter.js'; +import { renderAdSlot } from '../adSlot.js'; import { recordResult } from '../pb.js'; import { recordTest } from '../profileStats.js'; import { renderCornerRail } from '../cornerRail.js'; @@ -82,6 +83,7 @@ export function renderEndScreen(root, { stats, text, attribution, explanation, s <div class="mp-standings-heading">Standings</div> <div class="mp-standings-rows"></div> </div>` : ''} + <div class="end-screen-ad"></div> <div class="end-screen-buttons"> <button class="end-screen-button primary" data-action="again">Play Again</button> </div> @@ -145,6 +147,9 @@ export function renderEndScreen(root, { stats, text, attribution, explanation, s const cleanupTheme = renderTopRail(root, { onShowAccount, onShowFriends }); // Share here carries the result just achieved, not just the site. const cleanupFooter = renderSiteFooter(root, { result: stats, onShowPrivacy }); + // Below the results and above Play Again: read after the run is over, + // never during it. + const cleanupAd = renderAdSlot(root.querySelector('.end-screen-ad')); root.querySelectorAll('[data-action="menu"]').forEach(el => el.addEventListener('click', onMainMenu)); root.querySelector('[data-action="again"]').addEventListener('click', onPlayAgain); @@ -234,6 +239,7 @@ export function renderEndScreen(root, { stats, text, attribution, explanation, s onLeaveRace?.(); cleanupTheme(); cleanupFooter(); + cleanupAd(); window.removeEventListener('resize', redraw); offTheme(); canvas.removeEventListener('mousemove', handleMove); diff --git a/web/src/screens/friendsScreen.js b/web/src/screens/friendsScreen.js index 14a0b2d..e121f4c 100644 --- a/web/src/screens/friendsScreen.js +++ b/web/src/screens/friendsScreen.js @@ -43,7 +43,7 @@ export function renderFriendsScreen(root, { onBack, onShowFriends, onShowStats, <div class="leaderboard-row"> <div class="leaderboard-name">${escapeHtml(r.username)}</div> <button class="menu-button small" data-action="accept" data-id="${r.friendship_id}">Accept</button> - <button class="menu-button small ghost" data-action="decline" data-id="${r.friendship_id}">Decline</button> + <button class="menu-button small quiet" data-action="decline" data-id="${r.friendship_id}">Decline</button> </div> `).join('')} </div>` : ''} @@ -55,7 +55,7 @@ export function renderFriendsScreen(root, { onBack, onShowFriends, onShowStats, <div class="leaderboard-row"> <div class="leaderboard-name">${escapeHtml(r.username)}</div> <div class="leaderboard-acc">Pending</div> - <button class="menu-button small ghost" data-action="cancel" data-id="${r.friendship_id}">Cancel</button> + <button class="menu-button small quiet" data-action="cancel" data-id="${r.friendship_id}">Cancel</button> </div> `).join('')} </div>` : ''} @@ -66,7 +66,7 @@ export function renderFriendsScreen(root, { onBack, onShowFriends, onShowStats, ${data.friends.map(f => ` <div class="leaderboard-row"> <div class="leaderboard-name">${escapeHtml(f.username)}</div> - <button class="menu-button small ghost" data-action="unfriend" data-id="${f.friendship_id}">Remove</button> + <button class="menu-button small quiet" data-action="unfriend" data-id="${f.friendship_id}">Remove</button> </div> `).join('')} </div>` : `<div class="stats-empty">No friends yet - add one by username above.</div>`} diff --git a/web/src/screens/icons.js b/web/src/screens/icons.js index 21c0548..462c6b0 100644 --- a/web/src/screens/icons.js +++ b/web/src/screens/icons.js @@ -114,3 +114,38 @@ export const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 <path d="M5 5L19 19"/> <path d="M19 5L5 19"/> </svg>`; + +// Racer sprites. One per player in a multiplayer race, so a bar is a +// character rather than a coloured line with a name attached. Drawn in the +// same angular language as the rest of this set: straight edges, flat fill, +// no gradients, and they take their colour from the racer they belong to. +// +// Six of them, which is the room capacity, so no two racers in a full room +// share a sprite. +export const RACER_SPRITES = { + // A forward-leaning chevron, the fastest-reading shape in the set. + dart: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 4L17 12L3 20L7 12Z"/><rect x="19" y="10" width="3" height="4"/></svg>`, + // A blocky rocket. + rocket: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L16 8V16H8V8Z"/><path d="M8 16L5 21L9 19Z"/><path d="M16 16L19 21L15 19Z"/><rect x="10" y="6" width="4" height="4" fill="var(--background-color)"/></svg>`, + // A visored helmet. + helm: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 11L12 3L20 11V18H4Z"/><rect x="7" y="11" width="10" height="4" fill="var(--background-color)"/><rect x="4" y="18" width="16" height="3"/></svg>`, + // A hexagonal core. + core: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L20 7V17L12 22L4 17V7Z"/><rect x="9" y="9" width="6" height="6" fill="var(--background-color)"/></svg>`, + // A signal mast. + signal: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><rect x="11" y="8" width="2" height="14"/><path d="M4 3L8 7L4 11Z"/><path d="M20 3L16 7L20 11Z"/><rect x="8" y="6" width="8" height="2"/></svg>`, + // A pair of stacked blades. + blade: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 6H14L18 10H6Z"/><path d="M6 14H18L22 18H10Z"/></svg>`, +}; + +export const RACER_SPRITE_IDS = Object.keys(RACER_SPRITES); + +// Flair added alongside the original three. Same construction: flat fill, +// straight edges, readable at 16px beside a username. +Object.assign(FLAIR_ICONS, { + crown: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 7L7 12L12 5L17 12L21 7V19H3Z"/></svg>`, + circuit: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><rect x="9" y="9" width="6" height="6"/><rect x="11" y="2" width="2" height="6"/><rect x="11" y="16" width="2" height="6"/><rect x="2" y="11" width="6" height="2"/><rect x="16" y="11" width="6" height="2"/></svg>`, + eye: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 12L7 6H17L22 12L17 18H7Z"/><rect x="10" y="10" width="4" height="4" fill="var(--background-color)"/></svg>`, + shard: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L18 10L12 22L6 10Z"/></svg>`, + moth: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 4L4 8L6 16L12 13Z"/><path d="M12 4L20 8L18 16L12 13Z"/><rect x="11" y="4" width="2" height="16"/></svg>`, + reactor: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L21 20H3Z"/><rect x="10" y="12" width="4" height="5" fill="var(--background-color)"/></svg>`, +}); diff --git a/web/src/screens/leaderboardScreen.js b/web/src/screens/leaderboardScreen.js index 5d56a97..8aeda98 100644 --- a/web/src/screens/leaderboardScreen.js +++ b/web/src/screens/leaderboardScreen.js @@ -89,7 +89,7 @@ export function renderLeaderboardScreen(root, { onBack, onShowPublicProfile, onS root.querySelectorAll('.leaderboard-modes [data-mode]').forEach(btn => { btn.addEventListener('click', () => { selectedMode = btn.dataset.mode; - root.querySelectorAll('.leaderboard-modes [data-mode]').forEach(b => b.classList.toggle('ghost', b !== btn)); + root.querySelectorAll('.leaderboard-modes [data-mode]').forEach(b => b.classList.toggle('active', b === btn)); load(); }); }); @@ -100,7 +100,6 @@ export function renderLeaderboardScreen(root, { onBack, onShowPublicProfile, onS deviceFilterBtn.textContent = `Devices: ${desktopOnly ? 'Desktop only' : 'All'}`; // A filter that is doing something should look like it is. deviceFilterBtn.classList.toggle('active', desktopOnly); - deviceFilterBtn.classList.toggle('ghost', !desktopOnly); load(); }); diff --git a/web/src/screens/mainMenu.js b/web/src/screens/mainMenu.js index 2d27468..f812509 100644 --- a/web/src/screens/mainMenu.js +++ b/web/src/screens/mainMenu.js @@ -168,7 +168,7 @@ export function renderMainMenu(root, props) { </div> <div class="settings-hint">Dev</div> <button class="menu-button small" data-action="simulate-test" data-tooltip="Jump straight to the end screen with fake results, no typing required.">Simulate Test → End Screen</button> - <button class="menu-button small ghost" data-action="close-settings">Close</button> + <button class="menu-button small quiet" data-action="close-settings">Close</button> </div> <div class="custom-panel" hidden> @@ -197,7 +197,7 @@ export function renderMainMenu(root, props) { </div>` : ''} <div class="custom-panel-row"> <button class="menu-button small" data-action="use-pasted">Use this text</button> - <button class="menu-button small ghost" data-action="close-custom">Cancel</button> + <button class="menu-button small quiet" data-action="close-custom">Cancel</button> </div> <div class="custom-error"></div> </div> diff --git a/web/src/screens/multiplayerScreen.js b/web/src/screens/multiplayerScreen.js index 7292bcc..d3ae7d9 100644 --- a/web/src/screens/multiplayerScreen.js +++ b/web/src/screens/multiplayerScreen.js @@ -1,5 +1,5 @@ import { escapeHtml } from '../util.js'; -import { CLOSE_ICON } from './icons.js'; +import { CLOSE_ICON, RACER_SPRITES, RACER_SPRITE_IDS } from './icons.js'; import { renderCornerRail } from '../cornerRail.js'; import { attachTooltips } from '../tooltip.js'; import { renderTopRail } from '../topRail.js'; @@ -27,6 +27,18 @@ function racerColors(players, myId) { return colors; } +// A sprite per racer, assigned from the server's own player order so every +// client in the room draws the same person as the same character. Derived +// rather than stored: the order is identical everywhere, and a race is short +// enough that nobody needs to keep a sprite between rooms. +function racerSprites(players) { + const sprites = {}; + players.forEach((p, i) => { + sprites[p.id] = RACER_SPRITE_IDS[i % RACER_SPRITE_IDS.length]; + }); + return sprites; +} + // A style="" attribute in markup is refused by the Content-Security-Policy; // the same property set through element.style is not. Markup carries the // value in a data attribute and this applies it. @@ -76,7 +88,7 @@ export function renderMultiplayerScreen(root, { onBack, onFinish, onShowStats, o <h2>Multiplayer</h2> <div class="account-panel"> <input class="account-input" type="text" id="mp-name" placeholder="Your name" value="${escapeHtml(getUser()?.username || '')}"> - <button class="menu-button small ghost" data-action="toggle-device-filter" data-tooltip="Who you get matched with, and the setting any room you open uses. Joining by code always uses that room's setting.">Match: Everyone</button> + <button class="menu-button small quiet" data-action="toggle-device-filter" data-tooltip="Who you get matched with, and the setting any room you open uses. Joining by code always uses that room's setting.">Match: Everyone</button> <button class="menu-button primary" data-action="quick" data-tooltip="Drops you straight into a race with whoever else is looking. No code to share.">Find a Race</button> <div class="mp-divider"><span>or race friends</span></div> @@ -85,7 +97,7 @@ export function renderMultiplayerScreen(root, { onBack, onFinish, onShowStats, o <input class="account-input" type="text" id="mp-room-code" placeholder="CODE" maxlength="5" autocomplete="off" spellcheck="false"> <button class="menu-button" data-action="join">Join</button> </div> - <button class="menu-button ghost" data-action="create" data-tooltip="Opens an empty room and gives you a code to share.">Create a Room</button> + <button class="menu-button quiet" data-action="create" data-tooltip="Opens an empty room and gives you a code to share.">Create a Room</button> <div class="custom-error mp-error">${escapeHtml(landingError || '')}</div> </div> </div> @@ -168,7 +180,7 @@ export function renderMultiplayerScreen(root, { onBack, onFinish, onShowStats, o <div class="mp-countdown" hidden></div> <div class="leaderboard-list mp-player-list"></div> <button class="menu-button" data-action="ready">Ready</button> - <button class="menu-button small ghost" data-action="leave">Leave Room</button> + <button class="menu-button small quiet" data-action="leave">Leave Room</button> </div> `; root.querySelector('[data-action="menu"]').addEventListener('click', () => { leaveRoom(); onBack(); }); @@ -180,9 +192,10 @@ export function renderMultiplayerScreen(root, { onBack, onFinish, onShowStats, o const listEl = root.querySelector('.mp-player-list'); function paintPlayers() { const colors = racerColors(players, myId); + const sprites = racerSprites(players); listEl.innerHTML = players.map(p => ` <div class="leaderboard-row" data-racer-color="${colors[p.id]}"> - <div class="leaderboard-name"><span class="mp-racer-dot"></span>${escapeHtml(p.name)}${p.id === myId ? ' (you)' : ''}</div> + <div class="leaderboard-name"><span class="mp-racer-sprite">${RACER_SPRITES[sprites[p.id]]}</span>${escapeHtml(p.name)}${p.id === myId ? ' (you)' : ''}</div> <div class="leaderboard-acc">${p.ready ? 'Ready' : 'Not ready'}</div> </div> `).join(''); @@ -200,7 +213,7 @@ export function renderMultiplayerScreen(root, { onBack, onFinish, onShowStats, o connection.ready(); readyBtn.textContent = 'Waiting for players...'; readyBtn.disabled = true; - readyBtn.classList.add('ghost'); + readyBtn.classList.add('quiet'); } }); const offPlayerList = connection.on('playerList', list => { players = list; paintPlayers(); }); @@ -232,7 +245,7 @@ export function renderMultiplayerScreen(root, { onBack, onFinish, onShowStats, o connection.ready(); readyBtn.textContent = 'Waiting for players...'; readyBtn.disabled = true; - readyBtn.classList.add('ghost'); + readyBtn.classList.add('quiet'); }); root.querySelector('[data-action="leave"]').addEventListener('click', () => { leaveRoom(); renderLanding(); }); @@ -263,11 +276,13 @@ export function renderMultiplayerScreen(root, { onBack, onFinish, onShowStats, o // way theirs are. function paintOpponents() { const colors = racerColors(players, myId); + const sprites = racerSprites(players); opponents.innerHTML = players.map(p => { const me = p.id === myId; const prog = progressById[p.id] || {}; return ` <div class="mp-racer-row${me ? ' me' : ''}" data-racer-color="${colors[p.id]}"> + <span class="mp-racer-sprite">${RACER_SPRITES[sprites[p.id]]}</span> <span class="mp-racer-name">${escapeHtml(p.name)}${me ? ' (you)' : ''}</span> <div class="mp-racer-bar"><div class="mp-racer-bar-fill" data-percent="${prog.percent || 0}"></div></div> <span class="mp-racer-wpm">${Math.round(prog.wpm || 0)}</span> diff --git a/web/src/screens/storeScreen.js b/web/src/screens/storeScreen.js index d1675a7..f66e3c8 100644 --- a/web/src/screens/storeScreen.js +++ b/web/src/screens/storeScreen.js @@ -4,14 +4,16 @@ import { attachTooltips } from '../tooltip.js'; import { renderTopRail } from '../topRail.js'; import { api, ApiError } from '../api.js'; import { getUser } from '../auth.js'; -import { FLAIR_ICONS, CLOSE_ICON } from './icons.js'; +import { FLAIR_ICONS, RACER_SPRITES, CLOSE_ICON } from './icons.js'; function formatPrice(cents) { return `$${(cents / 100).toFixed(2)}`; } function categoryLabel(category) { - return category === 'caret' ? 'Caret Colors' : 'Flair'; + if (category === 'caret') return 'Caret Colours'; + if (category === 'sprite') return 'Race Sprites'; + return 'Flair'; } // Real purchase/equip flow against the catalog, but purchase is a stub -- @@ -30,17 +32,19 @@ export function renderStoreScreen(root, { onBack, onShowStats, onShowPlaceholder const signedOut = status === 'signed-out'; const owned = !signedOut && mine.owned.includes(item.id); const equipped = !signedOut && (mine.equipped_caret === item.id || mine.equipped_flair === item.id); + // A caret is a colour, so the swatch is the colour itself; flair and + // sprites are drawings, so the swatch is the drawing. const swatch = item.category === 'caret' ? `<span class="store-swatch" data-swatch-colour="${escapeHtml(item.value)}"></span>` - : `<span class="store-swatch store-flair-swatch">${FLAIR_ICONS[item.value] || ''}</span>`; + : `<span class="store-swatch store-flair-swatch">${(item.category === 'sprite' ? RACER_SPRITES : FLAIR_ICONS)[item.value] || ''}</span>`; return ` <div class="leaderboard-row store-item-row"> ${swatch} <div class="leaderboard-name">${escapeHtml(item.name)}</div> ${!owned ? `<div class="leaderboard-acc">${formatPrice(item.price_cents)}</div>` : ''} ${owned - ? `<button class="menu-button small${equipped ? '' : ' ghost'}" data-action="${equipped ? 'unequip' : 'equip'}" data-id="${item.id}" data-category="${item.category}" data-tooltip="${equipped ? 'Unequip - back to the default look' : `Replaces whichever ${categoryLabel(item.category).toLowerCase()} you have equipped now`}">${equipped ? 'Equipped' : 'Equip'}</button>` - : `<button class="menu-button small${signedOut ? ' ghost' : ''}" data-action="${signedOut ? 'go-account' : 'buy'}" data-id="${item.id}"${signedOut ? ' data-tooltip="Sign in to buy this"' : ''}>Buy</button>`} + ? `<button class="menu-button small${equipped ? ' active' : ' quiet'}" data-action="${equipped ? 'unequip' : 'equip'}" data-id="${item.id}" data-category="${item.category}" data-tooltip="${equipped ? 'Unequip - back to the default look' : `Replaces whichever ${categoryLabel(item.category).toLowerCase()} you have equipped now`}">${equipped ? 'Equipped' : 'Equip'}</button>` + : `<button class="menu-button small${signedOut ? ' quiet' : ''}" data-action="${signedOut ? 'go-account' : 'buy'}" data-id="${item.id}"${signedOut ? ' data-tooltip="Sign in to buy this"' : ''}>Buy</button>`} </div> `; } @@ -54,7 +58,7 @@ export function renderStoreScreen(root, { onBack, onShowStats, onShowPlaceholder ? `<div class="store-signin-note">Sign in to buy and equip these. <button class="menu-button small" data-action="go-account">Sign In</button></div>` : ''; - const categories = ['caret', 'flair']; + const categories = ['caret', 'flair', 'sprite']; return banner + categories.map(cat => { const items = catalog.filter(i => i.category === cat); if (items.length === 0) return ''; |