diff --git a/src/components/three-background/index.ts b/src/components/three-background/index.ts index 2f1bff3..04e89d8 100644 --- a/src/components/three-background/index.ts +++ b/src/components/three-background/index.ts @@ -1,5 +1,5 @@ import "./styles.css"; -import {initCamera, initCanvas, initScene, initStage, renderer} from "./scene.ts"; +import {initCamera, initScene, initStage, renderer} from "./scene.ts"; import {initBgMeshes} from "./particules.ts"; import {initPostProcess} from "./post-process.ts"; import {animate} from "./animation-loop.ts"; @@ -9,7 +9,6 @@ import {animate} from "./animation-loop.ts"; export function animatedBackground() { initStage(); initScene(); - initCanvas(); initCamera(); initBgMeshes(); initPostProcess(); diff --git a/src/components/three-background/scene.ts b/src/components/three-background/scene.ts index 413ab52..3e602f9 100644 --- a/src/components/three-background/scene.ts +++ b/src/components/three-background/scene.ts @@ -8,10 +8,7 @@ export let renderer: THREE.WebGLRenderer, windowWidth = window.innerWidth, windowHeight = window.innerHeight; -let graphicCanvas, - canvasWidth = 240, - canvasHeight = 240, - mouseX = 0, +let mouseX = 0, mouseY = 0, windowHalfWidth: number, windowHalfHeight: number; @@ -31,7 +28,7 @@ export const initStage = () => { export const initScene = () => { scene = new THREE.Scene(); scene.fog = new THREE.Fog(0x010102, 1, 3000); - scene.add( new THREE.AmbientLight( 0xcccccc ) ); + scene.add( new THREE.AmbientLight( 0x000000 ) ); renderer = new THREE.WebGLRenderer({ @@ -56,11 +53,7 @@ export const initCamera = () => { camera.position.z = 800; } -export const initCanvas = () => { - graphicCanvas = document.createElement('canvas'); - graphicCanvas.width = canvasWidth; - graphicCanvas.height = canvasHeight; -} + //----------------------------------------------------------------------- diff --git a/src/components/three-radar-chart/animation-loop.ts b/src/components/three-radar-chart/animation-loop.ts new file mode 100644 index 0000000..e5307e7 --- /dev/null +++ b/src/components/three-radar-chart/animation-loop.ts @@ -0,0 +1,9 @@ +import {renderWithPostProcess} from "./post-process.ts"; +import {sceneAnimation} from "./scene.ts"; + + +export const animate = () => { + requestAnimationFrame(animate); + sceneAnimation(); + renderWithPostProcess(); +} \ No newline at end of file diff --git a/src/components/three-radar-chart/index.ts b/src/components/three-radar-chart/index.ts new file mode 100644 index 0000000..41f2c94 --- /dev/null +++ b/src/components/three-radar-chart/index.ts @@ -0,0 +1,17 @@ +import {initCamera, initScene, initStage, renderer} from "./scene.ts"; +import {animate} from "./animation-loop.ts"; +import {initRadar, TechUsage} from "./radar.ts"; +import {initPostProcess} from "./post-process.ts"; + +export function threeDataViewer(radarValues: TechUsage[]) { + initStage(); + initScene(); + initCamera(); + initRadar(radarValues); + initPostProcess(); + animate(); + + return renderer.domElement; +} + +//----------------------------------------------------------------------- diff --git a/src/components/three-radar-chart/label.ts b/src/components/three-radar-chart/label.ts new file mode 100644 index 0000000..fcb1de6 --- /dev/null +++ b/src/components/three-radar-chart/label.ts @@ -0,0 +1,50 @@ +import * as THREE from "three"; +import {scene} from "./scene.ts"; + +const offset = 1.35; +const labelMeshes: THREE.Mesh[] = []; + + +//----------------------------------------------------------------------- + +export const label = (label: string, spawnPoint: {x: number, y: number}) => { + const texture = createLabelTexture(label); + const labelMesh = createLabelPlane(texture); + + labelMesh.position.set(spawnPoint.x, spawnPoint.y * offset, 0); + scene.add(labelMesh); + labelMeshes.push(labelMesh); +} + +//----------------------------------------------------------------------- + +const createLabelPlane = (texture: THREE.CanvasTexture) => { + const geometry = new THREE.PlaneGeometry(2, 1); + const material = new THREE.MeshBasicMaterial({ + map: texture, + side: THREE.DoubleSide, + transparent: true, + }); + + const plane = new THREE.Mesh(geometry, material); + return plane; +} + +const createLabelTexture = (label: string) => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + const width = 256; + const height = 128; + canvas.width = width; + canvas.height = height; + + context!.fillStyle = 'white'; + context!.font = '48px Arial'; + context!.textAlign = 'center'; + context!.textBaseline = 'middle'; + context!.fillText(label, width / 2, height / 2); + + const texture = new THREE.CanvasTexture(canvas); + return texture; +} + diff --git a/src/components/three-radar-chart/post-process.ts b/src/components/three-radar-chart/post-process.ts new file mode 100644 index 0000000..14d58b3 --- /dev/null +++ b/src/components/three-radar-chart/post-process.ts @@ -0,0 +1,39 @@ +import {camera, canvasHeight, canvasWidth, renderer, scene} from "./scene.ts"; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; +import {Vector2} from "three"; + +let composer: EffectComposer; + +const postProcessConfigs = { + size: Vector2, + threshold: 0.6, + strength: 0.3, + radius: 0.5, + exposure: 0 +}; + + +export const initPostProcess = () => { + const renderScene = new RenderPass( scene, camera ); + + const bloomPass = new UnrealBloomPass( + new Vector2( canvasWidth, canvasHeight ), // size + postProcessConfigs.strength, // strength + postProcessConfigs.radius, // radius + postProcessConfigs.threshold // threshold + ); + + const outputPass = new OutputPass(); + + composer = new EffectComposer( renderer ); + composer.addPass( renderScene ); + composer.addPass( bloomPass ); + composer.addPass( outputPass ); +} + +export const renderWithPostProcess = () => { + composer.render(); +} diff --git a/src/components/three-radar-chart/radar.ts b/src/components/three-radar-chart/radar.ts new file mode 100644 index 0000000..636190b --- /dev/null +++ b/src/components/three-radar-chart/radar.ts @@ -0,0 +1,109 @@ +import * as THREE from "three"; +import {scene} from "./scene.ts"; +import {label} from "./label.ts"; + +const graphicSize = 1.5; + +export type TechUsage = { + technology: string, + percentage: number +} + +const triangle = [ + {x: -graphicSize, y: -graphicSize}, + {x: 0, y: graphicSize}, + {x: graphicSize, y: -graphicSize} +] + +const extrudeSettings = { + steps: 1, + depth: 0, + bevelEnabled: false, +}; + + +//----------------------------------------------------------------------- + +export const initRadar = (techs: TechUsage[]) => { + const radarSize = determineRadarSize(techs.length); + if (radarSize == undefined) { return; } + + const shape = drawShape(radarSize); + const mainMesh = buildMesh(shape, 0xffffff); + scene.add(mainMesh); + + for (let i = 0; i < 4; i++) { + createDepthMesh(shape, i); + } + + for (let i = 0; i < techs.length; i++) { + label(techs[i].technology, radarSize[i]); + } + + const valueShape = determineValueShape(techs.map(tech => tech.percentage)); + const valueMesh = buildMesh(valueShape, 0x3BFFC5); + scene.add(valueMesh); +} + +//----------------------------------------------------------------------- + +const buildMesh = (shape: THREE.Shape, shapeColor: THREE.ColorRepresentation) => { + const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); + const material = new THREE.MeshBasicMaterial({ + color: shapeColor, + wireframe: true + }); + + return new THREE.Mesh(geometry, material); +} + +const createDepthMesh = (depthShape: THREE.Shape, iteration: number) => { + const mesh = buildMesh(depthShape, 0x333333); + const size = iteration * 0.25; + const offset = 2.5; + + mesh.scale.set(size, size, size); + mesh.position.z = iteration * 0.6 - offset; + scene.add(mesh); +} + +const drawShape = (points: {x: number, y: number}[]) => { + const shape = new THREE.Shape(); + + if (points.length > 0) { + shape.moveTo(points[0].x, points[0].y); + for (let i = 1; i < points.length; i++) { + shape.lineTo(points[i].x, points[i].y); + } + shape.lineTo(points[0].x, points[0].y); + } + + return shape; +} + +const determineValueShape = (values: number[]) => { + let valueShape: {x: number, y: number}[] = []; + const score = values.map(value => ((100 - value) / 100) * graphicSize); + + valueShape = [ + {x: triangle[0].x + score[0], y: triangle[0].y + score[0]}, + {x: triangle[1].x, y: triangle[1].y - score[1]}, + {x: triangle[2].x - score[2], y: triangle[2].y + score[2]} + ]; + + return drawShape(valueShape); +} + +const determineRadarSize = (size: number) => { + if (size == 3) { + return triangle; + } + else { + console.log("Invalid number of technology. Create a new radar size or modify the number of technologies."); + return; + } +} + + + + diff --git a/src/components/three-radar-chart/scene.ts b/src/components/three-radar-chart/scene.ts new file mode 100644 index 0000000..f855479 --- /dev/null +++ b/src/components/three-radar-chart/scene.ts @@ -0,0 +1,122 @@ +import * as THREE from "three"; + +export let renderer: THREE.WebGLRenderer, + scene: THREE.Scene, + camera: THREE.PerspectiveCamera, + cameraTarget = new THREE.Vector3(0, 0 ,3.75), + canvasWidth = 185, + canvasHeight = 185; + +let isDragging = false, + previousMousePosition = { x: 0, y: 0 }, + cameraLookAt = new THREE.Vector3(0, 0, 0); + +const initialCameraPosition = new THREE.Vector3(), + initialCameraRotation = new THREE.Euler(), + initialCameraTarget = new THREE.Vector3(0, 0, 3.75), + cameraTiltLimit = 2; + +//----------------------------------------------------------------------- +export const initStage = () => { + window.addEventListener('mousemove', onMouseMove, false); + window.addEventListener('mouseup', onMouseUp, false); +} + +export const initScene = () => { + scene = new THREE.Scene(); + //scene.fog = new THREE.Fog(0x010102, 1, 10); + createBackdrop(); + + renderer = new THREE.WebGLRenderer({ + antialias: true + }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(canvasWidth, canvasHeight); + renderer.setClearColor(0x000000, 1); + + renderer.domElement.addEventListener('mousedown', onMouseDown, false); +} + +export const initCamera = () => { + const fieldOfView = 75; + const aspectRatio = canvasWidth / canvasHeight; + const nearPlane = 1; + const farPlane = 30000; + camera = new THREE.PerspectiveCamera( + fieldOfView, + aspectRatio, + nearPlane, + farPlane); + camera.position.z = 3.75; + + initialCameraPosition.copy(camera.position); + initialCameraRotation.copy(camera.rotation); +} + +export const sceneAnimation = () => { + if (!isDragging) { + cameraTarget.lerp(initialCameraTarget, 0.1); + } + + camera.position.lerp(cameraTarget, 0.2); + camera.lookAt(cameraLookAt); +} + +//----------------------------------------------------------------------- + +const createBackdrop = () => { + const geometry = new THREE.PlaneGeometry(100, 100); + const material = new THREE.MeshStandardMaterial({ + color: 0xffffff, + }); + const mesh = new THREE.Mesh(geometry, material); + + const light1 = new THREE.PointLight( 0xffffff, 5 ); + light1.position.set( 0, -5, -3 ); + scene.add( light1 ); + + mesh.position.z = -16; + scene.add(mesh); +} + +const onMouseDown = (event: MouseEvent) => { + isDragging = true; + previousMousePosition = { + x: event.clientX, + y: event.clientY + }; +} + +const onMouseMove = (event: MouseEvent) => { + if (isDragging) { + const deltaMove = { + x: event.clientX - previousMousePosition.x, + y: event.clientY - previousMousePosition.y + }; + + const moveSpeed = 0.01; + const offsetX = deltaMove.x * moveSpeed; + const offsetY = deltaMove.y * moveSpeed; + + cameraTarget.x += offsetX; + cameraTarget.y -= offsetY; + + cameraTarget.clamp( + new THREE.Vector3(-cameraTiltLimit, -cameraTiltLimit, cameraTarget.z), + new THREE.Vector3(cameraTiltLimit, cameraTiltLimit, cameraTarget.z) + ); + + camera.lookAt(cameraTarget); + + previousMousePosition = { + x: event.clientX, + y: event.clientY + }; + } +} + +const onMouseUp = () => { + isDragging = false; +} + + diff --git a/src/components/vertical-nav/info-project.ts b/src/components/vertical-nav/info-project.ts index 68b371f..0f6d00d 100644 --- a/src/components/vertical-nav/info-project.ts +++ b/src/components/vertical-nav/info-project.ts @@ -1,4 +1,5 @@ -import {renderer} from "./project-data-cube.ts"; +import {threeDataViewer} from "../three-radar-chart"; +import {TechUsage} from "../three-radar-chart/radar.ts"; export type ButtonLink = [ @@ -9,13 +10,13 @@ export type ButtonLink = [ //----------------------------------------------------------------------- -export function projectInfo(buttons: ButtonLink[]) { +export function projectInfo(buttons: ButtonLink[], techs: TechUsage[]) { const container = document.createElement('div'); const buttonList = document.createElement('ul'); container.className = "project-info"; buttonList.className = "button-list"; - container.appendChild(dataSection()); + container.appendChild(dataSection(techs)); container.appendChild(buttonList); buttons.forEach(button => { @@ -46,13 +47,13 @@ function createButton(newButton: ButtonLink) { return btn; } -function dataSection() { +function dataSection(newTechRadar: TechUsage[]) { const detailSection = document.createElement("section"); const detailTitle = document.createElement("h4"); detailTitle.textContent = "Made with"; detailSection.appendChild(detailTitle); - detailSection.appendChild(renderer.domElement); + detailSection.appendChild(threeDataViewer(newTechRadar)); return detailSection; } diff --git a/src/components/vertical-nav/project-data-cube.ts b/src/components/vertical-nav/project-data-cube.ts deleted file mode 100644 index 1c9333e..0000000 --- a/src/components/vertical-nav/project-data-cube.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as THREE from 'three'; - -// TO REVIEW -// @ts-ignore -import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; - -const scene = new THREE.Scene(); -const camera = new THREE.PerspectiveCamera( 75, 1, 0.1, 1000 ); - -export const renderer = new THREE.WebGLRenderer(); -renderer.setSize( 185, 185 ); -renderer.setAnimationLoop( animate ); - -const geometry = new THREE.BoxGeometry( 1, 1, 1 ); -const material = new THREE.MeshBasicMaterial( { color: 0x111111 } ); -const cube = new THREE.Mesh( geometry, material ); - -const controls = new OrbitControls( camera, renderer.domElement ); -scene.add( cube ); - -camera.position.z = 2; -controls.panSpeed = 0; -controls.rotateSpeed = 0.5; -controls.update(); - -//----------------------------------------------------------------------- - -function animate() { - renderer.render( scene, camera ); -} \ No newline at end of file diff --git a/src/components/vertical-nav/styles.css b/src/components/vertical-nav/styles.css index c6e2d6e..b6550ff 100644 --- a/src/components/vertical-nav/styles.css +++ b/src/components/vertical-nav/styles.css @@ -53,6 +53,20 @@ padding: 20px 15px; } + canvas { + width: 100%; + height: 100%; + border-radius: 5px; + } + + canvas:hover { + cursor: pointer; + } + + canvas:active { + cursor: grabbing; + } + section { padding: 10px 0; diff --git a/src/content/projects/index.ts b/src/content/projects/index.ts index ed51f2f..10c6862 100644 --- a/src/content/projects/index.ts +++ b/src/content/projects/index.ts @@ -27,7 +27,7 @@ export function buildProjectPage(pageReference: string) { return; } const viewContent = projectView(page!.content); - const navInfo = projectInfo(page!.buttons); + const navInfo = projectInfo(page!.buttons, page!.techs); renderBreadcrumbs(breadcrumbs(trackBreadcrumbs(page!.content.title))); renderNavInfo(navInfo); diff --git a/src/content/projects/space-compass/index.ts b/src/content/projects/space-compass/index.ts index 6818cef..2709c5f 100644 --- a/src/content/projects/space-compass/index.ts +++ b/src/content/projects/space-compass/index.ts @@ -11,6 +11,7 @@ import SCREENSHOT_3 from "./assets/spaceCompass-screenshot-3.jpg" import SCREENSHOT_4 from "./assets/spaceCompass-screenshot-4.jpg" import THUMBNAIL from "./assets/spaceCompass-thumbnail.jpg"; +import {TechUsage} from "../../../components/three-radar-chart/radar.ts"; export const content: ProjectContent = { @@ -35,6 +36,21 @@ export const content: ProjectContent = { ] } +export const techs: TechUsage[] = [ + { + technology: "Unity 3D", + percentage: 90 + }, + { + technology: "Blender", + percentage: 80 + }, + { + technology: "Adobe XD", + percentage: 25 + } +] + export const buttons: ButtonLink[] = [ [ "Try It Online", diff --git a/src/index.ts b/src/index.ts index 639fc2e..f334d57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ const routes = [ routes.forEach(route => router.registerRoute(route.path, route.handler)); +//----------------------------------------------------------------------- function init() { const body = document.getElementsByTagName("body")[0];