Trent Larson
1 year ago
24 changed files with 1076 additions and 3 deletions
After Width: | Height: | Size: 4.5 MiB |
@ -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/) |
Binary file not shown.
@ -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 |
||||
|
} |
||||
|
] |
||||
|
} |
After Width: | Height: | Size: 3.6 MiB |
After Width: | Height: | Size: 4.7 MiB |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -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; |
||||
|
} |
@ -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; |
||||
|
} |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -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> |
Loading…
Reference in new issue