From 0a4f63cbd3aad4d3d6de4576cdbeca83d9425133 Mon Sep 17 00:00:00 2001 From: Lizzy Fleckenstein Date: Mon, 24 Jul 2023 23:14:08 +0200 Subject: bump --- init.js | 1057 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 920 insertions(+), 137 deletions(-) (limited to 'init.js') diff --git a/init.js b/init.js index 3b3c884..8b67048 100644 --- a/init.js +++ b/init.js @@ -4,23 +4,35 @@ 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"; +import { vec3, mat4, vec4 } from "gl-matrix"; +import Color from "colorjs.io"; -let target; - -// if something fishy happens to the local storage, errors or load could render the game unplayable +// if something fishy happens to the local storage, errors on 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 storageParse = item => { + try { + return JSON.parse(localStorage.getItem(item)); + } catch {} +}; + +let target = storageParse("position") || { lng: 9.142202119898826, lat: 49.97692244755174 }; +let completed = storageParse("completed") || {}; + +const enable3d = localStorage.getItem("enable3d") != "false"; + +let forceTouchControl = localStorage.getItem("touchcontrol") == "true"; +let gpsError = null; + +const touchControl = () => { + return forceTouchControl || !!gpsError; +}; const map = new maplibregl.Map({ container: "map", center: target, - minZoom: 16, - maxZoom: 21, - zoom: 20, + minZoom: 15, + maxZoom: 20, + zoom: 18, pitch: 45, minPitch: 1, antialias: true, @@ -28,29 +40,32 @@ const map = new maplibregl.Map({ scrollZoom: { around: "center" }, touchZoomRotate: { around: "center" }, doubleClickZoom: false, + attributionControl: false, + bearing: 180, + keyboard: 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", + style: "https://api.maptiler.com/maps/" + (enable3d ? "streets-v2" : "bright") + + "/style.json?key=DOnvuOySyPyQM83lAx0a", + /*{ + version: 8, + sources: { + osm: { + type: "raster", + tiles: ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"], + tileSize: 256, + // maxzoom: 21, + }, }, - ], - },*/ + layers: [ + { + id: "osm", + type: "raster", + source: "osm", + }, + ], + },*/ }); // hack. otherwise, zooming/rotating won't work while moving @@ -77,125 +92,569 @@ map.getCameraPosition = () => { }; }; -const clamp = (min, max, x) => Math.min(max, Math.max(min, x)); +const renderer = new THREE.WebGLRenderer({ + canvas: map.getCanvas(), + context: map.painter.context.gl, + antialias: true, +}); +renderer.shadowMap.enabled = true; +renderer.autoClear = false; const camera = new THREE.PerspectiveCamera(); const scene = new THREE.Scene(); +const gltfLoader = new GLTFLoader(); +const playerScale = 5; + +const raycaster = new THREE.Raycaster(); +const textures = new THREE.TextureLoader(); + +const openOverlay = (close) => { + const overlay = document.body.appendChild(document.createElement("center")); + overlay.style.position = "fixed"; + overlay.style.top = "0px"; + overlay.style.left = "0px"; + overlay.style.width = "100%"; + overlay.style.height = "100%"; + overlay.style.backgroundColor = "rgba(0, 0, 0, 0.3)"; + if (close) + overlay.addEventListener("click", evt => { + if (evt.target != overlay) return; + if (close instanceof Function) close(); + document.body.removeChild(overlay); + }); + return overlay; +} -const marker = new THREE.Group(); +// https://codepen.io/prisoner849/pen/abKdYgZ +const setUV = (geometry) => { + let pos = geometry.attributes.position; + let b3 = new THREE.Box3().setFromBufferAttribute(pos); + let size = new THREE.Vector3(); + b3.getSize(size); + let uv = []; + let v3 = new THREE.Vector2(); + for (let i = 0; i < pos.count; i++) { + v3.fromBufferAttribute(pos, i); + v3.sub(b3.min).divide(size); + uv.push(v3.x, v3.y); + } + geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uv, 2)); +}; -new SVGLoader().load("marker.svg", (data) => { +const markers = [ + { + title: "Liudolf", + name: "stiftskirche", + pos: { lng: 9.146006727402352, lat: 49.973420131538234 }, + year: 950, + }, + { + title: "Willigis", + name: "willigis_bruecke", + pos: { lng: 9.141077866185924, lat: 49.97184032912233 }, + year: 989, + }, + { + title: "Hund Otto", + name: "altstadt", + pos: { lng: 9.143238557511694, lat: 49.973269579558774 }, + year: 1122, + }, + { + title: "Albrecht von Brandenburg", + name: "schoental_ruine", + pos: { lng: 9.151180069019205, lat: 49.97534736445891 }, + year: 1544, + }, + { + title: "Georg Ridinger", + name: "schloss", + pos: { lng: 9.142131607183956, lat: 49.9755936415456 }, + year: 1605, + }, + { + title: "Johann Schweickard von Kronberg", + name: "kronberg", + pos: { lng: 9.143592104550777, lat: 49.97546881166781 }, + year: 1620, + }, + { + title: "Pilger", + name: "pilgerbrunnen", + pos: { lng: 9.145791266971258, lat: 49.97387844558844 }, + year: 1700, + }, + { + title: "Friedrich Carl von Erthal", + name: "schoental", + pos: { lng: 9.153218714184447, lat: 49.97449013687282 }, + year: 1775, + }, + { + title: "Karl Theodor von Dalberg", + name: "stadttheater", + pos: { lng: 9.144483317758414, lat: 49.9744341620889 }, + year: 1811, + }, + { + title: "Ludwig I von Bayern", + name: "pompejanum", + pos: { lng: 9.136472355974632, lat: 49.97739471769839 }, + year: 1840, + }, +]; + +new SVGLoader().load("marker-model.svg", (data) => { + const markerObj = new THREE.Group(); const material = new THREE.MeshBasicMaterial({ color: new THREE.Color(0), side: THREE.DoubleSide, depthWrite: true, transparent: false, + stencilWrite: true, + stencilWriteMask: 0x80, + stencilRef: 0x80, + stencilFunc: THREE.AlwaysStencilFunc, + stencilFail: THREE.KeepStencilOp, + stencilZFail: THREE.KeepStencilOp, + stencilZPass: THREE.ReplaceStencilOp, }); - for (const shape of data.paths.flatMap(SVGLoader.createShapes)) { + for (const [i, shape] of data.paths + .flatMap(SVGLoader.createShapes) + .entries()) { const geometry = new THREE.ShapeGeometry(shape); + setUV(geometry); 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, (1536 - 118.237) / 1792, i * 0.01); //mesh.position.set(-0.5, 0, (1536 - 118.237) / 1792); mesh.rotateX(Math.PI); //mesh.rotateX(-Math.PI/2); - marker.add(mesh); + markerObj.add(mesh); } - marker.scale.setScalar(50); - //marker.on("click", console.log); - //scene.add(marker); + markerObj.scale.setScalar(50); + + for (const marker of markers) { + marker.obj = markerObj.clone(); + marker.obj.marker = marker; + for (const child of marker.obj.children) + child.material = child.material.clone(); + + const texture = textures.load("markers/" + marker.name + "/icon.png"); + texture.flipY = false; + texture.encoding = THREE.sRGBEncoding; + + const material = marker.obj.children[1].material; + material.map = texture; + material.color = new THREE.Color(); + + scene.add(marker.obj); + } }); +const clamp = (min, max, x) => Math.min(max, Math.max(min, x)); +const rlerp = (min, max, x) => clamp(0, 1, (x - min) / (max - min)); + +const gradient = new Color("yellow").range(new Color("blue")); + 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 mercToThree = ({ x, y, z }) => + new THREE.Vector3(x, z, y).divideScalar( + mapToMerc(map.getCenter()) + .meterInMercatorCoordinateUnits(), + ); -const mapToThree = (lngLat, altitude) => { - return mercToThree(mapToMerc(lngLat, altitude)); -}; +const threeCenter = () => mercToThree(mapToMerc(map.getCenter())); -/*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) => { +const mapToThree = (lngLat) => + mercToThree(mapToMerc(lngLat, lngLat.altitude)).sub(threeCenter()); + +let player; + +const playerModels = [ + { + name: "Mei", + scale: 3.0, + }, + { + name: "Paul", + scale: 1.5, + }, + { + name: "Sonic", + scale: 1.5, + }, + { + name: "Naruto", + scale: 3.0, + animationIndex: 4, + doStop: true, + }, + { + name: "Luoli", + scale: 0.03, + doStop: true, + }, + { + name: "Timo", + scale: 1.0, + hook: (player) => { + player.scene.traverse((child) => { + if (child.name in { + "base": true, + "lamppost": true, + "space": true + }) + child.visible = false; + }); + + player.scene.rotateY(Math.PI); + + const grp = new THREE.Group(); + grp.add(player.scene); + player.scene = grp; + + player.walk.timeScale = 2; + } + }, +]; + +for (const m of playerModels) + m.path = "models/" + m.name.toLowerCase() + "/"; + +const setPlayerModel = async (model) => { + const gltf = await new Promise((res, rej) => { + gltfLoader + .setPath(model.path) + .setResourcePath(model.path) + .load("scene.gltf", res, null, rej); + }); + + if (player) + scene.remove(player.scene); + + document.getElementById("model-image").src = model.path + "preview.png"; + + localStorage.setItem("model", model.name); + + player = gltf; + player.doStop = model.doStop; + + player.scene.traverse((child) => { if (child.isMesh) { (child.material.isMaterial ? [child.material] : child.material).forEach( (m) => { m.castShadow = true; + m.stencilWrite = true; + m.stencilWriteMask = 0x80; + m.stencilRef = 0x80; + m.stencilFunc = THREE.AlwaysStencilFunc; + m.stencilFail = THREE.ReplaceStencilOp; + m.stencilZFail = THREE.ReplaceStencilOp; + m.stencilZPass = THREE.ReplaceStencilOp; }, ); child.castShadow = true; } }); + + player.scene.scale.setScalar(model.scale * playerScale); + + player.mixer = new THREE.AnimationMixer(player.scene); + player.walk = player.mixer.clipAction(player.animations[model.animationIndex || 0]); + + if (model.hook) + model.hook(player); + + if (!player.doStop) + player.walk.play(); + + scene.add(player.scene); }; -let player; -{ - const path = "mei/"; // jasper/ - const scale = 3.0; // 1.5 +window.playerModels = playerModels; +window.setPlayerModel = setPlayerModel; + +const openContainer = (close, center) => { + const overlay = openOverlay(close); + + const container = overlay.appendChild(document.createElement("div")); + container.style.backgroundColor = "#e4edd7"; + container.style.borderColor = "#7fb82e"; + container.style.borderStyle = "solid"; + container.style.borderRadius = "10px"; + container.style.borderWidth = "4px"; + container.style.position = "absolute"; + container.style.width = "90%"; + container.style.left = "5%"; + + if (center) { + container.style.top = "50%"; + container.style.transform = "translateY(-50%)"; + } - new GLTFLoader() - .setPath(path) - .setResourcePath(path) - .load("scene.gltf", (gltf) => { - player = gltf; + return [overlay, container]; +} + +const divOverlay = () => { + const overlay = document.createElement("div"); + overlay.style.position = "absolute"; + overlay.style.top = "0px"; + overlay.style.left = "0px"; + overlay.style.width = "100%"; + overlay.style.height = "100%"; + overlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; + return overlay; +}; + +const modelSelectionUI = (canClose) => { + const [overlay, container] = openContainer(canClose, true); + + const perRow = innerWidth > innerHeight ? 3 : 2; + + const h1 = container.appendChild(document.createElement("h1")); + h1.innerText = "Wähle dein Aussehen"; - enableShadow(player.scene); - player.scene.scale.setScalar(scale); + const table = container.appendChild(document.createElement("table")); + table.style.padding = "1em"; + table.style.paddingTop = ""; + table.style.width = "calc(100%-2em)"; - player.clock = new THREE.Clock(); - player.mixer = new THREE.AnimationMixer(player.scene); - player.walk = player.mixer.clipAction(player.animations[0]); + let clicked = false; + let tr; + for (const [i, model] of playerModels.entries()) { + if (i % perRow == 0) { + tr = table.appendChild(document.createElement("tr")); + } - scene.add(player.scene); + const td = tr.appendChild(document.createElement("td")); + + td.style.position = "relative"; + td.style.textAlign = "center"; + td.style.backgroundColor = "#d9e9c6"; + td.style.borderRadius = "10px"; + td.style.cursor = "pointer"; + + td.addEventListener("click", () => { + if (clicked) return; + clicked = true; + + const loadOverlay = td.appendChild(divOverlay()); + loadOverlay.style.borderRadius = "10px"; + loadOverlay.style.textAlign = "center"; + loadOverlay.style.display = "table"; + + const loadText = loadOverlay.appendChild(document.createElement("div")); + loadText.innerText = "Lädt..."; + loadText.style.fontSize = "1.5em"; + loadText.style.color = "white"; + loadText.style.display = "table-cell"; + loadText.style.verticalAlign = "middle"; + + setPlayerModel(model).then(() => { + document.body.removeChild(overlay); + }); }); + + td.appendChild(document.createElement("p")); + + const img = td.appendChild(document.createElement("img")); + img.src = model.path + "preview.png"; + img.style.width = "100%"; + img.style.maxWidth = (100/perRow) + "%"; + img.style.maxHeight = (70/Math.ceil(playerModels.length/perRow)) + "vh"; + + td.appendChild(document.createElement("p")).innerText = model.name; + } +}; + +{ + const button = document.getElementById("action-model"); + button.addEventListener("click", () => { + modelSelectionUI(true); + }); + + const modelName = localStorage.getItem("model"); + const model = modelName && playerModels.find((m) => m.name == modelName); + + if (model) + setPlayerModel(model); + else + modelSelectionUI(); } +const htmlContainer = (headline, htmlContent) => { + const [overlay, container] = openContainer(true, true); + + container.appendChild(document.createElement("h1")).innerText = headline; + + const content = container.appendChild(document.createElement("div")); + content.style.width = "90%"; + content.style.textAlign = "left"; + + content.innerHTML = htmlContent; + + return [overlay, container, content]; +}; + +document.getElementById("action-settings").addEventListener("click", () => { + const [overlay, container, content] = htmlContainer("Einstellungen", ` +

