# A physics lab manual — motion under gravity and thrust, with a lander to fly

Recipe No. 25, Labs and workshops. From The know.sh Cookbook: https://know.sh/cookbook/physics-lab

Every student wants to land on the Moon, and nearly every one crashes the first time. Make that crash the lesson. Each lab has students predict before they fly, and the landing lab holds a working lander with lunar gravity, an engine and a fuel gauge, so every equation they write gets tested.

- For: a physics teacher who wants students to fly the Moon landing
- You get: four labs on gravity and thrust, and a lunar lander to fly
- Time: an evening to build, a weekend to check
- Made with: Shelf, Document, Your AI assistant, Elements, Quiz, Public link

## How it works

1. **Build the unit from your plan.** Give your assistant your unit plan and lab sheets. Ask for a document per lab, each with *Aim*, *Predict*, *Method*, *Record*, *Explain* and *Safety*, and every prediction worked in a *Teacher’s answers* section.
2. **Put a lander in the landing lab.** Ask for a widget element: a lander with lunar gravity of 1.62 m/s², an engine and a fuel gauge. Hover five metres up and cut the engine: it should land at about 4 m/s.
3. **Predict first, then fly.** Nobody touches the lander until *Predict* is filled in. Pairs work out the free fall, fly it with the engine off, and the record table sets each prediction beside what they flew.
4. **Quiz the class before the next lab.** Share each lab with *Teacher’s answers* left out, then ask your assistant for a quiz on it. Each question cites its section, so a wrong answer points back to the page.

## Try this prompt

Your assistant, connected to know.sh (Claude, ChatGPT or a local model):

> Using know.sh, add a widget element to the overview of Landing on the Moon: a lunar lander with two pads, gravity 1.62 m/s², an engine giving 4.2 m/s² along its axis and burning 7 per cent of the tank a second, and a readout of height, speed and fuel. A landing is safe under 4 m/s and within 20 degrees of level. Say in the caption what it leaves out.

Your assistant, connected to know.sh (Claude, ChatGPT or a local model):

> Using know.sh, make a shelf called “Physics 1 — motion” with a document per lab: Free fall, Forces and the engine, Energy, and Landing on the Moon. Give every lab the sections Aim, Predict, Method, Record, Explain and Safety, and a section called Teacher’s answers that works every prediction in full, with the equation, the numbers and the units. Use 1.62 m/s² for lunar gravity.

## Elements in the specimens

Written as your assistant writes them through the know.sh MCP server. Each reads as plain words until you turn elements on (Account → Elements).

Widget, specimen 1:

