Trasa na mapce
AnimacjePodgląd
Ładowanie podglądu…
Znacznik porusza się po trasie na mapie z możliwością zmiany kierunku i zatrzymania.
Kod źródłowy
Licencja MIT — także do użytku komercyjnego. Zachowaj informacje o autorze i licencji.
HTML
<main class="pen" data-motion="paused"><header><div><h1>Trasa na mapce</h1><p>Przejazd od magazynu do portu. Zmień kierunek drogi.</p></div></header><section class="scene" aria-label="Wizualizacja"><svg id="map" viewBox="0 0 820 320" role="img" aria-label="Schemat trasy pomiędzy magazynem i portem"><defs><pattern id="streets" width="65" height="65" patternUnits="userSpaceOnUse"><path d="M65 0H0V65" fill="none" stroke="#b7cfb3" stroke-width="2"/></pattern></defs><rect width="820" height="320" fill="url(#streets)"/><path d="M620 0Q550 80 670 140T610 320H820V0Z" fill="#96d5dc"/><path id="route" d="M100 238H270Q300 238 300 205V118Q300 85 340 85H520Q560 85 560 120V230H705" fill="none" stroke="#365f3b" stroke-width="9" stroke-linecap="round"/><path d="M100 238H270Q300 238 300 205V118Q300 85 340 85H520Q560 85 560 120V230H705" fill="none" stroke="#f4fbda" stroke-width="2" stroke-dasharray="7 8"/><g id="van"><rect x="-12" y="-9" width="24" height="18" rx="2" fill="#b53e32" stroke="#fff" stroke-width="3"/></g><text x="75" y="284" fill="#23402e">Magazyn</text><text x="670" y="280" fill="#23402e">Port</text></svg></section><div class="toolbar"><button id="motion" type="button" aria-pressed="false">Uruchom</button><button id="direction" type="button">Odwróć kierunek</button><output id="route-status" aria-live="polite">Magazyn → Port</output></div></main>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; min-width: 280px; }
body { background: var(--paper); color: var(--ink); font: 16px/1.5 Arial, sans-serif; }
.pen { min-height: 100vh; padding: clamp(16px, 3vw, 30px); display: flex; flex-direction: column; gap: 16px; max-width: 1120px; margin: auto; }
header { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
h1 { font-size: clamp(26px, 4.1vw, 42px); line-height: 1.08; margin: 0 0 8px; letter-spacing: -.04em; }
p { margin: 0; max-width: 64ch; }
.scene { position: relative; min-height: 230px; flex: 1; display: grid; place-items: center; overflow: hidden; }
.scene canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
.toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 12px 20px; }
.toolbar label { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; min-height: 44px; }
button, select { font: inherit; color: var(--ink); background: var(--paper); border: 1px solid currentColor; border-radius: 0; min-height: 44px; padding: 8px 15px; cursor: pointer; }
button:hover { background: var(--ink); color: var(--paper); }
button:focus-visible, select:focus-visible, input:focus-visible { outline: 3px solid var(--accent); outline-offset: 4px; }
input[type=range] { width: 145px; max-width: 42vw; accent-color: var(--accent); min-height: 36px; cursor: pointer; }
output, .hint { font-size: 14px; }
[data-motion="paused"] .animated { animation-play-state: paused !important; }
@media (max-width: 500px) { header { gap: 6px; } .scene { min-height: 280px; } .toolbar { gap: 10px; } .toolbar label { font-size: 14px; } }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0s !important; scroll-behavior: auto !important; } }
body{--paper:#e9f1dc;--ink:#23402e;--accent:#a93428}.scene{background:#d7e5c7}svg{width:100%;height:100%;max-height:370px}svg text{font:16px Arial,sans-serif}
JavaScript
(() => {
const root = document.querySelector('.pen');
const preference = matchMedia('(prefers-reduced-motion: reduce)');
const motionButton = document.querySelector('#motion');
let playing = !preference.matches, elapsed = 0, paint = null;
function syncMotion() {
root.dataset.motion = playing ? 'running' : 'paused';
motionButton.textContent = playing ? 'Wstrzymaj' : 'Uruchom';
motionButton.setAttribute('aria-pressed', String(playing));
}
motionButton.addEventListener('click', () => { playing = !playing; syncMotion(); });
preference.addEventListener('change', event => { playing = !event.matches; syncMotion(); });
syncMotion();
function run(draw) {
paint = draw;
draw(0);
let last = performance.now(), lastPaint = 0;
function frame(now) {
if (playing && !document.hidden) { elapsed += Math.min(now - last, 48) / 1000; if (now - lastPaint >= 30) { draw(elapsed); lastPaint = now; } }
last = now;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
function canvasSurface(selector) {
const canvas = document.querySelector(selector), context = canvas.getContext('2d');
if (!context) { canvas.replaceWith(document.createTextNode('Ta wizualizacja wymaga obsługi Canvas 2D.')); return null; }
const surface = { canvas, context, width: 0, height: 0 };
function resize() {
const box = canvas.parentElement.getBoundingClientRect(), ratio = Math.min(devicePixelRatio || 1, 2);
surface.width = box.width; surface.height = box.height;
canvas.width = Math.max(1, Math.round(box.width * ratio)); canvas.height = Math.max(1, Math.round(box.height * ratio));
context.setTransform(ratio, 0, 0, ratio, 0, 0);
if (paint) paint(elapsed);
}
resize(); new ResizeObserver(resize).observe(canvas.parentElement);
return surface;
}
const route=document.querySelector('#route'),van=document.querySelector('#van'),length=route.getTotalLength();let direction=1;
document.querySelector('#direction').addEventListener('click',()=>{direction*=-1;document.querySelector('#route-status').value=direction===1?'Magazyn → Port':'Port → Magazyn';if(paint)paint(elapsed);});
run(time=>{let distance=(time*58)%length;if(direction<0)distance=length-distance;const point=route.getPointAtLength(distance),near=route.getPointAtLength(Math.max(0,Math.min(length,distance+direction*2)));van.setAttribute('transform',`translate(${point.x} ${point.y}) rotate(${Math.atan2(near.y-point.y,near.x-point.x)*180/Math.PI})`);});
})();
Edytuj swój pen
Podaj adres e-mail użyty przy zapisie. Wyślemy link do edycji.