| <!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> |