Live preview
Попробуйте механику
02
Что показывает эта механика
Превращает путь от поискового запроса до заявки в механику удержания внимания.
Где использовать
Контент, маркетинг, пользовательский путь. Для коммерческого проекта меняются правила, визуальный язык, тексты, аналитические события и действие после завершения.
Технологии
Семантический HTML, адаптивный CSS, Canvas API, Pointer Events и requestAnimationFrame. Внешние библиотеки и графические ассеты не требуются.
Полный исходный код / 35 строк
<!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="#111">
<meta name="robots" content="noindex, nofollow"><title>Пинг-понг внимания — mobile</title>
<style>
:root{--bg:#f3f0e9;--ink:#111;--accent:#ff5a1f;--muted:#6f6b64}*{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{max-width:760px;margin:auto;padding:14px}.card{border:1px solid var(--ink);overflow:hidden}.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:80%;font-size:clamp(34px,10.5vw,62px);line-height:.9;letter-spacing:-.055em}.copy{margin:14px 0 0;font-size:14px;line-height:1.5;color:#34312d}.stage{padding:12px}.canvas-wrap{position:relative;border:1px solid var(--ink);background:var(--ink);overflow:hidden}canvas{display:block;width:100%;height:clamp(340px,54svh,470px);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(--bg);padding:7px 9px;font-size:9px;letter-spacing:.07em;text-transform:uppercase;font-weight:800}.status{min-height:64px;padding:13px 2px 8px;font-size:14px;line-height:1.45}.mobile-controls{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px}.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,.mobile-controls button{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}.mobile-controls{display:none}.actions{display:flex;justify-content:flex-end}.actions .wide{grid-column:auto}.status{min-height:48px}}
</style>
</head><body><main><section class="card"><header class="head"><p class="kicker">Interactive demo / Attention</p><h1>Пинг-понг внимания</h1><div class="num">02</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 / 6</span><span class="chip" id="factor">Фактор: —</span></div></div><div class="status" id="status" aria-live="polite">Нажмите «Начать», затем ведите пальцем вверх и вниз по полю.</div><div class="mobile-controls"><button id="up" type="button">↑ Выше</button><button id="down" type="button">↓ Ниже</button></div><div class="actions"><button id="start" type="button">Начать</button><button class="secondary" id="reset" type="button">Сбросить</button><a class="btn wide" href="https://t.me/DMITRIY_LAVRUSHIN" target="_blank" rel="noopener">Обсудить сайт</a></div></div><footer class="help"><strong>Mobile:</strong> ведите пальцем по полю или используйте кнопки ↑ / ↓. Без библиотек.</footer></section></main>
<script>
(()=>{'use strict';
const c=document.getElementById('game'),x=c.getContext('2d'),start=document.getElementById('start'),reset=document.getElementById('reset'),status=document.getElementById('status'),score=document.getElementById('score'),factor=document.getElementById('factor'),up=document.getElementById('up'),down=document.getElementById('down');
const stages=['ЗАПРОС','КЛИК','ИНТЕРЕС','ДОВЕРИЕ','ЗАЯВКА','ВОЗВРАТ'],factors=['ПОНЯТНЫЙ ОФФЕР','ПОЛЕЗНЫЙ КОНТЕНТ','КЕЙС','СКОРОСТЬ','ПЕРЕЛИНКОВКА','АНАЛИТИКА'];
let W=320,H=400,dpr=1,raf=0,running=false,ended=false,last=0,hits=0,ball={},left={},right={},keys=new Set(),trail=[];
function track(action,detail={}){const p={event:'interactive_game',game:'attention-pong',action,...detail};window.dataLayer=window.dataLayer||[];window.dataLayer.push(p);window.dispatchEvent(new CustomEvent('lavrushin:game',{detail:p}));}
function fit(){const r=c.getBoundingClientRect();W=Math.max(280,Math.round(r.width));H=Math.max(320,Math.round(r.height));dpr=Math.min(2,window.devicePixelRatio||1);c.width=Math.round(W*dpr);c.height=Math.round(H*dpr);x.setTransform(dpr,0,0,dpr,0,0);setup(false);draw();}
function setup(resetHits=true){if(resetHits)hits=0;const ph=Math.max(70,Math.min(96,H*.22));left={x:14,y:H/2-ph/2,w:10,h:ph};right={x:W-24,y:H/2-ph/2,w:10,h:ph};ball={x:W*.42,y:H/2,r:7,vx:Math.max(210,W*.55),vy:80,label:stages[Math.min(hits,stages.length-1)]};trail=[];score.textContent='Удержано '+hits+' / '+stages.length;factor.textContent='Фактор: '+(hits?factors[Math.min(hits-1,factors.length-1)]:'—');}
function init(){cancelAnimationFrame(raf);running=false;ended=false;hits=0;setup(true);start.textContent='Начать';status.textContent='Нажмите «Начать», затем ведите пальцем вверх и вниз по полю.';draw();}
function begin(){if(running)return;if(ended||hits>=stages.length)init();setup(false);running=true;ended=false;last=performance.now();start.textContent='Игра идёт';status.textContent='Удерживайте «'+ball.label+'» на маршруте.';track('start');raf=requestAnimationFrame(loop);}
function moveTo(clientY){const r=c.getBoundingClientRect();const y=(clientY-r.top)*(H/r.height);right.y=Math.max(8,Math.min(H-right.h-8,y-right.h/2));}
function hit(p,goingRight){const bx=ball.x,by=ball.y,r=ball.r;if(!(by+r>p.y&&by-r<p.y+p.h))return false;if(goingRight&&bx+r>=p.x&&bx-r<=p.x+p.w){ball.x=p.x-r-1;return true}if(!goingRight&&bx-r<=p.x+p.w&&bx+r>=p.x){ball.x=p.x+p.w+r+1;return true}return false;}
function bounce(p,toLeft){const rel=(ball.y-(p.y+p.h/2))/(p.h/2),s=Math.min(430,Math.hypot(ball.vx,ball.vy)*1.025);ball.vx=(toLeft?-1:1)*s;ball.vy=rel*s*.72;}
function returned(){hits++;const i=Math.min(hits-1,factors.length-1);score.textContent='Удержано '+hits+' / '+stages.length;factor.textContent='Фактор: '+factors[i];track('return',{hits,factor:factors[i]});if(hits>=stages.length){finish(true);return;}ball.label=stages[hits];status.textContent=factors[i]+' удерживает внимание. Следующий этап: '+stages[hits]+'.';}
function finish(ok){running=false;ended=true;cancelAnimationFrame(raf);start.textContent=ok?'Сыграть ещё':'Попробовать ещё';status.innerHTML=ok?'<strong>Маршрут завершён.</strong> Запрос превратился в возврат. Поиск даёт вход, система сайта удерживает внимание.':'<strong>Внимание потеряно.</strong> Клик сам по себе ничего не гарантирует — следующий шаг должен быть очевидным.';track(ok?'complete':'fail',{hits});draw(ok?'ЗАПРОС → ВОЗВРАТ':'ВНИМАНИЕ ПОТЕРЯНО');}
function loop(t){if(!running)return;const dt=Math.min(.03,(t-last)/1000||.016);last=t;const s=360*dt;if(keys.has('up'))right.y-=s;if(keys.has('down'))right.y+=s;right.y=Math.max(8,Math.min(H-right.h-8,right.y));const target=ball.y-left.h/2,leftStep=Math.max(-260*dt,Math.min(260*dt,target-left.y));left.y=Math.max(8,Math.min(H-left.h-8,left.y+leftStep));trail.push({x:ball.x,y:ball.y,a:1});if(trail.length>10)trail.shift();trail.forEach(p=>p.a*=.82);ball.x+=ball.vx*dt;ball.y+=ball.vy*dt;if(ball.y-ball.r<8){ball.y=8+ball.r;ball.vy=Math.abs(ball.vy)}if(ball.y+ball.r>H-8){ball.y=H-8-ball.r;ball.vy=-Math.abs(ball.vy)}if(ball.vx<0&&hit(left,false))bounce(left,false);if(ball.vx>0&&hit(right,true)){bounce(right,true);returned();if(!running)return;}if(ball.x>W+28){finish(false);return;}if(ball.x<-28){ball.x=W*.35;ball.y=H/2;ball.vx=Math.abs(ball.vx||240)}draw();raf=requestAnimationFrame(loop);}
function draw(message=''){x.clearRect(0,0,W,H);x.fillStyle='#111';x.fillRect(0,0,W,H);x.strokeStyle='rgba(243,240,233,.12)';for(let y=0;y<H;y+=34){x.beginPath();x.moveTo(0,y);x.lineTo(W,y);x.stroke()}x.setLineDash([6,9]);x.strokeStyle='rgba(243,240,233,.32)';x.beginPath();x.moveTo(W/2,10);x.lineTo(W/2,H-10);x.stroke();x.setLineDash([]);x.fillStyle='#f3f0e9';x.font='800 9px Arial';x.textAlign='left';x.fillText('ПОИСК',14,H-13);x.textAlign='right';x.fillText('САЙТ',W-14,H-13);trail.forEach((p,i)=>{x.save();x.globalAlpha=p.a*.3;x.fillStyle='#ff5a1f';x.beginPath();x.arc(p.x,p.y,Math.max(2,ball.r*(i+1)/trail.length),0,Math.PI*2);x.fill();x.restore()});x.fillStyle='#f3f0e9';x.fillRect(left.x,left.y,left.w,left.h);x.fillStyle='#ff5a1f';x.fillRect(right.x,right.y,right.w,right.h);x.fillStyle='#ff5a1f';x.beginPath();x.arc(ball.x,ball.y,ball.r,0,Math.PI*2);x.fill();x.font='800 9px Arial';x.textAlign='center';const tw=x.measureText(ball.label).width+12;x.fillStyle='#f3f0e9';x.fillRect(ball.x-tw/2,ball.y-28,tw,17);x.fillStyle='#111';x.fillText(ball.label,ball.x,ball.y-16);if(!running&&!ended&&!message){x.fillStyle='rgba(17,17,17,.50)';x.fillRect(0,0,W,H);x.fillStyle='#f3f0e9';x.font='800 17px Arial';x.fillText('SEARCH → SITE',W/2,H/2-4);x.fillStyle='#ff5a1f';x.font='11px Arial';x.fillText('ведите пальцем по правой стороне',W/2,H/2+20)}if(message){x.fillStyle='rgba(17,17,17,.84)';x.fillRect(10,H/2-44,W-20,88);x.strokeStyle='#f3f0e9';x.strokeRect(10,H/2-44,W-20,88);x.fillStyle='#f3f0e9';x.font='800 '+Math.max(14,Math.min(22,W/16))+'px Arial';x.fillText(message,W/2,H/2+5);}}
function pointer(e){if(e.cancelable)e.preventDefault();if(!running)begin();moveTo(e.clientY);}
c.addEventListener('pointerdown',pointer,{passive:false});c.addEventListener('pointermove',e=>{if(running&&e.buttons!==0)pointer(e)},{passive:false});c.addEventListener('touchstart',e=>{if(e.cancelable)e.preventDefault();if(!running)begin();if(e.touches[0])moveTo(e.touches[0].clientY)},{passive:false});c.addEventListener('touchmove',e=>{if(e.cancelable)e.preventDefault();if(running&&e.touches[0])moveTo(e.touches[0].clientY)},{passive:false});
function hold(btn,key){const on=e=>{e.preventDefault();keys.add(key);if(!running)begin()},off=e=>{e.preventDefault();keys.delete(key)};btn.addEventListener('pointerdown',on);btn.addEventListener('pointerup',off);btn.addEventListener('pointercancel',off);btn.addEventListener('pointerleave',off);}
hold(up,'up');hold(down,'down');start.addEventListener('click',begin);reset.addEventListener('click',()=>{init();track('reset')});document.addEventListener('keydown',e=>{if(e.code==='ArrowUp'){keys.add('up');e.preventDefault()}if(e.code==='ArrowDown'){keys.add('down');e.preventDefault()}});document.addEventListener('keyup',e=>{if(e.code==='ArrowUp')keys.delete('up');if(e.code==='ArrowDown')keys.delete('down')});
if('ResizeObserver'in window)new ResizeObserver(fit).observe(c);else addEventListener('resize',fit,{passive:true});requestAnimationFrame(()=>{fit();init();});
})();
</script></body></html>