```element widget
{
  "title": "Lunar lander",
  "caption": "Arrow keys turn and fire the engine (or the buttons, on a phone); set down on a flat pad at under 4 m/s, roughly level and not sliding fast; up to 5 m/s is a hard landing at half points. The small pad scores triple.",
  "height": 470,
  "html": "<div class=\"lander\" tabindex=\"-1\" aria-label=\"Lunar lander. Arrow keys steer, up or space fires the engine, R starts again.\">\n  <canvas aria-hidden=\"true\"></canvas>\n  <button type=\"button\" class=\"cover\"><span class=\"go\"><span class=\"fine\">Click</span><span class=\"touch\">Tap</span> to <span class=\"what\">play</span></span><span class=\"how\">Arrow keys steer; up or space fires the engine</span></button>\n  <p class=\"hud\" aria-live=\"off\"><span data-k=\"alt\"></span><span data-k=\"vx\"></span><span data-k=\"vy\"></span><span data-k=\"fuel\"></span><span data-k=\"score\"></span></p>\n  <p class=\"say\" aria-live=\"polite\"></p>\n  <div class=\"pads\" aria-label=\"Controls\">\n    <button type=\"button\" data-hold=\"left\" aria-label=\"Turn left\">&#8592;</button>\n    <button type=\"button\" data-hold=\"thrust\">Thrust</button>\n    <button type=\"button\" data-hold=\"right\" aria-label=\"Turn right\">&#8594;</button>\n    <button type=\"button\" data-tap=\"again\">Again</button>\n  </div>\n</div>",
  "css": ".lander { position: relative; outline: none; }\n.lander:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }\ncanvas { display: block; width: 100%; aspect-ratio: 16 / 9; border-bottom: 1px solid var(--rule); }\n.cover { position: absolute; left: 0; top: 0; width: 100%; aspect-ratio: 16 / 9; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; margin: 0; padding: 0; border: 0; border-radius: 0; background: color-mix(in srgb, var(--paper) 72%, transparent); color: var(--ink); cursor: pointer; }\n.cover[hidden] { display: none; }\n.cover .go { padding: 12px 22px; background: var(--ink); color: var(--paper); font: 500 16px/1 var(--sans); }\n.cover:hover .go, .cover:focus-visible .go { outline: 2px solid var(--ink); outline-offset: 3px; }\n.cover:focus-visible { outline: none; }\n.cover .how { font: 400 13px/1.3 var(--sans); color: var(--ink-2); background: var(--paper); padding: 2px 6px; }\n.cover .touch { display: none; }\n@media (hover: none), (pointer: coarse) { .cover .fine, .cover .how { display: none; } .cover .touch { display: inline; } }\n.hud { display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 8px 0 0; font: 400 13px/1.3 var(--sans); color: var(--ink-2); font-variant-numeric: tabular-nums; }\n.hud b { font-weight: 600; color: var(--ink); }\n.say { min-height: 1.5em; margin: 6px 0 0; font: italic 400 17px/1.4 var(--serif); }\n.pads { display: flex; gap: 8px; margin-top: 10px; }\n.pads button { flex: 1; min-height: 44px; font: 500 15px/1 var(--sans); color: var(--ink); background: var(--paper); border: 1px solid var(--ink); border-radius: 0; touch-action: none; user-select: none; -webkit-user-select: none; }\n.pads button[aria-pressed='true'] { background: var(--ink); color: var(--paper); }\n@media (hover: hover) and (pointer: fine) { .pads button[data-hold] { display: none; } }",
  "js": "(function () {\n  var root = document.querySelector('.lander');\n  var canvas = root.querySelector('canvas');\n  var ctx = canvas.getContext('2d');\n  var say = root.querySelector('.say');\n  var hudEls = {};\n  root.querySelectorAll('[data-k]').forEach(function (el) { hudEls[el.dataset.k] = el; });\n\n  // World units are metres; the view is 320 m wide and 180 m tall.\n  var W = 320, H = 180, G = 1.62, THRUST = 4.2, TURN = 2.2, BURN = 7;\n  var SAFE_VY = 4, HARD_VY = 5, SAFE_VX = 2.5, SAFE_TILT = 0.35;\n  var keys = { left: false, right: false, thrust: false };\n  var seed = 7, level = 1, total = 0, ship, ground, pads, state, sparks, last = 0;\n  // The game runs only while it has the keys; otherwise the cover asks for a click.\n  var cover = root.querySelector('.cover'), active = false;\n\n  function rand() { seed = (seed * 16807) % 2147483647; return (seed - 1) / 2147483646; }\n  function ink(name, fallback) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback; }\n\n  function makeGround() {\n    ground = []; pads = [];\n    var x = 0, y = 40 + rand() * 30, padCount = 0;\n    while (x < W) {\n      if (padCount < 2 && x > 30 + padCount * 130 && rand() < 0.28) {\n        var width = padCount = 0 ? 34 : 22;\n        pads.push({ x0: x, x1: x + width, y: y, bonus: padCount = 0 ? 1 : 3 });\n        ground.push([x, y], [x + width, y]);\n        x += width; padCount++;\n        continue;\n      }\n      x += 6 + rand() * 12;\n      y = Math.max(12, Math.min(110, y + (rand() - 0.5) * 34));\n      ground.push([x, y]);\n    }\n    if (pads.length === 0) { pads.push({ x0: 140, x1: 174, y: 30, bonus: 1 }); ground.push([140, 30], [174, 30]); ground.sort(function (a, b) { return a[0] - b[0]; }); }\n  }\n\n  function groundAt(px) {\n    for (var i = 1; i < ground.length; i++) {\n      var a = ground[i - 1], b = ground[i];\n      if (px >= a[0] && px <= b[0]) return b[0] === a[0] ? a[1] : a[1] + (b[1] - a[1]) * (px - a[0]) / (b[0] - a[0]);\n    }\n    return 0;\n  }\n\n  function start(fresh) {\n    if (fresh) { level = 1; total = 0; }\n    seed = 7 + level * 101;\n    makeGround();\n    ship = { x: 40 + rand() * 60, y: 168, vx: 2 + rand() * 3, vy: 0, a: -0.2, fuel: 100 };\n    state = 'flying'; sparks = []; say.textContent = level === 1 && fresh ? 'Land on a flat pad. Gently.' : 'Level ' + level + '.';\n  }\n\n  function end(ok, why, bonus) {\n    state = ok ? 'landed' : 'crashed';\n    if (ok) {\n      var points = Math.round((50 + ship.fuel) * bonus * (why === 'hard' ? 0.5 : 1));\n      total += points; level++;\n      say.textContent = (why === 'hard' ? 'A hard landing, but down. ' : 'Landed. ') + points + ' points' + (bonus > 1 ? ', on the small pad' : '') + '. Space or Again for the next level.';\n    } else {\n      say.textContent = 'Crashed: ' + why + '. Press R or Again.';\n      for (var i = 0; i < 18; i++) sparks.push({ x: ship.x, y: ship.y, vx: (rand() - 0.5) * 30, vy: rand() * 20, a: rand() * 6, t: 1.6 });\n    }\n  }\n\n  function step(dt) {\n    if (!active) return;\n    if (state !== 'flying') { sparks.forEach(function (s) { s.vy -= G * 4 * dt; s.x += s.vx * dt; s.y += s.vy * dt; s.t -= dt; }); return; }\n    if (keys.left) ship.a -= TURN * dt;\n    if (keys.right) ship.a += TURN * dt;\n    ship.a = Math.max(-1.6, Math.min(1.6, ship.a));\n    var burning = keys.thrust && ship.fuel > 0;\n    if (burning) { ship.vx += Math.sin(ship.a) * THRUST * dt; ship.vy += Math.cos(ship.a) * THRUST * dt; ship.fuel = Math.max(0, ship.fuel - BURN * dt); }\n    ship.vy -= G * dt;\n    ship.x += ship.vx * dt; ship.y += ship.vy * dt;\n    if (ship.x < 0) ship.x += W; if (ship.x > W) ship.x -= W;\n    ship.burning = burning;\n    var floor = groundAt(ship.x);\n    if (ship.y - 4 <= floor) {\n      ship.y = floor + 4;\n      var pad = pads.filter(function (p) { return ship.x >= p.x0 && ship.x <= p.x1; })[0];\n      if (!pad) end(false, 'not on a pad');\n      else if (Math.abs(ship.a) > SAFE_TILT) end(false, 'tilted ' + Math.round(Math.abs(ship.a) * 57.3) + ' degrees');\n      else if (-ship.vy > HARD_VY) end(false, 'coming down at ' + (-ship.vy).toFixed(1) + ' m/s');\n      else if (Math.abs(ship.vx) > SAFE_VX) end(false, 'sliding at ' + Math.abs(ship.vx).toFixed(1) + ' m/s');\n      else end(true, -ship.vy > SAFE_VY ? 'hard' : '', pad.bonus);\n    }\n  }\n\n  function hud() {\n    var ok = function (bad) { return bad ? '<b>' : ''; }, close = function (bad) { return bad ? '</b>' : ''; };\n    var vy = -ship.vy, vx = Math.abs(ship.vx);\n    hudEls.alt.innerHTML = 'Height ' + Math.max(0, Math.round(ship.y - groundAt(ship.x) - 4)) + ' m';\n    hudEls.vx.innerHTML = 'Across ' + ok(vx > SAFE_VX) + vx.toFixed(1) + ' m/s' + close(vx > SAFE_VX);\n    hudEls.vy.innerHTML = 'Down ' + ok(vy > SAFE_VY) + vy.toFixed(1) + ' m/s' + close(vy > SAFE_VY);\n    hudEls.fuel.innerHTML = 'Fuel ' + ok(ship.fuel < 20) + Math.round(ship.fuel) + '%' + close(ship.fuel < 20);\n    hudEls.score.innerHTML = 'Score ' + total;\n  }\n\n  function draw() {\n    var dpr = window.devicePixelRatio || 1, cw = canvas.clientWidth, ch = canvas.clientHeight;\n    if (canvas.width !== Math.round(cw * dpr)) { canvas.width = Math.round(cw * dpr); canvas.height = Math.round(ch * dpr); }\n    var s = canvas.width / W;\n    var paper = ink('--paper', '#fbfbf9'), black = ink('--ink', '#000'), grey = ink('--ink-3', '#66665f');\n    ctx.setTransform(1, 0, 0, 1, 0, 0);\n    ctx.fillStyle = paper; ctx.fillRect(0, 0, canvas.width, canvas.height);\n    ctx.setTransform(s, 0, 0, -s, 0, canvas.height);\n    ctx.lineJoin = 'round'; ctx.lineCap = 'round';\n\n    ctx.beginPath();\n    ground.forEach(function (p, i) { if (i === 0) ctx.moveTo(p[0], p[1]); else ctx.lineTo(p[0], p[1]); });\n    ctx.lineTo(W, 0); ctx.lineTo(0, 0); ctx.closePath();\n    ctx.fillStyle = paper; ctx.fill();\n    ctx.strokeStyle = black; ctx.lineWidth = 1 / s * 1.2; ctx.stroke();\n    pads.forEach(function (p) {\n      ctx.lineWidth = 3 / s; ctx.beginPath(); ctx.moveTo(p.x0, p.y); ctx.lineTo(p.x1, p.y); ctx.stroke();\n      ctx.save(); ctx.scale(1, -1); ctx.fillStyle = grey; ctx.font = (10 / s) + 'px sans-serif'; ctx.textAlign = 'center';\n      ctx.fillText(p.bonus > 1 ? '\\u00d7' + p.bonus : '', (p.x0 + p.x1) / 2, -p.y + 11 / s); ctx.restore();\n    });\n\n    if (state !== 'crashed') {\n      ctx.save(); ctx.translate(ship.x, ship.y); ctx.rotate(-ship.a); ctx.lineWidth = 1.2 / s; ctx.strokeStyle = black;\n      ctx.beginPath(); ctx.moveTo(-3, -1); ctx.lineTo(-3, 3); ctx.lineTo(0, 5.5); ctx.lineTo(3, 3); ctx.lineTo(3, -1); ctx.closePath(); ctx.fillStyle = paper; ctx.fill(); ctx.stroke();\n      ctx.beginPath(); ctx.moveTo(-2.5, -1); ctx.lineTo(-4.5, -4); ctx.moveTo(2.5, -1); ctx.lineTo(4.5, -4); ctx.moveTo(-5.5, -4); ctx.lineTo(-3.5, -4); ctx.moveTo(3.5, -4); ctx.lineTo(5.5, -4); ctx.stroke();\n      if (ship.burning) { var f = 3 + Math.random() * 3; ctx.beginPath(); ctx.moveTo(-1.4, -1.5); ctx.lineTo(0, -1.5 - f); ctx.lineTo(1.4, -1.5); ctx.stroke(); }\n      ctx.restore();\n    }\n    ctx.strokeStyle = black; ctx.lineWidth = 1 / s;\n    sparks.forEach(function (p) { if (p.t <= 0) return; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x + Math.cos(p.a) * 2.5, p.y + Math.sin(p.a) * 2.5); ctx.stroke(); });\n  }\n\n  function frame(t) {\n    var dt = Math.min(0.05, (t - last) / 1000 || 0); last = t;\n    if (!document.hidden) { step(dt); draw(); hud(); }\n    requestAnimationFrame(frame);\n  }\n\n  function next() { if (state = 'landed') start(false); else if (state = 'crashed') start(true); }\n  var map = { ArrowLeft: 'left', ArrowRight: 'right', ArrowUp: 'thrust', ' ': 'thrust' };\n  root.addEventListener('keydown', function (e) {\n    if (e.key = 'r' || e.key = 'R') { start(true); e.preventDefault(); return; }\n    if (e.key = ' ' && state ! 'flying') { next(); e.preventDefault(); return; }\n    if (map[e.key]) { keys[map[e.key]] = true; e.preventDefault(); }\n  });\n  root.addEventListener('keyup', function (e) { if (map[e.key]) keys[map[e.key]] = false; });\n  root.addEventListener('blur', function () { keys.left = keys.right = keys.thrust = false; });\n  canvas.addEventListener('pointerdown', function () { root.focus(); });\n  root.querySelectorAll('[data-hold]').forEach(function (b) {\n    var k = b.dataset.hold;\n    var on = function (e) { e.preventDefault(); if (!active) play(); keys[k] = true; b.setAttribute('aria-pressed', 'true'); };\n    var off = function () { keys[k] = false; b.setAttribute('aria-pressed', 'false'); };\n    b.addEventListener('pointerdown', on); b.addEventListener('pointerup', off); b.addEventListener('pointercancel', off); b.addEventListener('pointerleave', off);\n  });\n  root.querySelector('[data-tap=\"again\"]').addEventListener('click', function () { if (state === 'flying') start(true); else next(); play(); });\n\n  // Focus first, so hiding the cover does not drop it.\n  function play() { root.focus(); active = true; cover.hidden = true; }\n  function idle() {\n    if (!active) return;\n    active = false; keys.left = keys.right = keys.thrust = false;\n    root.querySelectorAll('[data-hold]').forEach(function (b) { b.setAttribute('aria-pressed', 'false'); });\n    cover.querySelector('.what').textContent = 'carry on';\n    cover.hidden = false;\n  }\n  cover.addEventListener('click', play);\n  root.addEventListener('focusout', function (e) { if (!root.contains(e.relatedTarget)) idle(); });\n  window.addEventListener('blur', idle);\n\n  start(true);\n  requestAnimationFrame(frame);\n})();"
}
```

