summaryrefslogtreecommitdiff
path: root/init.js
diff options
context:
space:
mode:
authorLizzy Fleckenstein <eliasfleckenstein@web.de>2023-07-15 01:47:15 +0200
committerLizzy Fleckenstein <eliasfleckenstein@web.de>2023-07-15 01:48:53 +0200
commit7a4d4da77891ad1d6979b6fff1cd4ac92c2b30b0 (patch)
tree96511887357fdc63216b2576d54b1d88e7bf367d /init.js
downloadaschaffenburg.fun-7a4d4da77891ad1d6979b6fff1cd4ac92c2b30b0.tar.xz
initial commit
Diffstat (limited to 'init.js')
-rw-r--r--init.js425
1 files changed, 425 insertions, 0 deletions
diff --git a/init.js b/init.js
new file mode 100644
index 0000000..3b3c884
--- /dev/null
+++ b/init.js
@@ -0,0 +1,425 @@
+import * as THREE from "three";
+import maplibregl from "maplibre-gl";
+import "maplibre-gl/dist/maplibre-gl.css";
+import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
+import { SVGLoader } from "three/addons/loaders/SVGLoader.js";
+import * as SunCalc from "suncalc";
+import { vec3, mat4 } from "gl-matrix";
+
+let target;
+
+// if something fishy happens to the local storage, errors or load could render the game unplayable
+// use try-catch to prevent this
+try {
+ target = JSON.parse(localStorage.getItem("position"));
+} catch {}
+target = target || { lng: 9.142202119898826, lat: 49.97692244755174 };
+
+const map = new maplibregl.Map({
+ container: "map",
+ center: target,
+ minZoom: 16,
+ maxZoom: 21,
+ zoom: 20,
+ pitch: 45,
+ minPitch: 1,
+ antialias: true,
+ dragPan: false,
+ scrollZoom: { around: "center" },
+ touchZoomRotate: { around: "center" },
+ doubleClickZoom: false,
+ // key leakage is part of maptiler's ecosystem *shrug*
+ // their "fix" is to allow restricting keys to certain 'Origin' headers ("pinky promise uwu")
+ // honestly api keys are cringe anyway
+ style:
+ "https://api.maptiler.com/maps/streets/style.json?key=DOnvuOySyPyQM83lAx0a",
+ /*{
+ version: 8,
+ sources: {
+ osm: {
+ type: "raster",
+ tiles: ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"],
+ tileSize: 256,
+ maxzoom: 20,
+ },
+ },
+ layers: [
+ {
+ id: "osm",
+ type: "raster",
+ source: "osm",
+ },
+ ],
+ },*/
+});
+
+// hack. otherwise, zooming/rotating won't work while moving
+map.stop = () => {};
+
+// https://github.com/maplibre/maplibre-gl-js/discussions/1521
+map.getCameraPosition = () => {
+ const pitch = map.transform._pitch;
+ const altitude = Math.cos(pitch) * map.transform.cameraToCenterDistance;
+ const latOffset = Math.tan(pitch) * map.transform.cameraToCenterDistance;
+ const latPosPointInPixels = map.transform.centerPoint.add(
+ new maplibregl.Point(0, latOffset),
+ );
+ const latLong = map.transform.pointLocation(latPosPointInPixels);
+ const verticalScaleConstant =
+ map.transform.worldSize /
+ (2 * Math.PI * 6378137 * Math.abs(Math.cos(latLong.lat * (Math.PI / 180))));
+ const altitudeInMeters = altitude / verticalScaleConstant;
+ return {
+ lng: latLong.lng,
+ lat: latLong.lat,
+ altitude: altitudeInMeters,
+ pitch: (pitch * 180) / Math.PI,
+ };
+};
+
+const clamp = (min, max, x) => Math.min(max, Math.max(min, x));
+
+const camera = new THREE.PerspectiveCamera();
+const scene = new THREE.Scene();
+
+const marker = new THREE.Group();
+
+new SVGLoader().load("marker.svg", (data) => {
+ const material = new THREE.MeshBasicMaterial({
+ color: new THREE.Color(0),
+ side: THREE.DoubleSide,
+ depthWrite: true,
+ transparent: false,
+ });
+
+ for (const shape of data.paths.flatMap(SVGLoader.createShapes)) {
+ const geometry = new THREE.ShapeGeometry(shape);
+ const mesh = new THREE.Mesh(geometry, material);
+ mesh.scale.setScalar(1 / 1792);
+ mesh.position.set(-0.5, (1536 - 118.237) / 1792, 0);
+ //mesh.position.set(-0.5, 0, (1536 - 118.237) / 1792);
+ mesh.rotateX(Math.PI);
+ //mesh.rotateX(-Math.PI/2);
+ marker.add(mesh);
+ }
+
+ marker.scale.setScalar(50);
+ //marker.on("click", console.log);
+ //scene.add(marker);
+});
+
+const mapToMerc = maplibregl.MercatorCoordinate.fromLngLat;
+
+const mercToThree = (pos, center = mapToMerc(map.getCenter(), 0)) => {
+ return new THREE.Vector3(
+ pos.x - center.x,
+ pos.z - center.z,
+ pos.y - center.y,
+ ).divideScalar(center.meterInMercatorCoordinateUnits());
+};
+
+const mapToThree = (lngLat, altitude) => {
+ return mercToThree(mapToMerc(lngLat, altitude));
+};
+
+/*new THREE.FBXLoader().load(
+ "raphtalia.fbx",
+ ((model) => {
+ model.traverse((child) => {
+ if (child.isMesh) {
+ // child.material.color = new THREE.Color(0xffffff);
+ // delete child.material.color;
+ (child.material.isMaterial ? [child.material] : child.material)
+ .forEach(m => {
+ m.castShadow = true;
+ })
+
+ child.castShadow = true;
+ }
+ });
+ this.scene.add(model);
+ player = model;
+ update(target);
+ }).bind(this)
+);*/
+
+const enableShadow = (model) => {
+ model.traverse((child) => {
+ if (child.isMesh) {
+ (child.material.isMaterial ? [child.material] : child.material).forEach(
+ (m) => {
+ m.castShadow = true;
+ },
+ );
+
+ child.castShadow = true;
+ }
+ });
+};
+
+let player;
+{
+ const path = "mei/"; // jasper/
+ const scale = 3.0; // 1.5
+
+ new GLTFLoader()
+ .setPath(path)
+ .setResourcePath(path)
+ .load("scene.gltf", (gltf) => {
+ player = gltf;
+
+ enableShadow(player.scene);
+ player.scene.scale.setScalar(scale);
+
+ player.clock = new THREE.Clock();
+ player.mixer = new THREE.AnimationMixer(player.scene);
+ player.walk = player.mixer.clipAction(player.animations[0]);
+
+ scene.add(player.scene);
+ });
+}
+
+// shadow plane
+{
+ const geometry = new THREE.PlaneGeometry(60, 60);
+ geometry.lookAt(new THREE.Vector3(0, 1, 0));
+
+ const material = new THREE.ShadowMaterial();
+ material.opacity = 0.3;
+
+ const plane = new THREE.Mesh(geometry, material);
+ plane.receiveShadow = true;
+ // plane.position.set(0, 0, 0.01);
+ scene.add(plane);
+}
+
+// animated circle around player
+{
+ const geometry = new THREE.CircleGeometry(7, 64);
+ geometry.lookAt(new THREE.Vector3(0, 1, 0));
+
+ const material = new THREE.MeshBasicMaterial({ color: 0xbebab6 });
+ material.transparent = true;
+
+ const circle = new THREE.Mesh(geometry, material);
+ circle.position.set(0, 0.01, 0);
+ scene.add(circle);
+
+ const clock = new THREE.Clock();
+
+ let t = 0.0;
+ const animate = () => {
+ const ph = [0.75, 0.9, 1.25];
+ t = (t + (clock.getDelta() * ph[2]) / 5) % ph[2];
+
+ const rlerp = (min, max, x) => clamp(0, 1, (x - min) / (max - min));
+ const pow = Math.pow;
+
+ circle.scale.setScalar(pow(rlerp(ph[0], ph[1], t), 2.0));
+ material.opacity = pow(1 - rlerp(ph[1], ph[2], t), 2.0) * 0.8;
+
+ requestAnimationFrame(animate);
+ };
+
+ animate();
+}
+
+class Celestial extends THREE.DirectionalLight {
+ constructor(color, intensity, positionFunc) {
+ super(color, intensity);
+
+ this.castShadow = true;
+ this.shadow.mapSize.width = 1024;
+ this.shadow.mapSize.height = 1024;
+
+ const frustumSize = 15;
+ this.shadow.camera = new THREE.OrthographicCamera(
+ -frustumSize / 2,
+ frustumSize / 2,
+ frustumSize / 2,
+ -frustumSize / 2,
+ 1,
+ 50,
+ );
+
+ this.positionFunc = positionFunc;
+ this.update();
+
+ //scene.add(new THREE.CameraHelper(this.shadow.camera));
+ scene.add(this);
+
+ this.time = 23;
+ addEventListener(
+ "keypress",
+ ((evt) => {
+ switch (evt.key) {
+ case "h":
+ this.time += 1;
+ break;
+ case "l":
+ this.time -= 1;
+ break;
+ default:
+ return;
+ }
+ evt.preventDefault();
+ }).bind(this),
+ );
+ }
+
+ update() {
+ const pos = map.getCenter();
+ const p = this.positionFunc(
+ new Date(this.time * 1000 * 60 * 10),
+ pos.lat,
+ pos.lng,
+ );
+
+ p.altitude = (p.altitude + Math.PI * 2) % (Math.PI * 2);
+ this.visible =
+ p.altitude > Math.PI * 0.05 && p.altitude < Math.PI * (1 - 0.05);
+
+ this.position
+ .set(1, 0, 0)
+ .applyEuler(new THREE.Euler(0, p.altitude, p.azimuth))
+ .multiplyScalar(5);
+
+ this.shadow.camera.position.copy(this.position);
+ this.shadow.camera.lookAt(scene.position);
+ }
+}
+
+scene.add(new THREE.AmbientLight(0xffffff, 0.8));
+
+const sun = new Celestial(0xffffff, 0.4, SunCalc.getPosition);
+// const moon = new Celestial(0x506886, 0.4, SunCalc.getMoonPosition);
+
+setInterval(() => {
+ sun.update();
+ // moon.update();
+}, 10);
+
+const renderer = new THREE.WebGLRenderer({
+ canvas: map.getCanvas(),
+ context: map.painter.context.gl,
+ antialias: true,
+});
+renderer.shadowMap.enabled = true;
+// renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+renderer.autoClear = false;
+
+const info = document.body.appendChild(document.createElement("span"));
+info.style.position = "absolute";
+info.style.zIndex = 5;
+info.style.color = "green";
+
+const render = (gl, mercViewProj) => {
+ if (player) player.mixer.update(player.clock.getDelta());
+
+ const camMap = map.getCameraPosition();
+ const camMerc = mapToMerc(camMap, camMap.altitude);
+ const cam = mercToThree(camMerc);
+
+ const mercViewProjI = mat4.invert([], mercViewProj);
+
+ const depthNCDtoThree = (depth) => {
+ const [x, y, z] = vec3.transformMat4([], [0, 0, depth], mercViewProjI);
+ return mercToThree({ x, y, z }, camMerc).length();
+ };
+
+ camera.aspect = innerWidth / innerHeight;
+ camera.fov = map.transform.fov;
+ camera.near = depthNCDtoThree(-1);
+ camera.far = depthNCDtoThree(+1);
+ camera.updateProjectionMatrix();
+
+ camera.position.copy(cam);
+ camera.lookAt(scene.position);
+
+ cam.y = 0;
+ marker.lookAt(cam);
+
+ renderer.resetState();
+ renderer.render(scene, camera);
+ map.triggerRepaint();
+};
+
+map.on("style.load", () => {
+ map.addLayer(
+ {
+ id: "3d-model",
+ type: "custom",
+ renderingMode: "3d",
+ render,
+ },
+ "building-3d",
+ );
+});
+
+addEventListener("resize", () => {
+ renderer.setSize(innerWidth, innerHeight);
+});
+
+let playerAnimDuration = 0;
+{
+ const clock = new THREE.Clock();
+
+ const animate = () => {
+ requestAnimationFrame(animate);
+
+ const dt = clock.getDelta();
+
+ if (playerAnimDuration <= 0) return;
+
+ const lerp = (a, b, x) => a * (1 - x) + b * x;
+ const x = Math.min(dt / playerAnimDuration, 1);
+
+ const center = map.getCenter();
+ center.lng = lerp(center.lng, target.lng, x);
+ center.lat = lerp(center.lat, target.lat, x);
+
+ playerAnimDuration -= dt;
+
+ if (playerAnimDuration <= 0) player.walk.stop();
+
+ map.setCenter(center);
+ };
+
+ animate();
+}
+
+const clock = new THREE.Clock();
+clock.getDelta();
+
+const setTarget = (pos) => {
+ const dt = clock.getDelta();
+
+ if (player) {
+ player.scene.lookAt(mapToThree(pos));
+ player.walk.play();
+ }
+
+ playerAnimDuration = Math.min(dt, 1.5);
+ localStorage.setItem("position", JSON.stringify((target = pos)));
+};
+
+const watchGeo = navigator.geolocation.watchPosition(
+ ({ coords: { longitude: lng, latitude: lat } }) => {
+ const pos = { lng, lat };
+ setTarget(pos);
+ },
+ (err) => {
+ // todo: err.message;
+ navigator.geolocation.clearWatch(watchGeo);
+
+ const click = (evt) => {
+ setTarget(evt.lngLat);
+ };
+
+ map.on("click", click);
+ map.on("touched", click);
+ },
+ {
+ enableHighAccuracy: true,
+ },
+);