Published
Edited
Sep 16, 2021
Fork of AntCraft
2 forks
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
aspectRatio = window.screen.width / window.screen.height
Insert cell
height = ~~(width / aspectRatio)
Insert cell
Insert cell
// Creates a key for a world cell
function key(cell) {
return cell.toString();
}
Insert cell
// Obtains a cell address from a world position (stored as a THREE.Vector3)
function positionToCell(pos) {
return pos.toArray().map((x) => Math.round(x));
}
Insert cell
// Obtains a world position from a cell
function cellToPosition(cell) {
return new THREE.Vector3(...cell);
}
Insert cell
// The world map
mutable worldMap = {
reset;
let cells = new Map();
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
cells.set(key([i, 0, j]), { address: [i, 0, j], type: "block" });
}
}
cells.set(key([1, 1, 1]), {
address: [1, 1, 1],
type: "player",
quat: [0, 0, 0, 1]
});
return cells;
}
Insert cell
// Function to set the world map to the configuration stored in the given string
function loadWorldMap(string) {
let array = JSON.parse(string);
const newMap = new Map();
for (let [key, value] of array) newMap.set(key, value);
blocksGroup.remove(...blocksGroup.children);
mutable worldMap = newMap;
}
Insert cell
// Function that can be called to write a file with a text representation of the world map
saveWorldMap = {
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
return function (fileName) {
let copyMap = new Map();
{
// Add player info to map that will be saved
let address = camera.position.toArray();
let quat = camera.quaternion.toArray();
let type = "player";
copyMap.set(key(address), { address, quat, type });
}
for (let [key, value] of worldMap) {
if (value.type == "player") continue; // Will disregard the player
let copyValue = { ...value }; // A copy of the value
delete copyValue.mesh; // Remove the three mesh object, if any
copyMap.set(key, copyValue);
}
let text = JSON.stringify([...copyMap]);
let blob = new Blob([text], {
type: "text/plain;charset=utf-8"
});
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
};
}
Insert cell
Insert cell
//
// Buttons to save the world to text file, or load the world from a text file
//
loadSaveButtons = {
let save = html`<button>Save</button>`;
let savename = html`<input type=text value=world.txt></text>`;
save.onclick = () => saveWorldMap(savename.value);
let load = html`<button>Load</button>`;
let hiddenLoad = html`<input type="file" style="display:none;"/>`;
hiddenLoad.onchange = (f) => {
let components = hiddenLoad.value.split(/\/|\\/);
savename.value = components[components.length - 1];
Files.text(hiddenLoad.files[0]).then((txt) => {
loadWorldMap(txt);
});
hiddenLoad.value = ""; // Clear for the next load
};
load.onclick = () => {
hiddenLoad.click();
};
return html`${load} ${save} ${savename}`;
}
Insert cell
// A reset button for resetting the scene to the default
viewof reset = html`<input type=button style="display:inline-block;margin-right:5px" value="Reset">`
Insert cell
Insert cell
//
// Animation utilities
//
animationManager = {
// Map of animations still being played out
let animMap = new Map();

// Starts animation to apply rotation quat to obj3d.
// When animation is finished, calls callback() if defined.
// The animation has duration steps * stepTime.
function rotate(obj3d, quat, callback, steps = 20, stepTime = 20) {
if (animMap.has(obj3d)) return; // Ignore if not finished animating

let oldRot = obj3d.quaternion.clone();
let newRot = obj3d.quaternion.clone().multiply(quat);
let counter = 0;
let id;
// Clean up code
const atEnd = () => {
clearInterval(id);
animMap.delete(obj3d);
if (callback) callback();
};
// Animation callback
const animation = () => {
counter += 1;
THREE.Quaternion.slerp(oldRot, newRot, obj3d.quaternion, counter / steps);
if (counter == steps) {
atEnd();
}
};
// Mark the animation map
animMap.set(obj3d, 1);

// start animation
id = setInterval(animation, stepTime);
}

// Starts animation to translate obj3d to a new position.
// When animation is finished, calls callback() if defined.
// The animation has duration steps * stepTime.
function translate(obj3d, newPos, callback, steps = 20, stepTime = 20) {
if (animMap.has(obj3d)) return; // Ignore if not finished animating

let oldPos = obj3d.position.clone();
let counter = 0;
let id;
// Clean up code
const atEnd = () => {
clearInterval(id);
animMap.delete(obj3d);
if (callback) callback();
};
// Animation callback
const animation = () => {
counter += 1;
obj3d.position.lerpVectors(oldPos, newPos, counter / steps);
if (counter == steps) atEnd();
};
// Mark the animation map
animMap.set(obj3d, 1);
// start animation
id = setInterval(animation, stepTime);
}

return { animMap, rotate, translate };
}
Insert cell
//
// Sets up callbacks for mouse and keyboard events. Renders the scene while
// animations are taking place
//
interaction = {
reset;

// Quaternions for the 4 3d rotations
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)
};

// Returns the local axes of a 3d object in world coordinates
const localAxes = (obj3d) => {
let axes = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()];
obj3d.matrix.extractBasis(...axes);
return axes;
};

// The cell in front of the user
const inFront = () =>
positionToCell(camera.position.clone().sub(localAxes(camera)[2]));

// The cell behind the user
const behind = () =>
positionToCell(camera.position.clone().add(localAxes(camera)[2]));

