| <!DOCTYPE html> |
| <html lang="en"> |
| <!-- |
| Copyright 2022 The Chromium Authors |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| |
| <head> |
| <style> |
| #scrubberframe::-webkit-slider-thumb { |
| appearance: none; |
| width: 20px; |
| height: 20px; |
| background-color: grey; |
| } |
| |
| #scrubberframe { |
| margin-top: 10px; |
| flex-grow: 1; |
| appearance: none; |
| background-color: #f0f0f0; |
| width: 100%; |
| } |
| |
| .scrubber::-webkit-slider-thumb { |
| appearance: none; |
| width: 20px; |
| height: 20px; |
| background-color: grey; |
| } |
| |
| .minMaxScrubber { |
| width: 100%; |
| display: flex; |
| } |
| |
| .minMaxScrubber > .scrubber { |
| appearance: none; |
| background-color: #f0f0f0; |
| margin: 0px; |
| flex-grow: 1; |
| flex-basis: 20px; /* width of slider-thumb */ |
| min-width: 0px; |
| } |
| |
| #url { |
| font-family: monospace; |
| font-size: smaller; |
| } |
| |
| #controls>#buttons { |
| display: flex; |
| } |
| |
| #log { |
| min-height: 100px; |
| max-height: 100%; |
| font-family: monospace; |
| overflow: auto; |
| } |
| |
| #connectionPanel, |
| #saveload { |
| display: inline-block; |
| } |
| |
| #connectionPanel, |
| #saveload, |
| #topPanel { |
| padding-bottom: 10px; |
| } |
| |
| #topPanel { |
| display: flex; |
| } |
| |
| #settings { |
| display: flex; |
| flex-direction: column; |
| font-size: small; |
| } |
| |
| .panelSection { |
| margin-right: 20px; |
| } |
| |
| .panelSection:last-child { |
| margin-right: 0px; |
| flex-grow: 1; |
| } |
| |
| #connection-status { |
| color: limegreen; |
| font-size: large; |
| padding-right: 5px; |
| } |
| |
| #connection-status.disconnected { |
| color: orange; |
| } |
| </style> |
| <link href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css" rel="stylesheet"> |
| <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script> |
| <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined"> |
| <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" /> |
| |
| <link rel='stylesheet' href='style.css'> |
| <script src='filter.js'></script> |
| <script src='filter-ui.js'></script> |
| <script src='frame.js'></script> |
| <script src='connection.js'></script> |
| <script src='thread.js'></script> |
| <script src='thread-ui.js'></script> |
| </head> |
| |
| <div id='connectionPanel'> |
| <div class='sectionTitle'> |
| <font class='disconnected' id='connection-status'>●</font>Connection |
| </div> |
| <div class='section'> |
| <div title="Expert usage only. Autoconnect should provide the dev tools websocket through /json/version discovery."> |
| <input id='url' name='url' size=60 type="text" |
| placeholder='WebSocket URL or leave empty for autoconnect...'></input> |
| </div> |
| <button class="mdc-button mdc-button--outline" id='connect'> |
| <div class="mdc-button__ripple"></div> |
| <span class="mdc-button__label">Connect</span> |
| </button> |
| <button class="mdc-button mdc-button--outline" id='disconnect' disabled=true> |
| <div class="mdc-button__ripple"></div> |
| <span class="mdc-button__label">Disconnect</span> |
| </button> |
| <input type="checkbox" id="autoconnect" name="autoconnect" checked="true"> |
| <label for="autoconnect" style="font-size:small; font:Roboto" > Autoconnect</label><br> |
| </div> |
| </div> |
| |
| <div id='saveload'> |
| <div class='sectionTitle'>Save/Load ...</div> |
| <div class='section'> |
| <button id='demo' class="mdc-button mdc-button--outline"> |
| <div class="mdc-button__ripple"></div> |
| <span class="mdc-button__label">Load demo data</span> |
| </button> |
| <button id='savedata' class="mdc-button mdc-button--outline" |
| title="Serializes current session stream to json file."> |
| <div class="mdc-button__ripple"></div> |
| <span class="mdc-button__label">Save to disk</span> |
| </button> |
| <button id='loaddata' class="mdc-button mdc-button--outline" |
| title="Deserializes json file of previous session and imports these frames into the App."> |
| <div class="mdc-button__ripple"></div> |
| <span class="mdc-button__label">Load from disk</span> |
| </button> |
| </div> |
| </div> |
| |
| <div id='logsPanel'> |
| <div class='panelSection collapsible'> |
| <div class='sectionTitle'> |
| Logs |
| [<span class="plus">+</span><span class="minus">-</span>] |
| <!--input size=40 placeholder='Comma separated filter ...'>(TODO)</input--> |
| </div> |
| <div class='section'> |
| <div id='log'></div> |
| </div> |
| </div> |
| </div> |
| |
| <div class='panelSection collapsible'> |
| <div class='sectionTitle' |
| title="Filter debug data stream. Filter operations occur in a left to right order with first match being applied."> |
| <i class="material-icons-outlined">filter_list</i> Filters |
| [<span class="plus">+</span><span class="minus">-</span>] |
| </div> |
| <div class='section'> |
| <div id='filters' class="mdc-chip-set mdc-chip-set--filter" role="grid" |
| title="Filter debug data stream. Filter operations occur in a left to right order with first match being applied."> |
| </div> |
| <button class="mdc-button mdc-button--outline" id='createfilter'> |
| <div class="mdc-button__ripple"></div> |
| <span class="mdc-button__label"><i class="material-icons-outlined">add_box</i> Add new filter</span> |
| </button> |
| </div> |
| </div> |
| |
| <div class='panelSection collapsible'> |
| <div class='sectionTitle'> |
| <i class="material-symbols-outlined"> |
| airwave |
| </i> |
| Threads |
| [<span class="plus">+</span><span class="minus">-</span>] |
| </div> |
| |
| <div id='threads' class='section'></div> |
| </div> |
| |
| <div class='panelSection collapsible'> |
| <div class='sectionTitle'> |
| Viewer Controls |
| [<span class="plus">+</span><span class="minus">-</span>] |
| </div> |
| <div class='section' id='controls'> |
| <div id='buttons'> |
| <button class="mdc-button mdc-button--outline" id='prev'> |
| <span class="mdc-button__label"><i class="material-icons-outlined">skip_previous</i> Previous frame</span> |
| </button> |
| <button class="mdc-button mdc-button--outline" id='play'> |
| <div class="mdc-button__ripple"></div> |
| <span class="mdc-button__label"><i class="material-icons-outlined">play_circle_outline</i> Play</span> |
| </button> |
| <button class="mdc-button mdc-button--outline" id='pause'> |
| <div class="mdc-button__ripple"></div> |
| <span class="mdc-button__label"><i class="material-icons-outlined">pause_circle_outline</i> Pause</span> |
| </button> |
| <button class="mdc-button mdc-button--outline" id='next'> |
| <span class="mdc-button__label">Next frame <i class="material-icons-outlined">skip_next</i></span> |
| </button> |
| <button class="mdc-button mdc-button--raised" id='live'> |
| <div class="mdc-button__ripple"></div> |
| <span class="mdc-button__label">Live <i class="material-icons-outlined">fast_forward</i></span> |
| </button> |
| |
| </div> |
| <div> |
| <div class='sectionTitle'>Frame Selection</div> |
| <input type='range' min='0' max='0' value='0' id='scrubberframe'></input> |
| </div> |
| <div> |
| <div class='sectionTitle'>Draw Selection<span id="drawRange"></span></div> |
| <div class="minMaxScrubber"> |
| <input type="range" id='minDrawScrubber' class="scrubber" |
| min="0" max="0" value="0" step="1"></input> |
| <input type="range" id='maxDrawScrubber' class="scrubber" |
| min="0" max="1" value="1" step="1"></input> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div class="panelSection collapsible"> |
| <div class='sectionTitle'> |
| Viewer |
| [<span class="plus">+</span><span class="minus">-</span>] |
| </div> |
| <div style='float: right' class='sectionTitle'> |
| Scale |
| <select id="viewerscale"> |
| <option id="100pct">100%</option> |
| <option id="50pct">50%</option> |
| <option id="200pct">200%</option> |
| <option id="freeCam">Free Camera</option> |
| </select> |
| </div> |
| |
| <div style='float: right' class='sectionTitle'> |
| Orientation |
| <select id="viewerorientation"> |
| <option id="0deg">0 deg clockwise</option> |
| <option id="90deg">90 deg clockwise</option> |
| <option id="180deg">180 deg clockwise</option> |
| <option id="270deg">270 deg clockwise</option> |
| <option id="hFlip">Horizontal Flip</option> |
| <option id="vFlip">Vertical Flip</option> |
| </select> |
| </div> |
| |
| <div class='section'> |
| <!--We need to fix viewer size to avoid scroll position change when |
| multiple displays are present. crbug.com/1358526--> |
| <div style="height:4000px; border: 1px dotted gray; background-color: #f0f0f0"> |
| <canvas id='canvas' style="top :0px"></canvas> |
| </div> |
| </div> |
| |
| <div class='modalContainer'></div> |
| </div> |
| |
| <div id='importtracing'> |
| <div class='sectionTitle'>Tracing (Prototype)...</div> |
| <div class='section'> |
| <button id='importtracebutton' class="mdc-button mdc-button--outline" |
| title="Import tracing data (json) into visual debugger app."> |
| <div class="mdc-button__ripple"></div> |
| <span class="mdc-button__label">Import Trace</span> |
| </button> |
| </div> |
| </div> |
| |
| <script> |
| function processIncomingFrame(json) { |
| if (!json) return; |
| |
| new DrawFrame(json); |
| Player.instance.onNewFrame(); |
| } |
| |
| async function testAnimate() { |
| const f = await fetch('demo.json'); |
| const text = await f.text(); |
| const json = JSON.parse(text); |
| for (const frame of json) { |
| processIncomingFrame(frame); |
| } |
| } |
| |
| async function saveDemoDataToDisk() { |
| const text = JSON.stringify(DrawFrame.frameBuffer.instances.map(d => d.toJSON())); |
| const blob = new Blob([text], { type: 'text/plain' }); |
| const link = document.createElement('a'); |
| link.download = 'cvd-stream.json'; |
| link.href = window.URL.createObjectURL(blob); |
| link.click(); |
| } |
| |
| window.onload = function () { |
| |
| Connection.initialize(); |
| |
| const addFilterButton = document.querySelector('#createfilter'); |
| addFilterButton.addEventListener('click', () => { |
| showCreateFilterPopup(addFilterButton); |
| }); |
| |
| const container = document.querySelector('.modalContainer'); |
| container.addEventListener('click', (event) => { |
| if (event.target == container) |
| hideModal(); |
| }); |
| |
| const demo = document.querySelector('#demo'); |
| demo.addEventListener('click', testAnimate); |
| |
| const savedata = document.querySelector('#savedata'); |
| savedata.addEventListener('click', saveDemoDataToDisk); |
| |
| const loaddata = document.querySelector('#loaddata'); |
| loaddata.addEventListener('click', () => { |
| const f = document.createElement('input'); |
| f.type = 'file'; |
| f.addEventListener('change', () => { |
| const file = new FileReader(f.files[0]); |
| file.addEventListener('load', () => { |
| const json = JSON.parse(file.result); |
| for (const frame of json) { |
| processIncomingFrame(frame); |
| } |
| }); |
| file.readAsText(f.files[0]); |
| }); |
| f.click(); |
| }); |
| |
| const importtracedata = document.querySelector('#importtracebutton'); |
| importtracedata.addEventListener('click', () => { |
| const f = document.createElement('input'); |
| f.type = 'file'; |
| f.addEventListener('change', () => { |
| async function handleBlob(blob) { |
| if (blob.type === 'application/x-gzip') { |
| // If the blob is gzipped, decompress it and recurse. |
| const ds = new DecompressionStream("gzip"); |
| const decompressedBlob = blob.stream().pipeThrough(ds); |
| handleBlob(await new Response(decompressedBlob).blob()); |
| } else { |
| try { |
| const json = JSON.parse(await blob.text()); |
| handleImportTraceJson(json); |
| } catch (e) { |
| if (e instanceof SyntaxError) { |
| // Not all JSON blobs have the right mimetype, so we just try |
| // and fail to check. |
| alert("Not valid JSON: " + blob.message); |
| } else { |
| throw e; |
| } |
| } |
| } |
| } |
| |
| handleBlob(f.files[0]); |
| }); |
| f.click(); |
| |
| function handleImportTraceJson(json) { |
| const traceEvents = json.traceEvents; |
| |
| curr_frame = "0"; |
| curr_draws = []; |
| sources_this_frame = [] |
| global_sources_mapping = [] |
| // Get the source index for an annotation, updating `global_sources_mapping` as needed. |
| let getSourceIndex = (anno) => { |
| let found_source_index = global_sources_mapping.indexOf(anno); |
| if (found_source_index === -1) { |
| global_sources_mapping.push(anno); |
| found_source_index = global_sources_mapping.length - 1; |
| sources_this_frame.push({ |
| "anno": anno, |
| "file": "none", |
| "func": "none", |
| "index": found_source_index, |
| "line": -1, |
| }); |
| } |
| return found_source_index; |
| }; |
| |
| threads_this_frame = new Set(); |
| global_threads = {}; |
| global_processes = {}; |
| // Return a faux thread ID that also includes the process ID. |
| // VizDebugger only tracks a thread ID, but we know the process ID as well in this case. |
| let trackThreadAndProcessId = (event) => { |
| // We're assuming the thread ID is not going to exceed u16. |
| let thread_id = (event.pid * 65536) + event.tid; |
| threads_this_frame.add({ |
| "thread_id": thread_id, |
| "thread_name": `${global_processes[event.pid]}/${global_threads[event.tid]}`, |
| }) |
| return thread_id; |
| } |
| let resolveThreadNamesAndResetThreadIdsThisFrame = () => { |
| let threads = []; |
| for (const thread of threads_this_frame) { |
| threads.push(thread); |
| } |
| threads_this_frame.clear(); |
| return threads; |
| }; |
| |
| |
| for (const event of traceEvents) { |
| if (event.name === "thread_name") { |
| global_threads[event.tid] = event.args.name; |
| } else if (event.name === "process_name") { |
| global_processes[event.pid] = event.args.name; |
| } else if (event.cat.includes("viz.visual_debugger")) { |
| if (event.name == "visual_debugger_sync") { |
| single_frame = { "drawcalls": [], "frame": "none", "logs": [], "new_sources": [], "time": "0", "version": 1, "windowx": 2400, "windowy": 1600, "threads": [{ "thread_id": "1", "thread_name": "allthreads" }] }; |
| single_frame.drawcalls = curr_draws; |
| curr_frame = event.args.last_presented_trace_id; |
| single_frame.frame = curr_frame; |
| single_frame.new_sources = sources_this_frame; |
| single_frame.windowx = parseInt(event.args.display_size.split("x")[0]); |
| single_frame.windowy = parseInt(event.args.display_size.split("x")[1]); |
| processIncomingFrame(single_frame); |
| curr_draws = []; |
| sources_this_frame = []; |
| threads_this_frame.clear(); |
| } |
| else { |
| const single_call = { "drawindex": curr_draws.length, "option": { "alpha": 5, "color": "#ff0000" }, "pos": [-1, -1], "size": [-1, -1], "source_index": -1, "thread_id": 1, "buff_id": -1 }; |
| |
| single_call.pos[0] = parseFloat(event.args.args.pos_x); |
| single_call.pos[1] = parseFloat(event.args.args.pos_y); |
| single_call.size[0] = parseFloat(event.args.args.size_x); |
| single_call.size[1] = parseFloat(event.args.args.size_y); |
| single_call.text = event.args.args.text; |
| single_call.source_index = getSourceIndex(event.name); |
| curr_draws.push(single_call); |
| } |
| } else if (event.cat.includes("viz.quads")) { |
| if (event.name === "cc::LayerTreeHostImpl") { |
| let draw_calls = []; |
| let logs = []; |
| |
| let render_pass_count = event.args.snapshot.frame.render_passes.length; |
| for (let i = 0; i < render_pass_count; i++) { |
| let render_pass = event.args.snapshot.frame.render_passes[i]; |
| |
| logs.push({ |
| "source_index": getSourceIndex("frame.render_pass.meta"), |
| "drawindex": draw_calls.length + logs.length, |
| "option": { "alpha": 0, "color": "#0000ff" }, |
| "thread_id": trackThreadAndProcessId(event), |
| "value": `Render pass id=${render_pass.id}, output_rect=${render_pass.output_rect}, damage_rect=${render_pass.damage_rect}, quad_list.size=${render_pass.quad_list.length}, copy_requests=${render_pass.copy_requests}`, |
| }); |
| |
| if (i < render_pass_count - 1) { |
| // Skip non-root render pass quads to reduce visual noise. |
| continue; |
| } |
| |
| draw_calls.push({ |
| "source_index": getSourceIndex("frame.render_pass.output_rect"), |
| "drawindex": draw_calls.length + logs.length, |
| "option": { "alpha": 5, "color": "#000000" }, |
| "pos": [render_pass.output_rect[0], render_pass.output_rect[1]], |
| "size": [render_pass.output_rect[2], render_pass.output_rect[3]], |
| "thread_id": trackThreadAndProcessId(event), |
| "buff_id": -1, |
| }); |
| |
| draw_calls.push({ |
| "source_index": getSourceIndex("frame.render_pass.damage"), |
| "drawindex": draw_calls.length + logs.length, |
| "option": { "alpha": 5, "color": "#000000" }, |
| "pos": [render_pass.damage_rect[0], render_pass.damage_rect[1]], |
| "size": [render_pass.damage_rect[2], render_pass.damage_rect[3]], |
| "thread_id": trackThreadAndProcessId(event), |
| "buff_id": -1, |
| }); |
| |
| for (let quad of render_pass.quad_list) { |
| draw_calls.push({ |
| "source_index": getSourceIndex("frame.render_pass.quad"), |
| "drawindex": draw_calls.length + logs.length, |
| "option": { "alpha": 5, "color": "#000000" }, |
| "pos": [ |
| quad.rect_as_target_space_quad[0], |
| quad.rect_as_target_space_quad[1], |
| ], |
| "size": [ |
| quad.rect_as_target_space_quad[2] - quad.rect_as_target_space_quad[0], |
| quad.rect_as_target_space_quad[5] - quad.rect_as_target_space_quad[1], |
| ], |
| "thread_id": trackThreadAndProcessId(event), |
| "buff_id": -1, |
| }); |
| |
| draw_calls.push({ |
| "source_index": getSourceIndex("frame.render_pass.material"), |
| "drawindex": draw_calls.length + logs.length, |
| "option": { "alpha": 0, "color": "#00ff00" }, |
| "pos": [ |
| quad.rect_as_target_space_quad[0], |
| quad.rect_as_target_space_quad[1], |
| ], |
| "size": [0, 0], |
| "text": `${quad.material}`, |
| "thread_id": trackThreadAndProcessId(event), |
| "buff_id": -1, |
| }); |
| } |
| } |
| |
| // This event contains a snapshot of the layer tree, so we can process a full frame immediately. |
| processIncomingFrame({ |
| "drawcalls": draw_calls, |
| "frame": event.tts, |
| "logs": logs, |
| "new_sources": sources_this_frame, |
| "time": event.ts, |
| "version": 1, |
| "windowx": event.args.snapshot.device_viewport_size.width, |
| "windowy": event.args.snapshot.device_viewport_size.height, |
| "threads": resolveThreadNamesAndResetThreadIdsThisFrame(), |
| }); |
| sources_this_frame = []; |
| } |
| } |
| } |
| } |
| }); |
| |
| document.querySelectorAll('.panelSection.collapsible').forEach( |
| (section) => { |
| let title = section.querySelector('.sectionTitle'); |
| title.addEventListener('click', () => { |
| section.classList.toggle('collapsed'); |
| }); |
| }); |
| |
| setUpPlayer(); |
| restoreFilters(); |
| } |
| |
| /** |
| * Gets value from localStorage if it exists, and sets the <option> with that |
| * id to be selected in options_el. |
| * @param {string} key: The localStorage key to get if present. |
| * @param {Element} options_el: The <select> Element to be updated. |
| * @returns {boolean} Whether a value from localStorage was used to change the |
| * the selected option. |
| */ |
| function getStoredOptionsValue(key, options_el) { |
| let storedId = localStorage.getItem(key); |
| if (storedId != null) { |
| let option = options_el.namedItem(storedId); |
| if (option == null) { |
| console.error(`No option with id ${storedId} found on ${options_el.id}`); |
| localStorage.removeItem(key); |
| } else { |
| options_el.selectedIndex = option.index; |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| function updateDrawScrubberSizes(minIndex, maxIndex, nDraws) { |
| const scrubberMin = document.querySelector('#minDrawScrubber'); |
| const scrubberMax = document.querySelector('#maxDrawScrubber'); |
| const drawRange = document.querySelector('#drawRange'); |
| |
| // The point where the sliders meet will be halway between the two values. |
| // This means the slider you select when clicking will always be the one |
| // closest to your mouse. |
| let mid = Math.floor((minIndex + maxIndex) / 2); |
| scrubberMin.max = mid; |
| scrubberMax.min = mid; |
| // Update the size of each slider to its fraction of the total range. |
| scrubberMin.style = `flex-grow: ${mid}`; |
| scrubberMax.style = `flex-grow: ${nDraws - mid}`; |
| } |
| |
| function setDrawScrubbers(minIndex, maxIndex, nDraws) { |
| const scrubberMin = document.querySelector('#minDrawScrubber'); |
| const scrubberMax = document.querySelector('#maxDrawScrubber'); |
| const drawRange = document.querySelector('#drawRange'); |
| |
| scrubberMax.max = nDraws; |
| scrubberMin.value = minIndex; |
| scrubberMax.value = maxIndex; |
| drawRange.textContent = ` [${minIndex}, ${maxIndex})`; |
| |
| updateDrawScrubberSizes(minIndex, maxIndex, nDraws); |
| } |
| |
| function updateFrameScrubber(oldest, newest, current) { |
| const scrubberFrame = document.querySelector('#scrubberframe'); |
| scrubberFrame.min = oldest; |
| scrubberFrame.max = newest; |
| scrubberFrame.value = current; |
| } |
| |
| function setUpPlayer() { |
| // First, set up the viewer. |
| const canvas = document.querySelector('#canvas'); |
| const logContainer = document.querySelector('#log'); |
| const viewer = new Viewer(canvas, logContainer); |
| // Now create the player for the viewer. |
| const player = new Player(viewer, (frame) => { |
| updateFrameScrubber( |
| DrawFrame.frameBuffer.oldestIndex(), |
| DrawFrame.frameBuffer.newestIndex(), |
| player.currentFrameIndex); |
| setDrawScrubbers( |
| frame.minIndex(), frame.maxIndex(), frame.submissionCount()); |
| }); |
| |
| document.querySelector('#pause').addEventListener('click', () => { |
| player.pause(); |
| pause.setAttribute('style', 'background:#000000;color:white'); |
| }); |
| |
| document.querySelector('#play').addEventListener('click', () => { |
| player.play(); |
| pause.removeAttribute('style'); |
| }); |
| |
| document.querySelector('#prev').addEventListener('click', () => { |
| player.rewind(); |
| }); |
| |
| document.querySelector('#next').addEventListener('click', () => { |
| player.forward(); |
| }); |
| |
| document.querySelector('#live').addEventListener('click', () => { |
| player.live(); |
| has_disconnected = false; |
| pause.removeAttribute('style'); |
| }); |
| |
| const scrubberFrame = document.querySelector('#scrubberframe'); |
| scrubberFrame.addEventListener('input', () => { |
| player.freezeFrame(parseInt(scrubberFrame.value)); |
| pause.setAttribute('style', 'background:#000000;color:white'); |
| }); |
| |
| const scrubberDrawMin = document.querySelector('#minDrawScrubber'); |
| const scrubberDrawMax = document.querySelector('#maxDrawScrubber'); |
| function drawScrubberChanged() { |
| let min = parseInt(scrubberDrawMin.value); |
| let max = parseInt(scrubberDrawMax.value); |
| player.freezeFrame(parseInt(scrubberFrame.value), min, max); |
| updateDrawScrubberSizes(min, max, parseInt(scrubberDrawMax.max)); |
| } |
| |
| scrubberDrawMin.addEventListener('input', () => { |
| drawScrubberChanged(); |
| }); |
| scrubberDrawMax.addEventListener('input', () => { |
| drawScrubberChanged() |
| }); |
| |
| let currentMouseX = 0; |
| let currentMouseY = 0; |
| let zoom = 100; |
| const viewerScale = document.querySelector("#viewerscale"); |
| |
| function scaleChanged() { |
| const selected = viewerScale.selectedOptions[0]; |
| if (selected.id != "freeCam") { |
| viewer.resetTranslation(); |
| player.setViewerScale(viewerScale.value); |
| zoom = parseInt(viewerScale.value); |
| } |
| else { |
| player.setViewerScale(zoom); |
| canvas.addEventListener('mouseenter', () => { |
| canvas.addEventListener('mousemove', function (event) { |
| currentMouseX = Math.round(event.offsetX); |
| currentMouseY = Math.round(event.offsetY); |
| }); |
| }); |
| canvas.addEventListener('wheel', function(e) { |
| e.preventDefault(); |
| var delta = e.deltaY; |
| const selected = viewerScale.selectedOptions[0]; |
| |
| if (selected.id == "freeCam") { |
| viewer.zoomToMouse(currentMouseX, currentMouseY, delta); |
| } |
| |
| }, {passive:false}); |
| } |
| } |
| const scaleKey = "scale"; |
| if (getStoredOptionsValue(scaleKey, viewerScale)) { |
| scaleChanged(); |
| } |
| viewerScale.addEventListener('input', () => { |
| scaleChanged(); |
| localStorage.setItem(scaleKey, viewerScale.selectedOptions[0].id); |
| }); |
| |
| const viewerOrientation = document.querySelector("#viewerorientation"); |
| |
| function orientationChanged() { |
| let selected = viewerOrientation.selectedOptions[0]; |
| // Change dropdown style when setting non-zero orientation. |
| if (selected.id != '0deg') { |
| viewerOrientation.classList.add("nonzero-orientation"); |
| } else { |
| viewerOrientation.classList.remove("nonzero-orientation"); |
| } |
| player.setViewerOrientation(viewerOrientation.value); |
| } |
| const orientationKey = 'orientation'; |
| if (getStoredOptionsValue(orientationKey, viewerOrientation)) { |
| orientationChanged(); |
| } |
| viewerOrientation.addEventListener('change', () => { |
| orientationChanged(); |
| localStorage.setItem(orientationKey, viewerOrientation.selectedOptions[0].id); |
| }); |
| |
| } |
| |
| |
| function showModal(element, focusSelector) { |
| const container = document.querySelector('.modalContainer'); |
| container.appendChild(element); |
| container.style.display = 'block'; |
| element.querySelector(focusSelector).focus(); |
| } |
| |
| function hideModal() { |
| const container = document.querySelector('.modalContainer'); |
| container.style.display = 'none'; |
| container.textContent = ''; |
| } |
| |
| </script> |
| |
| </html> |