srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/screens
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/screens')
-rw-r--r--web/src/screens/accountScreen.js4
-rw-r--r--web/src/screens/endScreen.js6
-rw-r--r--web/src/screens/friendsScreen.js6
-rw-r--r--web/src/screens/icons.js35
-rw-r--r--web/src/screens/leaderboardScreen.js3
-rw-r--r--web/src/screens/mainMenu.js4
-rw-r--r--web/src/screens/multiplayerScreen.js29
-rw-r--r--web/src/screens/storeScreen.js16
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 &rarr; 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 '';