Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 42 additions & 34 deletions assets/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -162,47 +162,55 @@ html, body {

.pad__log { text-align: center; color: var(--muted); min-height: 1.2em; }

/* ---- Analog driving pad (profile:"analog") ---- */
.pad__analog { width: 100%; display: flex; flex-direction: column; gap: 1.1rem; }
/* ---- Analog driving pad (profile:"analog") — compact landscape gamepad ---- */
/* Buttons hug the bottom-LEFT, the steering joystick hugs the bottom-RIGHT, so
the layout already reads sideways — the player naturally turns the phone. */
.pad__controls--drive { flex: 1; margin-top: 0.4rem; width: 100%; }
.pad__analog { flex: 1; width: 100%; display: flex; min-height: 0; }
.pad__analog[hidden] { display: none; }

.steer { position: relative; display: flex; flex-direction: column; gap: 0.5rem; }
.steer__tilt {
align-self: flex-end;
background: transparent; color: var(--muted);
border: 1px solid #232744; border-radius: 999px; padding: 0.35em 0.9em;
font-size: 0.85rem; font-weight: 600;
.drive {
flex: 1; display: flex; flex-direction: row;
align-items: flex-end; justify-content: space-between;
gap: 0.8rem; min-height: 0; padding-bottom: 0.3rem;
}
.drivecol { display: flex; flex-direction: column; }
.drivecol--left { align-items: flex-start; gap: 0.5rem; }
.drivecol--right { align-items: flex-end; }

/* Pedals (bottom-left): compact GAS/BRAKE, DRIFT beneath. */
.pedals { display: grid; grid-template-columns: repeat(2, 74px); gap: 0.45rem; }
.pedal {
height: 54px; border: none; border-radius: 15px; touch-action: none;
font-size: 0.9rem; font-weight: 800; color: white;
}
.steer__tilt[aria-pressed="true"] { color: var(--text); border-color: var(--accent); background: rgba(109,123,255,0.14); }
/* The drag surface. touch-action:none so steering never scrolls the page. */
.pedal--gas { grid-column: 1; grid-row: 1; background: linear-gradient(135deg, #16a34a, #38e8a0); }
.pedal--brake { grid-column: 2; grid-row: 1; background: linear-gradient(135deg, #d31027, #ea384d); }
.pedal--drift { grid-column: 1 / 3; grid-row: 2; height: 38px; font-size: 0.8rem; background: linear-gradient(135deg, #f5af19, #f12711); }
.pedal.is-active { filter: brightness(1.25); transform: translateY(1px); }

/* Aux buttons: small pills under the pedals. */
.pad__actions--aux { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0; }
.pad__actions--aux .action { min-width: 44px; height: 38px; padding: 0 0.55rem; font-size: 0.82rem; }

/* Steering joystick (bottom-right), compact + self-centering. */
.steer { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; }
.steer__track {
position: relative; width: 100%; height: 86px;
border-radius: 20px; background: var(--panel);
border: 1px solid #1c2040; touch-action: none; overflow: hidden;
}
/* Centre detent so "straight" is visible at a glance. */
.steer__track::before {
content: ""; position: absolute; top: 12%; bottom: 12%; left: 50%;
width: 2px; transform: translateX(-50%); background: #232744;
position: relative; width: min(150px, 42vw); height: min(150px, 42vw);
border-radius: 50%; touch-action: none;
background: radial-gradient(circle at 50% 42%, #171b34, var(--panel));
border: 1px solid #262c52; box-shadow: inset 0 2px 12px rgba(0,0,0,0.5);
}
.steer__knob {
position: absolute; top: 50%; left: 50%;
width: 62px; height: 62px; transform: translate(-50%, -50%);
width: 36%; height: 36%; transform: translate(-50%, -50%);
border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--accent-2));
box-shadow: 0 6px 16px rgba(0,0,0,0.4); pointer-events: none;
box-shadow: 0 5px 14px rgba(0,0,0,0.45); pointer-events: none;
}
.steer__hint { text-align: center; color: var(--muted); font-size: 0.8rem; }
.steer.is-tilt .steer__hint::after,
.pad__analog.is-tilt .steer__hint { color: var(--accent); }
.pad__analog.is-tilt .steer__track { opacity: 0.6; }

.pedals { display: grid; grid-template-columns: 1.2fr 1.2fr 1fr; gap: 0.7rem; }
.pedal {
height: 104px; border: none; border-radius: 20px;
font-size: 1.15rem; font-weight: 800; letter-spacing: 0.02em; color: white;
touch-action: none;
.steer__tilt {
background: transparent; color: var(--muted);
border: 1px solid #232744; border-radius: 999px; padding: 0.25em 0.75em;
font-size: 0.75rem; font-weight: 600;
}
.pedal--gas { background: linear-gradient(135deg, #16a34a, #38e8a0); }
.pedal--brake { background: linear-gradient(135deg, #d31027, #ea384d); }
.pedal--drift { background: linear-gradient(135deg, #f5af19, #f12711); font-size: 1rem; }
.pedal.is-active { filter: brightness(1.25); transform: translateY(1px); }
.steer__tilt[aria-pressed="true"] { color: var(--text); border-color: var(--accent); background: rgba(109,123,255,0.14); }
.pad__analog.is-tilt .steer__track { opacity: 0.55; }
82 changes: 61 additions & 21 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,13 @@ function renderControls(ctl) {
ctl && (ctl.profile === "buttons" || ctl.profile === "analog") ? ctl.profile : "dpad";
const buttons = (ctl && ctl.buttons) || [];
padDpad.hidden = profile !== "dpad";
setAnalog(profile === "analog"); // steering track + gas/brake pedals for driving games
setAnalog(profile === "analog"); // landscape steering + pedals for driving games
// Aux buttons sit in the LEFT column of the analog pad, else in the normal row.
const target = profile === "analog" ? analog.aux : padActions;
padActions.innerHTML = "";
for (const b of buttons) padActions.appendChild(makeAction(b));
padActions.appendChild(makeAction({ id: "back", label: "Back", sys: true }));
if (analog) analog.aux.innerHTML = "";
for (const b of buttons) target.appendChild(makeAction(b));
target.appendChild(makeAction({ id: "back", label: "Back", sys: true }));
}

function makeAction(b) {
Expand Down Expand Up @@ -177,6 +180,7 @@ let tiltCenter = null; // gyro reading captured as "straight" when tilt e
function setAnalog(on) {
if (on) ensureAnalog();
if (analog) analog.root.hidden = !on;
padControls.classList.toggle("pad__controls--drive", on); // landscape driving layout
if (on && !driveActive) {
driveActive = true;
requestAnimationFrame(driveLoop);
Expand All @@ -202,47 +206,83 @@ function resetDrive() {
function driveLoop() {
if (!driveActive) return;
if (driveDirty) {
// BRAKE = reverse/brake: negative throttle brakes while moving forward, then
// reverses once stopped (the runtime handles the transition). Sending a
// service brake too would pin the car and block reverse — so we don't.
const throttle = drive.gas ? 1 : drive.brake ? -1 : 0;
session.sendAnalog(drive.steer, throttle, drive.brake ? 1 : 0, drive.drift);
session.sendAnalog(drive.steer, throttle, 0, drive.drift);
driveDirty = false;
}
requestAnimationFrame(driveLoop);
}
function markDrive() { driveDirty = true; }

// Radius the knob can travel from the stick centre.
function knobRadius() {
if (!analog) return 0;
const d = Math.min(analog.track.clientWidth, analog.track.clientHeight);
return d * 0.5 - 26;
}

// Position the knob for a given steer value (used by tilt + self-centering).
function setKnob(steer) {
drive.steer = Math.max(-1, Math.min(1, steer));
if (analog) analog.knob.style.left = `${(drive.steer * 0.5 + 0.5) * 100}%`;
if (analog) {
const x = drive.steer * knobRadius();
analog.knob.style.transform = `translate(calc(-50% + ${x}px), -50%)`;
}
}

function steerFromEvent(e) {
// Virtual thumbstick: the knob follows the finger within a circle; steering is
// the horizontal component. Self-centers on release.
function stickFromEvent(e) {
const rect = analog.track.getBoundingClientRect();
const rel = rect.width ? (e.clientX - rect.left) / rect.width : 0.5;
setKnob(rel * 2 - 1);
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const r = Math.min(rect.width, rect.height) * 0.5 - 26 || 1;
let dx = e.clientX - cx;
let dy = e.clientY - cy;
const mag = Math.hypot(dx, dy);
if (mag > r) { dx = (dx / mag) * r; dy = (dy / mag) * r; }
drive.steer = expoSteer(Math.max(-1, Math.min(1, dx / r)));
analog.knob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
markDrive();
}

// Mild steering expo: softens the mid-range so the car turns a bit less sharply,
// while full stick deflection still reaches full lock. The knob position stays
// linear to the thumb; only the transmitted steer is eased.
function expoSteer(v) {
const k = 1.35;
return Math.sign(v) * Math.pow(Math.abs(v), k);
}

function ensureAnalog() {
if (analog) return;
const root = document.createElement("div");
root.className = "pad__analog";
// Landscape driving surface: buttons on the LEFT, steering on the RIGHT.
root.innerHTML = `
<div class="steer" aria-label="Steering">
<button class="steer__tilt" type="button" aria-pressed="false">Tilt: off</button>
<div class="steer__track"><span class="steer__knob"></span></div>
<div class="steer__hint">Drag to steer</div>
</div>
<div class="pedals" role="group" aria-label="Pedals">
<button class="pedal pedal--gas" data-pedal="gas" type="button">GAS</button>
<button class="pedal pedal--brake" data-pedal="brake" type="button">BRAKE</button>
<button class="pedal pedal--drift" data-pedal="drift" type="button">DRIFT</button>
<div class="drive">
<div class="drivecol drivecol--left">
<div class="pedals" role="group" aria-label="Pedals">
<button class="pedal pedal--gas" data-pedal="gas" type="button">GAS</button>
<button class="pedal pedal--brake" data-pedal="brake" type="button">BRAKE</button>
<button class="pedal pedal--drift" data-pedal="drift" type="button">DRIFT</button>
</div>
<div class="pad__actions pad__actions--aux" id="analogAux" role="group" aria-label="Actions"></div>
</div>
<div class="drivecol drivecol--right steer" aria-label="Steering">
<button class="steer__tilt" type="button" aria-pressed="false">Tilt: off</button>
<div class="steer__track"><span class="steer__knob"></span></div>
</div>
</div>`;
// Insert the driving surface above the aux action buttons.
padControls.insertBefore(root, padActions);

const track = root.querySelector(".steer__track");
const knob = root.querySelector(".steer__knob");
const tilt = root.querySelector(".steer__tilt");
const aux = root.querySelector("#analogAux");

// Steering: drag anywhere on the track. One pointer owns steering at a time so
// a second thumb on the pedals doesn't hijack it.
Expand All @@ -251,10 +291,10 @@ function ensureAnalog() {
e.preventDefault();
steerPointer = e.pointerId;
try { track.setPointerCapture(e.pointerId); } catch { /* older browser */ }
steerFromEvent(e);
stickFromEvent(e);
});
track.addEventListener("pointermove", (e) => {
if (steerPointer === e.pointerId) steerFromEvent(e);
if (steerPointer === e.pointerId) stickFromEvent(e);
});
const endSteer = (e) => {
if (steerPointer !== e.pointerId) return;
Expand Down Expand Up @@ -282,7 +322,7 @@ function ensureAnalog() {

tilt.addEventListener("click", () => toggleTilt(tilt));

analog = { root, track, knob, tilt, pedals };
analog = { root, track, knob, tilt, pedals, aux };
}

// ---- Tilt (gyro) steering --------------------------------------------------
Expand Down
Loading