Trent Larson
2 years ago
17 changed files with 461 additions and 3 deletions
After Width: | Height: | Size: 735 KiB |
@ -0,0 +1,86 @@ |
|||
// from https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80
|
|||
|
|||
import { createCamera } from "./components/camera.js"; |
|||
import { createLights } from "./components/lights.js"; |
|||
import { createScene } from "./components/scene.js"; |
|||
import { createRenderer } from "./systems/renderer.js"; |
|||
import { Loop } from "./systems/Loop.js"; |
|||
import { Resizer } from "./systems/Resizer.js"; |
|||
import { createControls } from "./systems/controls.js"; |
|||
|
|||
import createTerrain from "./components/objects/terrain.js"; |
|||
|
|||
// These variables are module-scoped: we cannot access them
|
|||
// from outside the module
|
|||
|
|||
const color = "#42b883"; |
|||
const color2 = "#4eac82"; |
|||
|
|||
let camera; |
|||
let renderer; |
|||
let scene; |
|||
let loop; |
|||
|
|||
class World { |
|||
constructor(container) { |
|||
// Instances of camera, scene, and renderer
|
|||
camera = createCamera(); |
|||
scene = createScene(color2); |
|||
renderer = createRenderer(); |
|||
|
|||
// Initializate Loop
|
|||
loop = new Loop(camera, scene, renderer); |
|||
|
|||
container.append(renderer.domElement); |
|||
|
|||
// Orbit Controls
|
|||
const controls = createControls(camera, renderer.domElement); |
|||
|
|||
// Light Instance, with optional light helper
|
|||
const { light, lightHelper } = createLights(color); |
|||
|
|||
// Random values for terrain vertices
|
|||
// We could do this on the terrain.js file,
|
|||
// but if we want to have a single random
|
|||
// number array for more than one terrain
|
|||
// instance, then we would be in trouble.
|
|||
const randomVals = []; |
|||
for (let i = 0; i < 12675; i++) { |
|||
randomVals.push(Math.random() - 0.5); |
|||
} |
|||
|
|||
// Terrain Instance
|
|||
const terrain = createTerrain({ |
|||
color: color, |
|||
randVertexArr: randomVals, |
|||
}); |
|||
|
|||
loop.updatables.push(controls); |
|||
loop.updatables.push(light); |
|||
loop.updatables.push(terrain); |
|||
|
|||
scene.add(light, terrain); |
|||
|
|||
// Responsive handler
|
|||
const resizer = new Resizer(container, camera, renderer); |
|||
resizer.onResize = () => { |
|||
this.render(); |
|||
}; |
|||
} |
|||
|
|||
render() { |
|||
// draw a single frame
|
|||
renderer.render(scene, camera); |
|||
} |
|||
|
|||
// Animation handlers
|
|||
start() { |
|||
loop.start(); |
|||
} |
|||
|
|||
stop() { |
|||
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
|
|||
100 // far clipping plane
|
|||
); |
|||
|
|||
// move the camera back so we can view the scene
|
|||
camera.position.set(0, 10, 30); |
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|||
camera.tick = (delta) => {}; |
|||
|
|||
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(0, 30, 30); |
|||
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|||
light.tick = (delta) => {}; |
|||
|
|||
return { light, lightHelper }; |
|||
} |
|||
|
|||
export { createLights }; |
@ -0,0 +1,60 @@ |
|||
import { |
|||
PlaneBufferGeometry, |
|||
MeshStandardMaterial, |
|||
Mesh, |
|||
TextureLoader, |
|||
} from "three"; |
|||
|
|||
export default function createTerrain(props) { |
|||
const loader = new TextureLoader(); |
|||
const height = loader.load("img/textures/height.png"); |
|||
// w h
|
|||
const geometry = new PlaneBufferGeometry(150, 150, 64, 64); |
|||
|
|||
const material = new MeshStandardMaterial({ |
|||
color: props.color, |
|||
flatShading: true, |
|||
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; |
|||
|
|||
//Randomizing our vertices position
|
|||
const { array } = plane.geometry.attributes.position; |
|||
for (let i = 0; i < array.length; i++) { |
|||
props.randVertexArr.push(Math.random()); |
|||
} |
|||
|
|||
plane.geometry.attributes.position.randomValues = props.randVertexArr; |
|||
|
|||
let frame = 0; |
|||
plane.tick = (delta) => { |
|||
frame += 0.01; |
|||
// destructuring of the random values, the original position and the current vertex position
|
|||
const { array, originalPosition, randomValues } = |
|||
plane.geometry.attributes.position; |
|||
|
|||
// Animation for loop
|
|||
// In our vertex array, we have 3 coordinates: x, y and z. We are
|
|||
// only going to animate on ONE coordinate: the z coordinate.
|
|||
// This means we have to omit the x and y coords, hence the i+=3 in our loop.
|
|||
// Also, to access that y coordinate on each vertex, we have to add 2 to our
|
|||
// current i each time.
|
|||
for (let i = 0; i < array.length; i += 3) { |
|||
// Accessing the z coord
|
|||
array[i + 2] = |
|||
// Try switching this numbers up, or using sine instead of cosine, see how the animation changes
|
|||
originalPosition[i + 2] + Math.cos(frame + randomValues[i + 2]) * 0.002; |
|||
} |
|||
plane.geometry.attributes.position.needsUpdate = true; |
|||
}; |
|||
|
|||
return plane; |
|||
} |
@ -0,0 +1,11 @@ |
|||
import { Color, Scene, Fog } 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,26 @@ |
|||
const setSize = (container, camera, renderer) => { |
|||
camera.aspect = window.innerWidth / window.innerHeight; |
|||
camera.updateProjectionMatrix(); |
|||
|
|||
renderer.setSize(window.innerWidth, window.innerHeight); |
|||
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,36 @@ |
|||
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 = true; |
|||
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.enablePan = false; |
|||
|
|||
controls.tick = () => controls.update(); |
|||
|
|||
return controls; |
|||
} |
|||
|
|||
export { createControls }; |
@ -0,0 +1,12 @@ |
|||
import { WebGLRenderer } from "three"; |
|||
|
|||
function createRenderer() { |
|||
const renderer = new WebGLRenderer({ antialias: true }); |
|||
|
|||
// turn on the physically correct lighting model
|
|||
renderer.physicallyCorrectLights = true; |
|||
|
|||
return renderer; |
|||
} |
|||
|
|||
export { createRenderer }; |
@ -0,0 +1,83 @@ |
|||
<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> |
|||
<div id="scene-container"></div> |
|||
<canvas ref="worldCanvas"></canvas> |
|||
</section> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Component, Vue } from "vue-facing-decorator"; |
|||
import { World } from "@/components/World/World.js"; |
|||
|
|||
@Component |
|||
export default class StatisticsView extends Vue { |
|||
mounted() { |
|||
const container = document.querySelector("#scene-container"); |
|||
const world = new World(container); |
|||
world.start(); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
#scene-container { |
|||
position: fixed; |
|||
width: 100%; |
|||
height: 100%; |
|||
left: 0; |
|||
top: 0; |
|||
} |
|||
</style> |
Loading…
Reference in new issue