diff --git a/index.html b/index.html index 4b1fdf3..239f671 100644 --- a/index.html +++ b/index.html @@ -136,34 +136,34 @@
-
-
- - - - - -
-
- - - +
+ + + + +
+
+
+ + + +
diff --git a/js/main.js b/js/main.js index 20901d2..dee440e 100644 --- a/js/main.js +++ b/js/main.js @@ -414,7 +414,7 @@ class Player extends PrimaryView { // 1: turn-based mode, at the start of a tic // 2: turn-based mode, in mid-tic, with the game frozen waiting for input this.turn_mode = 0; - this.turn_based_checkbox = this.root.querySelector('.controls .control-turn-based'); + this.turn_based_checkbox = this.root.querySelector('.control-turn-based'); this.turn_based_checkbox.checked = false; this.turn_based_checkbox.addEventListener('change', ev => { if (this.turn_based_checkbox.checked) { @@ -432,19 +432,19 @@ class Player extends PrimaryView { }); // Bind buttons - this.pause_button = this.root.querySelector('.controls .control-pause'); + this.pause_button = this.root.querySelector('.control-pause'); this.pause_button.addEventListener('click', ev => { this.toggle_pause(); ev.target.blur(); }); - this.restart_button = this.root.querySelector('.controls .control-restart'); + this.restart_button = this.root.querySelector('.control-restart'); this.restart_button.addEventListener('click', ev => { new ConfirmOverlay(this.conductor, "Abandon this attempt and try again?", () => { this.restart_level(); }).open(); ev.target.blur(); }); - this.undo_button = this.root.querySelector('.controls .control-undo'); + this.undo_button = this.root.querySelector('.control-undo'); this.undo_button.addEventListener('click', ev => { let player_cell = this.level.player.cell; // Keep undoing until (a) we're on another cell and (b) we're not sliding, i.e. we're @@ -469,7 +469,7 @@ class Player extends PrimaryView { this._redraw(); ev.target.blur(); }); - this.rewind_button = this.root.querySelector('.controls .control-rewind'); + this.rewind_button = this.root.querySelector('.control-rewind'); this.rewind_button.addEventListener('click', ev => { if (this.state === 'rewinding') { this.set_state('playing'); @@ -480,19 +480,19 @@ class Player extends PrimaryView { }); // Game actions // TODO do these need buttons?? feel like they're not discoverable otherwise - this.drop_button = this.root.querySelector('.actions .action-drop'); + this.drop_button = this.root.querySelector('#player-actions .action-drop'); this.drop_button.addEventListener('click', ev => { // Use the set of "buttons pressed between tics" because it's cleared automatically; // otherwise these will stick around forever this.current_keys_new.add('q'); ev.target.blur(); }); - this.cycle_button = this.root.querySelector('.actions .action-cycle'); + this.cycle_button = this.root.querySelector('#player-actions .action-cycle'); this.cycle_button.addEventListener('click', ev => { this.current_keys_new.add('e'); ev.target.blur(); }); - this.swap_button = this.root.querySelector('.actions .action-swap'); + this.swap_button = this.root.querySelector('#player-actions .action-swap'); this.swap_button.addEventListener('click', ev => { this.current_keys_new.add('c'); ev.target.blur(); @@ -1878,58 +1878,41 @@ class Player extends PrimaryView { if (style['display'] === 'none') return; - let is_portrait = !! style.getPropertyValue('--is-portrait'); + let is_portrait = window.matchMedia('(orientation: portrait)').matches; // 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, plus half a + // tile's worth of padding around the game area, plus a quarter tile spacing let base_x, base_y; if (is_portrait) { - base_x = this.renderer.tileset.size_x * this.renderer.viewport_size_x; - base_y = this.renderer.tileset.size_y * (this.renderer.viewport_size_y + 2); + base_x = this.renderer.tileset.size_x * (this.renderer.viewport_size_x + 0.5); + base_y = this.renderer.tileset.size_y * (this.renderer.viewport_size_y + 2.75); } else { - base_x = this.renderer.tileset.size_x * (this.renderer.viewport_size_x + 4); - base_y = this.renderer.tileset.size_y * this.renderer.viewport_size_y; + base_x = this.renderer.tileset.size_x * (this.renderer.viewport_size_x + 4.75); + base_y = this.renderer.tileset.size_y * (this.renderer.viewport_size_y + 0.5); } - // Unfortunately, finding the available space is a little tricky. The container is a CSS - // flex item, and the flex cell doesn't correspond directly to any element, so there's no - // way for us to query its size directly. We also have various stuff up top and down below - // that shouldn't count as available space. So instead we take a rough guess by adding up: - // - the space currently taken up by the canvas - 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; - } - else { - avail_x += this.inventory_el.offsetWidth; - } - // - the difference between the size of the play area and the size of our root (which will - // add in any gap around the player, e.g. if the controls stretch the root to be wider) - let root_rect = this.root.getBoundingClientRect(); - let player_rect = this.root.querySelector('#player-game-area').getBoundingClientRect(); - avail_x += root_rect.width - player_rect.width; - avail_y += root_rect.height - player_rect.height; + // The element hierarchy is: the root is a wrapper that takes up the entire flex cell; + // within that is the main player element which contains everything; and within that is the + // game area which is the part we can scale. The available space is the size of the root, + // but minus the size of the controls and whatnot placed around it, which are the difference + // between the player container and the game area + let player = this.root.querySelector('#player-main'); + let game_area = this.root.querySelector('#player-game-area'); + let avail_x = this.root.offsetWidth - (player.offsetWidth - game_area.offsetWidth); + let avail_y = this.root.offsetHeight - (player.offsetHeight - game_area.offsetHeight); // ...minus the width of the debug panel, if visible if (this.debug.enabled) { avail_x -= this.root.querySelector('#player-debug').getBoundingClientRect().width; } - // - 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; - } + // If there's already a scrollbar, the extra scrolled space is unavailable + avail_x -= Math.max(0, document.body.scrollWidth - document.body.clientWidth); + avail_y -= Math.max(0, document.body.scrollHeight - document.body.clientHeight); let dpr = window.devicePixelRatio || 1.0; - // 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). - let maxfrac = is_portrait ? 1 : 0.9; + // Divide to find the biggest scale that still fits. Leave a LITTLE wiggle room for pixel + // rounding and breathing (except on small screens, where being too small REALLY hurts), but + // not too much since there's already a flex gap between the game and header/footer + let maxfrac = is_portrait ? 1 : 0.99; let scale = Math.floor(maxfrac * dpr * Math.min(avail_x / base_x, avail_y / base_y)); if (scale <= 1) { scale = 1; @@ -3534,9 +3517,9 @@ class Conductor { if (this.current) { this.current.deactivate(); } - this.splash.activate(); this.current = this.splash; document.body.setAttribute('data-mode', 'splash'); + this.splash.activate(); } switch_to_editor() { @@ -3547,9 +3530,9 @@ class Conductor { this.editor.load_level(this.stored_level); this.loaded_in_editor = true; } - this.editor.activate(); this.current = this.editor; document.body.setAttribute('data-mode', 'editor'); + this.editor.activate(); } switch_to_player() { @@ -3560,9 +3543,11 @@ class Conductor { this.player.load_level(this.stored_level); this.loaded_in_player = true; } - this.player.activate(); this.current = this.player; document.body.setAttribute('data-mode', 'player'); + // Activate last, so any DOM inspection (ahem, auto-scaling) already sees the effects of + // data-mode revealing the header + this.player.activate(); } reload_all_options() { diff --git a/style.css b/style.css index d35af06..c86b6a9 100644 --- a/style.css +++ b/style.css @@ -5,6 +5,7 @@ html { body { height: 100%; margin: 0; + box-sizing: border-box; display: flex; flex-direction: column; @@ -201,8 +202,8 @@ svg.svg-icon { .radio-faux-button-set > label > input:checked + span { background: hsl(225, 80%, 50%); box-shadow: - inset 0 1px 0 1px hsl(225, 10%, 20%), - inset 0 -0em 2em 0.5em hsl(225, 50%, 30%), + inset 0 0 1px 1px hsl(225, 50%, 40%), + inset 0 -0.125em 0.5em 0.25em hsl(225, 50%, 30%), 0 1px 1px hsl(225, 10%, 10%); } @@ -433,7 +434,7 @@ body > header { align-items: center; gap: 0.5em; - padding: 0.5em; + padding: 0.25em 0.5em; line-height: 1.125; } body > header h1 { @@ -453,6 +454,7 @@ body > header > nav { } body > header button { font-size: 0.75em; + white-space: nowrap; } body > header h1 a { color: inherit !important; @@ -899,15 +901,149 @@ ol.packtest-summary > li { * debug panel can use that to sit against the right edge; absolute positioning excludes * margins, so if it were positioned as a child of THIS element, it would be stuffed into the * game area (oops!) */ - display: flex; - flex-direction: column; + /* It does also make auto-sizing easier! */ + /* Default to a landscape layout, with the buttons on the left */ + display: grid; + grid: + "buttons game actions" + "buttons game actions" + ". music music" + / 1fr auto 1fr + ; justify-content: stretch; gap: 0.5em; margin: auto; /* center in both directions baby */ } +#player-controls, +#player-actions { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 0.25em; +} +#player-controls { + grid-area: buttons; + align-self: start; +} +#player-controls button, +#player-actions button svg { + font-size: 1.5em; +} +#player-controls button { + padding: 0.5em; + line-height: 1; +} +#player-controls .radio-faux-button-set { + margin: 0; +} +#player-actions { + grid-area: actions; + align-self: end; +} +#player-actions button { + text-align: center; +} +#player-actions button svg { + display: block; + margin: 0.125em auto; +} +#player-music { + grid-area: music; + margin: 0 calc(var(--tile-width) * var(--scale) / 4); + text-transform: lowercase; + color: #909090; +} +/* Key hints are placed on the sides */ +#player button { + position: relative; +} +#player button .keyhint { + font-size: 1rem; + position: absolute; + top: 1.25em; + margin: auto; + color: #404040; +} +#player-controls button .keyhint { + left: -2em; +} +#player-actions button .keyhint { + right: -2em; +} +#player button:disabled .keyhint { + display: none; +} +@media (orientation: portrait) { + /* On a portrait screen, put the controls on top */ + #player-main { + grid: + "buttons actions" + "game game" + "music music" + ; + } + #player-controls, + #player-actions { + flex-direction: row; + white-space: nowrap; + } + #player-controls button, + #player-actions button svg { + font-size: 1em; + } + #player-controls button { + padding: 0.25em 0.5em; + line-height: 1.33; + } + /* Hackily remove the
s in "turn based mode" */ + #player-controls .radio-faux-button-set br { + display: none; + } + #player-actions { + justify-content: end; + } + #player-actions button svg { + display: inline-block; + margin: 0.125em; + } + #player button .keyhint { + top: -2em; + left: 0; + right: 0; + } +} +@media (orientation: portrait) and (max-width: 800px) { + /* On a /small/ portrait screen, also put the controls in two rows */ + #player-main { + grid: + "buttons" + "actions" + "game" + "music" + ; + } + #player-controls, + #player-actions { + justify-content: center; + } + #player .keyhint { + /* Hide key hints; there's nowhere to put them and they take up surprisingly a lot of space */ + display: none; + } +} +@media (max-width: 800px) { + #player-music { + font-size: 0.875em; + } +} + #player-game-area { + grid-area: game; + /* don't stretch if the buttons or music blow out somehow */ + justify-self: center; + align-self: center; + isolation: isolate; - align-self: center; /* don't stretch if the buttons or music blow out somehow */ display: grid; align-items: center; grid: @@ -919,22 +1055,19 @@ ol.packtest-summary > li { /* Need explicit min-content to force the hint to wrap */ / min-content min-content ; - column-gap: 2em; - row-gap: 0.5em; + column-gap: calc(var(--tile-width) * var(--scale) / 4); + row-gap: calc(var(--tile-height) * var(--scale) / 4); - padding: 1em; + padding: calc(var(--tile-height) * var(--scale) / 4) calc(var(--tile-width) * var(--scale) / 4); background: hsl(225, 10%, 20%); box-shadow: 0 0.25em 1em black; } -#player > .controls { - order: -1; -} .level { grid-area: level; position: relative; - border: 2px solid black; + outline: 2px solid black; } .level canvas { display: block; @@ -956,8 +1089,6 @@ ol.packtest-summary > li { height: 0; min-height: 100%; box-sizing: border-box; - /* Copy the canvas's border width too */ - border: 2px solid transparent; z-index: 2; font-size: calc(0.5 * var(--tile-width) * var(--scale)); @@ -1080,7 +1211,7 @@ dl.score-chart .-sum { .time h3, .bonus h3 { flex: 1; - font-size: 1.25em; + font-size: 1.5em; line-height: 1; color: hsl(225, 20%, 90%); } @@ -1148,6 +1279,8 @@ dl.score-chart .-sum { * parent happens to be. Magic! */ height: 0; min-height: 100%; + width: 0; + min-width: 100%; } #player-game-area > .player-hint-wrapper.--visible { display: initial; @@ -1185,50 +1318,29 @@ dl.score-chart .-sum { background: #0009; color: white; } -#player .actions { - display: flex; - gap: 0.5em; -} -#player .actions button { - flex: 1; - white-space: nowrap; -} -#player-music { - grid-area: music; - margin: 0 1em; - text-transform: lowercase; - color: #909090; -} - -#player .controls { - grid-area: controls; - display: flex; - gap: 0.25em; - justify-content: space-between; -} -#player button { - position: relative; -} -#player button .keyhint { - position: absolute; - left: 0; - right: 0; - top: -2em; - margin: auto; - color: #404040; -} -#player button:disabled .keyhint { - display: none; -} - -.play-controls { - display: flex; - align-items: center; - gap: 0.25em; -} -.play-controls { - align-self: start; +@media (orientation: portrait) { + #player-game-area { + /* Rearrange the grid to be vertical */ + grid: + "level level" + "chips inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3) + "time inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3) + "bonus inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3) + / 1fr min-content + ; + } + #player .inventory { + /* stick me in the center right */ + place-self: center end; + } + #player-game-area > .player-hint-wrapper { + /* Overlay hints on the inventory area */ + grid-row: chips / bonus; + grid-column: level; + z-index: 1; + font-size: calc(var(--tile-height) * var(--scale) / 2.5); + } } /* Debug stuff */ @@ -1413,56 +1525,6 @@ body.--debug #player-debug { } -@media (max-width: 800px) { - #player { - /* sentinel for js */ - --is-portrait: 1; - /* The play area isn't necessarily the biggest thing any more, and it's ugly when stretched */ - align-items: center; - } - #player .controls { - flex-direction: column; - } - #player-game-area { - /* Rearrange the grid to be vertical */ - grid: - "level level" - "chips inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3) - "time inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3) - "bonus inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3) - /* FIXME ideally the first column would be 1fr so the hearts/time have space, but that - * allows hints to grow to the entire width of the window, which incredibly sucks. i - * don't know how to get around this except by giving the grid a fixed width, which i - * guess wouldn't be that hard */ - / min-content min-content - ; - row-gap: 0.5em; - column-gap: 1em; - - padding: 0.5em; - } - #player .inventory { - /* stick me in the center right */ - place-self: center end; - } - #player-game-area > .player-hint-wrapper { - /* Overlay hints on the inventory area */ - grid-row: chips / bonus; - grid-column: level; - z-index: 1; - font-size: calc(var(--tile-height) * var(--scale) / 2.5); - } - #player .keyhint { - /* Hide key hints, they take up surprisingly a lot of space */ - display: none; - } - #player-music { - /* Stack the title/artist on the volume, since they don't fit well side by side */ - font-size: 0.875em; - } -} - - /**************************************************************************************************/ /* Editor */