|
|
@@ -359,29 +359,43 @@
|
|
|
<button class="btn" id="btnNext4" type="button" disabled>Далее →</button>
|
|
|
</section>
|
|
|
|
|
|
- <!-- SCENE 5 -->
|
|
|
+ <!-- SCENE 5 -->
|
|
|
<section class="scene" id="scene5">
|
|
|
<h2>Виды волн. Звук — продольная волна</h2>
|
|
|
+
|
|
|
<div class="controls">
|
|
|
<button class="btn" id="btnLong" type="button">Продольная</button>
|
|
|
<button class="btn pink" id="btnTrans" type="button">Поперечная</button>
|
|
|
</div>
|
|
|
- <div class="canvas-wrap"><canvas id="typeCanvas" width="800" height="200"></canvas></div>
|
|
|
|
|
|
- <div class="controls">
|
|
|
- <div class="slider-box">
|
|
|
- <label>Частота волны: <span id="typeFreqVal">3</span> Гц</label>
|
|
|
- <input type="range" id="typeFreqSlider" min="1" max="10" value="3">
|
|
|
+ <div class="canvas-wrap" style="position:relative;">
|
|
|
+ <canvas id="typeCanvas" width="800" height="220"></canvas>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 🎮 Мини-игра -->
|
|
|
+ <div id="waveGame" style="max-width:720px;text-align:center;">
|
|
|
+ <p style="margin-bottom:8px;">
|
|
|
+ 🎯 <b>Мини-игра:</b> нажми <b>«Толкнуть поршень»</b> и кликай по
|
|
|
+ <span style="color:var(--neon-pink);font-weight:800;">розовым сгусткам (сжатиям)</span>,
|
|
|
+ когда они проплывают мимо.
|
|
|
+ </p>
|
|
|
+ <div class="controls">
|
|
|
+ <button class="btn green" id="btnPiston" type="button">🔊 Толкнуть поршень</button>
|
|
|
+ <button class="btn pink" id="btnReset5" type="button">🔄 Сброс</button>
|
|
|
</div>
|
|
|
+ <p class="hint" id="scene5Hint">Поймано сжатий: 0 / 5</p>
|
|
|
</div>
|
|
|
|
|
|
- <div class="table" style="max-width:720px;">
|
|
|
- 💡 <b>Главное:</b> молекулы среды <span>колеблются на месте</span>, а волна переносит
|
|
|
- <span>энергию</span>, а не вещество.<br>
|
|
|
- 🔊 В продольной волне чередуются <span>сжатия</span> и <span>разрежения</span> — так звук идёт через воздух.
|
|
|
+ <!-- 💡 Пояснение (появляется после победы) -->
|
|
|
+ <div id="waveFact" class="table" style="max-width:720px;display:none;">
|
|
|
+ 🎓 <b>Что ты сейчас сделал:</b><br>
|
|
|
+ Поршень толкает молекулы → они <span>сжимаются</span>, потом расходятся → возникает
|
|
|
+ <span>разрежение</span>. Волна <span>бежит вправо</span>, а каждая молекула
|
|
|
+ <span>колеблется на месте</span> и никуда не улетает.<br>
|
|
|
+ 🔊 Именно так звук распространяется через воздух — продольной волной!
|
|
|
</div>
|
|
|
|
|
|
- <button class="btn" id="btnNext5" type="button">Далее →</button>
|
|
|
+ <button class="btn" id="btnNext5" type="button" disabled>Далее →</button>
|
|
|
</section>
|
|
|
|
|
|
<!-- SCENE 6 -->
|
|
|
@@ -814,39 +828,102 @@ document.getElementById('btnCheck4').addEventListener('click', function(){
|
|
|
});
|
|
|
document.getElementById('btnNext4').addEventListener('click', function(){ goToScene(4); });
|
|
|
|
|
|
-/* ================= SCENE 5: виды волн ================= */
|
|
|
+/* ================= SCENE 5: виды волн + мини-игра ================= */
|
|
|
var typeCanvas = document.getElementById('typeCanvas');
|
|
|
var typeCtx = typeCanvas.getContext('2d');
|
|
|
var typeT = 0;
|
|
|
var waveType = 'long'; // 'long' | 'trans'
|
|
|
-var typeFreq = 3; // 🎚️ частота волны (1–10 Гц)
|
|
|
|
|
|
-// Слайдер частоты продольной/поперечной волны
|
|
|
-document.getElementById('typeFreqSlider').addEventListener('input', function(e){
|
|
|
- typeFreq = +e.target.value;
|
|
|
- document.getElementById('typeFreqVal').textContent = typeFreq;
|
|
|
- clickSound();
|
|
|
-});
|
|
|
+// ---- Мини-игра ----
|
|
|
+var gameActive = false; // идёт ли волна сейчас
|
|
|
+var gameCaught = 0; // сколько сжатий поймано
|
|
|
+var gamePhase = 0; // фаза волны (0..2π повторяется)
|
|
|
+var gameSpeed = 0.06; // скорость бега волны
|
|
|
+var gameBursts = []; // визуальные "вспышки" от кликов
|
|
|
+var gameZones = []; // текущие зоны сжатий (пересчитывается каждый кадр)
|
|
|
+
|
|
|
+// Параметры волны
|
|
|
+var WAVE_N = 50; // сколько молекул
|
|
|
+var WAVE_LEN = 4; // сколько длин волн на экране
|
|
|
|
|
|
-// Переключатели вида волны
|
|
|
+// 🔘 Переключатели вида волны
|
|
|
document.getElementById('btnLong').addEventListener('click', function(){
|
|
|
waveType = 'long'; clickSound();
|
|
|
+ document.getElementById('waveGame').style.display = 'block';
|
|
|
});
|
|
|
document.getElementById('btnTrans').addEventListener('click', function(){
|
|
|
waveType = 'trans'; clickSound();
|
|
|
+ // В поперечной волне мини-игра не нужна — прячем
|
|
|
+ document.getElementById('waveGame').style.display = 'none';
|
|
|
});
|
|
|
|
|
|
-// Кнопка «Далее»
|
|
|
-document.getElementById('btnNext5').addEventListener('click', function(){ goToScene(5); });
|
|
|
+// 🎮 Кнопка "Толкнуть поршень"
|
|
|
+document.getElementById('btnPiston').addEventListener('click', function(){
|
|
|
+ if(gameActive) return; // уже идёт
|
|
|
+ gameActive = true;
|
|
|
+ gamePhase = 0;
|
|
|
+ gameCaught = 0;
|
|
|
+ gameBursts = [];
|
|
|
+ document.getElementById('scene5Hint').textContent = 'Поймано сжатий: 0 / 5';
|
|
|
+ document.getElementById('waveFact').style.display = 'none';
|
|
|
+ document.getElementById('btnNext5').disabled = true;
|
|
|
+ ensureAudio();
|
|
|
+ beep(180, 0.25, 'sawtooth', 0.20); // низкий "толчок поршня"
|
|
|
+});
|
|
|
+
|
|
|
+// 🔄 Кнопка "Сброс"
|
|
|
+document.getElementById('btnReset5').addEventListener('click', function(){
|
|
|
+ gameActive = false;
|
|
|
+ gameCaught = 0;
|
|
|
+ gamePhase = 0;
|
|
|
+ gameBursts = [];
|
|
|
+ document.getElementById('scene5Hint').textContent = 'Поймано сжатий: 0 / 5';
|
|
|
+ document.getElementById('waveFact').style.display = 'none';
|
|
|
+ document.getElementById('btnNext5').disabled = true;
|
|
|
+ clickSound();
|
|
|
+});
|
|
|
+
|
|
|
+// 👆 Клик по canvas — попытка поймать сжатие
|
|
|
+typeCanvas.addEventListener('click', function(e){
|
|
|
+ if(!gameActive || waveType !== 'long') return;
|
|
|
+ var rect = typeCanvas.getBoundingClientRect();
|
|
|
+ var scaleX = typeCanvas.width / rect.width;
|
|
|
+ var clickX = (e.clientX - rect.left) * scaleX;
|
|
|
+
|
|
|
+ // Ищем, попал ли клик в зону сжатия
|
|
|
+ var hit = false;
|
|
|
+ for(var i=0; i<gameZones.length; i++){
|
|
|
+ var z = gameZones[i];
|
|
|
+ if(clickX >= z.x - z.w/2 && clickX <= z.x + z.w/2){
|
|
|
+ hit = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if(hit){
|
|
|
+ gameCaught++;
|
|
|
+ correctSound();
|
|
|
+ gameBursts.push({ x: clickX, y: typeCanvas.height/2, r: 10, alpha: 1 });
|
|
|
+ document.getElementById('scene5Hint').textContent =
|
|
|
+ 'Поймано сжатий: ' + gameCaught + ' / 5';
|
|
|
+ if(gameCaught >= 5){
|
|
|
+ gameActive = false;
|
|
|
+ document.getElementById('scene5Hint').textContent =
|
|
|
+ '🎉 Отлично! Ты собрал продольную волну!';
|
|
|
+ document.getElementById('waveFact').style.display = 'block';
|
|
|
+ document.getElementById('btnNext5').disabled = false;
|
|
|
+ correctSound();
|
|
|
+ setTimeout(function(){ beep(1320, 0.4, 'triangle', 0.2); }, 200);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ wrongSound();
|
|
|
+ }
|
|
|
+});
|
|
|
|
|
|
// 🔁 Основной цикл анимации
|
|
|
function drawType(){
|
|
|
var W = typeCanvas.width, H = typeCanvas.height;
|
|
|
typeCtx.clearRect(0,0,W,H);
|
|
|
|
|
|
- // 🎚️ Скорость фазы зависит от частоты (чем больше Гц — тем быстрее бежит волна)
|
|
|
- typeT += 0.02 * typeFreq;
|
|
|
-
|
|
|
if(waveType === 'long'){
|
|
|
drawLongitudinal(W, H);
|
|
|
} else {
|
|
|
@@ -855,25 +932,36 @@ function drawType(){
|
|
|
requestAnimationFrame(drawType);
|
|
|
}
|
|
|
|
|
|
-// 🌊 ПРОДОЛЬНАЯ ВОЛНА — сжатия и разрежения
|
|
|
+// 🌊 ПРОДОЛЬНАЯ ВОЛНА — сжатия/разрежения + мини-игра
|
|
|
function drawLongitudinal(W, H){
|
|
|
- var N = 45;
|
|
|
- var spacing = W / (N + 1);
|
|
|
+ var spacing = W / (WAVE_N + 1);
|
|
|
|
|
|
- // Рисуем «коридор» среды
|
|
|
+ // Если игра активна — фаза бежит. Иначе — статичная картинка.
|
|
|
+ if(gameActive){
|
|
|
+ gamePhase += gameSpeed;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Фоновый "коридор" среды
|
|
|
typeCtx.fillStyle = 'rgba(0,229,255,.04)';
|
|
|
- typeCtx.fillRect(0, H/2 - 40, W, 80);
|
|
|
+ typeCtx.fillRect(0, H/2 - 45, W, 90);
|
|
|
+
|
|
|
+ // Определяем зоны сжатий (там, где sin > 0.7)
|
|
|
+ gameZones = [];
|
|
|
|
|
|
- for(var i=0;i<N;i++){
|
|
|
+ // Рисуем молекулы
|
|
|
+ for(var i=0; i<WAVE_N; i++){
|
|
|
var baseX = spacing * (i + 1);
|
|
|
|
|
|
- // Смещение молекулы: колеблется ВОКРУГ своей позиции, но не уезжает вместе с волной
|
|
|
- var shift = Math.sin(typeT - i * 0.35) * 12;
|
|
|
+ // Смещение молекул: колеблются вокруг своей позиции.
|
|
|
+ // Формула: shift = sin(phase - i * k) — классическая продольная волна.
|
|
|
+ var k = (2 * Math.PI * WAVE_LEN) / WAVE_N;
|
|
|
+ var shift = Math.sin(gamePhase - i * k) * 14;
|
|
|
+
|
|
|
var x = baseX + shift;
|
|
|
|
|
|
- // Плотность: сжатие / разрежение (используется для радиуса и яркости)
|
|
|
- var density = 0.5 + 0.5 * Math.sin(typeT - i * 0.35); // 0..1
|
|
|
- var r = 5 + density * 3; // сжатие → крупнее и ярче
|
|
|
+ // Плотность: сжатие / разрежение
|
|
|
+ var density = 0.5 + 0.5 * Math.sin(gamePhase - i * k); // 0..1
|
|
|
+ var r = 4 + density * 4; // сжатие = крупнее
|
|
|
var alpha = 0.35 + density * 0.65;
|
|
|
|
|
|
typeCtx.beginPath();
|
|
|
@@ -881,56 +969,78 @@ function drawLongitudinal(W, H){
|
|
|
typeCtx.shadowBlur = 12; typeCtx.shadowColor = '#ff2ec4';
|
|
|
typeCtx.arc(x, H/2, r, 0, Math.PI*2);
|
|
|
typeCtx.fill();
|
|
|
+
|
|
|
+ // Запоминаем позиции сжатий (для игры)
|
|
|
+ if(density > 0.85){
|
|
|
+ gameZones.push({ x: x, w: spacing * 1.6 });
|
|
|
+ }
|
|
|
}
|
|
|
typeCtx.shadowBlur = 0;
|
|
|
|
|
|
+ // 🎯 Подсветка зон сжатий (пульсирующий кружок)
|
|
|
+ if(gameActive){
|
|
|
+ for(var g=0; g<gameZones.length; g++){
|
|
|
+ var z = gameZones[g];
|
|
|
+ var pulse = 1 + 0.3 * Math.sin(typeT * 6);
|
|
|
+ typeCtx.beginPath();
|
|
|
+ typeCtx.strokeStyle = 'rgba(57,255,20,.6)';
|
|
|
+ typeCtx.lineWidth = 2;
|
|
|
+ typeCtx.shadowBlur = 15; typeCtx.shadowColor = '#39ff14';
|
|
|
+ typeCtx.arc(z.x, H/2, 22 * pulse, 0, Math.PI*2);
|
|
|
+ typeCtx.stroke();
|
|
|
+ }
|
|
|
+ typeCtx.shadowBlur = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ✨ Вспышки от успешных кликов
|
|
|
+ for(var b=gameBursts.length - 1; b >= 0; b--){
|
|
|
+ var burst = gameBursts[b];
|
|
|
+ typeCtx.beginPath();
|
|
|
+ typeCtx.strokeStyle = 'rgba(57,255,20,' + burst.alpha + ')';
|
|
|
+ typeCtx.lineWidth = 3;
|
|
|
+ typeCtx.shadowBlur = 25; typeCtx.shadowColor = '#39ff14';
|
|
|
+ typeCtx.arc(burst.x, burst.y, burst.r, 0, Math.PI*2);
|
|
|
+ typeCtx.stroke();
|
|
|
+ burst.r += 4;
|
|
|
+ burst.alpha -= 0.05;
|
|
|
+ if(burst.alpha <= 0) gameBursts.splice(b, 1);
|
|
|
+ }
|
|
|
+ typeCtx.shadowBlur = 0;
|
|
|
+
|
|
|
+ // 🎚️ ПОРШЕНЬ слева
|
|
|
+ typeCtx.fillStyle = '#39ff14';
|
|
|
+ typeCtx.shadowBlur = 20; typeCtx.shadowColor = '#39ff14';
|
|
|
+ typeCtx.fillRect(10, H/2 - 30, 22, 60);
|
|
|
+ typeCtx.shadowBlur = 0;
|
|
|
+ typeCtx.fillStyle = '#39ff14';
|
|
|
+ typeCtx.font = 'bold 12px Segoe UI';
|
|
|
+ typeCtx.fillText('🔊', 14, H/2 + 5);
|
|
|
+
|
|
|
+ // 👂 УХО справа
|
|
|
+ typeCtx.fillStyle = '#00e5ff';
|
|
|
+ typeCtx.font = 'bold 22px Segoe UI';
|
|
|
+ typeCtx.fillText('👂', W - 35, H/2 + 8);
|
|
|
+
|
|
|
// Подписи
|
|
|
typeCtx.fillStyle = '#00e5ff';
|
|
|
typeCtx.font = 'bold 14px Segoe UI';
|
|
|
- typeCtx.fillText('Продольная (звук в воздухе)', 20, 30);
|
|
|
+ typeCtx.fillText('Продольная (звук в воздухе)', 50, 25);
|
|
|
|
|
|
- // Стрелка направления движения волны
|
|
|
+ // Стрелка направления волны
|
|
|
typeCtx.strokeStyle = '#39ff14';
|
|
|
typeCtx.lineWidth = 2;
|
|
|
- typeCtx.shadowBlur = 12; typeCtx.shadowColor = '#39ff14';
|
|
|
typeCtx.beginPath();
|
|
|
- typeCtx.moveTo(W - 140, 30);
|
|
|
- typeCtx.lineTo(W - 40, 30);
|
|
|
- typeCtx.lineTo(W - 55, 22);
|
|
|
- typeCtx.moveTo(W - 40, 30);
|
|
|
- typeCtx.lineTo(W - 55, 38);
|
|
|
+ typeCtx.moveTo(W - 140, 25);
|
|
|
+ typeCtx.lineTo(W - 45, 25);
|
|
|
+ typeCtx.lineTo(W - 60, 18);
|
|
|
+ typeCtx.moveTo(W - 45, 25);
|
|
|
+ typeCtx.lineTo(W - 60, 32);
|
|
|
typeCtx.stroke();
|
|
|
- typeCtx.shadowBlur = 0;
|
|
|
- typeCtx.fillStyle = '#39ff14';
|
|
|
- typeCtx.font = '12px Segoe UI';
|
|
|
- typeCtx.fillText('волна →', W - 135, 20);
|
|
|
|
|
|
- // Пунктирная разметка «сжатие / разрежение»
|
|
|
- typeCtx.strokeStyle = 'rgba(255,255,255,.08)';
|
|
|
- typeCtx.setLineDash([4,6]);
|
|
|
- typeCtx.lineWidth = 1;
|
|
|
- for(var k=0;k<5;k++){
|
|
|
- var cx = 60 + k * (W - 120) / 4;
|
|
|
- typeCtx.beginPath();
|
|
|
- typeCtx.moveTo(cx, H/2 - 45);
|
|
|
- typeCtx.lineTo(cx, H/2 + 45);
|
|
|
- typeCtx.stroke();
|
|
|
- }
|
|
|
- typeCtx.setLineDash([]);
|
|
|
-
|
|
|
- // Подписи сжатие/разрежение (плавно переключаются)
|
|
|
- var labels = ['сжатие', 'разрежение'];
|
|
|
- for(var m=0;m<4;m++){
|
|
|
- var lx = 60 + m * (W - 120) / 4 + 30;
|
|
|
- var phase = Math.sin(typeT - m * 1.4);
|
|
|
- var lbl = phase > 0 ? 'сжатие' : 'разрежение';
|
|
|
- typeCtx.fillStyle = phase > 0 ? 'rgba(255,46,196,.85)' : 'rgba(0,229,255,.85)';
|
|
|
- typeCtx.font = '11px Segoe UI';
|
|
|
- typeCtx.fillText(lbl, lx, H/2 + 60);
|
|
|
- }
|
|
|
+ typeT += 0.03;
|
|
|
}
|
|
|
|
|
|
-// 🌊 ПОПЕРЕЧНАЯ ВОЛНА — синусоида
|
|
|
+// 🌊 ПОПЕРЕЧНАЯ ВОЛНА — синусоида, ширина зависит от периода
|
|
|
function drawTransverse(W, H){
|
|
|
// Ось
|
|
|
typeCtx.strokeStyle = 'rgba(0,229,255,.15)';
|
|
|
@@ -940,22 +1050,39 @@ function drawTransverse(W, H){
|
|
|
typeCtx.lineTo(W, H/2);
|
|
|
typeCtx.stroke();
|
|
|
|
|
|
+ // 4 гребня на экране
|
|
|
+ var wavesAcross = 4;
|
|
|
+ var pixelAmp = 55;
|
|
|
+
|
|
|
+ // Пунктирные границы
|
|
|
+ typeCtx.strokeStyle = 'rgba(255,46,196,.2)';
|
|
|
+ typeCtx.setLineDash([4,4]);
|
|
|
+ typeCtx.lineWidth = 1;
|
|
|
+ typeCtx.beginPath();
|
|
|
+ typeCtx.moveTo(0, H/2 - pixelAmp); typeCtx.lineTo(W, H/2 - pixelAmp);
|
|
|
+ typeCtx.moveTo(0, H/2 + pixelAmp); typeCtx.lineTo(W, H/2 + pixelAmp);
|
|
|
+ typeCtx.stroke();
|
|
|
+ typeCtx.setLineDash([]);
|
|
|
+
|
|
|
// Сама волна
|
|
|
typeCtx.strokeStyle = '#00e5ff';
|
|
|
typeCtx.lineWidth = 2;
|
|
|
typeCtx.shadowBlur = 15; typeCtx.shadowColor = '#00e5ff';
|
|
|
typeCtx.beginPath();
|
|
|
- for(var x=0; x<W; x+=4){
|
|
|
- var y = H/2 + Math.sin(x * 0.02 + typeT) * 55;
|
|
|
+ for(var x=0; x<W; x+=3){
|
|
|
+ var phaseX = (x / W) * Math.PI * 2 * wavesAcross;
|
|
|
+ var y = H/2 + Math.sin(phaseX + typeT * 0.8) * pixelAmp;
|
|
|
if(x===0) typeCtx.moveTo(x,y); else typeCtx.lineTo(x,y);
|
|
|
}
|
|
|
typeCtx.stroke();
|
|
|
typeCtx.shadowBlur = 0;
|
|
|
|
|
|
// Частицы на гребнях
|
|
|
- for(var k=0;k<20;k++){
|
|
|
- var xx = (k+1) * (W/21);
|
|
|
- var yy = H/2 + Math.sin(xx * 0.02 + typeT) * 55;
|
|
|
+ var dotsCount = Math.round(wavesAcross * 6);
|
|
|
+ for(var k=0;k<dotsCount;k++){
|
|
|
+ var xx = (k + 0.5) * (W / dotsCount);
|
|
|
+ var phaseXDot = (xx / W) * Math.PI * 2 * wavesAcross;
|
|
|
+ var yy = H/2 + Math.sin(phaseXDot + typeT * 0.8) * pixelAmp;
|
|
|
typeCtx.beginPath();
|
|
|
typeCtx.fillStyle = '#ff2ec4';
|
|
|
typeCtx.shadowBlur = 15; typeCtx.shadowColor = '#ff2ec4';
|
|
|
@@ -969,7 +1096,10 @@ function drawTransverse(W, H){
|
|
|
typeCtx.fillText('Поперечная (волны на воде, струна)', 20, 30);
|
|
|
}
|
|
|
|
|
|
-// Запуск
|
|
|
+// ▶️ Кнопка Далее
|
|
|
+document.getElementById('btnNext5').addEventListener('click', function(){ goToScene(5); });
|
|
|
+
|
|
|
+// Запуск цикла
|
|
|
drawType();
|
|
|
|
|
|
/* ================= SCENE 6: карточки + пары ================= */
|