From 334a55bc716dd8eeb8cb5171ab87a62718639c28 Mon Sep 17 00:00:00 2001 From: Charles Doucet Date: Thu, 20 Feb 2025 17:24:03 -0500 Subject: [PATCH 1/8] wip: triangle data viewer --- .../index.ts} | 27 +++++++++++++++---- src/components/three-data-viewer/scene.ts | 0 src/components/vertical-nav/info-project.ts | 2 +- src/index.ts | 1 + 4 files changed, 24 insertions(+), 6 deletions(-) rename src/components/{vertical-nav/project-data-cube.ts => three-data-viewer/index.ts} (55%) create mode 100644 src/components/three-data-viewer/scene.ts diff --git a/src/components/vertical-nav/project-data-cube.ts b/src/components/three-data-viewer/index.ts similarity index 55% rename from src/components/vertical-nav/project-data-cube.ts rename to src/components/three-data-viewer/index.ts index 1c9333e..7f0cdc6 100644 --- a/src/components/vertical-nav/project-data-cube.ts +++ b/src/components/three-data-viewer/index.ts @@ -8,17 +8,34 @@ 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 width = 1; + +const shape = new THREE.Shape(); +shape.moveTo( -width, -width ); +shape.lineTo( 0, width ); +shape.lineTo( width, -width ); +shape.lineTo( -width, -width ); + +const extrudeSettings = { + steps: 2, + depth: 1, + bevelEnabled: false, +}; + + +const geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings ); +const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); +const mesh = new THREE.Mesh( geometry, material ) ; +scene.add( mesh ); const controls = new OrbitControls( camera, renderer.domElement ); -scene.add( cube ); +scene.add( mesh ); -camera.position.z = 2; +camera.position.z = 3; controls.panSpeed = 0; controls.rotateSpeed = 0.5; controls.update(); diff --git a/src/components/three-data-viewer/scene.ts b/src/components/three-data-viewer/scene.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/components/vertical-nav/info-project.ts b/src/components/vertical-nav/info-project.ts index 68b371f..94c6dd6 100644 --- a/src/components/vertical-nav/info-project.ts +++ b/src/components/vertical-nav/info-project.ts @@ -1,4 +1,4 @@ -import {renderer} from "./project-data-cube.ts"; +import {renderer} from "./../three-data-viewer"; export type ButtonLink = [ 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]; From 26b029f3c9c9f35618c290d35424b5dfe321c201 Mon Sep 17 00:00:00 2001 From: Charles Doucet Date: Thu, 20 Feb 2025 17:46:00 -0500 Subject: [PATCH 2/8] chore: three component directory setup --- .../three-data-viewer/animation-loop.ts | 6 +++ src/components/three-data-viewer/geometry.ts | 38 +++++++++++++ src/components/three-data-viewer/index.ts | 53 ++++--------------- src/components/three-data-viewer/scene.ts | 44 +++++++++++++++ src/components/vertical-nav/info-project.ts | 4 +- 5 files changed, 101 insertions(+), 44 deletions(-) create mode 100644 src/components/three-data-viewer/animation-loop.ts create mode 100644 src/components/three-data-viewer/geometry.ts diff --git a/src/components/three-data-viewer/animation-loop.ts b/src/components/three-data-viewer/animation-loop.ts new file mode 100644 index 0000000..8ed4a09 --- /dev/null +++ b/src/components/three-data-viewer/animation-loop.ts @@ -0,0 +1,6 @@ +import {camera, scene, renderer} from "../three-data-viewer/scene.ts"; + +export const animate = () => { + requestAnimationFrame(animate); + renderer.render( scene, camera ); +} \ No newline at end of file diff --git a/src/components/three-data-viewer/geometry.ts b/src/components/three-data-viewer/geometry.ts new file mode 100644 index 0000000..547f110 --- /dev/null +++ b/src/components/three-data-viewer/geometry.ts @@ -0,0 +1,38 @@ +import * as THREE from "three"; +import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; +import {camera, renderer, scene} from "./scene.ts"; + +const width = 1.5; + +const shape = new THREE.Shape(); +shape.moveTo( -width, -width ); +shape.lineTo( 0, width ); +shape.lineTo( width, -width ); +shape.lineTo( -width, -width ); + +const extrudeSettings = { + steps: 0, + depth: 1, + bevelEnabled: false, +}; + +const material = new THREE.MeshBasicMaterial({ + color: 0xffffff, + wireframe: true +}); + +//----------------------------------------------------------------------- + +export const initGeometry = () => { + const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + const controls = new OrbitControls(camera, renderer.domElement); + scene.add(mesh); + + camera.position.z = 3; + controls.panSpeed = 0; + controls.rotateSpeed = 0.5; + controls.update(); +} \ No newline at end of file diff --git a/src/components/three-data-viewer/index.ts b/src/components/three-data-viewer/index.ts index 7f0cdc6..3a25db6 100644 --- a/src/components/three-data-viewer/index.ts +++ b/src/components/three-data-viewer/index.ts @@ -1,47 +1,16 @@ -import * as THREE from 'three'; +import {initCamera, initCanvas, initScene, renderer} from "./scene.ts"; +import {animate} from "./animation-loop.ts"; +import {initGeometry} from "./geometry.ts"; -// TO REVIEW -// @ts-ignore -import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; +export function threeDataViewer() { + initScene(); + initCanvas(); + initCamera(); + initGeometry(); + animate(); -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 width = 1; - -const shape = new THREE.Shape(); -shape.moveTo( -width, -width ); -shape.lineTo( 0, width ); -shape.lineTo( width, -width ); -shape.lineTo( -width, -width ); - -const extrudeSettings = { - steps: 2, - depth: 1, - bevelEnabled: false, -}; - - -const geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings ); -const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); -const mesh = new THREE.Mesh( geometry, material ) ; -scene.add( mesh ); - -const controls = new OrbitControls( camera, renderer.domElement ); -scene.add( mesh ); - -camera.position.z = 3; -controls.panSpeed = 0; -controls.rotateSpeed = 0.5; -controls.update(); + return renderer.domElement; +} //----------------------------------------------------------------------- - -function animate() { - renderer.render( scene, camera ); -} \ No newline at end of file diff --git a/src/components/three-data-viewer/scene.ts b/src/components/three-data-viewer/scene.ts index e69de29..4c6c107 100644 --- a/src/components/three-data-viewer/scene.ts +++ b/src/components/three-data-viewer/scene.ts @@ -0,0 +1,44 @@ +import * as THREE from "three"; + +export let renderer: THREE.WebGLRenderer, + scene: THREE.Scene, + camera: THREE.PerspectiveCamera; + +let graphicCanvas, + canvasWidth = 185, + canvasHeight = 185; + +//----------------------------------------------------------------------- + +export const initScene = () => { + scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0x010102, 1, 3000); + scene.add( new THREE.AmbientLight( 0xcccccc ) ); + + renderer = new THREE.WebGLRenderer({ + alpha: true, + antialias: true + }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(canvasWidth, canvasHeight); + renderer.setClearColor(0x000000, 0); +} + +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 = 800; +} + +export const initCanvas = () => { + graphicCanvas = document.createElement('canvas'); + graphicCanvas.width = canvasWidth; + graphicCanvas.height = canvasHeight; +} \ No newline at end of file diff --git a/src/components/vertical-nav/info-project.ts b/src/components/vertical-nav/info-project.ts index 94c6dd6..f7ac301 100644 --- a/src/components/vertical-nav/info-project.ts +++ b/src/components/vertical-nav/info-project.ts @@ -1,4 +1,4 @@ -import {renderer} from "./../three-data-viewer"; +import {threeDataViewer} from "./../three-data-viewer"; export type ButtonLink = [ @@ -52,7 +52,7 @@ function dataSection() { detailTitle.textContent = "Made with"; detailSection.appendChild(detailTitle); - detailSection.appendChild(renderer.domElement); + detailSection.appendChild(threeDataViewer()); return detailSection; } From 38e299434ebde10b2ef96d9e7da90e1a44a0f8f3 Mon Sep 17 00:00:00 2001 From: Charles Doucet Date: Fri, 21 Feb 2025 12:02:43 -0500 Subject: [PATCH 3/8] feat: easy label creation workflow --- src/components/three-background/scene.ts | 2 +- src/components/three-data-viewer/geometry.ts | 39 +++++++++++++--- src/components/three-data-viewer/label.ts | 48 ++++++++++++++++++++ src/components/three-data-viewer/scene.ts | 11 +++-- 4 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 src/components/three-data-viewer/label.ts diff --git a/src/components/three-background/scene.ts b/src/components/three-background/scene.ts index 413ab52..759d25a 100644 --- a/src/components/three-background/scene.ts +++ b/src/components/three-background/scene.ts @@ -31,7 +31,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({ diff --git a/src/components/three-data-viewer/geometry.ts b/src/components/three-data-viewer/geometry.ts index 547f110..2d191f2 100644 --- a/src/components/three-data-viewer/geometry.ts +++ b/src/components/three-data-viewer/geometry.ts @@ -1,18 +1,23 @@ import * as THREE from "three"; import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; import {camera, renderer, scene} from "./scene.ts"; +import {label} from "./label.ts"; const width = 1.5; +const pointA = new THREE.Vector2( -width, -width ); +const pointB = new THREE.Vector2( 0, width ); +const pointC = new THREE.Vector2( width, -width ); + const shape = new THREE.Shape(); -shape.moveTo( -width, -width ); -shape.lineTo( 0, width ); -shape.lineTo( width, -width ); -shape.lineTo( -width, -width ); +shape.moveTo( pointA.x, pointA.y ); +shape.lineTo( pointB.x, pointB.y ); +shape.lineTo( pointC.x, pointC.y ); +shape.lineTo( pointA.x, pointA.y ); const extrudeSettings = { - steps: 0, - depth: 1, + steps: 1, + depth: -1, bevelEnabled: false, }; @@ -21,6 +26,8 @@ const material = new THREE.MeshBasicMaterial({ wireframe: true }); + + //----------------------------------------------------------------------- export const initGeometry = () => { @@ -31,8 +38,26 @@ export const initGeometry = () => { const controls = new OrbitControls(camera, renderer.domElement); scene.add(mesh); - camera.position.z = 3; + label("Unity 3D", pointA); + label("Blender", pointB); + label("Adobe XD", pointC); + + camera.position.z = 3.75; controls.panSpeed = 0; controls.rotateSpeed = 0.5; controls.update(); + + backdrop(); +} + +//----------------------------------------------------------------------- + +const backdrop = () => { + const geometry = new THREE.SphereGeometry(10); + const material = new THREE.MeshBasicMaterial({ + color: 0xffffff, + }); + const mesh = new THREE.Mesh(geometry, material); + mesh.position.z = -15; + scene.add(mesh); } \ No newline at end of file diff --git a/src/components/three-data-viewer/label.ts b/src/components/three-data-viewer/label.ts new file mode 100644 index 0000000..ddea6bb --- /dev/null +++ b/src/components/three-data-viewer/label.ts @@ -0,0 +1,48 @@ +import * as THREE from "three"; +import {scene} from "./scene.ts"; +import {Vector2} from "three"; + +const offset = 1.5 + +//----------------------------------------------------------------------- + +export const label = (label: string, spawnPoint: Vector2) => { + const texture = createLabelTexture(label); + const labelMesh = createLabelPlane(texture); + + labelMesh.position.set(spawnPoint.x, spawnPoint.y * offset, 0); + scene.add(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-data-viewer/scene.ts b/src/components/three-data-viewer/scene.ts index 4c6c107..fa65a7b 100644 --- a/src/components/three-data-viewer/scene.ts +++ b/src/components/three-data-viewer/scene.ts @@ -12,8 +12,8 @@ let graphicCanvas, export const initScene = () => { scene = new THREE.Scene(); - scene.fog = new THREE.Fog(0x010102, 1, 3000); - scene.add( new THREE.AmbientLight( 0xcccccc ) ); + scene.fog = new THREE.Fog(0x010102, 1, 10); + scene.add( new THREE.DirectionalLight( 0x00ff00, 10) ); renderer = new THREE.WebGLRenderer({ alpha: true, @@ -21,7 +21,7 @@ export const initScene = () => { }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(canvasWidth, canvasHeight); - renderer.setClearColor(0x000000, 0); + renderer.setClearColor(0x333333, 1); } export const initCamera = () => { @@ -41,4 +41,7 @@ export const initCanvas = () => { graphicCanvas = document.createElement('canvas'); graphicCanvas.width = canvasWidth; graphicCanvas.height = canvasHeight; -} \ No newline at end of file +} + +//----------------------------------------------------------------------- + From 11a4239ab3a4f6852fc8a04ce2d8ce53ebf675e0 Mon Sep 17 00:00:00 2001 From: Charles Doucet Date: Fri, 21 Feb 2025 14:58:37 -0500 Subject: [PATCH 4/8] wip: shape value inside main shape --- src/components/three-data-viewer/geometry.ts | 79 ++++++++++++++++---- src/components/three-data-viewer/label.ts | 11 ++- src/components/three-data-viewer/scene.ts | 6 +- 3 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/components/three-data-viewer/geometry.ts b/src/components/three-data-viewer/geometry.ts index 2d191f2..872ca7b 100644 --- a/src/components/three-data-viewer/geometry.ts +++ b/src/components/three-data-viewer/geometry.ts @@ -5,6 +5,10 @@ import {label} from "./label.ts"; const width = 1.5; +const pointAvalue = 85; +const pointBvalue = 25; +const pointCvalue = 50; + const pointA = new THREE.Vector2( -width, -width ); const pointB = new THREE.Vector2( 0, width ); const pointC = new THREE.Vector2( width, -width ); @@ -15,28 +19,30 @@ shape.lineTo( pointB.x, pointB.y ); shape.lineTo( pointC.x, pointC.y ); shape.lineTo( pointA.x, pointA.y ); +const valueShape = new THREE.Shape(); +valueShape.moveTo( pointA.x + pointAvalue/100, pointA.y + pointAvalue/100 ); +valueShape.lineTo( pointB.x, pointB.y ); +valueShape.lineTo( pointC.x, pointC.y ); + const extrudeSettings = { steps: 1, - depth: -1, + depth: 0, bevelEnabled: false, }; -const material = new THREE.MeshBasicMaterial({ - color: 0xffffff, - wireframe: true -}); - - //----------------------------------------------------------------------- export const initGeometry = () => { - const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); - const mesh = new THREE.Mesh(geometry, material); - scene.add(mesh); + drawMainShape(shape); + + for (let i = 0; i < 4; i++) { + drawDepthShape(shape, i); + } + + drawValueShape(valueShape); const controls = new OrbitControls(camera, renderer.domElement); - scene.add(mesh); label("Unity 3D", pointA); label("Blender", pointB); @@ -52,12 +58,57 @@ export const initGeometry = () => { //----------------------------------------------------------------------- -const backdrop = () => { - const geometry = new THREE.SphereGeometry(10); + +const drawMainShape = (mainShape: THREE.Shape) => { + const geometry = new THREE.ExtrudeGeometry(mainShape, extrudeSettings); const material = new THREE.MeshBasicMaterial({ color: 0xffffff, + wireframe: true }); + const mesh = new THREE.Mesh(geometry, material); - mesh.position.z = -15; + scene.add(mesh); +} + + +const drawDepthShape = (depthShape: THREE.Shape, iteration: number) => { + const geometry = new THREE.ExtrudeGeometry(depthShape, extrudeSettings); + const material = new THREE.MeshBasicMaterial({ + color: 0x333333, + wireframe: true + }); + + const mesh = new THREE.Mesh(geometry, material); + 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 drawValueShape = (valueShape: THREE.Shape) => { + const geometry = new THREE.ExtrudeGeometry(valueShape, extrudeSettings); + const material = new THREE.MeshBasicMaterial({ + color: 0x3BFFC5, + wireframe: true + }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); +} + +const backdrop = () => { + 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); } \ No newline at end of file diff --git a/src/components/three-data-viewer/label.ts b/src/components/three-data-viewer/label.ts index ddea6bb..3f48579 100644 --- a/src/components/three-data-viewer/label.ts +++ b/src/components/three-data-viewer/label.ts @@ -2,7 +2,9 @@ import {scene} from "./scene.ts"; import {Vector2} from "three"; -const offset = 1.5 +const offset = 1.5; +const labelMeshes: THREE.Mesh[] = []; + //----------------------------------------------------------------------- @@ -12,6 +14,13 @@ export const label = (label: string, spawnPoint: Vector2) => { labelMesh.position.set(spawnPoint.x, spawnPoint.y * offset, 0); scene.add(labelMesh); + labelMeshes.push(labelMesh); +} + +export const updateLabelMeshes = (camera: THREE.Camera) => { + labelMeshes.forEach(mesh => { + mesh.lookAt(camera.position); + }); } //----------------------------------------------------------------------- diff --git a/src/components/three-data-viewer/scene.ts b/src/components/three-data-viewer/scene.ts index fa65a7b..40a49c6 100644 --- a/src/components/three-data-viewer/scene.ts +++ b/src/components/three-data-viewer/scene.ts @@ -12,16 +12,14 @@ let graphicCanvas, export const initScene = () => { scene = new THREE.Scene(); - scene.fog = new THREE.Fog(0x010102, 1, 10); - scene.add( new THREE.DirectionalLight( 0x00ff00, 10) ); + //scene.fog = new THREE.Fog(0x010102, 1, 10); renderer = new THREE.WebGLRenderer({ - alpha: true, antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(canvasWidth, canvasHeight); - renderer.setClearColor(0x333333, 1); + renderer.setClearColor(0x000000, 1); } export const initCamera = () => { From 1fda1aefe9a0bb60f7a48c90ced8b5bcbb2348a0 Mon Sep 17 00:00:00 2001 From: Charles Doucet Date: Fri, 21 Feb 2025 16:36:13 -0500 Subject: [PATCH 5/8] feat: draw shape value --- src/components/three-data-viewer/geometry.ts | 79 +++++++------------- src/components/three-data-viewer/scene.ts | 16 ++++ 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/src/components/three-data-viewer/geometry.ts b/src/components/three-data-viewer/geometry.ts index 872ca7b..ee5bad6 100644 --- a/src/components/three-data-viewer/geometry.ts +++ b/src/components/three-data-viewer/geometry.ts @@ -13,16 +13,6 @@ const pointA = new THREE.Vector2( -width, -width ); const pointB = new THREE.Vector2( 0, width ); const pointC = new THREE.Vector2( width, -width ); -const shape = new THREE.Shape(); -shape.moveTo( pointA.x, pointA.y ); -shape.lineTo( pointB.x, pointB.y ); -shape.lineTo( pointC.x, pointC.y ); -shape.lineTo( pointA.x, pointA.y ); - -const valueShape = new THREE.Shape(); -valueShape.moveTo( pointA.x + pointAvalue/100, pointA.y + pointAvalue/100 ); -valueShape.lineTo( pointB.x, pointB.y ); -valueShape.lineTo( pointC.x, pointC.y ); const extrudeSettings = { steps: 1, @@ -34,13 +24,21 @@ const extrudeSettings = { //----------------------------------------------------------------------- export const initGeometry = () => { - drawMainShape(shape); + const shape = drawShape(); + const mainMesh = buildMesh(shape, 0xffffff); + scene.add(mainMesh); for (let i = 0; i < 4; i++) { - drawDepthShape(shape, i); + createDepthMesh(shape, i); } - drawValueShape(valueShape); + const valueShape = drawShape( + {x: pointAvalue/100, y: pointAvalue/100}, + {x: pointBvalue/100, y: pointBvalue/100}, + {x: pointCvalue/100, y: pointCvalue/100} + ); + const valueMesh = buildMesh(valueShape, 0x3BFFC5); + scene.add(valueMesh); const controls = new OrbitControls(camera, renderer.domElement); @@ -52,33 +50,23 @@ export const initGeometry = () => { controls.panSpeed = 0; controls.rotateSpeed = 0.5; controls.update(); - - backdrop(); } //----------------------------------------------------------------------- - -const drawMainShape = (mainShape: THREE.Shape) => { - const geometry = new THREE.ExtrudeGeometry(mainShape, extrudeSettings); +const buildMesh = (shape: THREE.Shape, shapeColor: THREE.ColorRepresentation) => { + const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); const material = new THREE.MeshBasicMaterial({ - color: 0xffffff, + color: shapeColor, wireframe: true }); const mesh = new THREE.Mesh(geometry, material); - scene.add(mesh); + return mesh; } - -const drawDepthShape = (depthShape: THREE.Shape, iteration: number) => { - const geometry = new THREE.ExtrudeGeometry(depthShape, extrudeSettings); - const material = new THREE.MeshBasicMaterial({ - color: 0x333333, - wireframe: true - }); - - const mesh = 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; @@ -87,28 +75,19 @@ const drawDepthShape = (depthShape: THREE.Shape, iteration: number) => { scene.add(mesh); } -const drawValueShape = (valueShape: THREE.Shape) => { - const geometry = new THREE.ExtrudeGeometry(valueShape, extrudeSettings); - const material = new THREE.MeshBasicMaterial({ - color: 0x3BFFC5, - wireframe: true - }); +const drawShape = ( + translationA: {x: number, y: number} = {x: 0, y: 0}, + translationB: {x: number, y: number} = {x: 0, y: 0}, + translationC: {x: number, y: number} = {x: 0, y: 0} +) => { + const shape = new THREE.Shape(); - const mesh = new THREE.Mesh(geometry, material); - scene.add(mesh); -} + shape.moveTo(pointA.x + translationA.x, pointA.y + translationA.y); + shape.lineTo(pointB.x, pointB.y - translationB.y); + shape.lineTo(pointC.x - translationC.x, pointC.y + translationC.y); + shape.lineTo(pointA.x + translationA.x, pointA.y + translationA.y); -const backdrop = () => { - const geometry = new THREE.PlaneGeometry(100, 100); - const material = new THREE.MeshStandardMaterial({ - color: 0xffffff, - }); - const mesh = new THREE.Mesh(geometry, material); + return shape; +} - const light1 = new THREE.PointLight( 0xffffff, 5 ); - light1.position.set( 0, -5, -3 ); - scene.add( light1 ); - mesh.position.z = -16; - scene.add(mesh); -} \ No newline at end of file diff --git a/src/components/three-data-viewer/scene.ts b/src/components/three-data-viewer/scene.ts index 40a49c6..bcf2e7c 100644 --- a/src/components/three-data-viewer/scene.ts +++ b/src/components/three-data-viewer/scene.ts @@ -13,6 +13,7 @@ let graphicCanvas, export const initScene = () => { scene = new THREE.Scene(); //scene.fog = new THREE.Fog(0x010102, 1, 10); + createBackdrop(); renderer = new THREE.WebGLRenderer({ antialias: true @@ -43,3 +44,18 @@ export const initCanvas = () => { //----------------------------------------------------------------------- +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); +} + From 5aeea8c2c8ead7ef0fb80d59daffebe1783b11c5 Mon Sep 17 00:00:00 2001 From: Charles Doucet Date: Sat, 22 Feb 2025 15:10:23 -0500 Subject: [PATCH 6/8] feat: radar chart draws value from new content variable --- src/components/three-data-viewer/geometry.ts | 93 --------------- .../animation-loop.ts | 2 +- .../index.ts | 6 +- .../label.ts | 3 +- src/components/three-radar-chart/radar.ts | 109 ++++++++++++++++++ .../scene.ts | 2 +- src/components/vertical-nav/info-project.ts | 11 +- src/content/projects/index.ts | 2 +- src/content/projects/space-compass/index.ts | 16 +++ 9 files changed, 138 insertions(+), 106 deletions(-) delete mode 100644 src/components/three-data-viewer/geometry.ts rename src/components/{three-data-viewer => three-radar-chart}/animation-loop.ts (59%) rename src/components/{three-data-viewer => three-radar-chart}/index.ts (68%) rename src/components/{three-data-viewer => three-radar-chart}/label.ts (94%) create mode 100644 src/components/three-radar-chart/radar.ts rename src/components/{three-data-viewer => three-radar-chart}/scene.ts (98%) diff --git a/src/components/three-data-viewer/geometry.ts b/src/components/three-data-viewer/geometry.ts deleted file mode 100644 index ee5bad6..0000000 --- a/src/components/three-data-viewer/geometry.ts +++ /dev/null @@ -1,93 +0,0 @@ -import * as THREE from "three"; -import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; -import {camera, renderer, scene} from "./scene.ts"; -import {label} from "./label.ts"; - -const width = 1.5; - -const pointAvalue = 85; -const pointBvalue = 25; -const pointCvalue = 50; - -const pointA = new THREE.Vector2( -width, -width ); -const pointB = new THREE.Vector2( 0, width ); -const pointC = new THREE.Vector2( width, -width ); - - -const extrudeSettings = { - steps: 1, - depth: 0, - bevelEnabled: false, -}; - - -//----------------------------------------------------------------------- - -export const initGeometry = () => { - const shape = drawShape(); - const mainMesh = buildMesh(shape, 0xffffff); - scene.add(mainMesh); - - for (let i = 0; i < 4; i++) { - createDepthMesh(shape, i); - } - - const valueShape = drawShape( - {x: pointAvalue/100, y: pointAvalue/100}, - {x: pointBvalue/100, y: pointBvalue/100}, - {x: pointCvalue/100, y: pointCvalue/100} - ); - const valueMesh = buildMesh(valueShape, 0x3BFFC5); - scene.add(valueMesh); - - const controls = new OrbitControls(camera, renderer.domElement); - - label("Unity 3D", pointA); - label("Blender", pointB); - label("Adobe XD", pointC); - - camera.position.z = 3.75; - controls.panSpeed = 0; - controls.rotateSpeed = 0.5; - controls.update(); -} - -//----------------------------------------------------------------------- - -const buildMesh = (shape: THREE.Shape, shapeColor: THREE.ColorRepresentation) => { - const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); - const material = new THREE.MeshBasicMaterial({ - color: shapeColor, - wireframe: true - }); - - const mesh = new THREE.Mesh(geometry, material); - return mesh; -} - -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 = ( - translationA: {x: number, y: number} = {x: 0, y: 0}, - translationB: {x: number, y: number} = {x: 0, y: 0}, - translationC: {x: number, y: number} = {x: 0, y: 0} -) => { - const shape = new THREE.Shape(); - - shape.moveTo(pointA.x + translationA.x, pointA.y + translationA.y); - shape.lineTo(pointB.x, pointB.y - translationB.y); - shape.lineTo(pointC.x - translationC.x, pointC.y + translationC.y); - shape.lineTo(pointA.x + translationA.x, pointA.y + translationA.y); - - return shape; -} - - diff --git a/src/components/three-data-viewer/animation-loop.ts b/src/components/three-radar-chart/animation-loop.ts similarity index 59% rename from src/components/three-data-viewer/animation-loop.ts rename to src/components/three-radar-chart/animation-loop.ts index 8ed4a09..f873666 100644 --- a/src/components/three-data-viewer/animation-loop.ts +++ b/src/components/three-radar-chart/animation-loop.ts @@ -1,4 +1,4 @@ -import {camera, scene, renderer} from "../three-data-viewer/scene.ts"; +import {camera, scene, renderer} from ".//scene.ts"; export const animate = () => { requestAnimationFrame(animate); diff --git a/src/components/three-data-viewer/index.ts b/src/components/three-radar-chart/index.ts similarity index 68% rename from src/components/three-data-viewer/index.ts rename to src/components/three-radar-chart/index.ts index 3a25db6..832a2c3 100644 --- a/src/components/three-data-viewer/index.ts +++ b/src/components/three-radar-chart/index.ts @@ -1,12 +1,12 @@ import {initCamera, initCanvas, initScene, renderer} from "./scene.ts"; import {animate} from "./animation-loop.ts"; -import {initGeometry} from "./geometry.ts"; +import {initRadar, TechUsage} from "./radar.ts"; -export function threeDataViewer() { +export function threeDataViewer(radarValues: TechUsage[]) { initScene(); initCanvas(); initCamera(); - initGeometry(); + initRadar(radarValues); animate(); diff --git a/src/components/three-data-viewer/label.ts b/src/components/three-radar-chart/label.ts similarity index 94% rename from src/components/three-data-viewer/label.ts rename to src/components/three-radar-chart/label.ts index 3f48579..8743006 100644 --- a/src/components/three-data-viewer/label.ts +++ b/src/components/three-radar-chart/label.ts @@ -1,6 +1,5 @@ import * as THREE from "three"; import {scene} from "./scene.ts"; -import {Vector2} from "three"; const offset = 1.5; const labelMeshes: THREE.Mesh[] = []; @@ -8,7 +7,7 @@ const labelMeshes: THREE.Mesh[] = []; //----------------------------------------------------------------------- -export const label = (label: string, spawnPoint: Vector2) => { +export const label = (label: string, spawnPoint: {x: number, y: number}) => { const texture = createLabelTexture(label); const labelMesh = createLabelPlane(texture); diff --git a/src/components/three-radar-chart/radar.ts b/src/components/three-radar-chart/radar.ts new file mode 100644 index 0000000..2df2256 --- /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, 0x00ff00); + 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); + + 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-data-viewer/scene.ts b/src/components/three-radar-chart/scene.ts similarity index 98% rename from src/components/three-data-viewer/scene.ts rename to src/components/three-radar-chart/scene.ts index bcf2e7c..f9516c9 100644 --- a/src/components/three-data-viewer/scene.ts +++ b/src/components/three-radar-chart/scene.ts @@ -33,7 +33,7 @@ export const initCamera = () => { aspectRatio, nearPlane, farPlane); - camera.position.z = 800; + camera.position.z = 3.75; } export const initCanvas = () => { diff --git a/src/components/vertical-nav/info-project.ts b/src/components/vertical-nav/info-project.ts index f7ac301..0f6d00d 100644 --- a/src/components/vertical-nav/info-project.ts +++ b/src/components/vertical-nav/info-project.ts @@ -1,4 +1,5 @@ -import {threeDataViewer} from "./../three-data-viewer"; +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(threeDataViewer()); + detailSection.appendChild(threeDataViewer(newTechRadar)); return detailSection; } 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..91479e9 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: 85 + }, + { + technology: "Blender", + percentage: 50 + }, + { + technology: "Adobe XD", + percentage: 50 + } +] + export const buttons: ButtonLink[] = [ [ "Try It Online", From 8fd734c674512439aeb65ac27ad36d1a2e2c9c31 Mon Sep 17 00:00:00 2001 From: Charles Doucet Date: Sat, 22 Feb 2025 15:49:08 -0500 Subject: [PATCH 7/8] feat: post-process in radar chart --- .../three-radar-chart/animation-loop.ts | 4 +- src/components/three-radar-chart/index.ts | 3 +- src/components/three-radar-chart/label.ts | 8 +--- .../three-radar-chart/post-process.ts | 39 +++++++++++++++++++ src/components/three-radar-chart/radar.ts | 4 +- src/components/three-radar-chart/scene.ts | 5 ++- src/components/vertical-nav/styles.css | 6 +++ src/content/projects/space-compass/index.ts | 6 +-- 8 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 src/components/three-radar-chart/post-process.ts diff --git a/src/components/three-radar-chart/animation-loop.ts b/src/components/three-radar-chart/animation-loop.ts index f873666..3bd90d0 100644 --- a/src/components/three-radar-chart/animation-loop.ts +++ b/src/components/three-radar-chart/animation-loop.ts @@ -1,6 +1,6 @@ -import {camera, scene, renderer} from ".//scene.ts"; +import {renderWithPostProcess} from "./post-process.ts"; export const animate = () => { requestAnimationFrame(animate); - renderer.render( scene, camera ); + 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 index 832a2c3..18e9d98 100644 --- a/src/components/three-radar-chart/index.ts +++ b/src/components/three-radar-chart/index.ts @@ -1,15 +1,16 @@ import {initCamera, initCanvas, initScene, 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[]) { initScene(); initCanvas(); 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 index 8743006..fcb1de6 100644 --- a/src/components/three-radar-chart/label.ts +++ b/src/components/three-radar-chart/label.ts @@ -1,7 +1,7 @@ import * as THREE from "three"; import {scene} from "./scene.ts"; -const offset = 1.5; +const offset = 1.35; const labelMeshes: THREE.Mesh[] = []; @@ -16,12 +16,6 @@ export const label = (label: string, spawnPoint: {x: number, y: number}) => { labelMeshes.push(labelMesh); } -export const updateLabelMeshes = (camera: THREE.Camera) => { - labelMeshes.forEach(mesh => { - mesh.lookAt(camera.position); - }); -} - //----------------------------------------------------------------------- const createLabelPlane = (texture: THREE.CanvasTexture) => { 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 index 2df2256..636190b 100644 --- a/src/components/three-radar-chart/radar.ts +++ b/src/components/three-radar-chart/radar.ts @@ -41,7 +41,7 @@ export const initRadar = (techs: TechUsage[]) => { } const valueShape = determineValueShape(techs.map(tech => tech.percentage)); - const valueMesh = buildMesh(valueShape, 0x00ff00); + const valueMesh = buildMesh(valueShape, 0x3BFFC5); scene.add(valueMesh); } @@ -83,7 +83,7 @@ const drawShape = (points: {x: number, y: number}[]) => { const determineValueShape = (values: number[]) => { let valueShape: {x: number, y: number}[] = []; - const score = values.map(value => (100 - value) / 100); + const score = values.map(value => ((100 - value) / 100) * graphicSize); valueShape = [ {x: triangle[0].x + score[0], y: triangle[0].y + score[0]}, diff --git a/src/components/three-radar-chart/scene.ts b/src/components/three-radar-chart/scene.ts index f9516c9..70afc86 100644 --- a/src/components/three-radar-chart/scene.ts +++ b/src/components/three-radar-chart/scene.ts @@ -4,8 +4,9 @@ export let renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera; -let graphicCanvas, - canvasWidth = 185, +let graphicCanvas; + +export const canvasWidth = 185, canvasHeight = 185; //----------------------------------------------------------------------- diff --git a/src/components/vertical-nav/styles.css b/src/components/vertical-nav/styles.css index c6e2d6e..f74fc81 100644 --- a/src/components/vertical-nav/styles.css +++ b/src/components/vertical-nav/styles.css @@ -53,6 +53,12 @@ padding: 20px 15px; } + canvas { + width: 100%; + height: 100%; + border-radius: 5px; + } + section { padding: 10px 0; diff --git a/src/content/projects/space-compass/index.ts b/src/content/projects/space-compass/index.ts index 91479e9..2709c5f 100644 --- a/src/content/projects/space-compass/index.ts +++ b/src/content/projects/space-compass/index.ts @@ -39,15 +39,15 @@ export const content: ProjectContent = { export const techs: TechUsage[] = [ { technology: "Unity 3D", - percentage: 85 + percentage: 90 }, { technology: "Blender", - percentage: 50 + percentage: 80 }, { technology: "Adobe XD", - percentage: 50 + percentage: 25 } ] From 1aed4d55396a0996c4c47fa863de6df29a3818a7 Mon Sep 17 00:00:00 2001 From: Charles Doucet Date: Sat, 22 Feb 2025 17:20:11 -0500 Subject: [PATCH 8/8] feat: added interactivity to the radar chart --- src/components/three-background/index.ts | 3 +- src/components/three-background/scene.ts | 11 +-- .../three-radar-chart/animation-loop.ts | 3 + src/components/three-radar-chart/index.ts | 4 +- src/components/three-radar-chart/scene.ts | 76 +++++++++++++++++-- src/components/vertical-nav/styles.css | 8 ++ 6 files changed, 84 insertions(+), 21 deletions(-) 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 759d25a..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; @@ -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 index 3bd90d0..e5307e7 100644 --- a/src/components/three-radar-chart/animation-loop.ts +++ b/src/components/three-radar-chart/animation-loop.ts @@ -1,6 +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 index 18e9d98..41f2c94 100644 --- a/src/components/three-radar-chart/index.ts +++ b/src/components/three-radar-chart/index.ts @@ -1,11 +1,11 @@ -import {initCamera, initCanvas, initScene, renderer} from "./scene.ts"; +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(); - initCanvas(); initCamera(); initRadar(radarValues); initPostProcess(); diff --git a/src/components/three-radar-chart/scene.ts b/src/components/three-radar-chart/scene.ts index 70afc86..f855479 100644 --- a/src/components/three-radar-chart/scene.ts +++ b/src/components/three-radar-chart/scene.ts @@ -2,14 +2,25 @@ export let renderer: THREE.WebGLRenderer, scene: THREE.Scene, - camera: THREE.PerspectiveCamera; + camera: THREE.PerspectiveCamera, + cameraTarget = new THREE.Vector3(0, 0 ,3.75), + canvasWidth = 185, + canvasHeight = 185; -let graphicCanvas; +let isDragging = false, + previousMousePosition = { x: 0, y: 0 }, + cameraLookAt = new THREE.Vector3(0, 0, 0); -export const canvasWidth = 185, - canvasHeight = 185; +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(); @@ -22,6 +33,8 @@ export const initScene = () => { renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(canvasWidth, canvasHeight); renderer.setClearColor(0x000000, 1); + + renderer.domElement.addEventListener('mousedown', onMouseDown, false); } export const initCamera = () => { @@ -35,12 +48,18 @@ export const initCamera = () => { nearPlane, farPlane); camera.position.z = 3.75; + + initialCameraPosition.copy(camera.position); + initialCameraRotation.copy(camera.rotation); } -export const initCanvas = () => { - graphicCanvas = document.createElement('canvas'); - graphicCanvas.width = canvasWidth; - graphicCanvas.height = canvasHeight; +export const sceneAnimation = () => { + if (!isDragging) { + cameraTarget.lerp(initialCameraTarget, 0.1); + } + + camera.position.lerp(cameraTarget, 0.2); + camera.lookAt(cameraLookAt); } //----------------------------------------------------------------------- @@ -60,3 +79,44 @@ const createBackdrop = () => { 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/styles.css b/src/components/vertical-nav/styles.css index f74fc81..b6550ff 100644 --- a/src/components/vertical-nav/styles.css +++ b/src/components/vertical-nav/styles.css @@ -59,6 +59,14 @@ border-radius: 5px; } + canvas:hover { + cursor: pointer; + } + + canvas:active { + cursor: grabbing; + } + section { padding: 10px 0;