Follow the energy · Food-web lab
HTML for this example
Select the code and use your copy shortcut. Changes in this preview are temporary.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Follow the energy · Food-web lab</title>
<style>
:root{color-scheme:light;--ink:#20352e;--paper:#fbf7ec;--green:#265c46;--line:#d6d9c9;--accent:#d9ed9e}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:17px/1.55 system-ui,sans-serif}main{max-width:1100px;margin:auto;padding:clamp(18px,4vw,44px)}h1{font:700 clamp(30px,5vw,48px)/1.12 Georgia,serif;margin:8px 0 18px}h2{font:700 25px/1.25 Georgia,serif;margin:0 0 14px}h3{font-size:18px;margin:0 0 8px}p{margin:10px 0 18px}.eyebrow{letter-spacing:.1em;text-transform:uppercase;font-size:16px;font-weight:700}.rule{background:var(--ink);color:#fff;padding:16px 20px;border-radius:12px;font-size:20px;margin:20px 0 28px}.rule strong{color:var(--accent)}.workspace{display:grid;grid-template-columns:1fr 1.05fr;gap:28px;align-items:start}.board{background:#edf0e4;border:1px solid var(--line);border-radius:20px;padding:20px}.board svg{display:block;width:100%;height:auto;max-height:390px}.node{fill:var(--paper);stroke:var(--green);stroke-width:3}.node-label{font:700 28px system-ui;fill:var(--ink);text-anchor:middle;dominant-baseline:central}.edge{stroke:var(--green);stroke-width:4;fill:none;marker-end:url(#arrow)}.removed{display:none}.legend{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:10px 0 18px}.legend span{font-size:16px}.card{padding:16px 0;border-top:1px solid var(--line)}.choices{display:flex;gap:12px;flex-wrap:wrap}button{font:600 17px/1.3 system-ui;cursor:pointer;border:1.5px solid var(--green);border-radius:9px;background:#fffdf6;color:var(--ink);padding:12px 18px;min-height:48px}button:hover{background:#e5efcf}button:focus-visible,input:focus-visible,summary:focus-visible{outline:3px solid #b85419;outline-offset:3px}button:disabled{cursor:default;opacity:.6}button.selected{background:var(--green);color:#fff;opacity:1}button.primary{background:var(--green);color:#fff}.hint{font-size:16px;min-height:25px;margin:10px 0 0}.status{font-weight:700}.links{padding-left:24px;margin-bottom:12px}.round{margin-top:30px;padding:24px 0;border-top:2px solid var(--ink)}label{display:block;font-weight:600;margin-bottom:12px}input{font:inherit;width:110px;max-width:100%;border:1.5px solid var(--green);border-radius:8px;padding:12px;background:white;color:var(--ink)}.row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.answer{background:#e8edda;border-radius:10px;padding:16px;margin-top:16px}.answer:empty{display:none}.actions{display:flex;gap:12px;flex-wrap:wrap;margin:16px 0}details{border-top:1px solid var(--line);padding:20px 0}summary{cursor:pointer;font-weight:700}footer{font-size:16px;border-top:1px solid var(--line);padding-top:20px}mark{background:var(--accent);color:var(--ink);padding:0 3px}#networkSummary{margin-top:14px}.small-title{margin:0 0 4px;font-size:16px}#edgeList{overflow-wrap:anywhere}@media(max-width:650px){.workspace{grid-template-columns:1fr;gap:24px}.board{padding:16px}.round{padding-top:22px}.rule{padding:16px}main{padding:20px}.legend{gap:8px}.board svg{max-height:330px}}
</style>
</head>
<body>
<main>
<header><div class="eyebrow">Classroom field notes / fictional model</div><h1>Follow the energy</h1><p>“S eats G.” Which way does the arrow go? Build five feeding links, then find what remains when G leaves the web.</p><div class="rule">Every arrow means <strong>food → consumer</strong>.</div></header>
<div class="workspace">
<section class="board" aria-labelledby="boardTitle">
<h2 id="boardTitle">Your growing food web</h2><p class="small-title">Only correctly answered cards add arrows.</p>
<svg id="webDiagram" viewBox="0 0 420 400" role="img" aria-labelledby="diagramTitle diagramDesc">
<title id="diagramTitle">Fictional food web</title><desc id="diagramDesc">Four nodes, no links yet. The link list below gives the same information.</desc>
<defs><marker id="arrow" markerWidth="16" markerHeight="14" refX="14" refY="6" orient="auto" markerUnits="userSpaceOnUse"><path d="M0,0 L14,6 L0,12 Z" fill="#265c46"/></marker></defs>
<g id="drawnEdges"></g>
<g id="node-P"><circle class="node" cx="210" cy="340" r="32"/><text class="node-label" x="210" y="340">P</text></g>
<g id="node-G"><circle class="node" cx="70" cy="205" r="32"/><text class="node-label" x="70" y="205">G</text></g>
<g id="node-S"><circle class="node" cx="350" cy="205" r="32"/><text class="node-label" x="350" y="205">S</text></g>
<g id="node-L"><circle class="node" cx="210" cy="70" r="32"/><text class="node-label" x="210" y="70">L</text></g>
</svg>
<div class="legend"><span>P · Producer</span><span>G · Grazer</span><span>S · Small consumer</span><span>L · Large consumer</span></div>
<p id="networkSummary" class="status" aria-live="polite">0 links in your web.</p><ul id="edgeList" class="links"><li>No links yet.</li></ul><p>P uses an external energy source. These four roles are invented for this activity.</p>
</section>
<section aria-labelledby="cardsTitle"><h2 id="cardsTitle">1. Choose the energy arrow</h2><p>Read each feeding card. Name the food first, then choose where its energy goes.</p><div id="cards"></div><p id="buildResult" class="status" aria-live="polite">0 of 5 feeding cards complete.</p></section>
</div>
<section class="round" aria-labelledby="routeTitle"><h2 id="routeTitle">2. Follow every route from P to L</h2><p>Finish the five cards, then trace the arrowheads. A route may pass through more than one middle node.</p><label id="routeQuestion" for="routeCount">How many different P-to-L routes are in the completed web?</label><div class="row"><input id="routeCount" type="number" min="0" max="20" step="1" inputmode="numeric"><button id="checkRoutes" class="primary" type="button">Check routes</button></div><div id="routeFeedback" class="answer" role="status"></div></section>
<section class="round" aria-labelledby="changeTitle"><h2 id="changeTitle">3. What changes without G?</h2><p>Predict which arrows touch G. After building all five links, remove G and compare the diagram with your prediction.</p><div class="actions"><button id="removeG" type="button" disabled>Remove G</button><button id="resetGame" type="button">Reset activity</button></div><div id="changeResult" class="answer" role="status"></div><p><mark>Connection is not a population count.</mark> Can the remaining arrows tell you how many large consumers survive? Explain what information you would need.</p></section>
<details id="teacherKey"><summary>Teacher key and discussion</summary><p>Five links: P→G, P→S, G→S, G→L, S→L. The three routes are P→G→L, P→S→L, P→G→S→L.</p><p>There is no direct P→L link because no card says L eats P. Removing G deletes P→G, G→S, G→L. P→S and S→L remain.</p><p>The remaining path is P→S→L. This model has no population counts, feeding rates, or energy amounts, so it cannot determine L's population change.</p></details>
<footer>Original fictional teaching model. Answers stay in this open page only. For a class record, ask students to write the food, consumer, and reason for each arrow.</footer>
</main>
<script>
(() => {
'use strict';
const find = (selector) => document.querySelector(selector);
const feedingCards = [
{food:'P',consumer:'G',statement:'G eats P',correctFirst:true},
{food:'P',consumer:'S',statement:'S eats P',correctFirst:false},
{food:'G',consumer:'S',statement:'S eats G',correctFirst:true},
{food:'G',consumer:'L',statement:'L eats G',correctFirst:false},
{food:'S',consumer:'L',statement:'L eats S',correctFirst:true}
];
const positions = {P:[210,340],G:[70,205],S:[350,205],L:[210,70]};
const completed = new Set();
let grazerRemoved = false;
const format = (card) => card.food + '→' + card.consumer;
const currentCards = () => feedingCards.filter((card,index) => completed.has(index) && (!grazerRemoved || (card.food !== 'G' && card.consumer !== 'G')));
function renderNetwork() {
const active = currentCards();
find('#drawnEdges').innerHTML = active.map((card) => {
const [x1,y1] = positions[card.food];
const [x2,y2] = positions[card.consumer];
const length = Math.hypot(x2-x1,y2-y1);
const dx = (x2-x1)/length, dy = (y2-y1)/length;
return '<path class="edge" data-edge="'+format(card)+'" d="M'+(x1+dx*36)+','+(y1+dy*36)+' L'+(x2-dx*39)+','+(y2-dy*39)+'"/>';
}).join('');
find('#node-G').setAttribute('class',grazerRemoved ? 'removed' : '');
find('#node-G').setAttribute('aria-label',grazerRemoved ? 'G removed from model' : 'G grazer');
find('#edgeList').innerHTML = active.length ? active.map(card => '<li>'+format(card)+' · '+card.statement+'</li>').join('') : '<li>No links yet.</li>';
const summary = grazerRemoved ? '2 links remain: P→S and S→L. Remaining route: P→S→L.' : active.length+' links in your web.';
find('#networkSummary').textContent = summary;
find('#diagramDesc').textContent = summary+' '+active.map(format).join(', ');
find('#buildResult').textContent = completed.size+' of 5 feeding cards complete.';
find('#removeG').disabled = completed.size !== 5 || grazerRemoved;
}
function choose(index, correct) {
if (completed.has(index) || grazerRemoved) return;
const card = feedingCards[index];
const hint = find('#hint-'+index);
if (!correct) {
hint.textContent = 'Try again. '+card.food+' supplies food; '+card.consumer+' receives its energy.';
return;
}
completed.add(index);
hint.textContent = 'Correct: '+format(card)+'. '+card.consumer+' gets food from '+card.food+'.';
find('#correct-'+index).classList.add('selected');
find('#correct-'+index).setAttribute('aria-pressed','true');
find('#correct-'+index).disabled = true;
find('#reverse-'+index).disabled = true;
renderNetwork();
}
function createCards() {
find('#cards').innerHTML = feedingCards.map((card,index) => {
const correctButton = '<button type="button" id="correct-'+index+'" aria-pressed="false" aria-label="Card '+(index+1)+': '+format(card)+'">'+format(card)+'</button>';
const reverse = card.consumer+'→'+card.food;
const reverseButton = '<button type="button" id="reverse-'+index+'" aria-label="Card '+(index+1)+': '+reverse+'">'+reverse+'</button>';
return '<article class="card" aria-labelledby="card-title-'+index+'"><h3 id="card-title-'+index+'">'+(index+1)+'. '+card.statement+'</h3><div class="choices">'+(card.correctFirst ? correctButton+reverseButton : reverseButton+correctButton)+'</div><p id="hint-'+index+'" class="hint" aria-live="polite">Choose the energy direction.</p></article>';
}).join('');
feedingCards.forEach((card,index) => {
find('#correct-'+index).addEventListener('click',() => choose(index,true));
find('#reverse-'+index).addEventListener('click',() => choose(index,false));
});
}
function checkRoutes() {
const feedback = find('#routeFeedback');
if (completed.size !== 5) {feedback.textContent = 'Complete all five feeding cards before counting routes.';return;}
const raw = find('#routeCount').value.trim();
const count = Number(raw);
if (raw === '' || !Number.isInteger(count) || count < 0 || count > 20) {feedback.textContent = 'Enter a whole number from 0 to 20.';return;}
const expected = grazerRemoved ? 1 : 3;
if (count !== expected) {feedback.textContent = grazerRemoved ? 'Try again. Follow P→S, then S→L.' : 'Try again. Also look for a route that visits both G and S.';return;}
feedback.textContent = grazerRemoved ? 'Correct: 1 route after removing G. P→S→L.' : 'Correct: 3 routes. P→G→L; P→S→L; P→G→S→L. No direct P→L link is supplied.';
}
find('#routeCount').addEventListener('input',() => {find('#routeFeedback').textContent = '';});
find('#checkRoutes').addEventListener('click',checkRoutes);
find('#routeCount').addEventListener('keydown',(event) => {if(event.key === 'Enter'){event.preventDefault();checkRoutes();}});
find('#removeG').addEventListener('click',() => {
if(completed.size !== 5 || grazerRemoved) return;
grazerRemoved = true;
find('#routeQuestion').textContent = 'How many P-to-L routes remain after removing G?';
find('#changeResult').textContent = 'Removed P→G, G→S, G→L. Remaining links: P→S, S→L. Remaining route: P→S→L. L population change cannot be determined from these cards.';
find('#routeCount').value = '';
find('#routeFeedback').textContent = 'G is removed. Count the routes in the changed web and check again.';
renderNetwork();
});
find('#resetGame').addEventListener('click',() => {
completed.clear();grazerRemoved = false;
find('#routeQuestion').textContent = 'How many different P-to-L routes are in the completed web?';
find('#routeCount').value = '';
find('#routeFeedback').textContent = '';
find('#changeResult').textContent = '';
find('#teacherKey').open = false;
createCards();renderNetwork();
});
createCards();renderNetwork();
})();
</script>
</body>
</html>