diff --git a/README.md b/README.md index d692f224a..7648ce3c4 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ See https://tea.xyz ## Other +### Reference Material + ``` // reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83 @@ -183,3 +185,9 @@ export const createAndStoreIdentifier = async (mnemonicPassword) => { return importAndStoreIdentifier(mnemonic, mnemonicPassword, false, []) } ``` + +## Kudos + +* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80) +* [Many libraries]() such as Veramo.io, Vuejs.org, threejs +* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439) diff --git a/package-lock.json b/package-lock.json index 0e0114669..22903247e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/vue-fontawesome": "^3.0.3", "@pvermeer/dexie-encrypted-addon": "^2.0.5", + "@tweenjs/tween.js": "^20.0.3", "@veramo/core": "^5.1.2", "@veramo/credential-w3c": "^5.1.4", "@veramo/data-store": "^5.1.2", @@ -44,6 +45,7 @@ "readable-stream": "^4.3.0", "reflect-metadata": "^0.1.13", "register-service-worker": "^1.7.2", + "three": "^0.152.2", "vue": "^3.2.47", "vue-axios": "^3.5.2", "vue-class-component": "^8.0.0-0", @@ -54,6 +56,7 @@ }, "devDependencies": { "@types/ramda": "^0.28.23", + "@types/three": "^0.152.0", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "@vue/cli-plugin-babel": "~5.0.8", @@ -7741,6 +7744,11 @@ "node": ">=10.13.0" } }, + "node_modules/@tweenjs/tween.js": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-20.0.3.tgz", + "integrity": "sha512-SYUe1UgY5HM05EB4+0B4arq2IPjvyzKXoklXKxSYrc2IFxGm1cBrqg5XbiB5uwbs0xY5j+rj986NAJMM0KZaUw==" + }, "node_modules/@types/bn.js": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz", @@ -8006,6 +8014,31 @@ "optional": true, "peer": true }, + "node_modules/@types/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w==", + "dev": true + }, + "node_modules/@types/three": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.152.0.tgz", + "integrity": "sha512-9QdaV5bfZEqeQi0xkXLdnoJt7lgYZbppdBAgJSWRicdtZoCYJ34nS2QkdeuzXt+UXExofk4OWqMzdX71HeDOVg==", + "dev": true, + "dependencies": { + "@tweenjs/tween.js": "~18.6.4", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.6.9", + "lil-gui": "~0.17.0" + } + }, + "node_modules/@types/three/node_modules/@tweenjs/tween.js": { + "version": "18.6.4", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-18.6.4.tgz", + "integrity": "sha512-lB9lMjuqjtuJrx7/kOkqQBtllspPIN+96OvTCeJ2j5FEzinoAXTdAMFnDAQT1KVPRlnYfBrqxtqP66vDM40xxQ==", + "dev": true + }, "node_modules/@types/trusted-types": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.2.tgz", @@ -8023,6 +8056,12 @@ "integrity": "sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==", "dev": true }, + "node_modules/@types/webxr": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz", + "integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.5.3.tgz", @@ -14789,6 +14828,12 @@ "optional": true, "peer": true }, + "node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "dev": true + }, "node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/figures/-/figures-2.0.0.tgz", @@ -18304,6 +18349,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lil-gui": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.17.0.tgz", + "integrity": "sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ==", + "dev": true + }, "node_modules/lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.0.6.tgz", @@ -25184,6 +25235,11 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/three": { + "version": "0.152.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.152.2.tgz", + "integrity": "sha512-Ff9zIpSfkkqcBcpdiFo2f35vA9ZucO+N8TNacJOqaEE6DrB0eufItVMib8bK8Pcju/ZNT6a7blE1GhTpkdsILw==" + }, "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", diff --git a/package.json b/package.json index 879ac1dd2..395188301 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/vue-fontawesome": "^3.0.3", "@pvermeer/dexie-encrypted-addon": "^2.0.5", + "@tweenjs/tween.js": "^20.0.3", "@veramo/core": "^5.1.2", "@veramo/credential-w3c": "^5.1.4", "@veramo/data-store": "^5.1.2", @@ -44,6 +45,7 @@ "readable-stream": "^4.3.0", "reflect-metadata": "^0.1.13", "register-service-worker": "^1.7.2", + "three": "^0.152.2", "vue": "^3.2.47", "vue-axios": "^3.5.2", "vue-class-component": "^8.0.0-0", @@ -54,6 +56,7 @@ }, "devDependencies": { "@types/ramda": "^0.28.23", + "@types/three": "^0.152.0", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "@vue/cli-plugin-babel": "~5.0.8", diff --git a/project.yaml b/project.yaml index 64601fada..82f597f83 100644 --- a/project.yaml +++ b/project.yaml @@ -5,14 +5,16 @@ - replace user-affecting console.logs with error messages (eg. catches) +- stats v1 : + - 01 show numeric stats + - 01 link to world for specific stats + - contacts v1 : - - .1 remove 'copy' until it works - .5 Add page to show seed. - 01 Provide a way to import the non-sensitive data. - 01 Provide way to share your contact info. - .2 move all "identity" references to temporary account access - .5 make deploy for give-only features - - .5 get 'copy' to work on account page - contacts v+ : - .5 make advanced "show/hide amounts" button into a nice UI toggle @@ -32,7 +34,16 @@ - backup all data -- Next Viable Product afterward +- .5 customize favicon +- .5 make advanced features harder to access + +- Release Minimum Viable Product : + - Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot). + +- Stats : + - 01 point out user's location on the world + - 01 present a credential selected from the stats + - 04 show gives spreading to other places - Connect with phone contacts diff --git a/public/img/textures/forest-floor.png b/public/img/textures/forest-floor.png new file mode 100644 index 000000000..5a7a58e60 Binary files /dev/null and b/public/img/textures/forest-floor.png differ diff --git a/public/models/lupine_plant/license.txt b/public/models/lupine_plant/license.txt new file mode 100644 index 000000000..9a6980cc7 --- /dev/null +++ b/public/models/lupine_plant/license.txt @@ -0,0 +1,11 @@ +Model Information: +* title: Lupine Plant +* source: https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439 +* author: rufusrockwell (https://sketchfab.com/rufusrockwell) + +Model License: +* license type: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/) +* requirements: Author must be credited. Commercial use is allowed. + +If you use this 3D model in your project be sure to copy paste this credit wherever you share it: +This work is based on "Lupine Plant" (https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439) by rufusrockwell (https://sketchfab.com/rufusrockwell) licensed under CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/public/models/lupine_plant/scene.bin b/public/models/lupine_plant/scene.bin new file mode 100644 index 000000000..e523e7ddb Binary files /dev/null and b/public/models/lupine_plant/scene.bin differ diff --git a/public/models/lupine_plant/scene.gltf b/public/models/lupine_plant/scene.gltf new file mode 100644 index 000000000..0ec199716 --- /dev/null +++ b/public/models/lupine_plant/scene.gltf @@ -0,0 +1,229 @@ +{ + "accessors": [ + { + "bufferView": 2, + "componentType": 5126, + "count": 2759, + "max": [ + 41.3074951171875, + 40.37548828125, + 87.85917663574219 + ], + "min": [ + -35.245540618896484, + -36.895416259765625, + -0.9094290137290955 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 33108, + "componentType": 5126, + "count": 2759, + "max": [ + 0.9999382495880127, + 0.9986748695373535, + 0.9985831379890442 + ], + "min": [ + -0.9998949766159058, + -0.9975876212120056, + -0.411094069480896 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 2759, + "max": [ + 0.9987699389457703, + 0.9998998045921326, + 0.9577858448028564, + 1.0 + ], + "min": [ + -0.9987726807594299, + -0.9990445971488953, + -0.999801516532898, + 1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 2759, + "max": [ + 1.0061479806900024, + 0.9993550181388855 + ], + "min": [ + 0.00279300007969141, + 0.0011620000004768372 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "componentType": 5125, + "count": 6378, + "type": "SCALAR" + } + ], + "asset": { + "extras": { + "author": "rufusrockwell (https://sketchfab.com/rufusrockwell)", + "license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)", + "source": "https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439", + "title": "Lupine Plant" + }, + "generator": "Sketchfab-12.68.0", + "version": "2.0" + }, + "bufferViews": [ + { + "buffer": 0, + "byteLength": 25512, + "name": "floatBufferViews", + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 22072, + "byteOffset": 25512, + "byteStride": 8, + "name": "floatBufferViews", + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 66216, + "byteOffset": 47584, + "byteStride": 12, + "name": "floatBufferViews", + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 44144, + "byteOffset": 113800, + "byteStride": 16, + "name": "floatBufferViews", + "target": 34962 + } + ], + "buffers": [ + { + "byteLength": 157944, + "uri": "scene.bin" + } + ], + "images": [ + { + "uri": "textures/lambert2SG_baseColor.png" + }, + { + "uri": "textures/lambert2SG_normal.png" + } + ], + "materials": [ + { + "alphaCutoff": 0.2, + "alphaMode": "MASK", + "doubleSided": true, + "name": "lambert2SG", + "normalTexture": { + "index": 1 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0 + }, + "metallicFactor": 0.0 + } + } + ], + "meshes": [ + { + "name": "Object_0", + "primitives": [ + { + "attributes": { + "NORMAL": 1, + "POSITION": 0, + "TANGENT": 2, + "TEXCOORD_0": 3 + }, + "indices": 4, + "material": 0, + "mode": 4 + } + ] + } + ], + "nodes": [ + { + "children": [ + 1 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.220446049250313e-16, + -1.0, + 0.0, + 0.0, + 1.0, + 2.220446049250313e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ], + "name": "Sketchfab_model" + }, + { + "children": [ + 2 + ], + "name": "LupineSF.obj.cleaner.materialmerger.gles" + }, + { + "mesh": 0, + "name": "Object_2" + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987, + "wrapS": 10497, + "wrapT": 10497 + } + ], + "scene": 0, + "scenes": [ + { + "name": "Sketchfab_Scene", + "nodes": [ + 0 + ] + } + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 1 + } + ] +} diff --git a/public/models/lupine_plant/textures/lambert2SG_baseColor.png b/public/models/lupine_plant/textures/lambert2SG_baseColor.png new file mode 100644 index 000000000..211290ef7 Binary files /dev/null and b/public/models/lupine_plant/textures/lambert2SG_baseColor.png differ diff --git a/public/models/lupine_plant/textures/lambert2SG_normal.png b/public/models/lupine_plant/textures/lambert2SG_normal.png new file mode 100644 index 000000000..a4e6ffed8 Binary files /dev/null and b/public/models/lupine_plant/textures/lambert2SG_normal.png differ diff --git a/src/components/World/World.js b/src/components/World/World.js new file mode 100644 index 000000000..dca75c139 --- /dev/null +++ b/src/components/World/World.js @@ -0,0 +1,104 @@ +// from https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80 + +import * as TWEEN from "@tweenjs/tween.js"; +import * as THREE from "three"; + +import { createCamera } from "./components/camera.js"; +import { createLights } from "./components/lights.js"; +import { createScene } from "./components/scene.js"; +import { loadLandmarks } from "./components/objects/landmarks.js"; +import { createTerrain } from "./components/objects/terrain.js"; +import { Loop } from "./systems/Loop.js"; +import { Resizer } from "./systems/Resizer.js"; +import { createControls } from "./systems/controls.js"; +import { createRenderer } from "./systems/renderer.js"; + +const COLOR1 = "#dddddd"; +const COLOR2 = "#0055aa"; + +class World { + constructor(container, vue) { + this.PLATFORM_BORDER = 5; + this.PLATFORM_EDGE_FOR_UNKNOWNS = 10; + this.PLATFORM_SIZE = 100; // note that the loadLandmarks calculations may still assume 100 + + this.update = this.update.bind(this); + + // Instances of camera, scene, and renderer + this.camera = createCamera(); + this.scene = createScene(COLOR2); + this.renderer = createRenderer(); + + // necessary for models, says https://threejs.org/docs/index.html#examples/en/loaders/GLTFLoader + this.renderer.outputColorSpace = THREE.SRGBColorSpace; + + this.light = null; + this.lights = []; + this.bushes = []; + + // Initialize Loop + this.loop = new Loop(this.camera, this.scene, this.renderer); + + container.append(this.renderer.domElement); + + // Orbit Controls + const controls = createControls(this.camera, this.renderer.domElement); + + // Light Instance, with optional light helper + const { light } = createLights(COLOR1); + + // Terrain Instance + const terrain = createTerrain({ + color: COLOR1, + height: this.PLATFORM_SIZE + this.PLATFORM_BORDER * 2, + width: + this.PLATFORM_SIZE + + this.PLATFORM_BORDER * 2 + + this.PLATFORM_EDGE_FOR_UNKNOWNS * 2, + }); + + this.loop.updatables.push(controls); + this.loop.updatables.push(light); + this.loop.updatables.push(terrain); + + this.scene.add(light, terrain); + + loadLandmarks(vue, this, this.scene, this.loop); + + requestAnimationFrame(this.update); + + // Responsive handler + const resizer = new Resizer(container, this.camera, this.renderer); + resizer.onResize = () => { + this.render(); + }; + } + + update(time) { + TWEEN.update(time); + this.lights.forEach((light) => { + light.updateMatrixWorld(); + light.target.updateMatrixWorld(); + }); + this.lights.forEach((bush) => { + bush.updateMatrixWorld(); + }); + requestAnimationFrame(this.update); + } + + render() { + // draw a single frame + this.renderer.render(this.scene, this.camera); + } + + // Animation handlers + start() { + this.loop.start(); + } + + stop() { + this.loop.stop(); + } +} + +export { World }; diff --git a/src/components/World/components/camera.js b/src/components/World/components/camera.js new file mode 100644 index 000000000..e561cc531 --- /dev/null +++ b/src/components/World/components/camera.js @@ -0,0 +1,19 @@ +import { PerspectiveCamera } from "three"; + +function createCamera() { + const camera = new PerspectiveCamera( + 35, // fov = Field Of View + 1, // aspect ratio (dummy value) + 0.1, // near clipping plane + 350 // far clipping plane + ); + + // move the camera back so we can view the scene + camera.position.set(0, 100, 200); + // eslint-disable-next-line @typescript-eslint/no-empty-function + camera.tick = () => {}; + + return camera; +} + +export { createCamera }; diff --git a/src/components/World/components/lights.js b/src/components/World/components/lights.js new file mode 100644 index 000000000..8071335e0 --- /dev/null +++ b/src/components/World/components/lights.js @@ -0,0 +1,14 @@ +import { DirectionalLight, DirectionalLightHelper } from "three"; + +function createLights(color) { + const light = new DirectionalLight(color, 4); + const lightHelper = new DirectionalLightHelper(light, 0); + light.position.set(60, 100, 30); + + // eslint-disable-next-line @typescript-eslint/no-empty-function + light.tick = () => {}; + + return { light, lightHelper }; +} + +export { createLights }; diff --git a/src/components/World/components/objects/landmarks.js b/src/components/World/components/objects/landmarks.js new file mode 100644 index 000000000..7a0555a23 --- /dev/null +++ b/src/components/World/components/objects/landmarks.js @@ -0,0 +1,225 @@ +import axios from "axios"; +import * as R from "ramda"; +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 { AppString } from "@/constants/app"; +import { accountsDB, db } from "@/db"; +import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; +import { accessToken } from "@/libs/crypto"; + +const ANIMATION_DURATION_SECS = 10; +const BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; +const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/"; + +export async function loadLandmarks(vue, world, scene, loop) { + const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER; + try { + await db.open(); + const settings = await db.settings.get(MASTER_SETTINGS_KEY); + const activeDid = settings?.activeDid || ""; + await accountsDB.open(); + const accounts = await accountsDB.accounts.toArray(); + const account = R.find((acc) => acc.did === activeDid, accounts); + const identity = JSON.parse(account?.identity || "undefined"); + const token = await accessToken(identity); + + const url = + endorserApiServer + "/api/v2/report/claims?claimType=GiveAction"; + const headers = { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }; + const resp = await axios.get(url, { headers: headers }); + if (resp.status === 200) { + const minDate = resp.data.data[resp.data.data.length - 1].issuedAt; + const maxDate = resp.data.data[0].issuedAt; + const minTimeMillis = new Date(minDate).getTime(); + const fullTimeMillis = new Date(maxDate).getTime() - minTimeMillis; + // 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 = resp.data.data.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 < resp.data.data.length; i++) { + // claim is a GiveServerRecord (see endorserServer.ts) + const claim = resp.data.data[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 < resp.data.data.length; i++) { + // claim is a GiveServerRecord (see endorserServer.ts) + const claim = resp.data.data[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.log( + "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.log("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 + 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 in a ULID. + * + * We'll use the first 20 bits for the x coordinate and next 20 for the z. + * That's a bit over a trillion locations... should be enough for pretty + * unique locations for this fun kind of application within a network + * -- though they're not guaranteed unique without the full ID. + * (We're leaving 40 bits for other properties.) + * + * @param ulid + * @returns {x: float, z: float} where 0 <= x & z < 100 + */ +function locForUlid(ulid) { + // The random parts of a ULID come after the first 10 characters. + const randomness = ulid.substring(10); + + // That leaves 16 characters of randomness, or 80 bits. + + // We're currently only using 32 possible x and z values + // because the display is pretty low-fidelity at this point. + + // Similar code is below. + const x = (100 * BASE32.indexOf(randomness.substring(0, 1))) / 32; + const z = (100 * BASE32.indexOf(randomness.substring(4, 5))) / 32; + return { x, z }; +} + +/** + * See locForUlid + * @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(11); + // We'll take the first 4 bits for 16 possible x & z values. + const xOff = parseInt(Number("0x" + randomness.substring(0, 1)), 10); + const x = (xOff * 100) / 16; + // ... and since we're reserving 20 bits total for x, start with character 5. + const zOff = parseInt(Number("0x" + randomness.substring(5, 6)), 10); + const z = (zOff * 100) / 16; + 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; +} diff --git a/src/components/World/components/objects/terrain.js b/src/components/World/components/objects/terrain.js new file mode 100644 index 000000000..b7d1f54ec --- /dev/null +++ b/src/components/World/components/objects/terrain.js @@ -0,0 +1,29 @@ +import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three"; + +export function createTerrain(props) { + const loader = new TextureLoader(); + const height = loader.load("img/textures/forest-floor.png"); + // w h + const geometry = new PlaneGeometry(props.width, props.height, 64, 64); + + const material = new MeshLambertMaterial({ + color: props.color, + flatShading: true, + map: height, + //displacementMap: height, + //displacementScale: 5, + }); + + const plane = new Mesh(geometry, material); + plane.position.set(0, 0, 0); + plane.rotation.x -= Math.PI * 0.5; + + //Storing our original vertices position on a new attribute + plane.geometry.attributes.position.originalPosition = + plane.geometry.attributes.position.array; + + // eslint-disable-next-line @typescript-eslint/no-empty-function + plane.tick = () => {}; + + return plane; +} diff --git a/src/components/World/components/scene.js b/src/components/World/components/scene.js new file mode 100644 index 000000000..2bb3a1eba --- /dev/null +++ b/src/components/World/components/scene.js @@ -0,0 +1,11 @@ +import { Color, Scene } from "three"; + +function createScene(color) { + const scene = new Scene(); + + scene.background = new Color(color); + //scene.fog = new Fog(color, 60, 90); + return scene; +} + +export { createScene }; diff --git a/src/components/World/systems/Loop.js b/src/components/World/systems/Loop.js new file mode 100644 index 000000000..a4e26caa2 --- /dev/null +++ b/src/components/World/systems/Loop.js @@ -0,0 +1,33 @@ +import { Clock } from "three"; + +const clock = new Clock(); + +class Loop { + constructor(camera, scene, renderer) { + this.camera = camera; + this.scene = scene; + this.renderer = renderer; + this.updatables = []; + } + + start() { + this.renderer.setAnimationLoop(() => { + this.tick(); + // render a frame + this.renderer.render(this.scene, this.camera); + }); + } + + stop() { + this.renderer.setAnimationLoop(null); + } + + tick() { + const delta = clock.getDelta(); + for (const object of this.updatables) { + object.tick(delta); + } + } +} + +export { Loop }; diff --git a/src/components/World/systems/Resizer.js b/src/components/World/systems/Resizer.js new file mode 100644 index 000000000..3ffe3e4f8 --- /dev/null +++ b/src/components/World/systems/Resizer.js @@ -0,0 +1,33 @@ +const setSize = (container, camera, renderer) => { + // These are great for full-screen, which adjusts to a window. + const height = window.innerHeight; + const width = window.innerWidth - 50; + // These are better for fitting in a container, which stays that size. + //const height = container.scrollHeight; + //const width = container.scrollWidth; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + renderer.setPixelRatio(window.devicePixelRatio); +}; + +class Resizer { + constructor(container, camera, renderer) { + // set initial size on load + setSize(container, camera, renderer); + + window.addEventListener("resize", () => { + // set the size again if a resize occurs + setSize(container, camera, renderer); + // perform any custom actions + this.onResize(); + }); + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + onResize() {} +} + +export { Resizer }; diff --git a/src/components/World/systems/controls.js b/src/components/World/systems/controls.js new file mode 100644 index 000000000..310b7b02b --- /dev/null +++ b/src/components/World/systems/controls.js @@ -0,0 +1,38 @@ +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; +import { MathUtils } from "three"; + +function createControls(camera, canvas) { + const controls = new OrbitControls(camera, canvas); + + //enable controls? + controls.enabled = true; + controls.autoRotate = false; + //controls.autoRotateSpeed = 0.2; + + // control limits + // It's recommended to set some control boundaries, + // to prevent the user from clipping with the objects. + + // y axis + controls.minPolarAngle = MathUtils.degToRad(40); // default + controls.maxPolarAngle = MathUtils.degToRad(75); + + // x axis + // controls.minAzimuthAngle = ... + // controls.maxAzimuthAngle = ... + + //smooth camera: + // remember to add to loop updatables to work + controls.enableDamping = true; + + //controls.enableZoom = false; + controls.maxDistance = 250; + + //controls.enablePan = false; + + controls.tick = () => controls.update(); + + return controls; +} + +export { createControls }; diff --git a/src/components/World/systems/renderer.js b/src/components/World/systems/renderer.js new file mode 100644 index 000000000..ea423fced --- /dev/null +++ b/src/components/World/systems/renderer.js @@ -0,0 +1,13 @@ +import { WebGLRenderer } from "three"; + +function createRenderer() { + const renderer = new WebGLRenderer({ antialias: true }); + + // turn on the physically correct lighting model + // (The browser complains: "THREE.WebGLRenderer: .physicallyCorrectLights has been removed. Set enderer.useLegacyLights instead." However, that changes the lighting in a way that doesn't look better.) + renderer.physicallyCorrectLights = true; + + return renderer; +} + +export { createRenderer }; diff --git a/src/router/index.ts b/src/router/index.ts index f9f5f8221..4ffddd27d 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -121,6 +121,14 @@ const routes: Array = [ component: () => import(/* webpackChunkName: "start" */ "../views/StartView.vue"), }, + { + path: "/statistics", + name: "statistics", + component: () => + import( + /* webpackChunkName: "statistics" */ "../views/StatisticsView.vue" + ), + }, ]; /** @type {*} */ diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 7a387a8e3..9c86192d8 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -291,6 +291,17 @@ +
+ +
+
+ + +
+ +
+ +

{{ alertTitle }}

+

{{ alertMessage }}

+
+ + + + diff --git a/vue.config.js b/vue.config.js index c38a5949a..26772193a 100644 --- a/vue.config.js +++ b/vue.config.js @@ -7,4 +7,9 @@ module.exports = defineConfig({ topLevelAwait: true, }, }, + pwa: { + iconPaths: { + faviconSVG: 'img/icons/safari-pinned-tab.svg', + } + } });