game.objects.feather.speed.add(vector);
}
+function start(game) {
+
+ function pinwheelPositionUpdate(game, viewportX, viewportY) {
+ const vFOV = THREE.MathUtils.degToRad(game.view.camera.fov);
+ const viewportHeight = 2 * Math.tan(vFOV / 2) * (game.view.camera.position.z - game.objects.pinwheel.position.z);
+ game.objects.pinwheel.cameraX = viewportHeight * viewportX;
+ game.objects.pinwheel.cameraY = - viewportHeight * viewportY;
+ }
+
+ function cursorMoveEvent(canvasLocalX, canvasLocalY) {
+ const canvasBb = game.view.canvas.getBoundingClientRect();
+ // Intentional division by height instead of width in the following line, since
+ // three.js controls the vertical FOV. So if we ever change the aspect ratio from 1:1,
+ // y will still be in range (-0.5, 0.5), but the range for x will be smaller or larger.
+ const x = (canvasLocalX - canvasBb.x - (canvasBb.width / 2)) / canvasBb.height;
+ const y = (canvasLocalY - canvasBb.y - (canvasBb.height / 2)) / canvasBb.height;
+ pinwheelPositionUpdate(game, x, y);
+ }
+
+ document.body.addEventListener('mousemove', e => cursorMoveEvent(e.clientX, e.clientY));
+ document.body.addEventListener('touchmove', e => cursorMoveEvent(e.touches[0].clientX, e.touches[0].clientY));
+}
+
function init(game, canvas) {
game.timeProgress = 0;
game.objects = {};
game.view = {};
+ game.view.canvas = canvas;
const scene = new THREE.Scene();
game.view.camera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);
game.objects.clouds.push(cloud);
}
+ game.view.camera.position.set(-5, -game.courseRadius, game.view.camera.position.z);
+
// All vectors used by the game loop (no allocations inside)
- const featherLocalPos = new THREE.Vector3();
- const featherBorderForce = new THREE.Vector3();
- const pinwheelDistance = new THREE.Vector3();
- const notCollectedPos = new THREE.Vector3();
- const collectedPos = new THREE.Vector3();
- const letterPos = new THREE.Vector3();
- function animate() {
- let delta = Math.min(game.view.clock.getDeltaTime(), 1 / 12);
+ game.var = {};
+ game.var.featherLocalPos = new THREE.Vector3();
+ game.var.featherBorderForce = new THREE.Vector3();
+ game.var.pinwheelDistance = new THREE.Vector3();
+ game.var.notCollectedPos = new THREE.Vector3();
+ game.var.collectedPos = new THREE.Vector3();
+ game.var.letterPos = new THREE.Vector3();
+ renderer.setAnimationLoop(() => { animate(game, renderer, scene); });
+}
- game.timeProgress = (game.timeProgress + delta) % game.timeTotal; // play infinitely for now
- const angle = 2 * Math.PI * (game.timeProgress / game.timeTotal);
- game.view.camera.position.x = game.courseRadius * Math.sin(angle);
- game.view.camera.position.y = - game.courseRadius * Math.cos(angle);
+function animate(game, renderer, scene) {
+ if(game.ui.currentPage != 'gameplay') {
+ game.view.camera.position.setY(-game.courseRadius + 0.1 * Math.sin(game.view.clock.getElapsedTime() * 0.5));
+ renderer.render(scene, game.view.camera);
+ return;
+ }
+ let delta = Math.min(game.view.clock.getDeltaTime(), 1 / 12);
- const sunsetValue = 2.0 * easeInOut(Math.min(1, Math.max(0, ((game.timeProgress / game.timeTotal) - 0.3) / 0.6)));
- for(let i = 0; i < 6; i++) {
- game.view.materials['cloud' + i].uniforms.lerp.value = sunsetValue;
- }
+ game.timeProgress = (game.timeProgress + delta) % game.timeTotal; // play infinitely for now
+ const angle = 2 * Math.PI * (game.timeProgress / game.timeTotal);
+ game.view.camera.position.x = game.courseRadius * Math.sin(angle);
+ game.view.camera.position.y = - game.courseRadius * Math.cos(angle);
- featherLocalPos.subVectors(game.objects.feather.position, game.view.camera.position).setZ(0);
- featherBorderForce.set(0, 0, 0);
- for(let coord of [0, 1]) {
- if(Math.abs(featherLocalPos.getComponent(coord)) > 3) {
- featherBorderForce.setComponent(coord, 3 * Math.sign(featherLocalPos.getComponent(coord)) - featherLocalPos.getComponent(coord));
- }
- }
- applyForceToFeather(game, featherBorderForce);
- const tiltedGravity = game.gravity.clone();
- pinwheelDistance.subVectors(game.objects.feather.position, game.objects.pinwheel.position).setZ(0);
+ const sunsetValue = 2.0 * easeInOut(Math.min(1, Math.max(0, ((game.timeProgress / game.timeTotal) - 0.3) / 0.6)));
+ for(let i = 0; i < 6; i++) {
+ game.view.materials['cloud' + i].uniforms.lerp.value = sunsetValue;
+ }
- const pinwheelForce = 0.5 * Math.max(0, Math.pow(pinwheelDistance.length(), - 0.5) - 0.5);
- applyForceToFeather(game, pinwheelDistance.normalize().multiplyScalar(pinwheelForce));
- if(pinwheelForce < 0.2) {
- if(game.objects.feather.swayDirection > 0 && game.objects.feather.speed.x > 1.5) {
- game.objects.feather.swayDirection *= -1;
- } else if(game.objects.feather.swayDirection < 0 && game.objects.feather.speed.x < -1.5) {
- game.objects.feather.swayDirection *= -1;
- }
- tiltedGravity.x += game.objects.feather.swayDirection;
+ game.var.featherLocalPos.subVectors(game.objects.feather.position, game.view.camera.position).setZ(0);
+ game.var.featherBorderForce.set(0, 0, 0);
+ for(let coord of [0, 1]) {
+ if(Math.abs(game.var.featherLocalPos.getComponent(coord)) > 3) {
+ game.var.featherBorderForce.setComponent(coord, 3 * Math.sign(game.var.featherLocalPos.getComponent(coord)) - game.var.featherLocalPos.getComponent(coord));
}
- if(game.objects.feather.speed.y > -1) {
- applyForceToFeather(game, tiltedGravity);
+ }
+ applyForceToFeather(game, game.var.featherBorderForce);
+ const tiltedGravity = game.gravity.clone();
+ game.var.pinwheelDistance.subVectors(game.objects.feather.position, game.objects.pinwheel.position).setZ(0);
+
+ const pinwheelForce = 0.5 * Math.max(0, Math.pow(game.var.pinwheelDistance.length(), - 0.5) - 0.5);
+ applyForceToFeather(game, game.var.pinwheelDistance.normalize().multiplyScalar(pinwheelForce));
+ if(pinwheelForce < 0.2) {
+ if(game.objects.feather.swayDirection > 0 && game.objects.feather.speed.x > 1.5) {
+ game.objects.feather.swayDirection *= -1;
+ } else if(game.objects.feather.swayDirection < 0 && game.objects.feather.speed.x < -1.5) {
+ game.objects.feather.swayDirection *= -1;
}
- game.objects.feather.speed.multiplyScalar(1 - delta);
- game.objects.feather.rotation.z = -0.1 * game.objects.feather.speed.x;
- game.objects.feather.position.addScaledVector(game.objects.feather.speed, delta);
+ tiltedGravity.x += game.objects.feather.swayDirection;
+ }
+ if(game.objects.feather.speed.y > -1) {
+ applyForceToFeather(game, tiltedGravity);
+ }
+ game.objects.feather.speed.multiplyScalar(1 - delta);
+ game.objects.feather.rotation.z = -0.1 * game.objects.feather.speed.x;
+ game.objects.feather.position.addScaledVector(game.objects.feather.speed, delta);
- if(pinwheelForce > 0.2) {
- if(game.objects.feather.twistSpeed < 0.0001) {
- game.objects.feather.twistSpeed = (Math.random() - 0.5) * 0.01;
- }
- game.objects.feather.twistSpeed = Math.sign(game.objects.feather.twistSpeed) * 0.1 * game.objects.feather.speed.length();
- } else {
- game.objects.feather.twistSpeed = 0.98 * game.objects.feather.twistSpeed;
- if(Math.abs(game.objects.feather.twistSpeed < 0.1)) {
- let rotationDelta = game.objects.feather.rotation.x;
- if(rotationDelta >= Math.PI) {
- rotationDelta -= 2 * Math.PI;
- }
- game.objects.feather.twistSpeed -= rotationDelta * 0.02;
+ if(pinwheelForce > 0.2) {
+ if(game.objects.feather.twistSpeed < 0.0001) {
+ game.objects.feather.twistSpeed = (Math.random() - 0.5) * 0.01;
+ }
+ game.objects.feather.twistSpeed = Math.sign(game.objects.feather.twistSpeed) * 0.1 * game.objects.feather.speed.length();
+ } else {
+ game.objects.feather.twistSpeed = 0.98 * game.objects.feather.twistSpeed;
+ if(Math.abs(game.objects.feather.twistSpeed < 0.1)) {
+ let rotationDelta = game.objects.feather.rotation.x;
+ if(rotationDelta >= Math.PI) {
+ rotationDelta -= 2 * Math.PI;
}
+ game.objects.feather.twistSpeed -= rotationDelta * 0.02;
}
+ }
- game.objects.feather.twistSpeed = Math.min(0.13, game.objects.feather.twistSpeed);
- game.objects.feather.rotation.x = (game.objects.feather.rotation.x + game.objects.feather.twistSpeed) % (2 * Math.PI);
- game.objects.pinwheel.rotation.z -= 5 * delta;
- game.objects.pinwheel.position.x = game.view.camera.position.x + game.objects.pinwheel.cameraX;
- game.objects.pinwheel.position.y = game.view.camera.position.y + game.objects.pinwheel.cameraY;
+ game.objects.feather.twistSpeed = Math.min(0.13, game.objects.feather.twistSpeed);
+ game.objects.feather.rotation.x = (game.objects.feather.rotation.x + game.objects.feather.twistSpeed) % (2 * Math.PI);
+ game.objects.pinwheel.rotation.z -= 5 * delta;
+ game.objects.pinwheel.position.x = game.view.camera.position.x + game.objects.pinwheel.cameraX;
+ game.objects.pinwheel.position.y = game.view.camera.position.y + game.objects.pinwheel.cameraY;
- let collectedScale = lerp(0.6, 0.3, 1 - Math.pow(1 - game.objects.words.collectedCount / game.objects.words.length, 2));
- for(let i = 0; i < game.objects.words.length; i++) {
- let word = game.objects.words[i];
- if(!word.collected &&
- game.objects.feather.speed.length() < 10.0 &&
- new THREE.Vector3().subVectors(word.mapPos, game.objects.feather.position).length() < 1) {
- word.collected = game.view.clock.getElapsedTime();
- game.objects.words.collectedCount += 1;
+ let collectedScale = lerp(0.6, 0.3, 1 - Math.pow(1 - game.objects.words.collectedCount / game.objects.words.length, 2));
+ for(let i = 0; i < game.objects.words.length; i++) {
+ let word = game.objects.words[i];
+ if(!word.collected &&
+ game.objects.feather.speed.length() < 10.0 &&
+ new THREE.Vector3().subVectors(word.mapPos, game.objects.feather.position).length() < 1) {
+ word.collected = game.view.clock.getElapsedTime();
+ game.objects.words.collectedCount += 1;
+ }
+ let x, y, z;
+ let collectionAnimationDuration = 1.0;
+ for(let j = 0; j < word.children.length; j++) {
+ game.var.notCollectedPos.set(0, 0, 0);
+ game.var.collectedPos.set(0, 0, 0);
+ let letter = word.children[j];
+ let wordProgress = j / word.children.length;
+ let animationProgress = (((game.timeProgress + 5 * word.randomAnimOffset) % 5) / 5 + j / 37) % 1;
+ if(!word.collected || game.view.clock.getElapsedTime() - word.collected <= collectionAnimationDuration) {
+ const wordAnimationRadius = 0.2;
+ x = word.mapPos.x + wordAnimationRadius * Math.cos(animationProgress * 5 * Math.PI * 2);
+ y = word.mapPos.y + wordAnimationRadius * Math.sin(animationProgress * 4 * Math.PI * 2);
+ z = wordAnimationRadius * Math.sin(animationProgress * 6 * Math.PI * 2);
+ game.var.notCollectedPos.set(x, y, z);
}
- let x, y, z;
- let collectionAnimationDuration = 1.0;
- for(let j = 0; j < word.children.length; j++) {
- notCollectedPos.set(0, 0, 0);
- collectedPos.set(0, 0, 0);
- let letter = word.children[j];
- let wordProgress = j / word.children.length;
- let animationProgress = (((game.timeProgress + 5 * word.randomAnimOffset) % 5) / 5 + j / 37) % 1;
- if(!word.collected || game.view.clock.getElapsedTime() - word.collected <= collectionAnimationDuration) {
- const wordAnimationRadius = 0.2;
- x = word.mapPos.x + wordAnimationRadius * Math.cos(animationProgress * 5 * Math.PI * 2);
- y = word.mapPos.y + wordAnimationRadius * Math.sin(animationProgress * 4 * Math.PI * 2);
- z = wordAnimationRadius * Math.sin(animationProgress * 6 * Math.PI * 2);
- notCollectedPos.set(x, y, z);
- }
- if(word.collected) {
- x = game.objects.feather.scale.x * 0.5 * Math.cos(animationProgress * 1 * Math.PI * 2);
- y = 0.2 * Math.sin(animationProgress * 7 * Math.PI * 2);
- z = 0.2 * Math.cos(animationProgress * 7 * Math.PI * 2);
- x = x * Math.cos(game.objects.feather.rotation.z) - y * Math.sin(game.objects.feather.rotation.z);
- y = y * Math.cos(game.objects.feather.rotation.z) + x * Math.sin(game.objects.feather.rotation.z);
- x += game.objects.feather.position.x;
- y += game.objects.feather.position.y;
- z += game.objects.feather.position.z;
- collectedPos.set(x, y, z);
- }
- if(notCollectedPos.length() > 0 && collectedPos.length() > 0) {
- let collectingProgress = easeInOut(Math.max(0.0, Math.min(1.0, (game.view.clock.getElapsedTime() - word.collected) / collectionAnimationDuration)));
- letterPos.lerpVectors(notCollectedPos, collectedPos, collectingProgress);
- let scale = lerp(1.0, collectedScale, collectingProgress);
- letter.scale.set(scale, scale, scale);
- } else if(notCollectedPos.length() > 0) {
- letterPos.set(notCollectedPos.x, notCollectedPos.y, notCollectedPos.z);
- } else if(collectedPos.length() > 0) {
- letterPos.set(collectedPos.x, collectedPos.y, collectedPos.z);
- }
- letter.position.set(letterPos.x, letterPos.y, letterPos.z);
- let rotation = (game.timeProgress * 3) % (2 * Math.PI);
- letter.rotation.set(rotation + j, rotation + 2*j, rotation + 3*j);
+ if(word.collected) {
+ x = game.objects.feather.scale.x * 0.5 * Math.cos(animationProgress * 1 * Math.PI * 2);
+ y = 0.2 * Math.sin(animationProgress * 7 * Math.PI * 2);
+ z = 0.2 * Math.cos(animationProgress * 7 * Math.PI * 2);
+ x = x * Math.cos(game.objects.feather.rotation.z) - y * Math.sin(game.objects.feather.rotation.z);
+ y = y * Math.cos(game.objects.feather.rotation.z) + x * Math.sin(game.objects.feather.rotation.z);
+ x += game.objects.feather.position.x;
+ y += game.objects.feather.position.y;
+ z += game.objects.feather.position.z;
+ game.var.collectedPos.set(x, y, z);
+ }
+ if(game.var.notCollectedPos.length() > 0 && game.var.collectedPos.length() > 0) {
+ let collectingProgress = easeInOut(Math.max(0.0, Math.min(1.0, (game.view.clock.getElapsedTime() - word.collected) / collectionAnimationDuration)));
+ game.var.letterPos.lerpVectors(game.var.notCollectedPos, game.var.collectedPos, collectingProgress);
+ let scale = lerp(1.0, collectedScale, collectingProgress);
+ letter.scale.set(scale, scale, scale);
+ } else if(game.var.notCollectedPos.length() > 0) {
+ game.var.letterPos.set(game.var.notCollectedPos.x, game.var.notCollectedPos.y, game.var.notCollectedPos.z);
+ } else if(game.var.collectedPos.length() > 0) {
+ game.var.letterPos.set(game.var.collectedPos.x, game.var.collectedPos.y, game.var.collectedPos.z);
}
+ letter.position.set(game.var.letterPos.x, game.var.letterPos.y, game.var.letterPos.z);
+ let rotation = (game.timeProgress * 3) % (2 * Math.PI);
+ letter.rotation.set(rotation + j, rotation + 2*j, rotation + 3*j);
}
-
- renderer.render(scene, game.view.camera);
- }
- renderer.setAnimationLoop(animate);
-
- function pinwheelPositionUpdate(game, viewportX, viewportY) {
- const vFOV = THREE.MathUtils.degToRad(game.view.camera.fov);
- const viewportHeight = 2 * Math.tan(vFOV / 2) * (game.view.camera.position.z - game.objects.pinwheel.position.z);
- game.objects.pinwheel.cameraX = viewportHeight * viewportX;
- game.objects.pinwheel.cameraY = - viewportHeight * viewportY;
}
- function cursorMoveEvent(canvasLocalX, canvasLocalY) {
- const canvasBb = canvas.getBoundingClientRect();
- // Intentional division by height instead of width in the following line, since
- // three.js controls the vertical FOV. So if we ever change the aspect ratio from 1:1,
- // y will still be in range (-0.5, 0.5), but the range for x will be smaller or larger.
- const x = (canvasLocalX - canvasBb.x - (canvasBb.width / 2)) / canvasBb.height;
- const y = (canvasLocalY - canvasBb.y - (canvasBb.height / 2)) / canvasBb.height;
- pinwheelPositionUpdate(game, x, y);
- }
-
- document.body.addEventListener('mousemove', e => cursorMoveEvent(e.clientX, e.clientY));
- document.body.addEventListener('touchmove', e => cursorMoveEvent(e.touches[0].clientX, e.touches[0].clientY));
+ renderer.render(scene, game.view.camera);
}
document.querySelector('#enableMusic').checked = false;
+document.querySelector('.startGame').addEventListener('click', () => {
+ game.ui.moveToPage('gameplay');
+ start(game);
+});
window['game'] = {
state: 'loadingAssets',
ui: {
page.style.display = 'none';
}, fadeDuration, page);
});
- const targetElem = game.ui.root.querySelector('.ui-page.' + target + '');
- targetElem.style.opacity = '0';
- targetElem.style.display = 'flex';
- setTimeout((targetElem) => {
- targetElem.style.opacity = '1';
- }, fadeDuration, targetElem);
+ const targetElems = [game.ui.root.querySelector('.ui-page.' + target + '')];
+ if(game.ui.root.querySelector('.ui-page.gameplay').style.opacity != '1') {
+ targetElems.push(game.ui.root.querySelector('.ui-page.gameplay'));
+ }
+ for(let targetElem of targetElems) {
+ if(!targetElem.classList.contains('gameplay')) {
+ targetElem.style.opacity = '0';
+ }
+ targetElem.style.display = 'flex';
+ setTimeout((targetElem) => {
+ targetElem.style.opacity = '1';
+ }, fadeDuration, targetElem);
+ }
+ game.ui.currentPage = target;
};
loadAllAssets(game, (progress) => {
let percentage = Math.floor(100 * progress);
document.querySelector('.ui-page.loading progress').value = percentage;
document.querySelector('.ui-page.loading span').innerText = percentage;
}).then(() => {
- game.ui.moveToPage('gameplay');
+ game.ui.moveToPage('title');
init(window['game'], document.querySelector('canvas'));
document.querySelector('#enableMusic').addEventListener('change', () => toggleMusic(window['game']));
}, (err) => {