3D-Modus

+

Im 3D-Modus werden Gebäude dreidimensional auf der Karte angezeigt. Auf leistungsschwachen Geräten kann das zu Leistungsproblemen führen. Nach Änderung dieser Einstellung ist ein Neustart des Spiels notwendig.

+ + + + +

Steuerung

+

Das Spiel kann entweder durch deinen Standort oder durch Berühren der Karte gesteuert werden. Falls dein Gerät keine Standortinformationen unterstützt, wird die Berührsteuerung automatisch eingeschaltet.

+ + + +
+
+ + + +
+
+ `); + + const buttonClose = document.getElementById("close-settings"); + const boxEnable3d = document.getElementById("checkbox-enable3d"); + const boxTouchControl = document.getElementById("checkbox-touchcontrol"); + + boxEnable3d.checked = enable3d; + boxTouchControl.checked = touchControl(); + boxTouchControl.disabled = !!gpsError; + + const updateCloseButton = () => { + if (boxEnable3d.checked != enable3d) + buttonClose.innerText = "Speichern und Neustarten"; + else if (boxTouchControl.checked != touchControl()) + buttonClose.innerText = "Speichern und Schließen"; + else + buttonClose.innerText = "Schließen"; + }; + + boxEnable3d.addEventListener("input", updateCloseButton); + boxTouchControl.addEventListener("input", updateCloseButton); + + buttonClose.addEventListener("click", () => { + localStorage.setItem("enable3d", boxEnable3d.checked.toString()); + if (!gpsError) + localStorage.setItem("touchcontrol", (forceTouchControl = boxTouchControl.checked).toString()); + + if (boxEnable3d.checked != enable3d) { + location.reload(); + } else { + document.body.removeChild(overlay); + } + }); +}); + +import licenseFile from "./LICENSE?url"; + +document.getElementById("action-info").addEventListener("click", () => { + const modelLicenses = playerModels + .map(({ name, path }) => `
  • ${name}
  • `) + .join(""); + + const [overlay, container, content] = htmlContainer("Informationen zum Spiel", ` +

    Entwicklung

    + + Idee und Leitung: Ruth Pabst
    + Programmierung und Design: Charlotte Pabst (Pseudonym: "Lizzy Fleckenstein")
    + Inhalte: Schülerinnen und Schüler der Klasse 4a der Christian-Schad-Schule
    + +

    + Der Quelltext des Programms steht unter + + https://github.com/LizzyFleckenstein03/aschaffenburg.fun + zur Verfügung.

    +

    Lizenzinformationen

    +

    Quellcodelizenz: GPLv3

    +
    +Dieses Programm ist Freie Software: Sie können es unter den Bedingungen
    +der GNU General Public License, wie von der Free Software Foundation,
    +Version 3 der Lizenz oder (nach Ihrer Wahl) jeder neueren
    +veröffentlichten Version, weiter verteilen und/oder modifizieren.
    +
    +Dieses Programm wird in der Hoffnung bereitgestellt, dass es nützlich sein wird, jedoch
    +OHNE JEDE GEWÄHR,; sogar ohne die implizite
    +Gewähr der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK.
    +Siehe die GNU General Public License für weitere Einzelheiten.
    +
    +Sie sollten eine Kopie der GNU General Public License zusammen mit diesem
    +Programm erhalten haben. Wenn nicht, siehe https://www.gnu.org/licenses/
    +		
    + +

    Lizenzen der 3D-Modelle

    + + +

    Quellen verwendeter Bilder und Sounds

    + + +

    Karten-Anbieter

    + © MapTiler © OpenStreetMap contributors + +

    + `); + + //
  • clamp(0, 1, (x - min) / (max - min)); const pow = Math.pow; circle.scale.setScalar(pow(rlerp(ph[0], ph[1], t), 2.0)); @@ -232,23 +690,23 @@ class Celestial extends THREE.DirectionalLight { this.shadow.mapSize.width = 1024; this.shadow.mapSize.height = 1024; - const frustumSize = 15; + const frustumSize = 15 * playerScale; this.shadow.camera = new THREE.OrthographicCamera( -frustumSize / 2, frustumSize / 2, frustumSize / 2, -frustumSize / 2, - 1, - 50, + 1 * playerScale, + 50 * playerScale, ); this.positionFunc = positionFunc; this.update(); - //scene.add(new THREE.CameraHelper(this.shadow.camera)); + // scene.add(new THREE.CameraHelper(this.shadow.camera)); scene.add(this); - this.time = 23; + this.time = 0; addEventListener( "keypress", ((evt) => { @@ -268,21 +726,21 @@ class Celestial extends THREE.DirectionalLight { } update() { + const date = new Date(new Date().getTime() + this.time * 1000 * 60 * 10); + //const date = new Date(); + const pos = map.getCenter(); - const p = this.positionFunc( - new Date(this.time * 1000 * 60 * 10), - pos.lat, - pos.lng, - ); + const p = this.positionFunc(date, 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); + const old = this.position.clone(); this.position .set(1, 0, 0) - .applyEuler(new THREE.Euler(0, p.altitude, p.azimuth)) - .multiplyScalar(5); + .applyEuler(new THREE.Euler(0, p.azimuth, p.altitude)) + .multiplyScalar(5 * playerScale); this.shadow.camera.position.copy(this.position); this.shadow.camera.lookAt(scene.position); @@ -299,32 +757,37 @@ setInterval(() => { // 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"; +window.mapToMerc = mapToMerc; +window.mat4 = mat4; +window.vec3 = vec3; +window.vec4 = vec4; +window.mapToThree = mapToThree; +window.camera = camera; +window.map = map; + const render = (gl, mercViewProj) => { - if (player) player.mixer.update(player.clock.getDelta()); + const cam = mapToThree(map.getCameraPosition()); - const camMap = map.getCameraPosition(); - const camMerc = mapToMerc(camMap, camMap.altitude); - const cam = mercToThree(camMerc); + window.mat = mercViewProj; + + /* + camera.position.copy(cam); + camera.lookAt(scene.position); 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(); + const pos = mercToThree({ x, y, z }).sub(threeCenter()); + + pos.applyMatrix4(camera.matrixWorldInverse); + + return -pos.z; }; camera.aspect = innerWidth / innerHeight; @@ -332,12 +795,40 @@ const render = (gl, mercViewProj) => { camera.near = depthNCDtoThree(-1); camera.far = depthNCDtoThree(+1); camera.updateProjectionMatrix(); - - camera.position.copy(cam); - camera.lookAt(scene.position); + */ + + const p = mapToMerc(map.getCenter()); + const s = p.meterInMercatorCoordinateUnits(); + + camera.projectionMatrix = new THREE.Matrix4() + .fromArray(mercViewProj) + .multiply( + new THREE.Matrix4() + .makeTranslation(p.x, p.y, p.z) + .scale(new THREE.Vector3(s, -s, s)), + ) + .multiply( + new THREE.Matrix4().makeRotationAxis( + new THREE.Vector3(1, 0, 0), + Math.PI / 2, + ), + ); cam.y = 0; - marker.lookAt(cam); + + for (const marker of markers) { + if (!marker.obj) continue; + + marker.obj.position.copy(mapToThree(marker.pos)); + marker.obj.lookAt(cam); + + const close = rlerp(40, 60, marker.obj.position.length()); + + marker.obj.children[0].material.color = new THREE.Color().fromArray( + new Color("darkblue").mix(new Color("#2c88ff"), close, {space: "hwb", outputSpace: "srgb"}).srgb + ); + marker.reachable = close < 0.5; + } renderer.resetState(); renderer.render(scene, camera); @@ -352,7 +843,10 @@ map.on("style.load", () => { renderingMode: "3d", render, }, - "building-3d", + map.getStyle().layers.find(layer => layer.type in { + "fill-extrusion": true, + "symbol": true, + })?.id, ); }); @@ -363,13 +857,24 @@ addEventListener("resize", () => { let playerAnimDuration = 0; { const clock = new THREE.Clock(); + let fadeOut = 0; const animate = () => { requestAnimationFrame(animate); - const dt = clock.getDelta(); - if (playerAnimDuration <= 0) return; + player?.mixer.update(dt); + + if (playerAnimDuration <= 0) { + if (player) { + player.walk.paused = true; + if (player.doStop) + player.walk.stop(); + } + return; + } + + player?.walk.play(); const lerp = (a, b, x) => a * (1 - x) + b * x; const x = Math.min(dt / playerAnimDuration, 1); @@ -379,9 +884,6 @@ let playerAnimDuration = 0; center.lat = lerp(center.lat, target.lat, x); playerAnimDuration -= dt; - - if (playerAnimDuration <= 0) player.walk.stop(); - map.setCenter(center); }; @@ -392,34 +894,315 @@ const clock = new THREE.Clock(); clock.getDelta(); const setTarget = (pos) => { + console.log(pos); const dt = clock.getDelta(); if (player) { player.scene.lookAt(mapToThree(pos)); player.walk.play(); + player.walk.paused = false; } playerAnimDuration = Math.min(dt, 1.5); localStorage.setItem("position", JSON.stringify((target = pos))); }; +function getHeight(el, type) { + return el.offsetWidth + parseInt(s.getPropertyValue('margin-left')) + parseInt(s.getPropertyValue('margin-right')); +} + +const triggerMarker = (marker) => { + let finishAudio; + let onClose; + + const audio = new Audio("markers/" + marker.name + "/sound.mp3"); + audio.addEventListener("ended", () => { + finishAudio(); + // document.body.removeChild(overlay); + }); + audio.play(); + + const [overlay, container] = openContainer(() => { + audio.pause(); + + if (onClose) + onClose(); + }); + + container.style.width = "calc(100% - 20px)"; + container.style.height = "calc(100% - 20px)"; + container.style.top = "4px"; + container.style.left = "4px"; + + container.style.display = "flex"; + container.style.flexFlow = "column"; + + const header = container.appendChild(document.createElement("div")); + + const headline = header.appendChild(document.createElement("h1")); + headline.innerHTML = marker.title; + headline.style.flex = "0 1 auto"; + headline.style.fontSize = "1.5em"; + + const remainCont = container.appendChild(document.createElement("div")); + remainCont.style.flex = "1"; + + const markerImage = name => { + const img = document.createElement("img"); + + img.alt = "Lädt..."; + img.src = "markers/" + name + "/image.jpg"; + img.style.height = "0"; + img.style.minHeight = "90%"; + img.style.width = "0"; + img.style.minWidth = "90%"; + img.style.objectFit = "scale-down"; + + return img; + } + + const img = remainCont.appendChild(markerImage(marker.name)); + + finishAudio = () => { + remainCont.removeChild(img); + + header.removeChild(headline); + const help = header.appendChild(document.createElement("p")); + help.textAlign = "left"; + help.innerHTML = completed[marker.name] + ? `Du hast ${marker.title} bereits in der Zeitleiste eingeordnet.` + : `Ordne ${marker.title} auf der Zeitleiste ein.`; + //
    Scrolle oder verwende die 'Früher'- und 'Später'-Knöpfe um den richtigen Ausschnitt in der Zeitleiste zu finden, dann tippe auf das passende Fragezeichen! + + const timeContOuter = remainCont.appendChild(document.createElement("div")); + timeContOuter.style.height = "calc(100% - 1em)"; + timeContOuter.style.width = "90%"; + timeContOuter.style.position = "relative"; + timeContOuter.style.backgroundColor = "#d9e9c6"; + timeContOuter.style.borderRadius = "10px"; + timeContOuter.style.boxShadow = "0 0 0 4px inset #7fb82e"; + + const timeCont = timeContOuter.appendChild(document.createElement("div")); + timeCont.style.position = "absolute"; + timeCont.style.top = "40px"; + timeCont.style.left = "4px"; + timeCont.style.height = "calc(100% - 80px)"; + timeCont.style.width = "calc(100% - 8px)"; + timeCont.style.overflow = "auto"; + timeCont.style.scrollbarColor = "#7fb82e #d9e9c6"; + timeCont.style.direction = "rtl"; + + const pxPerYear = 5; + const startYear = 900; + + for (let i = startYear; i <= 1900; i += 5) { + const p = timeCont.appendChild(document.createElement("span")); + p.style.position = "absolute"; + p.style.top = (i-startYear) * pxPerYear + "px"; + p.style.left = "0.5em"; + p.style.width = "2.5em"; + p.style.direction = "ltr"; + + const showNum = i % 25 == 0; + p.innerText = showNum ? i : "-"; + //p.style.textAlign = showNum ? "right" : "left"; + p.style.textAlign = "right"; + } + + for (const [dir, title, mult] of [ + ["top", "Früher", -1], + ["bottom", "Später", 1], // 🥺 + ]) { + const button = timeContOuter.appendChild(document.createElement("button")); + button.innerText = title; + button.style[dir] = "0px"; + button.style.position = "absolute"; + button.style.left = "0px"; + button.style.width = "100%"; + button.style.height = "40px"; + button.style.borderStyle = "none"; + button.style.fontSize = "1em"; + button.style.backgroundColor = "#7fb82e"; + button.style.cursor = "pointer"; + + button.style["border-" + dir + "-left-radius"] = "7px"; + button.style["border-" + dir + "-right-radius"] = "7px"; + + button.addEventListener("click", () => { + timeCont.scrollBy({ + top: 300 * mult, + behavior: "smooth", + }); + }) + } + + const size = 32; + for (const [i, { year, name, title }] of markers.entries()) { + const want = year - size/2; + + const topCompromise = markers[i-1] && ((year+markers[i-1].year)/2+1) || want; + const bottomCompromise = markers[i+1] && ((year+markers[i+1].year)/2-size-1) || want; + + let offset; + if (topCompromise > want) + offset = topCompromise; + else if (bottomCompromise < want) + offset = bottomCompromise; + else + offset = want; + + const color = "hsla(" + (i/markers.length*360) + ",100%,50%,0.5)"; + + const rect = timeCont.appendChild(document.createElement("div")); + rect.style.position = "absolute"; + rect.style.width = (size * pxPerYear) + "px"; + rect.style.height = (size * pxPerYear) + "px"; + rect.style.top = "calc(" + ((offset-startYear) * pxPerYear) + "px + 1ex)"; + rect.style.left = "calc(3em + 20px)"; + rect.style.backgroundColor = color; + rect.style.direction = "ltr"; + + rect.style.display = "flex"; + rect.style.flexFlow = "column"; + + const fillMarker = () => { + const remainRect = rect.appendChild(document.createElement("div")); + remainRect.style.flex = "1"; + + const img = remainRect.appendChild(markerImage(name)); + img.style.minHeight = "calc(100% - 3px)"; + img.style.minWidth = "calc(100% - 3px)"; + img.style.position = "relative"; + img.style.top = "3px"; + + const p = rect.appendChild(document.createElement("small")); + p.innerText = title; + p.style.padding = "3px"; + p.style.flex = "0 1 auto"; + }; + + const makeArrow = (color) => { + const arrow = timeCont.appendChild(document.createElement("div")); + arrow.style.position = "absolute"; + arrow.style.width = "0"; + arrow.style.height = "0"; + arrow.style.borderTop = "20px solid transparent"; + arrow.style.borderBottom = "20px solid transparent"; + arrow.style.borderRight = "20px solid " + color; + arrow.style.left = "3em"; + arrow.style.top = "calc(" + ((year-startYear) * pxPerYear) + "px - 20px + 1ex)"; + return arrow; + }; + + makeArrow(color); + + if (completed[name]) { + fillMarker(); + + if (name == marker.name) { + timeCont.scrollTo(rect); // TODO: lil color animation + } + } else { + const img = rect.appendChild(document.createElement("img")); + img.src = "unknown.svg"; + img.style.position = "absolute"; + img.style.width = "80%"; + img.style.height = "80%"; + img.style.top = "10%"; + img.style.left = "10%"; + + if (!completed[marker.name]) { + img.addEventListener("click", () => { + if (onClose) return; + + if (name == marker.name) { + rect.removeChild(img); + fillMarker(); + + // TODO: firework, completion + } else { + const rectOverlay = rect.appendChild(divOverlay()); + const arrowOverlay = makeArrow("rgba(0, 0, 0, 0.5)"); + + const nope = rectOverlay.appendChild(document.createElement("img")); + nope.src = "nope.svg"; + nope.position = "absolute"; + nope.style.position = "absolute"; + nope.style.width = "78%"; + nope.style.height = "78%"; + nope.style.top = "11%"; + nope.style.left = "11%"; + + const nopeSound = new Audio("nope.mp3"); + nopeSound.addEventListener("ended", () => { + onClose = null; + rect.removeChild(rectOverlay); + timeCont.removeChild(arrowOverlay); + }); + nopeSound.play(); + onClose = () => { + nopeSound.pause(); + }; + } + }); + } + } + } + }; +}; + +const click = (evt) => { + const mouse = new THREE.Vector2( + (evt.point.x / map.transform.width) * 2 - 1, + 1 - (evt.point.y / map.transform.height) * 2, + ); + + const camInverseProjection = new THREE.Matrix4() + .copy(camera.projectionMatrix) + .invert(); + const cameraPosition = new THREE.Vector3().applyMatrix4( + camInverseProjection, + ); + const mousePosition = new THREE.Vector3(mouse.x, mouse.y, 1).applyMatrix4( + camInverseProjection, + ); + const viewDirection = mousePosition + .clone() + .sub(cameraPosition) + .normalize(); + + raycaster.set(cameraPosition, viewDirection); + + for (const obj of raycaster.intersectObjects( + markers.map((x) => x.obj), + true, + )) { + const marker = obj.object.parent?.marker; + if (marker?.reachable) { + triggerMarker(marker); + return; + } + } + + if (touchControl()) + setTarget(evt.lngLat); +}; + +map.on("click", click); +map.on("touched", click); + const watchGeo = navigator.geolocation.watchPosition( ({ coords: { longitude: lng, latitude: lat } }) => { - const pos = { lng, lat }; - setTarget(pos); + if (!touchControl()) + setTarget({ lng, lat }); }, (err) => { - // todo: err.message; navigator.geolocation.clearWatch(watchGeo); - - const click = (evt) => { - setTarget(evt.lngLat); - }; - - map.on("click", click); - map.on("touched", click); + gpsError = err; }, { enableHighAccuracy: true, }, ); + +// ❤️ anna -- cgit v1.2.3