// Actions
const turnLeft = () => {
animationManager.rotate(camera, rotations.left, render);
};
const turnRight = () => {
animationManager.rotate(camera, rotations.right, render);
};
const moveAhead = () => {
animationManager.translate(camera, cellToPosition(inFront()), render);
};
const moveBack = () => {
animationManager.translate(camera, cellToPosition(behind()), render);
};

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":
case " ":
default:
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)`;
}
}
Insert cell
Insert cell
//
// Import the three.js library
//
THREE = {
const THREE = (window.THREE = await require("three@0.119.1"));
await require("three@0.119.1/examples/js/controls/TrackballControls.js").catch(
() => {}
);
await require("three@0.119.1/examples/js/controls/OrbitControls.js").catch(
() => {}
);
return THREE;
}
Insert cell
import { loadFont, textGeometry } from "@esperanc/3d-fonts"
Insert cell
//font = loadFont("droid sans mono")
Insert cell
renderer = new THREE.WebGLRenderer({ canvas })
Insert cell
//
// A camera mounted on the player
//
camera = {
let camera = new THREE.PerspectiveCamera(90, aspectRatio, 0.1, 1000);
camera.position.set(1, 1, 1);
// Obtain camera from worldMap if set
for (let obj of worldMap.values()) {
if (obj.type == "player") {
camera.position.set(...obj.address);
camera.quaternion.set(...obj.quat);
worldMap.delete(key(obj.address));
break;
}
}
return camera;
}
Insert cell
//
// An overview camera for watching the world from afar
//
overviewCamera = {
reset;
let overview = new THREE.PerspectiveCamera(60, aspectRatio, 0.1, 1000);
overview.position.set(0, 0, 15);
return overview;
}
Insert cell
//
// Set of mouse controls for moving around the scene with the overview camera
//
controls = {
let controls = new THREE.OrbitControls(overviewCamera, canvas);
// Disable keyboard controls
controls.keys = {
LEFT: null, //left arrow
UP: null, // up arrow
RIGHT: null, // right arrow
BOTTOM: null // down arrow
};
// Disable the overview by default. Enable by pressing the 'tab' key
controls.enabled = false;
invalidation.then(() => controls.dispose());
return controls;
}
Insert cell
//
// Arrange so that when the contols are enabled, any change in the camera settings triggers a render
//
controlsRenderCallback = {
controls.addEventListener("change", render);
invalidation.then(() => controls.removeEventListener("change", render));
}
Insert cell
//
// An arrow representing the player, attached to the camera object
//
player = {
// const geometry = new THREE.ConeGeometry(0.5, 1, 16);
const shape = new THREE.Shape();
shape.moveTo(0, 0.5);
shape.lineTo(0.5, 0.25);
shape.lineTo(0.25, 0.25);
shape.lineTo(0.25, -0.5);
shape.lineTo(-0.25, -0.5);
shape.lineTo(-0.25, 0.25);
shape.lineTo(-0.5, 0.25);
const extrudeSettings = {
steps: 1,
depth: 0.1,
bevelEnabled: true,
bevelThickness: 0.1,
bevelSize: 0.1,
bevelOffset: 0,
bevelSegments: 10
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const material = new THREE.MeshStandardMaterial({ color: 0x00aaaa });
const cone = new THREE.Mesh(geometry, material);
cone.rotation.set(-Math.PI / 2, 0, 0);
cone.position.set(0, -0.4, 0);
cone.scale.set(0.6, 0.6, 0.6);
const group = new THREE.Group();
group.add(cone);
group.visible = false;
camera.add(group);
invalidation.then(() => camera.remove(group));
return group;
}
Insert cell
blocksGroup = {
reset;
return new THREE.Group();
}
Insert cell
blockGeometry = new THREE.BoxGeometry(0.95, 0.95, 0.95)
Insert cell
blockMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 })
Insert cell
letterMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa })
Insert cell
function makeWorldBlocks() {
for (let obj of blocksGroup.children) {
if (!worldMap.has(obj.key)) blocksGroup.remove(obj);
}
for (let [k, v] of worldMap) {
if (!v.mesh) {
v.mesh = new THREE.Mesh(blockGeometry, blockMaterial);
v.mesh.position.set(...v.address);
if (v.quat) v.mesh.quaternion.set(...v.quat);
v.mesh.key = key(v.address);
blocksGroup.add(v.mesh);
}
}
}
Insert cell
//
// Array of images for building a cubemap
//
cubemapImages = [
await FileAttachment("px@2.png").image(),
await FileAttachment("nx@3.png").image(),
await FileAttachment("py@2.png").image(),
await FileAttachment("ny@2.png").image(),
await FileAttachment("pz@2.png").image(),
await FileAttachment("nz@2.png").image()
]
// [
// await FileAttachment("px@1.png").image(),
// await FileAttachment("nx@1.png").image(),
// await FileAttachment("py@1.png").image(),
// await FileAttachment("ny@1.png").image(),
// await FileAttachment("pz@1.png").image(),
// await FileAttachment("nz@1.png").image()
// ]
Insert cell
cubemap = {
let map = new THREE.CubeTexture(cubemapImages);
map.needsUpdate = true;
return map;
}
Insert cell
light = {
let group = new THREE.Group();
let dLight = new THREE.DirectionalLight(0xffffff, 0.8);
dLight.position.set(5, 10, 20);
let dLight2 = new THREE.DirectionalLight(0xffffff, 1);
dLight2.position.set(-5, -10, -20);
group.add(dLight, dLight2);
return group;
}
Insert cell
scene = {
reset;
let scene = new THREE.Scene();
scene.background = cubemap;
makeWorldBlocks();
scene.add(blocksGroup);
scene.add(light);
scene.add(camera);
return scene;
}
Insert cell
function render() {
if (controls.enabled) {
player.visible = true;
renderer.render(scene, overviewCamera);
} else {
player.visible = false;
renderer.render(scene, camera);
}
}
Insert cell
initialRender = {
reset;
canvas.focus();
render();
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more