forked from trent_larson/crowd-funder-for-time-pwa
Merge branch '3d-world'
This commit is contained in:
@@ -81,6 +81,8 @@ See https://tea.xyz
|
|||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
|
### Reference Material
|
||||||
|
|
||||||
```
|
```
|
||||||
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
|
// 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, [])
|
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)
|
||||||
|
|||||||
56
package-lock.json
generated
56
package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^2.0.5",
|
"@pvermeer/dexie-encrypted-addon": "^2.0.5",
|
||||||
|
"@tweenjs/tween.js": "^20.0.3",
|
||||||
"@veramo/core": "^5.1.2",
|
"@veramo/core": "^5.1.2",
|
||||||
"@veramo/credential-w3c": "^5.1.4",
|
"@veramo/credential-w3c": "^5.1.4",
|
||||||
"@veramo/data-store": "^5.1.2",
|
"@veramo/data-store": "^5.1.2",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"readable-stream": "^4.3.0",
|
"readable-stream": "^4.3.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
|
"three": "^0.152.2",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.2.47",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-class-component": "^8.0.0-0",
|
"vue-class-component": "^8.0.0-0",
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ramda": "^0.28.23",
|
"@types/ramda": "^0.28.23",
|
||||||
|
"@types/three": "^0.152.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||||
"@typescript-eslint/parser": "^5.57.0",
|
"@typescript-eslint/parser": "^5.57.0",
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
@@ -7741,6 +7744,11 @@
|
|||||||
"node": ">=10.13.0"
|
"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": {
|
"node_modules/@types/bn.js": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz",
|
||||||
@@ -8006,6 +8014,31 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": 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": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
"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==",
|
"integrity": "sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.3",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.5.3.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.5.3.tgz",
|
||||||
@@ -14789,6 +14828,12 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": 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": {
|
"node_modules/figures": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/figures/-/figures-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/figures/-/figures-2.0.0.tgz",
|
||||||
@@ -18304,6 +18349,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/lilconfig": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.0.6.tgz",
|
||||||
@@ -25184,6 +25235,11 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"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": {
|
"node_modules/throat": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^2.0.5",
|
"@pvermeer/dexie-encrypted-addon": "^2.0.5",
|
||||||
|
"@tweenjs/tween.js": "^20.0.3",
|
||||||
"@veramo/core": "^5.1.2",
|
"@veramo/core": "^5.1.2",
|
||||||
"@veramo/credential-w3c": "^5.1.4",
|
"@veramo/credential-w3c": "^5.1.4",
|
||||||
"@veramo/data-store": "^5.1.2",
|
"@veramo/data-store": "^5.1.2",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"readable-stream": "^4.3.0",
|
"readable-stream": "^4.3.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
|
"three": "^0.152.2",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.2.47",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-class-component": "^8.0.0-0",
|
"vue-class-component": "^8.0.0-0",
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ramda": "^0.28.23",
|
"@types/ramda": "^0.28.23",
|
||||||
|
"@types/three": "^0.152.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||||
"@typescript-eslint/parser": "^5.57.0",
|
"@typescript-eslint/parser": "^5.57.0",
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
|
|||||||
17
project.yaml
17
project.yaml
@@ -5,14 +5,16 @@
|
|||||||
|
|
||||||
- replace user-affecting console.logs with error messages (eg. catches)
|
- 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 :
|
- contacts v1 :
|
||||||
- .1 remove 'copy' until it works
|
|
||||||
- .5 Add page to show seed.
|
- .5 Add page to show seed.
|
||||||
- 01 Provide a way to import the non-sensitive data.
|
- 01 Provide a way to import the non-sensitive data.
|
||||||
- 01 Provide way to share your contact info.
|
- 01 Provide way to share your contact info.
|
||||||
- .2 move all "identity" references to temporary account access
|
- .2 move all "identity" references to temporary account access
|
||||||
- .5 make deploy for give-only features
|
- .5 make deploy for give-only features
|
||||||
- .5 get 'copy' to work on account page
|
|
||||||
|
|
||||||
- contacts v+ :
|
- contacts v+ :
|
||||||
- .5 make advanced "show/hide amounts" button into a nice UI toggle
|
- .5 make advanced "show/hide amounts" button into a nice UI toggle
|
||||||
@@ -32,7 +34,16 @@
|
|||||||
|
|
||||||
- backup all data
|
- 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
|
- Connect with phone contacts
|
||||||
|
|
||||||
|
|||||||
BIN
public/img/textures/forest-floor.png
Normal file
BIN
public/img/textures/forest-floor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 MiB |
11
public/models/lupine_plant/license.txt
Normal file
11
public/models/lupine_plant/license.txt
Normal file
@@ -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/)
|
||||||
BIN
public/models/lupine_plant/scene.bin
Normal file
BIN
public/models/lupine_plant/scene.bin
Normal file
Binary file not shown.
229
public/models/lupine_plant/scene.gltf
Normal file
229
public/models/lupine_plant/scene.gltf
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
public/models/lupine_plant/textures/lambert2SG_baseColor.png
Normal file
BIN
public/models/lupine_plant/textures/lambert2SG_baseColor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 MiB |
BIN
public/models/lupine_plant/textures/lambert2SG_normal.png
Normal file
BIN
public/models/lupine_plant/textures/lambert2SG_normal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 MiB |
104
src/components/World/World.js
Normal file
104
src/components/World/World.js
Normal file
@@ -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 };
|
||||||
19
src/components/World/components/camera.js
Normal file
19
src/components/World/components/camera.js
Normal file
@@ -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 };
|
||||||
14
src/components/World/components/lights.js
Normal file
14
src/components/World/components/lights.js
Normal file
@@ -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 };
|
||||||
225
src/components/World/components/objects/landmarks.js
Normal file
225
src/components/World/components/objects/landmarks.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
29
src/components/World/components/objects/terrain.js
Normal file
29
src/components/World/components/objects/terrain.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
11
src/components/World/components/scene.js
Normal file
11
src/components/World/components/scene.js
Normal file
@@ -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 };
|
||||||
33
src/components/World/systems/Loop.js
Normal file
33
src/components/World/systems/Loop.js
Normal file
@@ -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 };
|
||||||
33
src/components/World/systems/Resizer.js
Normal file
33
src/components/World/systems/Resizer.js
Normal file
@@ -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 };
|
||||||
38
src/components/World/systems/controls.js
vendored
Normal file
38
src/components/World/systems/controls.js
vendored
Normal file
@@ -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 };
|
||||||
13
src/components/World/systems/renderer.js
Normal file
13
src/components/World/systems/renderer.js
Normal file
@@ -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 };
|
||||||
@@ -121,6 +121,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "start" */ "../views/StartView.vue"),
|
import(/* webpackChunkName: "start" */ "../views/StartView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/statistics",
|
||||||
|
name: "statistics",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @type {*} */
|
/** @type {*} */
|
||||||
|
|||||||
@@ -291,6 +291,17 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="text-blue-500 px-2">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'statistics' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
>
|
||||||
|
See Your Statistics
|
||||||
|
</router-link>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-bind:class="computedAlertClassNames()">
|
<div v-bind:class="computedAlertClassNames()">
|
||||||
<button
|
<button
|
||||||
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
||||||
|
|||||||
212
src/views/StatisticsView.vue
Normal file
212
src/views/StatisticsView.vue
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<!-- QUICK NAV -->
|
||||||
|
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
||||||
|
<ul class="flex text-2xl p-2 gap-2">
|
||||||
|
<!-- Home Feed -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
|
||||||
|
<fa icon="house-chimney" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Search -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'discover' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
>
|
||||||
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Projects -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'projects' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
>
|
||||||
|
<fa icon="folder-open" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Contacts -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'contacts' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
>
|
||||||
|
<fa icon="users" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Profile -->
|
||||||
|
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'account' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
>
|
||||||
|
<fa icon="circle-user" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Heading -->
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
|
Your Statistics
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<button class="float-right" @click="captureGraphics()">Screenshot</button>
|
||||||
|
|
||||||
|
<!-- Another place to play with the sizing is in Resizer.setSize -->
|
||||||
|
<div id="scene-container" class="h-screen"></div>
|
||||||
|
|
||||||
|
<div v-bind:class="computedAlertClassNames()">
|
||||||
|
<button
|
||||||
|
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
||||||
|
@click="onClickClose()"
|
||||||
|
>
|
||||||
|
<fa icon="xmark"></fa>
|
||||||
|
</button>
|
||||||
|
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
|
||||||
|
<p>{{ alertMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { World } from "@/components/World/World.js";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class StatisticsView extends Vue {
|
||||||
|
world: World;
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
const container = document.querySelector("#scene-container");
|
||||||
|
const newWorld = new World(container, this);
|
||||||
|
newWorld.start();
|
||||||
|
this.world = newWorld;
|
||||||
|
}
|
||||||
|
|
||||||
|
public captureGraphics() {
|
||||||
|
/**
|
||||||
|
// from https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#examples
|
||||||
|
// Adds a blank image
|
||||||
|
const dataBlob = document
|
||||||
|
.querySelector("#scene-container")
|
||||||
|
.firstChild.toBlob((blob) => {
|
||||||
|
const newImg = document.createElement("img");
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
newImg.onload = () => {
|
||||||
|
// no longer need to read the blob so it's revoked
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
newImg.src = url;
|
||||||
|
document.body.appendChild(newImg);
|
||||||
|
});
|
||||||
|
**/
|
||||||
|
|
||||||
|
/**
|
||||||
|
// Yields a blank page with the iframe below
|
||||||
|
const dataUrl = document
|
||||||
|
.querySelector("#scene-container")
|
||||||
|
.firstChild.toDataURL("image/png");
|
||||||
|
**/
|
||||||
|
|
||||||
|
/**
|
||||||
|
// Yields a blank page with the iframe below
|
||||||
|
const dataUrl = this.world.renderer.domElement.toDataURL("image/png");
|
||||||
|
**/
|
||||||
|
|
||||||
|
/**
|
||||||
|
// Show the image in a new tab
|
||||||
|
const iframe = `
|
||||||
|
<iframe
|
||||||
|
src="${dataUrl}"
|
||||||
|
frameborder="0"
|
||||||
|
style="border:0; top:0px; left:0px; bottom:0px; right:0px; width:100%; height:100%;"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>`;
|
||||||
|
const win = window.open();
|
||||||
|
win.document.open();
|
||||||
|
win.document.write(iframe);
|
||||||
|
win.document.close();
|
||||||
|
**/
|
||||||
|
|
||||||
|
// from https://stackoverflow.com/a/17407392/845494
|
||||||
|
// This yields a file with funny formatting.
|
||||||
|
//const image = const dataUrl.replace("image/png", "image/octet-stream");
|
||||||
|
|
||||||
|
/**
|
||||||
|
// Yields a blank image at the bottom of the page
|
||||||
|
// from https://discourse.threejs.org/t/save-screenshot-on-server/39900/3
|
||||||
|
const img = new Image();
|
||||||
|
img.src = this.world.renderer.domElement.toDataURL();
|
||||||
|
document.body.appendChild(img);
|
||||||
|
**/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This yields an SVG that only shows white and black highlights
|
||||||
|
// from https://stackoverflow.com/questions/27632621/exporting-from-three-js-scene-to-svg-or-other-vector-format
|
||||||
|
**/
|
||||||
|
const rendererSVG = new SVGRenderer();
|
||||||
|
rendererSVG.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
rendererSVG.render(this.world.scene, this.world.camera);
|
||||||
|
//document.body.appendChild(rendererSVG.domElement);
|
||||||
|
ExportToSVG(rendererSVG, "test.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
alertTitle = "";
|
||||||
|
alertMessage = "";
|
||||||
|
isAlertVisible = false;
|
||||||
|
|
||||||
|
public setAlert(title, message) {
|
||||||
|
this.alertTitle = title;
|
||||||
|
this.alertMessage = message;
|
||||||
|
this.isAlertVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onClickClose() {
|
||||||
|
this.isAlertVisible = false;
|
||||||
|
this.alertTitle = "";
|
||||||
|
this.alertMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public computedAlertClassNames() {
|
||||||
|
return {
|
||||||
|
hidden: !this.isAlertVisible,
|
||||||
|
"dismissable-alert": true,
|
||||||
|
"bg-slate-100": true,
|
||||||
|
"p-5": true,
|
||||||
|
rounded: true,
|
||||||
|
"drop-shadow-lg": true,
|
||||||
|
fixed: true,
|
||||||
|
"top-3": true,
|
||||||
|
"inset-x-3": true,
|
||||||
|
"transition-transform": true,
|
||||||
|
"ease-in": true,
|
||||||
|
"duration-300": true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExportToSVG(rendererSVG, filename) {
|
||||||
|
const XMLS = new XMLSerializer();
|
||||||
|
const svgfile = XMLS.serializeToString(rendererSVG.domElement);
|
||||||
|
const svgData = svgfile;
|
||||||
|
const preface = '<?xml version="1.0" standalone="no"?>\r\n';
|
||||||
|
const svgBlob = new Blob([preface, svgData], {
|
||||||
|
type: "image/svg+xml;charset=utf-8",
|
||||||
|
});
|
||||||
|
const svgUrl = URL.createObjectURL(svgBlob);
|
||||||
|
const downloadLink = document.createElement("a");
|
||||||
|
|
||||||
|
downloadLink.href = svgUrl;
|
||||||
|
downloadLink.download = filename;
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
document.body.removeChild(downloadLink);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -7,4 +7,9 @@ module.exports = defineConfig({
|
|||||||
topLevelAwait: true,
|
topLevelAwait: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pwa: {
|
||||||
|
iconPaths: {
|
||||||
|
faviconSVG: 'img/icons/safari-pinned-tab.svg',
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user