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