07 / Canvas / один HTML-файл

Пинбол пути

Проводит пользователя через статью, кейс, стоимость, доверие и CTA.

Пинбол пути — JavaScript и Canvas
пинбол на JavaScript CanvasБез библиотек

Live preview

Попробуйте механику

Адаптировать
02

Что показывает эта механика

Проводит пользователя через статью, кейс, стоимость, доверие и CTA.

Где использовать

Воронка, навигация, контентные проекты. Для коммерческого проекта меняются правила, визуальный язык, тексты, аналитические события и действие после завершения.

Технологии

Семантический HTML, адаптивный CSS, Canvas API, Pointer Events и requestAnimationFrame. Внешние библиотеки и графические ассеты не требуются.

Полный исходный код / 12 строк
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#f3f0e9"><meta name="robots" content="noindex, nofollow"><title>Пинбол пути — Lavrushin Interactive</title><style>
:root{--bg:#f3f0e9;--ink:#111;--accent:#ff5a1f;--muted:#6f6b64;--line:rgba(17,17,17,.16)}
*{box-sizing:border-box}html,body{margin:0;min-height:100%;background:var(--bg);color:var(--ink);font-family:Arial,Helvetica,sans-serif;-webkit-text-size-adjust:100%}
body{padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)}main{width:100%;max-width:760px;margin:0 auto;padding:14px}.card{border:1px solid var(--ink);overflow:hidden;background:var(--bg)}
.head{position:relative;padding:20px 18px 16px;border-bottom:1px solid var(--ink)}.kicker{margin:0 0 10px;font-size:10px;letter-spacing:.16em;text-transform:uppercase}.num{position:absolute;right:16px;top:16px;font-size:58px;line-height:.8;font-weight:900;color:var(--accent);opacity:.17;letter-spacing:-.08em}
h1{margin:0;max-width:82%;font-size:clamp(36px,11vw,64px);line-height:.9;letter-spacing:-.055em}.copy{margin:14px 0 0;max-width:620px;font-size:14px;line-height:1.5;color:#34312d}.stage{padding:12px}.canvas-wrap{position:relative;border:1px solid var(--ink);overflow:hidden;background:#fff9}
canvas{display:block;width:100%;height:clamp(360px,58svh,500px);touch-action:none;-webkit-user-select:none;user-select:none}.hud{position:absolute;left:10px;right:10px;top:10px;display:flex;justify-content:space-between;gap:8px;pointer-events:none}.chip{background:var(--bg);border:1px solid var(--ink);padding:7px 9px;font-size:9px;letter-spacing:.08em;text-transform:uppercase;font-weight:800}.status{min-height:62px;padding:13px 2px 8px;font-size:14px;line-height:1.45}.actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.actions .wide{grid-column:1/-1}
button,a.btn{min-height:46px;border:1px solid var(--ink);border-radius:0;background:var(--ink);color:var(--bg);padding:12px 14px;font:800 11px/1 Arial,sans-serif;letter-spacing:.07em;text-transform:uppercase;text-decoration:none;display:flex;align-items:center;justify-content:center;-webkit-tap-highlight-color:transparent}button.secondary{background:transparent;color:var(--ink)}.help{padding:13px 18px;border-top:1px solid var(--ink);font-size:11px;line-height:1.45;color:var(--muted)}.help strong{color:var(--ink)}
@media(min-width:700px){main{padding:24px}.head{padding:24px}.stage{padding:18px}.actions{display:flex;justify-content:flex-end}.actions .wide{grid-column:auto}.status{min-height:48px}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
</style></head><body><main><section class="card" aria-labelledby="title"><header class="head"><p class="kicker">Interactive demo / User journey</p><h1 id="title">Пинбол пути</h1><div class="num" aria-hidden="true">06</div><p class="copy">Не дайте пользователю выпасть из сценария. Проведите его через контент, кейс, цену, доверие и действие.</p></header><div class="stage"><div class="canvas-wrap"><canvas id="game" aria-label="Игра Пинбол пользовательского пути"></canvas><div class="hud"><span class="chip" id="score">0 очков</span><span class="chip" id="path">0 / 5 точек</span></div></div><div class="status" id="status" aria-live="polite">Проведите пользователя через ARTICLE → CASE → PRICE → TRUST → CTA. Используйте флипперы внизу.</div><div class="actions"><button id="left">◀ Левый</button><button id="right">Правый ▶</button><button class="secondary wide" id="start">Запустить шар</button></div></div><footer class="help"><strong>Mobile-first:</strong> touch, Pointer Events, safe-area. Без библиотек и внешних ассетов.</footer></section></main><script>(()=>{'use strict';const c=document.getElementById('game'),x=c.getContext('2d'),scoreEl=document.getElementById('score'),pathEl=document.getElementById('path'),status=document.getElementById('status');let W=320,H=430,dpr=1,raf=0,last=0,running=false,ball={x:0,y:0,vx:0,vy:0,r:10},balls=3,score=0,hit=new Set(),LA=false,RA=false;const names=['ARTICLE','CASE','PRICE','TRUST','CTA'];let bumpers=[];
function track(action,d={}){const p={event:'interactive_game',game:'pinball-lite',action,...d};window.dataLayer=window.dataLayer||[];window.dataLayer.push(p)}function fit(){const r=c.getBoundingClientRect();W=Math.max(280,Math.round(r.width));H=Math.max(360,Math.round(r.height));dpr=Math.min(2,devicePixelRatio||1);c.width=W*dpr;c.height=H*dpr;x.setTransform(dpr,0,0,dpr,0,0);bumpers=[{x:W*.28,y:H*.24,n:0},{x:W*.70,y:H*.23,n:1},{x:W*.49,y:H*.42,n:2},{x:W*.25,y:H*.55,n:3},{x:W*.72,y:H*.58,n:4}];resetBall();draw()}function resetBall(){ball={x:W*.5,y:H*.12,vx:(Math.random()-.5)*85,vy:60,r:10}}function collideB(b){const dx=ball.x-b.x,dy=ball.y-b.y,d=Math.hypot(dx,dy),R=31+ball.r;if(d<R){const nx=dx/(d||1),ny=dy/(d||1),dot=ball.vx*nx+ball.vy*ny;ball.vx-=2*dot*nx;ball.vy-=2*dot*ny;ball.vx*=1.02;ball.vy*=1.02;ball.x=b.x+nx*R;ball.y=b.y+ny*R;score+=100;if(!hit.has(b.n)){hit.add(b.n);track('path_hit',{step:names[b.n]})}scoreEl.textContent=score+' очков';pathEl.textContent=hit.size+' / 5 точек'}}function flipper(side){const y=H-48,w=W*.22,xx=side==='l'?W*.28:W*.72,active=side==='l'?LA:RA,angle=(side==='l'?-1:1)*(active?.58:.17);return{x:xx,y,w,angle}}function collideF(f){const dx=ball.x-f.x,dy=ball.y-f.y,ca=Math.cos(-f.angle),sa=Math.sin(-f.angle),lx=dx*ca-dy*sa,ly=dx*sa+dy*ca;if(Math.abs(lx)<f.w/2+ball.r&&Math.abs(ly)<8+ball.r&&ball.vy>0){ball.vy=-Math.abs(ball.vy)-110;ball.vx+=Math.sin(f.angle)*170;score+=25}}
function loop(t){const dt=Math.min(.025,(t-last)/1000||.016);last=t;if(running){ball.vy+=315*dt;ball.x+=ball.vx*dt;ball.y+=ball.vy*dt;if(ball.x<ball.r){ball.x=ball.r;ball.vx=Math.abs(ball.vx)}if(ball.x>W-ball.r){ball.x=W-ball.r;ball.vx=-Math.abs(ball.vx)}if(ball.y<ball.r){ball.y=ball.r;ball.vy=Math.abs(ball.vy)}bumpers.forEach(collideB);collideF(flipper('l'));collideF(flipper('r'));if(ball.y>H+25){balls--;if(balls<=0){running=false;status.innerHTML='<strong>'+(hit.size===5?'Маршрут пройден.':'Часть пути потеряна.')+'</strong> Пользователь прошёл '+hit.size+' из 5 ключевых точек.';track('complete',{score,path:hit.size})}else resetBall()}}draw();raf=requestAnimationFrame(loop)}function draw(){x.clearRect(0,0,W,H);x.fillStyle='#f7f4ed';x.fillRect(0,0,W,H);x.strokeStyle='rgba(17,17,17,.12)';x.strokeRect(8,8,W-16,H-16);for(const b of bumpers){x.fillStyle=hit.has(b.n)?'#ff5a1f':'#111';x.beginPath();x.arc(b.x,b.y,31,0,Math.PI*2);x.fill();x.fillStyle='#f3f0e9';x.font='800 8px Arial';x.textAlign='center';x.fillText(names[b.n],b.x,b.y+3)}for(const side of ['l','r']){const f=flipper(side);x.save();x.translate(f.x,f.y);x.rotate(f.angle);x.strokeStyle=side==='l'?'#111':'#ff5a1f';x.lineWidth=12;x.lineCap='round';x.beginPath();x.moveTo(-f.w/2,0);x.lineTo(f.w/2,0);x.stroke();x.restore()}x.fillStyle='#ff5a1f';x.beginPath();x.arc(ball.x,ball.y,ball.r,0,Math.PI*2);x.fill();if(!running){x.fillStyle='rgba(17,17,17,.82)';x.fillRect(12,H/2-38,W-24,76);x.fillStyle='#f3f0e9';x.font='800 18px Arial';x.textAlign='center';x.fillText('USER JOURNEY',W/2,H/2+6)}}function set(side,val){if(side==='l')LA=val;else RA=val}for(const [id,side] of [['left','l'],['right','r']]){const el=document.getElementById(id);el.addEventListener('pointerdown',e=>{e.preventDefault();set(side,true)},{passive:false});['pointerup','pointercancel','pointerleave'].forEach(ev=>el.addEventListener(ev,()=>set(side,false)))}c.addEventListener('pointerdown',e=>{e.preventDefault();const r=c.getBoundingClientRect(),xx=e.clientX-r.left,yy=e.clientY-r.top;if(yy>H*.55)set(xx<W/2?'l':'r',true)},{passive:false});c.addEventListener('pointerup',()=>{LA=RA=false});document.getElementById('start').onclick=()=>{balls=3;score=0;hit.clear();running=true;scoreEl.textContent='0 очков';pathEl.textContent='0 / 5 точек';status.textContent='Проведите пользователя через ARTICLE → CASE → PRICE → TRUST → CTA. Используйте флипперы внизу.';resetBall();track('start')};if('ResizeObserver'in window)new ResizeObserver(fit).observe(c);else addEventListener('resize',fit);requestAnimationFrame(fit);last=performance.now();raf=requestAnimationFrame(loop)})();</script></body></html>