Table, specimen 2:

```element table
{
  "caption": "Lab 4 record: the worked prediction, then what the class flew",
  "columns": [
    {
      "label": "Question",
      "align": "left"
    },
    {
      "label": "Predicted, then flown (class mean)",
      "align": "left"
    }
  ],
  "rows": [
    [
      "Fall time, engine off",
      "Ground 121 m below: *t* = √(2*h*/*g*) = 12.2 s. *Flown:* 12.9 s; it passed the pad and fell 134 m."
    ],
    [
      "Speed at the ground",
      "*v* = *gt* = 19.8 m/s. *Flown:* 20.8 m/s on the Down readout."
    ],
    [
      "Where it lands",
      "Drifting 4.1 m/s: 4.1 × 12.2 = 50 m on, past the wide pad. *Flown:* crashed past the pad, not on a pad."
    ],
    [
      "Net acceleration",
      "Engine on, upright: measure it. *Flown:* 5.2 m/s gained in 2 s, so 2.6 m/s²."
    ],
    [
      "Highest safe cut-off",
      "To land under 4 m/s: *h* = *v*²/2*g* = 4.9 m. *Flown:* safe from 4 m, hard from 6 m, crash from 8 m."
    ],
    [
      "A full tank’s worth",
      "4.2 m/s² × 14.3 s = 60 m/s of speed change. *Flown:* 14.3 s of burning, 100% to 0."
    ]
  ],
  "notes": "Lunar gravity 1.62 m/s². The lander’s engine gives 4.2 m/s² along its axis and burns 7 per cent of the tank per second. Heights are read from the rounded readout, so expect a metre either way."
}
```
