import * as THREE from "./three.module.js"; import { GLTFLoader } from './GLTFLoader.js'; import { OrbitControls } from './OrbitControls.js'; import { GUI } from './dat.gui.module.js'; import { STLExporter } from './STLExporter.js'; const BASE_LENGTH = 0.834 const BASE_WIDTH = 0.167 const BASE_HEIGHT = 0.05 const CUBE_SIZE = 0.0143 const MAX_HEIGHT = 0.14 const FACE_ANGLE = 104.79 let username = "nat" let year = "" + (new Date()).getFullYear() let json = {} let font = undefined let fontSize = 0.025 let fontHeight = 0.00658 // Extrusion thickness let camera, scene, renderer let bronzeMaterial var exporter = new STLExporter(); var urlParams = new URLSearchParams(window.location.search); if (urlParams.has('username')) { username = urlParams.get('username') } if (urlParams.has('year')) { year = urlParams.get('year') } // Import JSON data async function loadJSON(username, year) { let url = `https://json-contributions-five.vercel.app/api/user?username=${username}&year=${year}` let response = await fetch(url) if (response.ok) { json = await response.json() init() render() } else { alert("HTTP-Error: " + response.status) } } loadJSON(username, year) const fixSideNormals = (geometry) => { let triangle = new THREE.Triangle() // "fix" side normals by removing z-component of normals for side faces var triangleAreaHeuristics = 0.1 * ( fontHeight * fontSize ); for (var i = 0; i < geometry.faces.length; i++) { let face = geometry.faces[i] if ( face.materialIndex == 1 ) { for ( var j = 0; j < face.vertexNormals.length; j ++ ) { face.vertexNormals[ j ].z = 0 face.vertexNormals[ j ].normalize() } let va = geometry.vertices[ face.a ] let vb = geometry.vertices[ face.b ] let vc = geometry.vertices[ face.c ] let s = triangle.set( va, vb, vc ).getArea() if ( s > triangleAreaHeuristics ) { for ( var j = 0; j < face.vertexNormals.length; j ++ ) { face.vertexNormals[ j ].copy(face.normal) } } } } } const createText = () => { let nameGeo = new THREE.TextGeometry(username, { font: font, size: fontSize, height: fontHeight }) nameGeo.computeBoundingBox() nameGeo.computeVertexNormals() let yearGeo = new THREE.TextGeometry(year, { font: font, size: fontSize, height: fontHeight }) yearGeo.computeBoundingBox() yearGeo.computeVertexNormals() fixSideNormals(nameGeo) fixSideNormals(yearGeo) nameGeo = new THREE.BufferGeometry().fromGeometry(nameGeo) let nameMesh = new THREE.Mesh(nameGeo, bronzeMaterial) nameMesh.position.x = -0.295 nameMesh.position.y = -0.075 nameMesh.position.z = -0.010 nameMesh.geometry.rotateX(FACE_ANGLE * Math.PI / 2) nameMesh.geometry.rotateY(Math.PI * 2) scene.add(nameMesh) yearGeo = new THREE.BufferGeometry().fromGeometry(yearGeo) let yearMesh = new THREE.Mesh(yearGeo, bronzeMaterial) yearMesh.position.x = 0.280 yearMesh.position.y = -0.075 yearMesh.position.z = -0.010 yearMesh.geometry.rotateX(FACE_ANGLE * Math.PI / 2) yearMesh.geometry.rotateY(Math.PI * 2) scene.add(yearMesh) } const init = () => { // SCENE scene = new THREE.Scene() // CAMERA camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.01, 100 ) // RENDERER renderer = new THREE.WebGLRenderer({ antialias: true }) renderer.setPixelRatio( window.devicePixelRatio ) renderer.setSize( window.innerWidth, window.innerHeight ) renderer.outputEncoding = THREE.sRGBEncoding renderer.setClearColor(0xffffff, 1) document.body.appendChild(renderer.domElement) // MATERIALS let phongMaterial = new THREE.MeshPhongMaterial( { color: 0x40c463, transparent: true, opacity: 0.2, side: THREE.DoubleSide } ) bronzeMaterial = new THREE.MeshPhysicalMaterial( { color: 0x645f61, metalness: 1, clearcoat: 0.5, clearcoatRoughness: 0.5, side: THREE.DoubleSide } ) // LIGHTS const dLight1 = new THREE.DirectionalLight(0xebeb8c, 0.8) dLight1.position.set(-2, -5, 5); dLight1.target.position.set(0, 0, 0); const dLight2 = new THREE.DirectionalLight(0xebeb8c, 0.8) dLight2.position.set(2, -5, 5); dLight2.target.position.set(0, 0, 0); scene.add(dLight1) scene.add(dLight2) // LOAD REFERENCE MODEL let loader = new GLTFLoader().setPath('../models/') loader.load('ashtom-orig.glb', function (gltf) { gltf.scene.traverse(function (child) { if (child.isMesh) { child.material = phongMaterial child.material.depthWrite = !child.material.transparent } }) gltf.scene.rotation.x = Math.PI/2 gltf.scene.rotation.y = -Math.PI // let worldAxis = new THREE.AxesHelper(2); // scene.add(worldAxis) render() }) // BASE GEOMETRY let baseLoader = new GLTFLoader().setPath('../models/') baseLoader.load('base.glb', function (base) { base.scene.traverse(function (child) { if (child.isMesh) { child.material = bronzeMaterial child.material.depthWrite = !child.material.transparent } }) base.scene.rotation.x = -Math.PI/2 base.scene.rotation.z = -Math.PI scene.add(base.scene) }) // USERNAME + YEAR let fontLoader = new THREE.FontLoader() fontLoader.load('../fonts/helvetiker_regular.typeface.json', function (response) { font = response createText() }) // CONTRIBUTION BARS let barGroup = new THREE.Group() let x = 0 let y = 0 json.contributions.forEach(week => { y = (CUBE_SIZE * 7) week.days.forEach(day => { y -= CUBE_SIZE // Adjust height around distribution of values // Needed so that a large day doesn't blow out the scale let height = (0).toFixed(4) if (day.count === json.min) { height = MAX_HEIGHT * 0.1 } else if (day.count > json.min && day.count <= json.p99) { height = ((MAX_HEIGHT * 0.1) + (((MAX_HEIGHT * 0.8) / json.p99) * day.count)).toFixed(4) } else if (day.count > json.p99) { height = ((MAX_HEIGHT * 0.9) + (((MAX_HEIGHT * 0.1) / json.max) * day.count)).toFixed(4) } let geometry = new THREE.BoxGeometry(CUBE_SIZE, CUBE_SIZE, height) let cube = new THREE.Mesh(geometry, bronzeMaterial) cube.position.x = x cube.position.y = y cube.position.z = BASE_HEIGHT / 2 + height / 2 barGroup.add(cube) }) x += CUBE_SIZE }) const groupBox = new THREE.Box3().setFromObject(barGroup) const groupCenter = groupBox.getCenter(new THREE.Vector3()) barGroup.position.x -= groupCenter.x barGroup.position.y -= groupCenter.y scene.add(barGroup) const box = new THREE.Box3().setFromObject(scene) const center = box.getCenter(new THREE.Vector3()) camera.lookAt(center) camera.position.y = -0.4 camera.position.z = 0.3 let controls = new OrbitControls(camera, renderer.domElement) controls.autoRotate = false controls.screenSpacePanning = true controls.addEventListener('change', render); var buttonExportASCII = document.getElementById( 'exportASCII' ); buttonExportASCII.addEventListener( 'click', exportASCII ); var buttonExportBinary = document.getElementById( 'exportBinary' ); buttonExportBinary.addEventListener( 'click', exportBinary ); } const render = () => { renderer.render(scene, camera) } function exportASCII() { var result = exporter.parse( bronzeMaterial ); saveString( result, username + '-' + year + '.stl' ); } function exportBinary() { var result = exporter.parse( scene, { binary: true } ); saveArrayBuffer( result, username + '-' + year + '.stl' ); } // // Event listeners // const onWindowResize = () => { let canvasWidth = window.innerWidth; let canvasHeight = window.innerHeight; renderer.setSize( canvasWidth, canvasHeight ); camera.aspect = canvasWidth / canvasHeight; camera.updateProjectionMatrix(); render() } window.addEventListener('resize', onWindowResize, false) var link = document.createElement( 'a' ); link.style.display = 'none'; document.body.appendChild( link ); function save( blob, filename ) { link.href = URL.createObjectURL( blob ); link.download = filename; link.click(); } function saveString( text, filename ) { save( new Blob( [ text ], { type: 'text/plain' } ), filename ); } function saveArrayBuffer( buffer, filename ) { save( new Blob( [ buffer ], { type: 'application/octet-stream' } ), filename ); }