Live preview
Попробуйте механику
02
Что показывает эта механика
Показывает, как семантика, структура, контент, дизайн, HTML и аналитика собираются в устойчивую систему.
Где использовать
Веб-разработка, SaaS, обучение, архитектура продукта. Для коммерческого проекта меняются правила, визуальный язык, тексты, аналитические события и действие после завершения.
Технологии
Семантический HTML, адаптивный CSS, Canvas API, Pointer Events и requestAnimationFrame. Внешние библиотеки и графические ассеты не требуются.
Полный исходный код / 63 строк
<!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>Башня сайта — mobile</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:80%;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 / Web system</p><h1 id="title">Башня сайта</h1><div class="num" aria-hidden="true">01</div><p class="copy">Поставьте блоки точно друг на друга. Сильный сайт держится на системе: семантика, структура, контент, дизайн, код, аналитика и API.</p></header>
<div class="stage">
<div class="canvas-wrap">
<canvas id="game" aria-label="Башня сайта. Нажимайте на поле, чтобы ставить движущийся блок."></canvas>
<div class="hud"><span class="chip" id="level">Слой 0 / 7</span><span class="chip" id="accuracy">Точность —</span></div>
</div>
<div class="status" id="status" aria-live="polite">Нажмите «Начать». Затем тапайте по игровому полю, чтобы ставить блоки.</div>
<div class="actions"><button id="start">Начать</button><button class="secondary" id="reset">Сбросить</button><a class="btn wide" href="https://t.me/DMITRIY_LAVRUSHIN" target="_blank" rel="noopener">Обсудить сайт</a></div>
</div>
<footer class="help"><strong>Mobile:</strong> тап по полю. Desktop: клик или Space. Без библиотек.</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'),levelEl=document.getElementById('level'),accEl=document.getElementById('accuracy');
const names=['СЕМАНТИКА','СТРУКТУРА','КОНТЕНТ','ДИЗАЙН','HTML','АНАЛИТИКА','API'];
let W=320,H=420,dpr=1,raf=0,running=false,ended=false,last=0,blocks=[],mover=null,frags=[],lvl=0,sum=0;
function track(action,detail={}){const p={event:'interactive_game',game:'tower',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(340,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);if(!blocks.length)init();draw();}
function init(){cancelAnimationFrame(raf);running=false;ended=false;last=0;lvl=0;sum=0;frags=[];const w=Math.min(260,W*.64),h=Math.max(34,Math.min(42,W*.1));blocks=[{x:(W-w)/2,y:H-50,w,h,label:'ЗАДАЧА'}];mover=null;levelEl.textContent='Слой 0 / '+names.length;accEl.textContent='Точность —';status.textContent='Нажмите «Начать». Затем тапайте по игровому полю, чтобы ставить блоки.';start.textContent='Начать';}
function spawn(){const p=blocks[blocks.length-1],dir=lvl%2===0?1:-1;mover={x:dir>0?-p.w-8:W+8,y:p.y-p.h-5,w:p.w,h:p.h,v:dir*(125+lvl*12),label:names[lvl]};}
function begin(){if(running)return;if(ended||lvl>=names.length)init();if(!mover)spawn();running=true;ended=false;last=performance.now();start.textContent='Игра идёт';status.textContent='Поставьте «'+mover.label+'» на предыдущий слой.';track('start');raf=requestAnimationFrame(loop);}
function drop(){if(!running||!mover)return;const p=blocks[blocks.length-1],L=Math.max(mover.x,p.x),R=Math.min(mover.x+mover.w,p.x+p.w),o=R-L;if(o<=4){finish(false);return;}const precision=Math.max(0,Math.min(100,Math.round(o/Math.min(mover.w,p.w)*100)));sum+=precision;blocks.push({x:L,y:mover.y,w:o,h:mover.h,label:mover.label});lvl++;levelEl.textContent='Слой '+lvl+' / '+names.length;accEl.textContent='Точность '+Math.round(sum/lvl)+'%';track('drop',{level:lvl,precision});if(lvl>=names.length){finish(true);return;}mover=null;spawn();status.textContent=(precision>92?'Точно. ':'Система держится. ')+'Следующий слой: «'+mover.label+'».';}
function finish(ok){running=false;ended=true;cancelAnimationFrame(raf);start.textContent=ok?'Собрать ещё раз':'Попробовать ещё';if(ok){const a=Math.round(sum/names.length);status.innerHTML='<strong>Система собрана.</strong> 7 слоёв, средняя точность '+a+'%. Такой сайт можно развивать, а не переделывать.';track('complete',{accuracy:a});}else{status.innerHTML='<strong>Башня потеряла устойчивость.</strong> Слабый фундамент сокращает запас для роста.';track('fail',{level:lvl});}draw(ok?'СИСТЕМА СОБРАНА':'СИСТЕМА НЕ СОБРАНА');}
function loop(t){if(!running)return;const dt=Math.min(.032,(t-last)/1000||.016);last=t;mover.x+=mover.v*dt;if(mover.v>0&&mover.x>W+12)mover.x=-mover.w-12;if(mover.v<0&&mover.x+mover.w<-12)mover.x=W+12;draw();raf=requestAnimationFrame(loop);}
function block(b,i){x.fillStyle=i===blocks.length-1&&i>0?'#ff5a1f':'#111';x.fillRect(b.x,b.y,b.w,b.h);x.fillStyle='#f3f0e9';x.font='800 '+Math.max(8,Math.min(11,b.w/18))+'px Arial';x.textAlign='center';x.textBaseline='middle';x.save();x.beginPath();x.rect(b.x,b.y,b.w,b.h);x.clip();x.fillText(b.label,b.x+b.w/2,b.y+b.h/2);x.restore();}
function draw(message=''){x.clearRect(0,0,W,H);x.fillStyle='#f7f4ed';x.fillRect(0,0,W,H);x.strokeStyle='rgba(17,17,17,.10)';x.lineWidth=1;for(let y=0;y<H;y+=32){x.beginPath();x.moveTo(0,y);x.lineTo(W,y);x.stroke()}blocks.forEach(block);if(mover){x.fillStyle='#ff5a1f';x.fillRect(mover.x,mover.y,mover.w,mover.h);x.fillStyle='#fff';x.textAlign='center';x.textBaseline='middle';x.font='800 '+Math.max(8,Math.min(11,mover.w/18))+'px Arial';x.save();x.beginPath();x.rect(mover.x,mover.y,mover.w,mover.h);x.clip();x.fillText(mover.label,mover.x+mover.w/2,mover.y+mover.h/2);x.restore();}if(message){x.fillStyle='rgba(17,17,17,.86)';x.fillRect(12,H/2-42,W-24,84);x.strokeStyle='#f3f0e9';x.strokeRect(12,H/2-42,W-24,84);x.fillStyle='#f3f0e9';x.textAlign='center';x.textBaseline='middle';x.font='800 '+Math.max(15,Math.min(24,W/15))+'px Arial';x.fillText(message,W/2,H/2);}}
function act(e){if(e&&e.cancelable)e.preventDefault();if(!running)begin();else drop();}
c.addEventListener('pointerdown',act,{passive:false});c.addEventListener('touchstart',act,{passive:false});document.addEventListener('keydown',e=>{if(e.code==='Space'){e.preventDefault();act(e);}});start.addEventListener('click',begin);reset.addEventListener('click',()=>{init();draw();track('reset')});
if('ResizeObserver'in window)new ResizeObserver(fit).observe(c);else addEventListener('resize',fit,{passive:true});requestAnimationFrame(fit);
})();
</script>
</body></html>
