cannons: run the demo behind the Play button
The page opened on a dark rectangle. It now opens on a battle already in progress, with Play over it -- attract mode, as cannons_reloaded 76b32ca added it. One iframe, not two: it starts on the demo and the click swaps it for the game. The demo is the client alone -- no socket, no WebAssembly server -- so the click still buys the payload the old placeholder was there to defer; the bar was never the client at ~360KB, it was the 3MB server. Two frame heights, because the demo and the game are not the same shape: attract mode shows GameAreaPanel alone at 760x400 and the game shows the whole applet at 770x540. Sized to the demo until Play is pressed, or the battle sits letterboxed between two black bars.
This commit is contained in:
@@ -32,3 +32,9 @@ export function loadCannons() {
|
||||
// $uri/index.html` would then have a file and a directory competing for the
|
||||
// same URL. Exactly why the notebook exports are at /nb/ and not /notebooks/.
|
||||
export const cannonsFrameUrl = '/cn/embedded.html';
|
||||
|
||||
// Attract mode: the same page, told to play scripted scenes instead of
|
||||
// connecting. It costs the client (classes.js) and nothing else -- no
|
||||
// WebSocket, and crucially not the WebAssembly server, which is an order of
|
||||
// magnitude larger and still waits for the click.
|
||||
export const cannonsDemoUrl = '/cn/embedded.html?demo=1';
|
||||
|
||||
+49
-23
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { loadCannons, cannonsFrameUrl } from '../lib/cannons.mjs';
|
||||
import { loadCannons, cannonsFrameUrl, cannonsDemoUrl } from '../lib/cannons.mjs';
|
||||
|
||||
const build = loadCannons();
|
||||
const mb = (n) => (n / 1048576).toFixed(1);
|
||||
@@ -46,9 +46,27 @@ const overTheWire = build ? mb(build.wireBytes) : null;
|
||||
computer opponent takes the other seat.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The battle below the button is a demo, and it has no server at all: the
|
||||
client’s packet handlers are ordinary methods, so a scene is just a script
|
||||
of the packets a real match would have sent, fed straight in. Which is why
|
||||
it can show you eight cannons opening with ICBMs at once — a thing no
|
||||
actual match can produce, because the computer opponents cannot aim a
|
||||
guided weapon.
|
||||
</p>
|
||||
|
||||
{
|
||||
build ? (
|
||||
<div class="cn-wrap" data-src={cannonsFrameUrl}>
|
||||
{/* Rendered here rather than created in the script, so Astro's scoped
|
||||
styles actually reach it -- see the note on .cn-frame below. It
|
||||
starts on the demo, which needs the client but no server. */}
|
||||
<iframe
|
||||
class="cn-frame"
|
||||
title="Cannons"
|
||||
allow="autoplay"
|
||||
src={cannonsDemoUrl}
|
||||
/>
|
||||
<button class="cn-start" type="button">
|
||||
<span class="cn-start-label">Play</span>
|
||||
<span class="cn-start-sub">
|
||||
@@ -56,10 +74,6 @@ const overTheWire = build ? mb(build.wireBytes) : null;
|
||||
the game's own art, compressed
|
||||
</span>
|
||||
</button>
|
||||
{/* Rendered here rather than created in the script, so Astro's scoped
|
||||
styles actually reach it -- see the note on .cn-frame below. With
|
||||
no src it loads nothing, so the payload still waits for the click. */}
|
||||
<iframe class="cn-frame" title="Cannons" allow="autoplay"></iframe>
|
||||
</div>
|
||||
) : (
|
||||
<p class="cn-missing">
|
||||
@@ -78,10 +92,10 @@ const overTheWire = build ? mb(build.wireBytes) : null;
|
||||
</Base>
|
||||
|
||||
<script>
|
||||
// Click-to-load rather than <iframe loading="lazy">: lazy only defers until
|
||||
// the frame nears the viewport, and this frame is above the fold, so the
|
||||
// whole payload would download for anyone who opened the page. Several MB
|
||||
// is a lot to spend on a visitor who came here to read the paragraph.
|
||||
// The frame is already showing the demo, which is the client alone. The
|
||||
// click swaps it for the real game, and THAT is what fetches the
|
||||
// WebAssembly server -- most of the payload by a wide margin, and still not
|
||||
// spent on a visitor who came here to read the paragraph.
|
||||
const wrap = document.querySelector('.cn-wrap');
|
||||
const button = wrap?.querySelector('.cn-start');
|
||||
const frame = wrap?.querySelector('.cn-frame');
|
||||
@@ -97,6 +111,9 @@ const overTheWire = build ? mb(build.wireBytes) : null;
|
||||
<style>
|
||||
.cn-wrap {
|
||||
margin: 1.5rem 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 770px;
|
||||
}
|
||||
/* 770x540 is the applet's own size; the frame is not responsive because the
|
||||
game's layout is fixed pixels and scaling it would blur the terrain.
|
||||
@@ -108,17 +125,35 @@ const overTheWire = build ? mb(build.wireBytes) : null;
|
||||
the HTML default of 300x150. That is exactly what shipped on 2026-08-28.
|
||||
Hence the iframe living in the template with the script only setting its
|
||||
src. */
|
||||
.cn-start,
|
||||
/* Two heights, because the demo and the game are not the same shape: attract
|
||||
mode shows GameAreaPanel alone (760x400) and the game shows the whole
|
||||
applet (770x540). Sized to the demo until Play is pressed, or the battle
|
||||
sits in a letterbox with a black bar above and below it. */
|
||||
.cn-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 770px;
|
||||
height: 540px;
|
||||
height: 402px;
|
||||
border: 1px solid var(--rule, #2a2f3d);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.cn-start {
|
||||
background: #0b0d12;
|
||||
}
|
||||
.cn-wrap.is-playing .cn-frame {
|
||||
height: 540px;
|
||||
}
|
||||
/* Over the demo rather than instead of it: the battle is the backdrop and
|
||||
this is the thing you press to take a seat in it. Translucent so the
|
||||
scene reads through, and covering the whole frame so a click anywhere
|
||||
starts the game rather than landing in a demo that ignores it. */
|
||||
.cn-start {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(11, 13, 18, 0.55);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
@@ -138,18 +173,9 @@ const overTheWire = build ? mb(build.wireBytes) : null;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
/* Not the `hidden` attribute: `display: block` above outranks the user
|
||||
agent's `[hidden] { display: none }`, so the frame would show anyway. */
|
||||
.cn-frame {
|
||||
background: #0b0d12;
|
||||
display: none;
|
||||
}
|
||||
.cn-wrap.is-playing .cn-start {
|
||||
display: none;
|
||||
}
|
||||
.cn-wrap.is-playing .cn-frame {
|
||||
display: block;
|
||||
}
|
||||
.cn-note,
|
||||
.cn-missing {
|
||||
font-size: 0.8rem;
|
||||
|
||||
Reference in New Issue
Block a user