diff --git a/challenges/birdsbirdbirds/challenge.json b/challenges/birdsbirdbirds/challenge.json
index 303414a..2d4703d 100644
--- a/challenges/birdsbirdbirds/challenge.json
+++ b/challenges/birdsbirdbirds/challenge.json
@@ -19,7 +19,7 @@
{
"id": "draw-bird",
"title": "Draw your bird",
- "hint": "Draw a bird on the page and press Save.",
+ "hint": "Draw a bird on the page and press Save my bird.",
"done_when": "The file bird.png exists in the workspace."
},
{
diff --git a/challenges/birdsbirdbirds/page/app.mjs b/challenges/birdsbirdbirds/page/app.mjs
index 05040db..df8dc41 100644
--- a/challenges/birdsbirdbirds/page/app.mjs
+++ b/challenges/birdsbirdbirds/page/app.mjs
@@ -42,6 +42,11 @@ const toSkyButton = document.getElementById('to-sky-button')
const drawAgainButton = document.getElementById('draw-again-button')
const sky = document.getElementById('sky')
+// The starting prompt is written once, in index.html, and read back here.
+const INITIAL_PROMPT = statusLine.textContent
+const DRAWN_PROMPT = 'Looking good! Press Save my bird when it’s done.'
+const EMPTY_CANVAS_ERROR = 'The canvas is empty. Draw your bird first!'
+
const pen = { colour: COLOURS[0].value, width: PEN_SIZES[1].width, erasing: false }
const stroke = { pointerId: undefined, lastPoint: undefined }
const undoSnapshots = []
@@ -191,6 +196,17 @@ function endStroke(event) {
stroke.pointerId = undefined
stroke.lastPoint = undefined
+ promptToSave()
+}
+
+/**
+ * Swaps the initial prompt, or the empty-canvas error, for a nudge to save
+ * once there is a drawing. Any other message (Undone, Cleared, Saved, a save
+ * failure) is left alone, since it reports something the attendee just did.
+ */
+function promptToSave() {
+ const showsPreDrawingMessage = statusLine.textContent === INITIAL_PROMPT || statusLine.textContent === EMPTY_CANVAS_ERROR
+ if (showsPreDrawingMessage && !isCanvasBlank()) setStatus(DRAWN_PROMPT)
}
/** Clears the canvas to transparent, undoably. */
@@ -219,7 +235,7 @@ function canvasToPng() {
/** Saves the drawing to the workspace as bird.png and reports how it went. */
async function saveBird() {
- if (isCanvasBlank()) return setStatus('The canvas is empty. Draw your bird first!', 'error')
+ if (isCanvasBlank()) return setStatus(EMPTY_CANVAS_ERROR, 'error')
saveButton.disabled = true
setStatus('Saving…')
@@ -281,6 +297,7 @@ function showDraw() {
skyMode.hidden = true
drawMode.hidden = false
sky.replaceChildren()
+ setStatus(isCanvasBlank() ? INITIAL_PROMPT : DRAWN_PROMPT)
toSkyButton.hidden = flight.entry === undefined
}
diff --git a/challenges/birdsbirdbirds/page/index.html b/challenges/birdsbirdbirds/page/index.html
index 4217111..43853c2 100644
--- a/challenges/birdsbirdbirds/page/index.html
+++ b/challenges/birdsbirdbirds/page/index.html
@@ -18,28 +18,37 @@
Draw your bird
-
-
+
+
+
-
+
-
diff --git a/challenges/birdsbirdbirds/page/style.css b/challenges/birdsbirdbirds/page/style.css
index 4d8a632..77b2aae 100644
--- a/challenges/birdsbirdbirds/page/style.css
+++ b/challenges/birdsbirdbirds/page/style.css
@@ -44,39 +44,51 @@ button:disabled {
opacity: 0.45;
}
+/* The booth station's Chrome is about 1000x597, so the whole drawing page,
+ Save button included, must fit one screen. The floor keeps shorter or
+ narrower windows usable by letting the page scroll instead of squashing. */
#draw-mode {
- max-width: 1000px;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+ max-width: 1280px;
+ height: max(100dvh, 560px);
margin: 0 auto;
- padding: 24px 20px 40px;
+ padding: 10px 20px 14px;
}
.draw-header h1 {
- margin: 0 0 6px;
- font-size: 2.4rem;
+ margin: 0 0 2px;
+ font-size: 1.7rem;
+ line-height: 1.15;
}
.lead {
- margin: 0 0 20px;
- font-size: 1.15rem;
+ margin: 0 0 8px;
+ font-size: 1rem;
}
.draw-layout {
display: flex;
- flex-wrap: wrap;
gap: 24px;
- align-items: flex-start;
+ min-height: 0;
+}
+
+.side-column {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ flex: 0 0 280px;
}
.toolbar {
display: flex;
flex-direction: column;
- gap: 16px;
- flex: 0 0 220px;
+ gap: 8px;
}
.tool-group {
margin: 0;
- padding: 12px;
+ padding: 4px 8px 8px;
border: 2px solid var(--slide-text);
border-radius: 14px;
background: #fff;
@@ -91,28 +103,29 @@ button:disabled {
.sizes,
.tools {
display: flex;
- flex-wrap: wrap;
- gap: 8px;
+ gap: 4px;
}
.swatch {
- width: 52px;
- height: 52px;
+ flex: 1;
+ min-width: 0;
+ aspect-ratio: 1;
+ max-height: 44px;
border: 3px solid #fff;
border-radius: 50%;
box-shadow: 0 0 0 2px var(--slide-text);
}
.swatch[aria-pressed="true"] {
- box-shadow: 0 0 0 4px var(--slide-text), 0 0 0 7px var(--slide-lime);
+ box-shadow: 0 0 0 3px var(--slide-text), 0 0 0 5px var(--slide-lime);
}
.size {
display: flex;
+ flex: 1;
align-items: center;
justify-content: center;
- width: 56px;
- height: 56px;
+ height: 44px;
border: 2px solid var(--slide-text);
border-radius: 12px;
background: #fff;
@@ -132,12 +145,12 @@ button:disabled {
.tool-button,
.action-button {
flex: 1;
- min-height: 52px;
- padding: 8px 14px;
+ min-height: 44px;
+ padding: 4px 14px;
border: 2px solid var(--slide-text);
border-radius: 12px;
background: #fff;
- font-size: 1.1rem;
+ font-size: 1.05rem;
font-weight: 700;
}
@@ -146,16 +159,27 @@ button:disabled {
gap: 8px;
}
-.canvas-column {
+/* Beside the tools, so Save is never below the fold. */
+.save-area {
display: flex;
flex-direction: column;
- align-items: center;
- gap: 14px;
- flex: 1 1 512px;
+ gap: 6px;
+ align-items: stretch;
+}
+
+/* Sized from the space left over, so the square canvas never overflows. */
+.canvas-column {
+ display: flex;
+ flex: 1;
+ align-items: flex-start;
+ justify-content: center;
+ min-width: 0;
+ container-type: size;
}
.canvas-frame {
- width: min(512px, 100%);
+ /* Room for the frame's 6px offset shadow. */
+ width: min(100cqw - 6px, 100cqh - 6px, 640px);
aspect-ratio: 1;
border: 3px solid var(--slide-text);
border-radius: 16px;
@@ -168,6 +192,29 @@ button:disabled {
background-size: 32px 32px;
}
+/* Too narrow for tools beside the canvas: draw first, tools below, and let the
+ page scroll. The canvas column needs a definite height for its container
+ query sizing. */
+@media (max-width: 40rem) {
+ #draw-mode {
+ height: auto;
+ }
+
+ .draw-layout {
+ flex-direction: column;
+ }
+
+ .canvas-column {
+ flex: none;
+ order: -1;
+ height: min(100vw - 40px, 640px);
+ }
+
+ .side-column {
+ flex-basis: auto;
+ }
+}
+
#drawing {
display: block;
width: 100%;
@@ -181,14 +228,13 @@ button:disabled {
}
.save-button {
- min-width: 260px;
- min-height: 64px;
- padding: 12px 32px;
+ min-height: 56px;
+ padding: 8px 16px;
border: 3px solid var(--slide-text);
border-radius: 16px;
background: var(--slide-lime);
box-shadow: 4px 4px 0 var(--slide-text);
- font-size: 1.5rem;
+ font-size: 1.4rem;
font-weight: 800;
}
@@ -198,9 +244,9 @@ button:disabled {
}
.status {
- min-height: 1.5em;
+ min-height: 2.6em;
margin: 0;
- font-size: 1.2rem;
+ font-size: 1rem;
font-weight: 600;
text-align: center;
}
@@ -215,14 +261,16 @@ button:disabled {
.waiting {
margin: 0;
- font-size: 1rem;
+ font-size: 0.9rem;
+ text-align: center;
opacity: 0.7;
}
.link-button {
+ padding: 2px;
border: none;
background: none;
- font-size: 1.1rem;
+ font-size: 1rem;
font-weight: 700;
text-decoration: underline;
}
diff --git a/challenges/birdsbirdbirds/server.mjs b/challenges/birdsbirdbirds/server.mjs
index d046b5d..52404c4 100644
--- a/challenges/birdsbirdbirds/server.mjs
+++ b/challenges/birdsbirdbirds/server.mjs
@@ -116,7 +116,7 @@ function sendSavedBird(response, workspace) {
body = readWorkspaceFile(path.join(workspace, 'bird.png'), MAX_PNG_BYTES)
} catch (error) {
if (error.code === 'ENOENT') return sendText(response, 404, 'no bird.png yet')
- if (error instanceof RefusedFileError) return sendText(response, 422, `${error.message}; press Save on the drawing page again`)
+ if (error instanceof RefusedFileError) return sendText(response, 422, `${error.message}; press Save my bird on the drawing page again`)
throw error
}
diff --git a/challenges/birdsbirdbirds/skills/start-challenge/SKILL.md b/challenges/birdsbirdbirds/skills/start-challenge/SKILL.md
index 8f7a871..2cde03c 100644
--- a/challenges/birdsbirdbirds/skills/start-challenge/SKILL.md
+++ b/challenges/birdsbirdbirds/skills/start-challenge/SKILL.md
@@ -41,15 +41,19 @@ starts (after `session_info`, when you have it): call `show_preview` with
`http://127.0.0.1:4322/` without asking first. Without `show_preview`, give
them that URL to open right away.
+Whenever `show_preview` returns `offered`, a dialog is still asking the
+participant whether to open the page. Say "Click **Open preview** to start
+drawing." Never say the page is open until a call returns `navigated`.
+
When you resume with `draw-bird` not yet complete, first use `bash` to check
whether `bird.png` exists in the current directory: a save made just before
a restart sends no message. If it exists, go straight to step 2.
-Tell them to draw any bird, then press Save. Saving tells you on its own, as
-a `[birdsbirdbirds]` message. If they say they saved but no message came, use
-`bash` to check that `bird.png` exists in the current directory; if it
-doesn't, ask them to press Save and look again. When it exists, go on to
-step 2.
+Tell them to draw any bird, then press **Save my bird**; always call the
+button that. Saving tells you on its own, as a `[birdsbirdbirds]` message. If
+they say they saved but no message came, use `bash` to check that `bird.png`
+exists in the current directory; if it doesn't, ask them to press **Save my
+bird** and look again. When it exists, go on to step 2.
## 2. Meet the bird
@@ -111,18 +115,22 @@ checker or edit it. When it passes, report `animate`.
The drawing page switches to the sky on its own once a valid `bird.json`
exists. Tell the participant to look at it (call `show_preview` again if the
-page was closed). They can press "Draw again" to redraw; if they do, start
+page was closed; on `offered`, say "Click **Open preview** to watch it
+fly."). They can press "Draw again" to redraw; if they do, start
again at step 1.
Ask how the flight looks and offer one tweak, such as faster, higher, or
more wobbly. If they want it, edit `bird.json`, rerun the checker, and let
-them watch again. When they say they've watched it fly, report `fly`.
+them watch again. When they say they've watched it fly, go on to step 5;
+`fly` is the last step, and reporting it moves the participant's focus to
+Finish, so it waits for the gallery answer.
## 5. Gallery
-Only with `publish_gallery_item`. Use the display name from `session_info`
-and ask: "Want this on the booth screens? It'll show with your name,
-[display name], and staff can remove it at any time."
+Only with `publish_gallery_item`; without it, or if you already published
+this bird in this conversation, skip to step 6. Use the display
+name from `session_info` and ask: "Want this on the booth screens? It'll show
+with your name, [display name], and staff can remove it at any time."
On a yes, run the checker once more, then `read` `bird.json` and call
`publish_gallery_item` with:
@@ -134,3 +142,9 @@ On a yes, run the checker once more, then `read` `bird.json` and call
- `payload`: exactly the object you just read from `bird.json`
On a no, move on without publishing.
+
+## 6. Finish
+
+Report `fly`. With `report_step`, end with the finish line, naming the
+points: "All steps are done. Press Enter, or choose Finish, to claim your 150
+pts." Finish closes the page, so don't invite them to keep watching it.
diff --git a/test/birdsbirdbirds.test.mjs b/test/birdsbirdbirds.test.mjs
index dae489a..7141bee 100644
--- a/test/birdsbirdbirds.test.mjs
+++ b/test/birdsbirdbirds.test.mjs
@@ -721,6 +721,7 @@ test('the server does not serve a symlinked bird.png', (testContext) => withWork
const response = await sendRequest({ port, requestPath: '/bird.png' })
assert.equal(response.status, 422)
+ assert.match(response.body.toString(), /press Save my bird on the drawing page again/)
}))
test('the server does not serve a bird.png over the size cap', (testContext) => withWorkspace(async ({ workspace }) => {