interaction = {
reset;
const halfTurn = Math.PI / 2;
const rot = (dx, dy) =>
new THREE.Quaternion().setFromEuler(
new THREE.Euler(dx * halfTurn, dy * halfTurn, 0)
);
const rotations = {
left: rot(0, 1),
right: rot(0, -1),
up: rot(1, 0),
down: rot(-1, 0)
};
const localAxes = (obj3d) => {
let axes = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()];
obj3d.matrix.extractBasis(...axes);
return axes;
};
const inFront = () =>
positionToCell(camera.position.clone().sub(localAxes(camera)[2]));
const behind = () =>
positionToCell(camera.position.clone().add(localAxes(camera)[2]));
const inFrontBelow = () => {
let axes = localAxes(camera);
return positionToCell(camera.position.clone().sub(axes[1]).sub(axes[2]));
};
// The cell behind and below the user
const behindBelow = () => {
let axes = localAxes(camera);
return positionToCell(camera.position.clone().sub(axes[1]).add(axes[2]));
};
// Actions
const turnLeft = () => {
animationManager.rotate(camera, rotations.left, render);
};
const turnRight = () => {
animationManager.rotate(camera, rotations.right, render);
};
const moveAhead = () => {
if (worldMap.has(key(inFrontBelow())) && !worldMap.has(key(inFront()))) {
// Straight ahead
animationManager.translate(camera, cellToPosition(inFront()), render);
} else if (worldMap.has(key(inFront()))) {
// Walk on wall ahead
animationManager.rotate(camera, rotations.up, render);
} else if (!worldMap.has(key(inFrontBelow()))) {
// Walk around ledge ahead
let A = cellToPosition(inFront());
let B = cellToPosition(inFrontBelow());
let pos2 = camera.position.clone().lerp(A, 0.5);
let pos3 = A.clone().lerp(B, 0.5);
animationManager.translate(
camera,
pos2,
() => {
animationManager.translateRotate(
camera,
pos3,
rotations.down,
() => {
animationManager.translate(camera, B, render, 10);
},
10
);
},
10
);
}
};
const moveBack = () => {
if (worldMap.has(key(behindBelow())) && !worldMap.has(key(behind()))) {
// Straight back
animationManager.translate(camera, cellToPosition(behind()), render);
} else if (worldMap.has(key(behind()))) {
// Walk on wall behind
animationManager.rotate(camera, rotations.down, render);
} else if (!worldMap.has(key(behindBelow()))) {
// Walk around ledge behind
let A = cellToPosition(behind());
let B = cellToPosition(behindBelow());
let pos2 = camera.position.clone().lerp(A, 0.5);
let pos3 = A.clone().lerp(B, 0.5);
animationManager.translate(
camera,
pos2,
() => {
animationManager.translateRotate(
camera,
pos3,
rotations.up,
() => {
animationManager.translate(camera, B, render, 10);
},
10
);
},
10
);
}
};
const makeBlock = (cell) => {
if (!worldMap.has(key(cell))) {
worldMap.set(key(cell), { address: cell, type: "block" });
makeWorldBlocks();
}
};
const removeBlock = (cell) => {
if (worldMap.has(key(cell))) {
worldMap.delete(key(cell));
makeWorldBlocks();
}
};
const makeLetter = (cell, char) => {
if (!worldMap.has(key(cell))) {
worldMap.set(key(cell), {
address: cell,
type: "char",
quat: camera.quaternion.toArray(),
char
});
makeWorldBlocks();
}
};
const switchCameras = () => {
controls.enabled = !controls.enabled;
render();
};
// Keyboard callback
const keyboardCallback = (e) => {
e.preventDefault();
switch (e.key) {
case "ArrowLeft":
turnLeft();
break;
case "ArrowRight":
turnRight();
break;
case "ArrowUp":
moveAhead();
break;
case "ArrowDown":
moveBack();
break;
case "Tab":
switchCameras();
break;
case "Delete":
case "Backspace": {
let cell = inFront();
if (worldMap.has(key(cell))) removeBlock(cell);
else {
cell = inFrontBelow();
if (worldMap.has(key(cell))) removeBlock(cell);
}
render();
break;
}
case " ": {
let cell = inFrontBelow();
if (!worldMap.has(key(cell))) makeBlock(cell);
else {
cell = inFront();
if (!worldMap.has(key(cell))) makeBlock(cell);
}
render();
break;
}
default:
if (e.key.length == 1) {
let cell = inFrontBelow();
if (!worldMap.has(key(cell))) makeLetter(cell, e.key);
else {
cell = inFront();
if (!worldMap.has(key(cell))) makeLetter(cell, e.key);
}
render();
}
break;
}
};
// Mouse callback
const mouseCallback = (e) => {
if (controls.enabled) return;
if (e.offsetX < canvas.width / 3) turnLeft();
else if (e.offsetX > (canvas.width / 3) * 2) turnRight();
else if (e.offsetY < canvas.height / 2) moveAhead();
else moveBack();
};
// Register callbacks
canvas.addEventListener("keydown", keyboardCallback);
canvas.addEventListener("mousedown", mouseCallback);
// House cleaning for when this cell is redefined
invalidation.then(() => {
canvas.removeEventListener("keydown", keyboardCallback);
canvas.removeEventListener("mousedown", mouseCallback);
});
// Loop to render frames while animations are taking place
for (let frame = 1; ; frame++) {
if (animationManager.animMap.size > 0) {
render();
yield `${frame} rendered`;
} else yield `${frame} (no render)`;
}
}