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