Improve behavior on mobile
- Hide the key hints in portrait mode - Make auto-scaling more robust; it now handles when the player root is wider than the actual play area, it better understands the inventory behavior in portrait mode, and it recognizes when it needs to shrink; with these changes, the game actually fills the screen on both Firefox and Chrome on my phone! - Replace the text buttons with SVG icons - Add a little more contrast to button edges - Fix alignment of the heart/time/score counters in portrait mode - Detect movement based on where the touch is relative to the level viewport, not the entire play area (oof)
This commit is contained in:
parent
1b6bd68879
commit
81c7f97d72
26
index.html
26
index.html
@ -10,7 +10,7 @@
|
|||||||
<meta name="og:image" content="https://c.eev.ee/lexys-labyrinth/og-preview.png">
|
<meta name="og:image" content="https://c.eev.ee/lexys-labyrinth/og-preview.png">
|
||||||
<meta name="og:title" content="Lexy's Labyrinth">
|
<meta name="og:title" content="Lexy's Labyrinth">
|
||||||
<meta name="og:description" content="A (work in progress) reimplementation of Chip's Challenge 1 and 2, using entirely free assets.">
|
<meta name="og:description" content="A (work in progress) reimplementation of Chip's Challenge 1 and 2, using entirely free assets.">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
</head>
|
</head>
|
||||||
<body data-mode="splash">
|
<body data-mode="splash">
|
||||||
<header id="header-main">
|
<header id="header-main">
|
||||||
@ -34,9 +34,13 @@
|
|||||||
<header id="header-level">
|
<header id="header-level">
|
||||||
<h3 id="level-name">Level 1 — Key Pyramid</h3>
|
<h3 id="level-name">Level 1 — Key Pyramid</h3>
|
||||||
<nav>
|
<nav>
|
||||||
<button id="main-prev-level" type="button">←</button>
|
<button id="main-prev-level" type="button">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16" title="previous"><path d="M14,1 2,8 14,14 z"></svg>
|
||||||
|
</button>
|
||||||
<button id="main-choose-level" type="button">Level select</button>
|
<button id="main-choose-level" type="button">Level select</button>
|
||||||
<button id="main-next-level" type="button">→</button>
|
<button id="main-next-level" type="button">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16" title="next"><path d="M2,1 14,8 2,14 z"></svg>
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main id="splash">
|
<main id="splash">
|
||||||
@ -117,10 +121,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="play-controls">
|
<div class="play-controls">
|
||||||
<button class="control-pause" type="button">Pause <span class="keyhint">(p)</span></button>
|
<button class="control-pause" type="button">
|
||||||
<button class="control-restart" type="button">Restart</button>
|
<svg class="svg-icon" viewBox="0 0 16 16" title="pause"><path d="M2,1 h4 v14 h-4 z M10,1 h4 v14 h-4 z"></svg>
|
||||||
<button class="control-undo" type="button">Undo</button>
|
<span class="keyhint">(p)</span></button>
|
||||||
<button class="control-rewind" type="button">Rewind <span class="keyhint">(z)</span></button>
|
<button class="control-restart" type="button">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16" title="restart"><path d="M13,13 A 7,7 270 1,1 13,3 L15,1 15,7 9,7 11,5 A 4,4 270 1,0 11,11 z"></svg>
|
||||||
|
</button>
|
||||||
|
<button class="control-undo" type="button">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16" title="undo"><path d="M6,5 6,2 1,7 6,12 6,9 A 10,10 60 0,1 15,12 A 10,10 90 0,0 6,5"></svg>
|
||||||
|
</button>
|
||||||
|
<button class="control-rewind" type="button">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16" title="rewind"><path d="M1,8 7,2 7,14 z M9,8 15,2 15,14 z"></svg>
|
||||||
|
<span class="keyhint">(z)</span></button>
|
||||||
<label><input class="control-turn-based" type="checkbox"> Turn-based mode</label>
|
<label><input class="control-turn-based" type="checkbox"> Turn-based mode</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo-controls">
|
<div class="demo-controls">
|
||||||
|
|||||||
54
js/main.js
54
js/main.js
@ -528,7 +528,7 @@ class Player extends PrimaryView {
|
|||||||
|
|
||||||
// Figure out where these touches are, relative to the game area
|
// Figure out where these touches are, relative to the game area
|
||||||
// TODO allow starting a level without moving?
|
// TODO allow starting a level without moving?
|
||||||
let rect = touch_target.getBoundingClientRect();
|
let rect = this.level_el.getBoundingClientRect();
|
||||||
for (let touch of ev.changedTouches) {
|
for (let touch of ev.changedTouches) {
|
||||||
// Normalize touch coordinates to [-1, 1]
|
// Normalize touch coordinates to [-1, 1]
|
||||||
let rx = (touch.clientX - rect.left) / rect.width * 2 - 1;
|
let rx = (touch.clientX - rect.left) / rect.width * 2 - 1;
|
||||||
@ -1243,10 +1243,13 @@ class Player extends PrimaryView {
|
|||||||
adjust_scale() {
|
adjust_scale() {
|
||||||
// TODO make this optional
|
// TODO make this optional
|
||||||
let style = window.getComputedStyle(this.root);
|
let style = window.getComputedStyle(this.root);
|
||||||
|
// If we're not visible, no layout information is available and this is impossible
|
||||||
|
if (style['display'] === 'none')
|
||||||
|
return;
|
||||||
|
|
||||||
let is_portrait = !! style.getPropertyValue('--is-portrait');
|
let is_portrait = !! style.getPropertyValue('--is-portrait');
|
||||||
// The base size is the size of the canvas, i.e. the viewport size times the tile size --
|
// The base size is the size of the canvas, i.e. the viewport size times the tile size --
|
||||||
// but note that we have 2x4 extra tiles for the inventory depending on layout
|
// but note that we have 2x4 extra tiles for the inventory depending on layout
|
||||||
// TODO if there's ever a portrait view for phones, this will need adjusting
|
|
||||||
let base_x, base_y;
|
let base_x, base_y;
|
||||||
if (is_portrait) {
|
if (is_portrait) {
|
||||||
base_x = this.conductor.tileset.size_x * this.renderer.viewport_size_x;
|
base_x = this.conductor.tileset.size_x * this.renderer.viewport_size_x;
|
||||||
@ -1256,24 +1259,43 @@ class Player extends PrimaryView {
|
|||||||
base_x = this.conductor.tileset.size_x * (this.renderer.viewport_size_x + 4);
|
base_x = this.conductor.tileset.size_x * (this.renderer.viewport_size_x + 4);
|
||||||
base_y = this.conductor.tileset.size_y * this.renderer.viewport_size_y;
|
base_y = this.conductor.tileset.size_y * this.renderer.viewport_size_y;
|
||||||
}
|
}
|
||||||
// The main UI is centered in a flex item with auto margins, so the extra space available is
|
// Unfortunately, finding the available space is a little tricky. The container is a CSS
|
||||||
// the size of those margins (which naturally discounts the size of the buttons and music
|
// flex item, and the flex cell doesn't correspond directly to any element, so there's no
|
||||||
// title and whatnot, so those automatically reserve their own space)
|
// way for us to query its size directly. We also have various stuff up top and down below
|
||||||
if (style['display'] === 'none') {
|
// that shouldn't count as available space. So instead we take a rough guess by adding up:
|
||||||
// the computed margins can be 'auto' in this case
|
// - the space currently taken up by the canvas
|
||||||
return;
|
let avail_x = this.renderer.canvas.offsetWidth;
|
||||||
|
let avail_y = this.renderer.canvas.offsetHeight;
|
||||||
|
// - the space currently taken up by the inventory, depending on orientation
|
||||||
|
if (is_portrait) {
|
||||||
|
avail_y += this.inventory_el.offsetHeight;
|
||||||
}
|
}
|
||||||
let extra_x = parseFloat(style['margin-left']) + parseFloat(style['margin-right']);
|
else {
|
||||||
let extra_y = parseFloat(style['margin-top']) + parseFloat(style['margin-bottom']);
|
avail_x += this.inventory_el.offsetWidth;
|
||||||
// The total available space, then, is the current size of the canvas (and inventory, when
|
}
|
||||||
// appropriate) plus the size of the margins
|
// - the difference between the size of the play area and the size of our root (which will
|
||||||
let total_x = extra_x + this.renderer.canvas.offsetWidth + this.inventory_el.offsetWidth;
|
// add in any gap around the player, e.g. if the controls stretch the root to be wider)
|
||||||
let total_y = extra_y + this.renderer.canvas.offsetHeight;
|
let root_rect = this.root.getBoundingClientRect();
|
||||||
|
let player_rect = this.root.querySelector('.-main-area').getBoundingClientRect();
|
||||||
|
avail_x += root_rect.width - player_rect.width;
|
||||||
|
avail_y += root_rect.height - player_rect.height;
|
||||||
|
// - the margins around our root, which consume all the extra space
|
||||||
|
let margin_x = parseFloat(style['margin-left']) + parseFloat(style['margin-right']);
|
||||||
|
let margin_y = parseFloat(style['margin-top']) + parseFloat(style['margin-bottom']);
|
||||||
|
avail_x += margin_x;
|
||||||
|
avail_y += margin_y;
|
||||||
|
// If those margins are zero, by the way, we risk being too big for the viewport already,
|
||||||
|
// and we need to subtract any extra scroll on the body
|
||||||
|
if (margin_x === 0 || margin_y === 0) {
|
||||||
|
avail_x -= document.body.scrollWidth - document.body.clientWidth;
|
||||||
|
avail_y -= document.body.scrollHeight - document.body.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
let dpr = window.devicePixelRatio || 1.0;
|
let dpr = window.devicePixelRatio || 1.0;
|
||||||
// Divide to find the biggest scale that still fits. But don't exceed 90% of the available
|
// Divide to find the biggest scale that still fits. But don't exceed 90% of the available
|
||||||
// space, or it'll feel cramped (except on small screens, where being too small HURTS).
|
// space, or it'll feel cramped (except on small screens, where being too small HURTS).
|
||||||
let maxfrac = total_x < 800 ? 1 : 0.9;
|
let maxfrac = is_portrait ? 1 : 0.9;
|
||||||
let scale = Math.floor(maxfrac * dpr * Math.min(total_x / base_x, total_y / base_y));
|
let scale = Math.floor(maxfrac * dpr * Math.min(avail_x / base_x, avail_y / base_y));
|
||||||
if (scale <= 1) {
|
if (scale <= 1) {
|
||||||
scale = 1;
|
scale = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
38
style.css
38
style.css
@ -37,7 +37,8 @@ button {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: white;
|
color: white;
|
||||||
background: var(--button-bg-color);
|
background: var(--button-bg-color);
|
||||||
border: none;
|
border: 1px solid hsl(225, 10%, 15%);
|
||||||
|
box-shadow: 0 1px 0 hsl(225, 10%, 10%);
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -45,6 +46,10 @@ button {
|
|||||||
button:hover {
|
button:hover {
|
||||||
background: var(--button-bg-hover-color);
|
background: var(--button-bg-hover-color);
|
||||||
}
|
}
|
||||||
|
button:active {
|
||||||
|
box-shadow: none;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
button:disabled {
|
button:disabled {
|
||||||
color: #606060;
|
color: #606060;
|
||||||
background: #202020;
|
background: #202020;
|
||||||
@ -110,6 +115,15 @@ a:active {
|
|||||||
color: hsl(0, 50%, 60%);
|
color: hsl(0, 50%, 60%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg.svg-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
stroke: none;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
/* Overlay styling */
|
/* Overlay styling */
|
||||||
.overlay {
|
.overlay {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -269,6 +283,13 @@ label.option .option-label {
|
|||||||
/* TODO */
|
/* TODO */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.dialog {
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Bits and pieces */
|
/* Bits and pieces */
|
||||||
img.compat-icon {
|
img.compat-icon {
|
||||||
margin: 0 0.25em 0.125em;
|
margin: 0 0.25em 0.125em;
|
||||||
@ -687,7 +708,7 @@ dl.score-chart .-sum {
|
|||||||
|
|
||||||
background-size: calc(var(--tile-width) * var(--scale)) calc(var(--tile-height) * var(--scale));
|
background-size: calc(var(--tile-width) * var(--scale)) calc(var(--tile-height) * var(--scale));
|
||||||
width: calc(4 * var(--tile-width) * var(--scale));
|
width: calc(4 * var(--tile-width) * var(--scale));
|
||||||
min-height: calc(2 * var(--tile-height) * var(--scale));
|
height: calc(2 * var(--tile-height) * var(--scale));
|
||||||
}
|
}
|
||||||
#player .inventory img {
|
#player .inventory img {
|
||||||
width: calc(var(--tile-width) * var(--scale));
|
width: calc(var(--tile-width) * var(--scale));
|
||||||
@ -738,6 +759,7 @@ dl.score-chart .-sum {
|
|||||||
.play-controls,
|
.play-controls,
|
||||||
.demo-controls {
|
.demo-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
}
|
}
|
||||||
.play-controls {
|
.play-controls {
|
||||||
@ -802,10 +824,10 @@ main.--has-demo .demo-controls {
|
|||||||
/* Rearrange the grid to be vertical */
|
/* Rearrange the grid to be vertical */
|
||||||
grid:
|
grid:
|
||||||
"level level"
|
"level level"
|
||||||
"chips inventory"
|
"chips inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
|
||||||
"time inventory"
|
"time inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
|
||||||
"bonus inventory"
|
"bonus inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
|
||||||
/ min-content min-content
|
/ 1fr min-content
|
||||||
;
|
;
|
||||||
row-gap: 0.5em;
|
row-gap: 0.5em;
|
||||||
column-gap: 1em;
|
column-gap: 1em;
|
||||||
@ -823,6 +845,10 @@ main.--has-demo .demo-controls {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
font-size: calc(var(--tile-height) * var(--scale) / 2.5);
|
font-size: calc(var(--tile-height) * var(--scale) / 2.5);
|
||||||
}
|
}
|
||||||
|
#player .controls .keyhint {
|
||||||
|
/* Hide key hints, they take up surprisingly a lot of space */
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
#player-music {
|
#player-music {
|
||||||
/* Stack the title/artist on the volume, since they don't fit well side by side */
|
/* Stack the title/artist on the volume, since they don't fit well side by side */
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user