You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

241 lines
8.8 KiB

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;
}