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