import * as THREE from "https://esm.sh/three@0.156.1";
import { OrbitControls } from "https://esm.sh/three@0.156.1/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "https://esm.sh/three@0.156.1/examples/jsm/loaders/GLTFLoader";
import { Reflector } from "https://esm.sh/three@0.156.1/examples/jsm/objects/Reflector";
import gsap from "https://esm.sh/gsap";
class App {
constructor() {
this.winWidth = window.innerWidth;
this.winHeight = window.innerHeight;
this.isMobile = window.innerWidth < 768;
this.gltfFile = "https://assets.codepen.io/264161/rabbit6.glb";
this.isReady = false;
// Game state variables
this.score = 0;
this.highScore = this.getHighScore();
this.gameActive = false;
this.gameTime = 120; // 2 minutes in seconds
this.pointsPerCarrot = 5; // Points per carrot
this.modalOpen = false; // Track modal state
this.isRestarting = false; // Prevent multiple restart calls
// Audio setup
this.setupAudio();
this.setupLoadingIndicator();
this.setupGameOverModal();
this.loadAssets();
}
setupAudio() {
this.eatSound = document.getElementById("eatSound");
this.audioStatusEl = document.getElementById("audioStatus");
this.audioInitialized = false;
// Set volume
this.eatSound.volume = 0.7;
// Initialize audio on first user interaction
const initAudio = async (event) => {
if (!this.audioInitialized) {
try {
// Create a new Audio instance for better mobile compatibility
this.audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
// Test audio playability
const testPlay = this.eatSound.play();
if (testPlay) {
await testPlay;
this.eatSound.pause();
this.eatSound.currentTime = 0;
}
this.audioInitialized = true;
this.audioStatusEl.textContent = "ЁЯФК Audio Ready";
this.audioStatusEl.style.color = "#7beeff";
// Remove listeners after successful initialization
document.removeEventListener("click", initAudio);
document.removeEventListener("touchstart", initAudio);
document.removeEventListener("touchend", initAudio);
document.removeEventListener("keydown", initAudio);
} catch (error) {
console.log("Audio initialization failed:", error);
this.audioStatusEl.textContent = "ЁЯФЗ Audio Unavailable";
this.audioStatusEl.style.color = "#ff3434";
}
}
};
// Add multiple event listeners for audio initialization
document.addEventListener("click", initAudio);
document.addEventListener("touchstart", initAudio);
document.addEventListener("touchend", initAudio);
document.addEventListener("keydown", initAudio);
// Handle audio loading errors
this.eatSound.addEventListener("error", () => {
this.audioStatusEl.textContent = "ЁЯФЗ Audio Unavailable";
this.audioStatusEl.style.color = "#ff3434";
});
}
playEatSound() {
if (this.audioInitialized && this.eatSound) {
try {
// Reset the audio to beginning and play
this.eatSound.currentTime = 0;
const playPromise = this.eatSound.play();
if (playPromise !== undefined) {
playPromise.catch((error) => {
console.log("Audio play failed:", error);
});
}
} catch (error) {
console.log("Audio error:", error);
}
}
}
setupLoadingIndicator() {
// Create a simple loading indicator
const loadingDiv = document.createElement("div");
loadingDiv.id = "loading";
loadingDiv.style.position = "absolute";
loadingDiv.style.top = "50%";
loadingDiv.style.left = "50%";
loadingDiv.style.transform = "translate(-50%, -50%)";
loadingDiv.style.color = "#7beeff";
loadingDiv.style.fontSize = "1.2em";
loadingDiv.style.fontFamily = '"Open Sans", sans-serif';
loadingDiv.textContent = "Loading 3D Rabbit Game...";
document.querySelector(".game-container").appendChild(loadingDiv);
this.loadingIndicator = loadingDiv;
}
// COMPLETELY REWRITTEN AND SIMPLIFIED modal setup for guaranteed mobile compatibility
setupGameOverModal() {
// Get modal elements
this.modal = document.getElementById("gameOverModal");
this.finalScoreEl = document.getElementById("finalScore");
this.highScoreEl = document.getElementById("highScore");
this.playAgainBtn = document.getElementById("playAgainBtn");
// ULTRA SIMPLE, RELIABLE restart handler
const handleRestart = () => {
console.log("Play Again button clicked!");
// Immediate visual feedback
this.playAgainBtn.classList.add("active");
// Prevent multiple calls
if (this.isRestarting) {
console.log("Already restarting, ignoring click");
return;
}
this.isRestarting = true;
// Hide modal immediately
this.hideModal();
// Restart game after brief delay
setTimeout(() => {
this.resetGameState();
this.startGame();
this.isRestarting = false;
this.playAgainBtn.classList.remove("active");
}, 300);
};
// REMOVE ALL existing event listeners and add ONE reliable handler
this.playAgainBtn.onclick = null;
this.playAgainBtn.ontouchstart = null;
this.playAgainBtn.ontouchend = null;
// Clone button to remove all event listeners
const newBtn = this.playAgainBtn.cloneNode(true);
this.playAgainBtn.parentNode.replaceChild(newBtn, this.playAgainBtn);
this.playAgainBtn = newBtn;
// Add SINGLE, simple event listener that works on all devices
this.playAgainBtn.addEventListener("click", handleRestart, {
passive: false,
once: false,
});
// Additional mobile touch support
this.playAgainBtn.addEventListener(
"touchend",
(e) => {
e.preventDefault();
e.stopPropagation();
handleRestart();
},
{
passive: false,
once: false,
}
);
// Prevent modal background clicks
this.modal.addEventListener("click", (e) => {
if (e.target === this.modal) {
e.preventDefault();
e.stopPropagation();
}
});
}
hideModal() {
this.modal.classList.remove("active");
this.modal.setAttribute("aria-hidden", "true");
this.modalOpen = false;
// Re-enable game canvas interactions
const canvas = document.querySelector(".webgl");
if (canvas) {
canvas.style.pointerEvents = "auto";
}
}
showModal() {
this.modalOpen = true;
// Disable game canvas interactions
const canvas = document.querySelector(".webgl");
if (canvas) {
canvas.style.pointerEvents = "none";
}
// Update modal content
this.finalScoreEl.textContent = this.score;
this.highScoreEl.textContent = this.highScore;
// Show modal
this.modal.classList.add("active");
this.modal.setAttribute("aria-hidden", "false");
// Focus button after modal is visible
setTimeout(() => {
if (this.playAgainBtn) {
this.playAgainBtn.focus();
}
}, 400);
}
resetGameState() {
// Clear any running timers
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
// Reset all game variables
this.score = 0;
this.gameTime = 120;
this.gameActive = false;
this.isExploding = false;
this.isJumping = false;
this.isLanding = false;
// Reset UI
document.getElementById("score").textContent = "0";
document.getElementById("timer").textContent = "2:00";
// Reset rabbit position and rotation
if (this.rabbit) {
this.rabbit.position.set(0, 0, 0);
this.rabbit.rotation.set(0, 0, 0);
if (this.rabbitBody) {
this.rabbitBody.rotation.set(0, 0, 0);
}
}
// Hide carrot
if (this.carrot) {
this.carrot.visible = false;
}
// Reset hero parameters
this.heroSpeed = new THREE.Vector2(0, 0);
this.heroAcc = new THREE.Vector2(0, 0);
this.targetHeroUVPos = new THREE.Vector2(0.5, 0.5);
this.heroOldUVPos = new THREE.Vector2(0.5, 0.5);
this.heroNewUVPos = new THREE.Vector2(0.5, 0.5);
// Hide all particles
if (this.particles1) {
this.particles1.forEach((p) => p.scale.set(0, 0, 0));
}
if (this.particles2) {
this.particles2.forEach((p) => p.scale.set(0, 0, 0));
}
console.log("Game state reset complete");
}
getHighScore() {
return parseInt(localStorage.getItem("rabbitGameHighScore") || 0);
}
saveHighScore(score) {
localStorage.setItem("rabbitGameHighScore", score);
}
updateScore(points = null) {
const pointsToAdd = points !== null ? points : this.pointsPerCarrot;
this.score += pointsToAdd;
const scoreElement = document.getElementById("score");
scoreElement.textContent = this.score;
// Add animation effect
scoreElement.classList.remove("score-bump");
// Trigger reflow to restart the animation
void scoreElement.offsetWidth;
scoreElement.classList.add("score-bump");
}
showScorePopup(position) {
// Create a new score popup element
const popup = document.createElement("div");
popup.className = "score-popup";
popup.textContent = `+${this.pointsPerCarrot}`;
// Calculate screen position from 3D world position
const widthHalf = this.winWidth / 2;
const heightHalf = this.winHeight / 2;
// Clone the position to avoid modifying the original
const pos = position.clone();
// Project the 3D position to screen space
pos.project(this.camera);
// Convert to screen coordinates
const x = pos.x * widthHalf + widthHalf;
const y = -(pos.y * heightHalf) + heightHalf;
// Set position
popup.style.left = `${x}px`;
popup.style.top = `${y}px`;
// Add to the DOM
document.querySelector(".game-container").appendChild(popup);
// Remove after animation completes
setTimeout(() => {
popup.remove();
}, 1000);
}
updateTimer() {
if (!this.gameActive) return;
this.gameTime--;
const minutes = Math.floor(this.gameTime / 60);
const seconds = this.gameTime % 60;
// Format timer display
document.getElementById("timer").textContent = `${minutes}:${
seconds < 10 ? "0" : ""
}${seconds}`;
// Check if time is up
if (this.gameTime <= 0) {
this.endGame();
}
}
startGame() {
console.log("Starting new game...");
this.gameActive = true;
// Start timer
this.timerInterval = setInterval(() => this.updateTimer(), 1000);
// Spawn initial carrot after a brief delay
setTimeout(() => {
if (this.gameActive) {
this.spawnCarrot();
}
}, 1000);
console.log("Game started successfully");
}
endGame() {
console.log("Ending game...");
this.gameActive = false;
// Clear timer
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
// Update high score if needed
if (this.score > this.highScore) {
this.highScore = this.score;
this.saveHighScore(this.highScore);
}
// Show modal after slight delay
setTimeout(() => {
this.showModal();
}, 500);
}
loadAssets() {
const loaderModel = new GLTFLoader();
loaderModel.load(
this.gltfFile,
(gltf) => {
this.model = gltf.scene;
this.setUpScene();
// Remove loading indicator when ready
if (this.loadingIndicator) {
this.loadingIndicator.remove();
}
this.isReady = true;
// Start the game after assets are loaded
this.startGame();
},
// Progress callback
(xhr) => {
if (this.loadingIndicator) {
const percent = Math.floor((xhr.loaded / xhr.total) * 100);
this.loadingIndicator.textContent = `Loading 3D Rabbit Game... ${percent}%`;
}
},
// Error callback
(error) => {
console.error("Error loading model:", error);
if (this.loadingIndicator) {
this.loadingIndicator.textContent =
"Error loading game assets. Please refresh the page.";
this.loadingIndicator.style.color = "#ff3434";
}
}
);
}
setUpScene() {
// Scene
this.scene = new THREE.Scene();
this.bgrColor = 0x332e2e;
this.fog = new THREE.Fog(this.bgrColor, 13, 20);
this.scene.fog = this.fog;
// Camera setup based on device
const fov = this.isMobile ? 70 : 60;
this.camera = new THREE.PerspectiveCamera(
fov,
this.winWidth / this.winHeight,
1,
100
);
// Adjust camera position based on device
if (this.isMobile) {
this.camera.position.set(0, 5, 10);
} else {
this.camera.position.set(0, 4, 8);
}
this.camera.lookAt(new THREE.Vector3());
this.scene.add(this.camera);
// Hero params
this.heroAngularSpeed = 0;
this.heroOldRot = 0;
this.heroDistance = 0;
this.heroOldUVPos = new THREE.Vector2(0.5, 0.5);
this.heroNewUVPos = new THREE.Vector2(0.5, 0.5);
this.heroSpeed = new THREE.Vector2(0, 0);
this.heroAcc = new THREE.Vector2(0, 0);
this.targetHeroUVPos = new THREE.Vector2(0.5, 0.5);
this.targetHeroAbsMousePos = new THREE.Vector2(0, 0);
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.isJumping = this.isLanding = false;
this.jumpParams = { jumpProgress: 0, landProgress: 0 };
// Clock
this.clock = new THREE.Clock();
this.time = 0;
this.deltaTime = 0;
// Core
this.createRenderer();
this.createSim();
this.createListeners();
// Environment
this.floorSize = this.determineFloorSize();
this.createMaterials();
this.processModel();
this.createFloor();
this.createLine();
this.createLight();
this.createParticles();
// Render loop
this.draw();
}
// Determine floor size based on device
determineFloorSize() {
return this.isMobile ? 20 : 30;
}
processModel() {
this.rabbit = this.model.getObjectByName("Rabbit");
this.rabbitBody = this.model.getObjectByName("body");
this.earRight = this.model.getObjectByName("earRight");
this.earLeft = this.model.getObjectByName("earLeft");
this.tail = this.model.getObjectByName("tail");
this.footLeft = this.model.getObjectByName("footLeft");
this.footRight = this.model.getObjectByName("footRight");
this.eyeLeft = this.model.getObjectByName("eyeLeft");
this.eyeRight = this.model.getObjectByName("eyeRight");
this.carrot = this.model.getObjectByName("carrot");
this.carrotLeaf = this.model.getObjectByName("carrotLeaf");
this.carrotLeaf2 = this.model.getObjectByName("carrotLeaf2");
this.carrot.rotation.z = 0.2;
this.carrot.rotation.x = 0.2;
this.rabbitBody.material = this.primMat;
this.earRight.material = this.primMat;
this.earLeft.material = this.primMat;
this.tail.material = this.primMat;
this.footLeft.material = this.secMat;
this.footRight.material = this.secMat;
this.eyeLeft.material = this.secMat;
this.eyeRight.material = this.secMat;
this.carrot.material = this.bonusMat;
this.carrotLeaf.material = this.primMat;
this.carrotLeaf2.material = this.primMat;
this.addOutline(this.rabbitBody);
this.addOutline(this.earRight);
this.addOutline(this.earLeft);
this.addOutline(this.tail);
this.addOutline(this.carrot);
this.rabbit.traverse((object) => {
if (object.isMesh) {
object.castShadow = true;
object.receiveShadow = true;
}
});
this.carrot.traverse((object) => {
if (object.isMesh) {
object.castShadow = true;
}
});
this.scene.add(this.rabbit);
this.scene.add(this.carrot);
// Hide carrot initially until spawned
this.carrot.visible = false;
}
createFloor() {
this.floor = new Reflector(
new THREE.PlaneGeometry(this.floorSize, this.floorSize),
{
color: new THREE.Color(this.bgrColor),
textureWidth: 1024,
textureHeight: 1024,
}
);
this.floor.rotation.x = -Math.PI / 2;
this.floor.receiveShadow = true;
this.modifyFloorShader();
this.scene.add(this.floor);
}
createLine() {
const material = new THREE.LineDashedMaterial({
color: 0x7beeff,
linewidth: 1,
scale: 1,
dashSize: 0.2,
gapSize: 0.1,
});
const points = [];
points.push(new THREE.Vector3(0, 0.2, 0));
points.push(new THREE.Vector3(3, 0.2, 3));
const geometry = new THREE.BufferGeometry().setFromPoints(points);
this.line = new THREE.Line(geometry, material);
this.scene.add(this.line);
}
createParticles() {
let bodyCount = this.isMobile ? 15 : 20;
let tailCount = this.isMobile ? 3 : 5;
let particleGeom = new THREE.BoxGeometry(0.2, 0.2, 0.2, 1, 1, 1);
this.particles1 = [];
this.particles2 = [];
let i = 0;
for (i = 0; i < bodyCount; i++) {
let m = new THREE.Mesh(particleGeom, this.bonusMat);
this.particles1.push(m);
m.scale.set(0, 0, 0);
this.scene.add(m);
}
for (i = 0; i < tailCount; i++) {
let m = new THREE.Mesh(particleGeom, this.primMat);
this.particles2.push(m);
m.scale.set(0, 0, 0);
this.scene.add(m);
}
}
createLight() {
this.ambientLight = new THREE.AmbientLight(0xffffff);
this.scene.add(this.ambientLight);
this.light = new THREE.DirectionalLight(0xffffff, 1);
this.light.position.set(1, 5, 1);
this.light.castShadow = true;
this.light.shadow.mapSize.width = 512;
this.light.shadow.mapSize.height = 512;
this.light.shadow.camera.near = 0.5;
this.light.shadow.camera.far = 12;
this.light.shadow.camera.left = -12;
this.light.shadow.camera.right = 12;
this.light.shadow.camera.bottom = -12;
this.light.shadow.camera.top = 12;
this.light.shadow.radius = 3;
this.light.shadow.blurSamples = 4;
this.scene.add(this.light);
}
createRenderer() {
const canvas = document.querySelector("canvas.webgl");
this.renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
preserveDrawingBuffer: true,
});
this.renderer.setClearColor(new THREE.Color(this.bgrColor));
this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio));
this.renderer.setSize(this.winWidth, this.winHeight);
this.renderer.toneMapping = THREE.LinearToneMapping;
this.renderer.toneMappingExposure = 1;
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.VSMShadowMap;
this.renderer.localClippingEnabled = true;
}
createSim() {
const fragmentShader = document.getElementById(
"simulationFragmentShader"
).textContent;
const vertexShader = document.getElementById(
"simulationVertexShader"
).textContent;
this.floorSimMat = new THREE.ShaderMaterial({
uniforms: {
inputTexture: { type: "t", value: null },
time: { value: 0.0 },
blade1PosOld: { value: new THREE.Vector2(0.5, 0.5) },
blade1PosNew: { value: new THREE.Vector2(0.5, 0.5) },
strength: { value: 0.0 },
},
vertexShader,
fragmentShader,
});
this.bufferSim = new BufferSim(
this.renderer,
1024,
1024,
this.floorSimMat
);
}
createMaterials() {
// Materials
this.primMat = new THREE.MeshToonMaterial({ color: 0x7beeff });
this.secMat = new THREE.MeshToonMaterial({ color: this.bgrColor });
this.bonusMat = new THREE.MeshToonMaterial({ color: 0xff3434 });
// outline Material
const fragmentShader = document.getElementById(
"outlineFragmentShader"
).textContent;
const vertexShader = document.getElementById(
"outlineVertexShader"
).textContent;
this.outlineMat = new THREE.ShaderMaterial({
uniforms: {
color: { value: new THREE.Color(0x000000) },
size: { type: "f", value: 0.02 },
},
vertexShader,
fragmentShader,
side: THREE.BackSide,
});
}
addOutline(origin) {
let outline = origin.clone();
outline.children = [];
outline.position.set(0, 0, 0);
outline.rotation.x = 0;
outline.rotation.y = 0;
outline.rotation.z = 0;
outline.scale.set(1, 1, 1);
outline.material = this.outlineMat;
origin.add(outline);
return outline;
}
// SIMPLIFIED event listener management with proper modal checking
createListeners() {
window.addEventListener("resize", this.onWindowResize.bind(this));
if ("ontouchstart" in window || navigator.maxTouchPoints) {
// Touch events for mobile with improved modal checking
document.addEventListener(
"touchmove",
this.onTouchMove.bind(this),
{ passive: false }
);
document.addEventListener(
"touchstart",
this.onTouchStart.bind(this),
{ passive: false }
);
} else {
// Mouse events for desktop
document.addEventListener(
"mousemove",
this.onMouseMove.bind(this),
false
);
document.addEventListener(
"mousedown",
this.onMouseDown.bind(this),
false
);
}
}
draw() {
this.updateGame();
this.renderer.render(this.scene, this.camera);
window.requestAnimationFrame(this.draw.bind(this));
}
updateGame() {
if (!this.isReady) return;
this.dt = Math.min(this.clock.getDelta(), 0.3);
this.time += this.dt;
if (this.rabbit && this.line) {
// Elastic string simulation
let constrainUVPosX = this.constrain(
this.targetHeroUVPos.x - 0.5,
-0.3,
0.3
);
let constrainUVPosY = this.constrain(
this.targetHeroUVPos.y - 0.5,
-0.3,
0.3
);
this.targetHeroAbsMousePos.x = constrainUVPosX * this.floorSize;
this.targetHeroAbsMousePos.y = -constrainUVPosY * this.floorSize;
let dx = this.targetHeroAbsMousePos.x - this.rabbit.position.x;
let dy = this.targetHeroAbsMousePos.y - this.rabbit.position.z;
let angle = Math.atan2(dy, dx);
this.heroDistance = Math.sqrt(dx * dx + dy * dy);
let ax = dx * this.dt * 0.5;
let ay = dy * this.dt * 0.5;
this.heroSpeed.x += ax;
this.heroSpeed.y += ay;
this.heroSpeed.x *= Math.pow(this.dt, 0.005);
this.heroSpeed.y *= Math.pow(this.dt, 0.005);
this.rabbit.position.x += this.heroSpeed.x;
this.rabbit.position.z += this.heroSpeed.y;
let targetRot = -angle + Math.PI / 2;
if (this.heroDistance > 0.3)
this.rabbit.rotation.y +=
this.getShortestAngle(targetRot - this.rabbit.rotation.y) *
3 *
this.dt;
this.heroAngularSpeed = this.getShortestAngle(
this.rabbit.rotation.y - this.heroOldRot
);
this.heroOldRot = this.rabbit.rotation.y;
if (!this.isJumping) {
this.earLeft.rotation.x = this.earRight.rotation.x =
-this.heroSpeed.length() * 2;
}
let p = this.line.geometry.attributes.position.array;
p[0] = this.targetHeroAbsMousePos.x;
p[2] = this.targetHeroAbsMousePos.y;
p[3] = this.rabbit.position.x;
p[4] = this.rabbit.position.y;
p[5] = this.rabbit.position.z;
this.line.geometry.attributes.position.needsUpdate = true;
this.line.computeLineDistances();
this.heroNewUVPos = new THREE.Vector2(
0.5 + this.rabbit.position.x / this.floorSize,
0.5 - this.rabbit.position.z / this.floorSize
);
this.floorSimMat.uniforms.time.value += this.dt;
this.floorSimMat.uniforms.blade1PosNew.value = this.heroNewUVPos;
this.floorSimMat.uniforms.blade1PosOld.value = this.heroOldUVPos;
this.floorSimMat.uniforms.strength.value = this.isJumping
? 0
: 1 / (1 + this.heroSpeed.length() * 10);
this.bufferSim.render();
this.renderer.setRenderTarget(null);
this.floor.material.uniforms.tScratches.value =
this.bufferSim.output.texture;
this.heroOldUVPos = this.heroNewUVPos.clone();
this.carrot.rotation.y += this.dt;
this.testCollision();
}
}
onWindowResize() {
this.winWidth = window.innerWidth;
this.winHeight = window.innerHeight;
this.isMobile = window.innerWidth < 768;
// Update camera aspect and position based on device
this.camera.aspect = this.winWidth / this.winHeight;
if (this.isMobile && this.camera) {
this.camera.fov = 70;
this.camera.position.set(0, 5, 10);
} else if (this.camera) {
this.camera.fov = 60;
this.camera.position.set(0, 4, 8);
}
this.camera.updateProjectionMatrix();
this.renderer.setSize(this.winWidth, this.winHeight);
// Update floor size based on device
if (this.floor) {
const newFloorSize = this.determineFloorSize();
if (newFloorSize !== this.floorSize) {
this.floorSize = newFloorSize;
// Re-create floor with new size
this.scene.remove(this.floor);
this.createFloor();
}
}
}
onMouseMove(event) {
// Don't process mouse events when modal is open
if (this.modalOpen) return;
const x = (event.clientX / this.winWidth) * 2 - 1;
const y = -((event.clientY / this.winHeight) * 2 - 1);
this.updateMouse(x, y);
}
onTouchMove(event) {
// Don't process touch events when modal is open
if (this.modalOpen) return;
if (event.touches.length == 1) {
event.preventDefault();
const x = (event.touches[0].pageX / this.winWidth) * 2 - 1;
const y = -((event.touches[0].pageY / this.winHeight) * 2 - 1);
this.updateMouse(x, y);
}
}
onTouchStart(event) {
// Don't process touch events when modal is open
if (this.modalOpen) return;
// Handle touch start for jumping
if (this.rabbit && !this.isJumping && this.gameActive) {
this.jump();
}
// Also update position on touch start for immediate movement
if (event.touches.length == 1) {
event.preventDefault();
const x = (event.touches[0].pageX / this.winWidth) * 2 - 1;
const y = -((event.touches[0].pageY / this.winHeight) * 2 - 1);
this.updateMouse(x, y);
}
}
updateMouse(x, y) {
this.mouse.x = x;
this.mouse.y = y;
if (this.floor) this.raycast();
}
onMouseDown(event) {
// Don't process mouse events when modal is open
if (this.modalOpen) return;
if (this.rabbit && !this.isJumping && this.gameActive) this.jump();
}
jump() {
this.isJumping = true;
let turns = Math.floor(this.heroSpeed.length() * 5) + 1;
let jumpDuration = 0.5 + turns * 0.2;
let targetRot =
this.heroAngularSpeed > 0
? Math.PI * 2 * turns
: -Math.PI * 2 * turns;
gsap.to(this.rabbitBody.rotation, {
duration: jumpDuration,
ease: "linear.none",
y: targetRot,
onComplete: () => {
this.rabbitBody.rotation.y = 0;
},
});
gsap.to([this.earLeft.rotation, this.earRight.rotation], {
duration: jumpDuration * 0.8,
ease: "power4.out",
x: Math.PI / 4,
});
gsap.to([this.earLeft.rotation, this.earRight.rotation], {
duration: jumpDuration * 0.2,
delay: jumpDuration * 0.8,
ease: "power4.in",
x: 0,
});
gsap.to(this.jumpParams, {
duration: jumpDuration * 0.5,
ease: "power2.out",
jumpProgress: 0.5,
onUpdate: () => {
let sin = Math.sin(this.jumpParams.jumpProgress * Math.PI);
this.rabbit.position.y = Math.pow(sin, 4) * turns;
},
});
gsap.to(this.jumpParams, {
duration: jumpDuration * 0.5,
ease: "power2.in",
delay: jumpDuration * 0.5,
jumpProgress: 1,
onUpdate: () => {
let sin = Math.sin(this.jumpParams.jumpProgress * Math.PI);
this.rabbit.position.y = Math.pow(sin, 1) * turns;
},
onComplete: () => {
this.rabbit.position.y = 0;
this.jumpParams.jumpProgress = 0;
this.isJumping = false;
},
});
}
raycast() {
// Direct raycast from mouse to floor
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects([this.floor]);
if (intersects.length > 0) {
// Get the exact point where the ray intersects the floor
this.targetHeroUVPos.x = intersects[0].uv.x;
this.targetHeroUVPos.y = intersects[0].uv.y;
}
}
getShortestAngle(v) {
let a = v % (Math.PI * 2);
if (a < -Math.PI) a += Math.PI * 2;
else if (a > Math.PI) a -= Math.PI * 2;
return a;
}
constrain(v, vMin, vMax) {
return Math.min(vMax, Math.max(vMin, v));
}
testCollision() {
if (this.isExploding || !this.gameActive) return;
if (!this.carrot.visible) return;
let distVec = this.rabbit.position.clone();
distVec.sub(this.carrot.position);
let l = distVec.length();
if (l <= 1) {
// Store carrot position before hiding it
const carrotPos = this.carrot.position.clone();
this.carrot.visible = false;
// Play eating sound automatically when carrot is collected
this.playEatSound();
// Show +5 score popup at carrot position
this.showScorePopup(carrotPos);
this.explode(carrotPos);
// Increment score when carrot is collected
this.updateScore();
}
}
explode(pos) {
this.isExploding = true;
let p1Count = this.particles1.length;
let p2Count = this.particles2.length;
let i = 0;
for (i = 0; i < p1Count; i++) {
let m = this.particles1[i];
m.position.x = pos.x;
m.position.y = pos.y;
m.position.z = pos.z;
m.scale.set(2, 2, 2);
gsap.to(m.position, {
x: pos.x + (-0.5 + Math.random()) * 1.5,
y: pos.y + (0.5 + Math.random()) * 1.5,
z: pos.z + (-0.5 + Math.random()) * 1.5,
duration: 1,
ease: "power4.out",
});
gsap.to(m.scale, {
x: 0,
y: 0,
z: 0,
duration: 1,
ease: "power4.out",
onComplete: () => {
if (this.gameActive) {
this.spawnCarrot();
}
this.isExploding = false;
},
});
}
for (i = 0; i < p2Count; i++) {
let m = this.particles2[i];
m.position.x = pos.x;
m.position.y = pos.y;
m.position.z = pos.z;
m.scale.set(2, 2, 2);
gsap.to(m.position, {
x: pos.x + (-0.5 + Math.random()) * 1.5,
y: pos.y + (0.5 + Math.random()) * 1.5,
z: pos.z + (-0.5 + Math.random()) * 1.5,
duration: 1,
ease: "power4.out",
});
gsap.to(m.scale, {
x: 0,
y: 0,
z: 0,
duration: 1,
ease: "power4.out",
});
}
}
spawnCarrot() {
// Only spawn if the game is active
if (!this.gameActive) return;
// Calculate safe boundaries to ensure carrot is within visible area
// Use 80% of floor size to keep away from edges
const safeRadius = this.floorSize * 0.4;
const px = ((Math.random() - 0.5) * safeRadius) / this.floorSize;
const py = ((Math.random() - 0.5) * safeRadius) / this.floorSize;
const h = 0.2 + Math.random() * 1;
// Ensure carrot is inside the playable area
this.carrot.position.x = px * this.floorSize;
this.carrot.position.z = py * this.floorSize;
this.carrot.position.y = -1;
this.carrot.scale.set(0, 0, 0);
this.carrot.visible = true;
gsap.to(this.carrot.scale, {
duration: 1.5,
ease: "elastic.out",
x: 1,
y: 1,
z: 1,
});
gsap.to(this.carrot.position, {
duration: 1.5,
ease: "elastic.out",
y: h,
});
}
modifyFloorShader() {
let renderTarget = this.floor.getRenderTarget();
const textureMatrix = this.floor.material.uniforms.textureMatrix;
const fragmentShader = document.getElementById(
"reflectorFragmentShader"
).textContent;
const vertexShader = document.getElementById(
"reflectorVertexShader"
).textContent;
const uniforms = THREE.UniformsUtils.merge([
THREE.UniformsLib["common"],
THREE.UniformsLib["shadowmap"],
THREE.UniformsLib["lights"],
this.floor.material.uniforms,
{
tScratches: { value: this.bufferSim.output.texture },
},
]);
this.floor.material.lights = true;
this.floor.material.uniforms = uniforms;
this.floor.material.uniforms.tDiffuse.value = renderTarget.texture;
this.floor.material.uniforms.textureMatrix.value =
textureMatrix.value;
this.floor.material.vertexShader = vertexShader;
this.floor.material.fragmentShader = fragmentShader;
}
}
class BufferSim {
constructor(renderer, width, height, shader) {
this.renderer = renderer;
this.shader = shader;
this.orthoScene = new THREE.Scene();
var fbo = new THREE.WebGLRenderTarget(width, height, {
wrapS: THREE.ClampToEdgeWrapping,
wrapT: THREE.ClampToEdgeWrapping,
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
type: THREE.FloatType,
stencilBuffer: false,
depthBuffer: false,
});
fbo.texture.generateMipmaps = false;
this.fbos = [fbo, fbo.clone()];
this.current = 0;
this.output = this.fbos[0];
this.orthoCamera = new THREE.OrthographicCamera(
width / -2,
width / 2,
height / 2,
height / -2,
0.00001,
1000
);
this.orthoQuad = new THREE.Mesh(
new THREE.PlaneGeometry(width, height),
this.shader
);
this.orthoScene.add(this.orthoQuad);
}
render() {
this.shader.uniforms.inputTexture.value =
this.fbos[this.current].texture;
this.input = this.fbos[this.current];
this.current = 1 - this.current;
this.output = this.fbos[this.current];
this.renderer.setRenderTarget(this.output);
this.renderer.render(this.orthoScene, this.orthoCamera);
this.renderer.setRenderTarget(null);
}
}
// Initialize the game when the page loads
document.addEventListener("DOMContentLoaded", () => new App());
How to Set Up This Code Showcase
Welcome to the Interactive Code Showcase!
This is a progressive learning experience designed to help you
understand HTML, CSS, and JavaScript step by step.
How It Works
The showcase uses a unique
progressive unlocking system that encourages
hands-on learning:
Step 1: HTML code is visible immediately - this
is your starting point
Step 2: Copy the HTML code to unlock the CSS
section (5-second countdown)
Step 3: Copy the CSS code to unlock the
JavaScript section (5-second countdown)
Interactive Learning: Each step builds upon the
previous one
Copying Code Instructions
To copy any code block:
Look for the copy button (ЁЯУЛ icon) in the
top-right corner of each code block
Click the copy button to copy the code to your clipboard
The button will show a ✅ checkmark when successfully copied
Paste the code into your preferred code editor or IDE
Getting Started
Here's what you need to do to set up and use this showcase
effectively:
Have a Code Editor Ready: Use VS Code, Sublime
Text, or any text editor
Create Project Folders: Make separate folders for
HTML, CSS, and JS files
Follow the Sequence: Start with HTML, then CSS,
then JavaScript
Test Your Code: Open HTML files in your browser
to see results
Experiment: Modify the copied code to learn how
it works
Pro Tips
Use browser developer tools (F12) to inspect and debug your code
Try modifying the copied code to see how changes affect the output
Create your own variations of the provided examples
Link your CSS and JavaScript files to your HTML for complete
projects
Having Trouble Copying Code?
If you're experiencing issues with copying the code or
understanding the process, watch the tutorial video above. It
provides a complete walkthrough of how to use this interactive
showcase and demonstrates the copying process step by step.
Troubleshooting
If you encounter any issues:
Code won't copy: Make sure your browser allows
clipboard access
Comments
Post a Comment