import * as THREE from 'three'; import { OrbitControls } from '../../examples/jsm/controls/OrbitControls.js'; export const threejsLessonUtils = { _afterPrettifyFuncs: [], init( options = { threejsOptions: {} } ) { if ( this.renderer ) { return; } const canvas = document.createElement( 'canvas' ); canvas.id = 'c'; document.body.appendChild( canvas ); const renderer = new THREE.WebGLRenderer( { canvas, alpha: true, antialias: true, powerPreference: 'low-power', ...options.threejsOptions, } ); this.pixelRatio = window.devicePixelRatio; this.renderer = renderer; this.elemToRenderFuncMap = new Map(); const resizeRendererToDisplaySize = ( renderer ) => { const canvas = renderer.domElement; const width = canvas.clientWidth * this.pixelRatio | 0; const height = canvas.clientHeight * this.pixelRatio | 0; const needResize = canvas.width !== width || canvas.height !== height; if ( needResize ) { renderer.setSize( width, height, false ); } return needResize; }; const clearColor = new THREE.Color( '#000' ); let needsUpdate = true; let rafRequestId; let rafRunning; const render = ( time ) => { rafRequestId = undefined; time *= 0.001; const resized = resizeRendererToDisplaySize( renderer ); // only update if we drew last time // so the browser will not recomposite the page // of nothing is being drawn. if ( needsUpdate ) { needsUpdate = false; renderer.setScissorTest( false ); renderer.setClearColor( clearColor, 0 ); renderer.clear( true, true ); renderer.setScissorTest( true ); } this.elementsOnScreen.forEach( elem => { const fn = this.elemToRenderFuncMap.get( elem ); const wasRendered = fn( renderer, time, resized ); needsUpdate = needsUpdate || wasRendered; } ); if ( needsUpdate ) { // maybe there is another way. Originally I used `position: fixed` // but the problem is if we can't render as fast as the browser // scrolls then our shapes lag. 1 or 2 frames of lag isn't too // horrible but iOS would often been 1/2 a second or worse. // By doing it this way the canvas will scroll which means the // worse that happens is part of the shapes scrolling on don't // get drawn for a few frames but the shapes that are on the screen // scroll perfectly. // // I'm using `transform` on the voodoo that it doesn't affect // layout as much as `top` since AFAIK setting `top` is in // the flow but `transform` is not though thinking about it // the given we're `position: absolute` maybe there's no difference? const transform = `translateY(${window.scrollY}px)`; renderer.domElement.style.transform = transform; } if ( rafRunning ) { startRAFLoop(); } }; function startRAFLoop() { rafRunning = true; if ( ! rafRequestId ) { rafRequestId = requestAnimationFrame( render ); } } this.elementsOnScreen = new Set(); this.intersectionObserver = new IntersectionObserver( ( entries ) => { entries.forEach( entry => { if ( entry.isIntersecting ) { this.elementsOnScreen.add( entry.target ); } else { this.elementsOnScreen.delete( entry.target ); } // Each entry describes an intersection change for one observed // target element: // entry.boundingClientRect // entry.intersectionRatio // entry.intersectionRect // entry.isIntersecting // entry.rootBounds // entry.target // entry.time } ); if ( this.elementsOnScreen.size > 0 ) { startRAFLoop(); } else { rafRunning = false; } } ); }, addDiagrams( diagrams ) { [ ...document.querySelectorAll( '[data-diagram]' ) ].forEach( ( elem ) => { const name = elem.dataset.diagram; const info = diagrams[ name ]; if ( ! info ) { throw new Error( `no diagram: ${name}` ); } this.addDiagram( elem, info ); } ); }, addDiagram( elem, info ) { this.init(); const scene = new THREE.Scene(); let targetFOVDeg = 60; const aspect = 1; const near = 0.1; const far = 50; let camera = new THREE.PerspectiveCamera( targetFOVDeg, aspect, near, far ); camera.position.z = 15; scene.add( camera ); const root = new THREE.Object3D(); scene.add( root ); const renderInfo = { pixelRatio: this.pixelRatio, camera, scene, root, renderer: this.renderer, elem, }; const obj3D = info.create( { scene, camera, renderInfo } ); const promise = ( obj3D instanceof Promise ) ? obj3D : Promise.resolve( obj3D ); const updateFunctions = []; const resizeFunctions = []; const settings = { lights: true, trackball: true, // resize(renderInfo) { // }, // update(time, renderInfo) { // }, render( renderInfo ) { renderInfo.renderer.render( renderInfo.scene, renderInfo.camera ); }, }; promise.then( ( result ) => { const info = result instanceof THREE.Object3D ? { obj3D: result, } : result; if ( info.obj3D ) { root.add( info.obj3D ); } if ( info.update ) { updateFunctions.push( info.update ); } if ( info.resize ) { resizeFunctions.push( info.resize ); } if ( info.camera ) { camera = info.camera; renderInfo.camera = camera; } Object.assign( settings, info ); targetFOVDeg = camera.fov; if ( settings.trackball !== false ) { const controls = new OrbitControls( camera, elem ); controls.rotateSpeed = 1 / 6; controls.enableZoom = false; controls.enablePan = false; elem.removeAttribute( 'tabIndex' ); //resizeFunctions.push(controls.handleResize.bind(controls)); updateFunctions.push( controls.update.bind( controls ) ); } // add the lights as children of the camera. // this is because TrackballControls move the camera. // We really want to rotate the object itself but there's no // controls for that so we fake it by putting all the lights // on the camera so they move with it. if ( settings.lights !== false ) { camera.add( new THREE.HemisphereLight( 0xaaaaaa, 0x444444, .5 ) ); const light = new THREE.DirectionalLight( 0xffffff, 1 ); light.position.set( - 1, 2, 4 - 15 ); camera.add( light ); } } ); let oldWidth = - 1; let oldHeight = - 1; const render = ( renderer, time ) => { root.rotation.x = time * .1; root.rotation.y = time * .11; const rect = elem.getBoundingClientRect(); if ( rect.bottom < 0 || rect.top > renderer.domElement.clientHeight || rect.right < 0 || rect.left > renderer.domElement.clientWidth ) { return false; } renderInfo.width = rect.width * this.pixelRatio; renderInfo.height = rect.height * this.pixelRatio; renderInfo.left = rect.left * this.pixelRatio; renderInfo.bottom = ( renderer.domElement.clientHeight - rect.bottom ) * this.pixelRatio; if ( renderInfo.width !== oldWidth || renderInfo.height !== oldHeight ) { oldWidth = renderInfo.width; oldHeight = renderInfo.height; resizeFunctions.forEach( fn => fn( renderInfo ) ); } updateFunctions.forEach( fn => fn( time, renderInfo ) ); const aspect = renderInfo.width / renderInfo.height; const fovDeg = aspect >= 1 ? targetFOVDeg : THREE.MathUtils.radToDeg( 2 * Math.atan( Math.tan( THREE.MathUtils.degToRad( targetFOVDeg ) * .5 ) / aspect ) ); camera.fov = fovDeg; camera.aspect = aspect; camera.updateProjectionMatrix(); renderer.setViewport( renderInfo.left, renderInfo.bottom, renderInfo.width, renderInfo.height ); renderer.setScissor( renderInfo.left, renderInfo.bottom, renderInfo.width, renderInfo.height ); settings.render( renderInfo ); return true; }; this.intersectionObserver.observe( elem ); this.elemToRenderFuncMap.set( elem, render ); }, onAfterPrettify( fn ) { this._afterPrettifyFuncs.push( fn ); }, afterPrettify() { this._afterPrettifyFuncs.forEach( ( fn ) => { fn(); } ); }, }; window.threejsLessonUtils = threejsLessonUtils;