import axios from "axios"; import * as THREE from "three"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader"; import * as SkeletonUtils from "three/addons/utils/SkeletonUtils"; import * as TWEEN from "@tweenjs/tween.js"; import { retrieveSettingsForActiveAccount } from "../../../../db"; import { getHeaders } from "../../../../libs/endorserServer"; const ANIMATION_DURATION_SECS = 10; const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/"; export async function loadLandmarks(vue, world, scene, loop) { vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS); try { const settings = await retrieveSettingsForActiveAccount(); const activeDid = settings.activeDid || ""; const apiServer = settings.apiServer; const headers = await getHeaders(activeDid); const url = apiServer + "/api/v2/report/claims?claimType=GiveAction"; const resp = await axios.get(url, { headers: headers }); if (resp.status === 200) { const landmarks = resp.data.data; const minDate = landmarks[landmarks.length - 1].issuedAt; const maxDate = landmarks[0].issuedAt; world.setExposedWorldProperties("startTime", minDate.replace("T", " ")); world.setExposedWorldProperties("endTime", maxDate.replace("T", " ")); const minTimeMillis = new Date(minDate).getTime(); const fullTimeMillis = maxDate > minDate ? new Date(maxDate).getTime() - minTimeMillis : 1; // avoid divide by zero // ratio of animation time to real time const fakeRealRatio = (ANIMATION_DURATION_SECS * 1000) / fullTimeMillis; // load plant model first because it takes a second const loader = new GLTFLoader(); // choose the right plant const modelLoc = "/models/lupine_plant/scene.gltf", // push with pokies modScale = 0.1; //const modelLoc = "/models/round_bush/scene.gltf", // green & pink // modScale = 1; //const modelLoc = "/models/coreopsis-flower.glb", // 3 flowers // modScale = 2; //const modelLoc = "/models/a_bush/scene.gltf", // purple leaves // modScale = 15; // calculate positions for each claim, especially because some are random const locations = landmarks.map((claim) => locForGive( claim, world.PLATFORM_SIZE, world.PLATFORM_EDGE_FOR_UNKNOWNS, ), ); // eslint-disable-next-line @typescript-eslint/no-this-alias loader.load( modelLoc, function (gltf) { gltf.scene.scale.set(0, 0, 0); for (let i = 0; i < landmarks.length; i++) { // claim is a GiveServerRecord (see endorserServer.ts) const claim = landmarks[i]; const newPlant = SkeletonUtils.clone(gltf.scene); const loc = locations[i]; newPlant.position.set(loc.x, 0, loc.z); world.scene.add(newPlant); const timeDelayMillis = fakeRealRatio * (new Date(claim.issuedAt).getTime() - minTimeMillis); new TWEEN.Tween(newPlant.scale) .delay(timeDelayMillis) .to({ x: modScale, y: modScale, z: modScale }, 5000) .start(); world.bushes = [...world.bushes, newPlant]; } }, undefined, function (error) { console.error(error); }, ); // calculate when lights shine on appearing claim area for (let i = 0; i < landmarks.length; i++) { // claim is a GiveServerRecord (see endorserServer.ts) const claim = landmarks[i]; const loc = locations[i]; const light = createLight(); light.position.set(loc.x, 20, loc.z); light.target.position.set(loc.x, 0, loc.z); loop.updatables.push(light); scene.add(light); scene.add(light.target); // now figure out the timing and shine a light const timeDelayMillis = fakeRealRatio * (new Date(claim.issuedAt).getTime() - minTimeMillis); new TWEEN.Tween(light) .delay(timeDelayMillis) .to({ intensity: 100 }, 10) .chain( new TWEEN.Tween(light.position) .to({ y: 5 }, 5000) .onComplete(() => { scene.remove(light); light.dispose(); }), ) .start(); world.lights = [...world.lights, light]; } } else { console.error( "Got bad server response status & data of", resp.status, resp.data, ); vue.setAlert( "Error With Server", "There was an error retrieving your claims from the server.", ); } } catch (error) { console.error("Got exception contacting server:", error); vue.setAlert( "Error With Server", "There was a problem retrieving your claims from the server.", ); } } /** * * @param giveClaim * @returns {x:float, z:float} where -50 <= x & z < 50 */ function locForGive(giveClaim, platformWidth, borderWidth) { let loc; if (giveClaim?.claim?.recipient?.identifier) { // this is directly to a person loc = locForEthrDid(giveClaim.claim.recipient.identifier); loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 }; } else if (giveClaim?.object?.isPartOf?.identifier) { // this is probably to a project const objId = giveClaim.object.isPartOf.identifier; if (objId.startsWith(ENDORSER_ENTITY_PREFIX)) { loc = locForUlid(objId.substring(ENDORSER_ENTITY_PREFIX.length)); loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 }; } } if (!loc) { // it must be outside our known addresses so let's put it somewhere random on the side const leftSide = Math.random() < 0.5; loc = { x: leftSide ? -platformWidth / 2 - borderWidth / 2 : platformWidth / 2 + borderWidth / 2, z: Math.random() * platformWidth - platformWidth / 2, }; } return loc; } /** * Generate a deterministic x & z location based on the randomness of an ID. * * We'd like the location to fully map back to the original ID. * This typically means we use half the ID for the x and half for the z. * * ... in this case: a ULID. * We'll use the first half (13 characters) for the x coordinate and next 13 for the z. * We recognize that this is only 3 characters = 15 bits = 32768 unique values * for the random part for the first half. We also recognize that those random * bits may be shared with previous ULIDs if they were generated in the same * millisecond, and therefore much of the evenness of the distribution depends * on the other dimension. * * Also: since the first 10 characters are time-based, we're going to reverse * the order of the characters to make the randomness more evenly distributed. * This is reversing the order of the 5-bit characters, not each of the bits. * Also wik: the first characters of the second half might be the same as * previous ULIDs if they were generated in the same millisecond. So it's * best to have that last character be the most significant bit so that there * is a more even distribution in that dimension. * * @param ulid * @returns {x: float, z: float} where 0 <= x & z < 100 */ function locForUlid(ulid) { const xChars = ulid.substring(0, 13).split("").reverse().join(""); const zChars = ulid.substring(13, 26).split("").reverse().join(""); // from https://github.com/ulid/javascript/blob/5e9727b527aec5b841737c395a20085c4361e971/lib/index.ts#L21 const BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford's Base32 // We're currently only using 1024 possible x and z values // because the display is pretty low-fidelity at this point. const rawX = BASE32.indexOf(xChars[1]) * 32 + BASE32.indexOf(xChars[0]); const rawZ = BASE32.indexOf(zChars[1]) * 32 + BASE32.indexOf(zChars[0]); const x = (100 * rawX) / 1024; const z = (100 * rawZ) / 1024; return { x, z }; } /** * See locForUlid. Similar, but for ethr DIDs. * @param did * @returns {x: float, z: float} where 0 <= x & z < 100 */ function locForEthrDid(did) { // "did:ethr:0x..." if (did.length < 51) { return { x: 0, z: 0 }; } else { const randomness = did.substring("did:ethr:0x".length); // We'll use all the randomness for fully unique x & z values. // But we'll only calculate this view with the first byte since our rendering resolution is low. const xOff = parseInt(Number("0x" + randomness.substring(0, 2)), 10); const x = (xOff * 100) / 256; // ... and since we're reserving 20 bytes total for x, start z with character 20, // again with one byte. const zOff = parseInt(Number("0x" + randomness.substring(20, 22)), 10); const z = (zOff * 100) / 256; return { x, z }; } } function createLight() { const light = new THREE.SpotLight(0xffffff, 0, 0, Math.PI / 8, 0.5, 0); // eslint-disable-next-line @typescript-eslint/no-empty-function light.tick = () => {}; return light; }