UNPKG

@acransac/vtk.js

Version:

Visualization Toolkit for the Web

439 lines (388 loc) 13.7 kB
import 'vtk.js/Sources/favicon'; import HttpDataAccessHelper from 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; import macro from 'vtk.js/Sources/macro'; import vtkDeviceOrientationToCamera from 'vtk.js/Sources/Interaction/Misc/DeviceOrientationToCamera'; import vtkForwardPass from 'vtk.js/Sources/Rendering/OpenGL/ForwardPass'; import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow'; import vtkRadialDistortionPass from 'vtk.js/Sources/Rendering/OpenGL/RadialDistortionPass'; import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; import vtkSkybox from 'vtk.js/Sources/Rendering/Core/Skybox'; import vtkSkyboxReader from 'vtk.js/Sources/IO/Misc/SkyboxReader'; import vtkURLExtract from 'vtk.js/Sources/Common/Core/URLExtract'; // import vtkMobileVR from 'vtk.js/Sources/Common/System/MobileVR'; import style from './SkyboxViewer.module.css'; // ---------------------------------------------- // Possible URL parameters to look for: // - fileURL // - position // - direction // - up // - vr // - eye // - viewAngle // - debug // - autoIncrement // - k1 // - k2 // - centerY // ---------------------------------------------- const userParams = vtkURLExtract.extractURLParameters(); let autoInit = true; const cameraFocalPoint = userParams.direction || [0, 0, -1]; const cameraViewUp = userParams.up || [0, 1, 0]; const cameraViewAngle = userParams.viewAngle || 100; const enableVR = !!userParams.vr; const eyeSpacing = userParams.eye || 0.0; const grid = userParams.debug || false; const autoIncrementTimer = userParams.timer || 0; const disableTouchNext = userParams.disableTouch || false; const distk1 = userParams.k1 || 0.2; const distk2 = userParams.k2 || 0.0; const cameraCenterY = userParams.centerY || 0.0; const body = document.querySelector('body'); let fullScreenMetod = null; ['requestFullscreen', 'msRequestFullscreen', 'webkitRequestFullscreen'].forEach( (m) => { if (body[m] && !fullScreenMetod) { fullScreenMetod = m; } } ); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } function createController(options) { if (options.length === 1) { return null; } const buffer = ['<select class="position">']; buffer.push(options.join('')); buffer.push('</select>'); return buffer.join(''); } function drawLine(ctx, x, y, text, delta = 10) { ctx.beginPath(); ctx.moveTo(x, y - delta); ctx.lineTo(x, y + delta); ctx.stroke(); ctx.fillText(text, x, y + delta + 10); } function createGrid(width, height) { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; canvas.style.width = '100%'; const ctx = canvas.getContext('2d'); ctx.strokeStyle = '#FFFFFF'; ctx.fillStyle = '#FFFFFF'; ctx.textAlign = 'center'; const totalWidth = width; const eyeCenter = width / 4; const y = window.screen.height / 2; ctx.clearRect(0, 0, width, height); const nbTicks = 20; for (let i = 0; i <= nbTicks; i++) { const value = (i - nbTicks / 2) / (nbTicks / 2); drawLine( ctx, eyeCenter + eyeCenter * (eyeSpacing + value), y, `${value}`, value ? 20 : 100 ); drawLine( ctx, totalWidth - (eyeCenter + eyeCenter * (eyeSpacing + value)), y, `${value}`, value ? 20 : 100 ); } ctx.fillText( `Current Offset ${eyeSpacing}`, eyeCenter + eyeCenter * eyeSpacing, y - 120 ); ctx.fillText( `Current Offset ${eyeSpacing}`, totalWidth - (eyeCenter + eyeCenter * eyeSpacing), y - 120 ); canvas.style.zIndex = 1000; canvas.style.position = 'fixed'; canvas.style.top = 0; canvas.style.left = 0; body.appendChild(canvas); } function createVisualization(container, mapReader) { // Empty container while (container.firstChild) { container.removeChild(container.firstChild); } const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ rootContainer: container, containerStyle: { height: '100%', width: '100%', position: 'absolute' }, }); const renderWindow = fullScreenRenderer.getRenderWindow(); const mainRenderer = fullScreenRenderer.getRenderer(); const interactor = fullScreenRenderer.getInteractor(); const actor = vtkSkybox.newInstance(); let camera = mainRenderer.getActiveCamera(); let leftRenderer = null; let rightRenderer = null; let updateCameraCallBack = mainRenderer.resetCameraClippingRange; // Connect viz pipeline actor.addTexture(mapReader.getOutputData()); // Update Camera configuration const cameraConfiguration = { focalPoint: cameraFocalPoint, position: [0, 0, 0], viewAngle: cameraViewAngle, physicalViewNorth: cameraFocalPoint, viewUp: cameraViewUp, physicalViewUp: cameraViewUp, }; function updateSkybox(position) { const selector = document.querySelector('.position'); if (selector && selector.value !== position) { selector.value = position; } actor.removeAllTextures(); mapReader.setPosition(`${position}`); mapReader.update(); actor.addTexture(mapReader.getOutputData()); renderWindow.render(); } function nextPosition() { const currentPosition = mapReader.getPosition(); const allPositions = mapReader.getPositions(); const nextIdx = (allPositions.indexOf(currentPosition) + 1) % allPositions.length; updateSkybox(allPositions[nextIdx]); } if (enableVR && vtkDeviceOrientationToCamera.isDeviceOrientationSupported()) { // vtkMobileVR.getVRHeadset().then((headset) => { // console.log('got headset'); // console.log(headset); // console.log(vtkMobileVR.hardware); // }); leftRenderer = vtkRenderer.newInstance(); rightRenderer = vtkRenderer.newInstance(); // Configure left/right renderers leftRenderer.setViewport(0, 0, 0.5, 1); leftRenderer.addActor(actor); const leftCamera = leftRenderer.getActiveCamera(); leftCamera.set(cameraConfiguration); leftCamera.setWindowCenter(-eyeSpacing, -cameraCenterY); rightRenderer.setViewport(0.5, 0, 1, 1); rightRenderer.addActor(actor); const rightCamera = rightRenderer.getActiveCamera(); rightCamera.set(cameraConfiguration); rightCamera.setWindowCenter(eyeSpacing, -cameraCenterY); // Provide custom update callback + fake camera updateCameraCallBack = () => { leftRenderer.resetCameraClippingRange(); rightRenderer.resetCameraClippingRange(); }; camera = { setDeviceAngles(alpha, beta, gamma, screen) { leftCamera.setDeviceAngles(alpha, beta, gamma, screen); rightCamera.setDeviceAngles(alpha, beta, gamma, screen); }, }; // Reconfigure render window renderWindow.addRenderer(leftRenderer); renderWindow.addRenderer(rightRenderer); renderWindow.removeRenderer(mainRenderer); const distPass = vtkRadialDistortionPass.newInstance(); distPass.setK1(distk1); distPass.setK2(distk2); distPass.setCameraCenterY(cameraCenterY); distPass.setCameraCenterX1(-eyeSpacing); distPass.setCameraCenterX2(eyeSpacing); distPass.setDelegates([vtkForwardPass.newInstance()]); fullScreenRenderer.getOpenGLRenderWindow().setRenderPasses([distPass]); // Hide any controller fullScreenRenderer.setControllerVisibility(false); // Remove window interactions interactor.unbindEvents(); // Attach touch control if (!disableTouchNext) { fullScreenRenderer .getRootContainer() .addEventListener('touchstart', nextPosition, true); if (fullScreenMetod) { fullScreenRenderer.getRootContainer().addEventListener( 'touchend', (e) => { body[fullScreenMetod](); }, true ); } } // Warning if browser does not support fullscreen /* eslint-disable */ if (navigator.userAgent.match('CriOS')) { alert( 'Chrome on iOS does not support fullscreen. Please use Safari instead.' ); } if (navigator.userAgent.match('FxiOS')) { alert( 'Firefox on iOS does not support fullscreen. Please use Safari instead.' ); } /* eslint-enable */ } else { camera.set(cameraConfiguration); mainRenderer.addActor(actor); // add vr option button if supported fullScreenRenderer.getOpenGLRenderWindow().onHaveVRDisplay(() => { if ( fullScreenRenderer.getOpenGLRenderWindow().getVrDisplay().capabilities .canPresent ) { const button = document.createElement('button'); button.style.position = 'absolute'; button.style.left = '10px'; button.style.bottom = '10px'; button.style.zIndex = 10000; button.textContent = 'Send To VR'; document.querySelector('body').appendChild(button); button.addEventListener('click', () => { if (button.textContent === 'Send To VR') { fullScreenRenderer.getOpenGLRenderWindow().startVR(); button.textContent = 'Return From VR'; } else { fullScreenRenderer.getOpenGLRenderWindow().stopVR(); button.textContent = 'Send To VR'; } }); } }); } renderWindow.render(); // handle auto incrementing position if (autoIncrementTimer !== 0) { setInterval(nextPosition, autoIncrementTimer * 1000); } // Update camera control if (vtkDeviceOrientationToCamera.isDeviceOrientationSupported()) { vtkDeviceOrientationToCamera.addWindowListeners(); const cameraListenerId = vtkDeviceOrientationToCamera.addCameraToSynchronize( interactor, camera, updateCameraCallBack ); interactor.requestAnimation('deviceOrientation'); // Test again after 100ms setTimeout(() => { if (!vtkDeviceOrientationToCamera.isDeviceOrientationSupported()) { vtkDeviceOrientationToCamera.removeCameraToSynchronize( cameraListenerId ); vtkDeviceOrientationToCamera.removeWindowListeners(); interactor.cancelAnimation('deviceOrientation'); } }, 100); } // Add Control UI const controller = createController( mapReader.getPositions().map((t) => `<option value="${t}">${t}</option>`) ); if (controller) { fullScreenRenderer.addController(controller); document.querySelector('.position').addEventListener('change', (e) => { updateSkybox(e.target.value); }); } // Apply url args to viz if (userParams.position) { updateSkybox(userParams.position); } if (grid) { console.log(fullScreenRenderer.getOpenGLRenderWindow().getSize()); createGrid(...fullScreenRenderer.getOpenGLRenderWindow().getSize()); } } /* eslint-disable import/prefer-default-export */ /* eslint-disable import/no-extraneous-dependencies */ export function initLocalFileLoader(container) { autoInit = false; const exampleContainer = document.querySelector('.content'); const rootBody = document.querySelector('body'); const myContainer = container || exampleContainer || rootBody; if (myContainer !== container) { myContainer.classList.add(style.fullScreen); rootBody.style.margin = '0'; rootBody.style.padding = '0'; } else { rootBody.style.margin = '0'; rootBody.style.padding = '0'; } const fileContainer = document.createElement('div'); fileContainer.innerHTML = `<div class="${style.bigFileDrop}"/><input type="file" accept=".skybox,.zip" style="display: none;"/>`; myContainer.appendChild(fileContainer); const fileInput = fileContainer.querySelector('input'); function handleFile(e) { preventDefaults(e); const dataTransfer = e.dataTransfer; const files = e.target.files || dataTransfer.files; if (files.length === 1) { myContainer.removeChild(fileContainer); const reader = vtkSkyboxReader.newInstance(); reader.parseAsArrayBuffer(files[0]); reader.getReadyPromise().then(() => { createVisualization(myContainer, reader); }); } } fileInput.addEventListener('change', handleFile); fileContainer.addEventListener('drop', handleFile); fileContainer.addEventListener('click', (e) => fileInput.click()); fileContainer.addEventListener('dragover', preventDefaults); } // Look for data to download if (userParams.fileURL) { autoInit = false; const exampleContainer = document.querySelector('.content'); const rootBody = document.querySelector('body'); const myContainer = exampleContainer || rootBody; if (myContainer) { myContainer.classList.add(style.fullScreen); rootBody.style.margin = '0'; rootBody.style.padding = '0'; } const progressContainer = document.createElement('div'); progressContainer.setAttribute('class', style.progress); myContainer.appendChild(progressContainer); const progressCallback = (progressEvent) => { if (progressEvent.lengthComputable) { const percent = Math.floor( (100 * progressEvent.loaded) / progressEvent.total ); progressContainer.innerHTML = `Loading ${percent}%`; } else { progressContainer.innerHTML = macro.formatBytesToProperUnit( progressEvent.loaded ); } }; HttpDataAccessHelper.fetchBinary(userParams.fileURL, { progressCallback, }).then((arrayBuffer) => { const reader = vtkSkyboxReader.newInstance(); reader.parseAsArrayBuffer(arrayBuffer); reader.getReadyPromise().then(() => { createVisualization(myContainer, reader); }); }); } // Auto setup if no method get called within 100ms setTimeout(() => { if (autoInit) { initLocalFileLoader(); } }, 100);