| <!-- |
| Copyright (C) 2012 Google Inc. All rights reserved. |
| |
| Redistribution and use in source and binary forms, with or without |
| modification, are permitted provided that the following conditions |
| are met: |
| |
| 1. Redistributions of source code must retain the above copyright |
| notice, this list of conditions and the following disclaimer. |
| 2. Redistributions in binary form must reproduce the above copyright |
| notice, this list of conditions and the following disclaimer in the |
| documentation and/or other materials provided with the distribution. |
| 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| its contributors may be used to endorse or promote products derived |
| from this software without specific prior written permission. |
| |
| THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| --> |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <style> |
| body { |
| margin: 0; |
| padding: 0; |
| font-size: 13px; |
| color: #222; |
| --arrow-width: 15px; |
| --arrow-height: 8px; |
| --shadow-up: 5px; |
| --shadow-down: -5px; |
| --shadow-direction: var(--shadow-up); |
| --arrow-up: polygon(0 0, 100% 0, 50% 100%); |
| --arrow-down: polygon(50% 0, 0 100%, 100% 100%); |
| --arrow: var(--arrow-up); |
| } |
| |
| body.platform-linux { |
| font-family: Roboto, Ubuntu, Arial, sans-serif; |
| } |
| |
| body.platform-mac { |
| color: rgb(48, 57, 66); |
| font-family: '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', sans-serif; |
| } |
| |
| body.platform-windows { |
| font-family: 'Segoe UI', Tahoma, sans-serif; |
| } |
| |
| .fill { |
| position: absolute; |
| top: 0; |
| right: 0; |
| bottom: 0; |
| left: 0; |
| } |
| |
| .dimmed { |
| background-color: rgba(0, 0, 0, 0.31); |
| } |
| |
| #canvas { |
| pointer-events: none; |
| } |
| |
| .controls-line { |
| display: flex; |
| justify-content: center; |
| margin: 10px 0; |
| } |
| |
| .message-box { |
| padding: 2px 4px; |
| display: flex; |
| align-items: center; |
| cursor: default; |
| overflow: hidden; |
| } |
| |
| #paused-in-debugger { |
| white-space: nowrap; |
| text-overflow: ellipsis; |
| overflow: hidden; |
| } |
| |
| .controls-line > * { |
| background-color: rgb(255, 255, 194); |
| border: 1px solid rgb(202, 202, 202); |
| height: 22px; |
| box-sizing: border-box; |
| } |
| |
| .controls-line .button { |
| width: 26px; |
| margin-left: -1px; |
| margin-right: 0; |
| padding: 0; |
| flex-shrink: 0; |
| flex-grow: 0; |
| cursor: pointer; |
| } |
| |
| .controls-line .button .glyph { |
| width: 100%; |
| height: 100%; |
| background-color: rgba(0, 0, 0, 0.75); |
| opacity: 0.8; |
| -webkit-mask-repeat: no-repeat; |
| -webkit-mask-position: center; |
| position: relative; |
| } |
| |
| .controls-line .button:active .glyph { |
| top: 1px; |
| left: 1px; |
| } |
| |
| #resume-button .glyph { |
| -webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAKCAYAAABv7tTEAAAAAXNSR0IArs4c6QAAAFJJREFUKM+10bEJgGAMBeEPbR3BLRzEVdzEVRzELRzBVohVwEJ+iODBlQfhBeJhsmHU4C0KnFjQV6J0x1SNAhdWDJUoPTB3PvLLeaUhypM3n3sD/qc7lDrdpIEAAAAASUVORK5CYII=); |
| -webkit-mask-size: 13px 10px; |
| background-color: rgb(66, 129, 235); |
| } |
| |
| #step-over-button .glyph { |
| -webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAKCAYAAAC5Sw6hAAAAAXNSR0IArs4c6QAAAOFJREFUKM+N0j8rhXEUB/DPcxW35CqhvIBrtqibkklhV8qkTHe4ZbdblcXgPVhuMdqUTUl5A2KRRCF5LGc4PT1P7qnfcr5/zu/8KdTHLFaxjHnc4RZXKI0QYxjgLQTVd42l/0wmg5iFX3iq5H6w22RS4DyRH7CB8cAXcBTGJT6xUmd0mEwuMdFQcA3fwXvGTAan8BrgPabTL9fRRyfx91PRMwyjGwcJ2EyCfsrfpPw2Pipz24NT/MZciiQYVshzOKnZ5Hturxt3k2MnCpS4SPkeHpPR8Sh3tYgttBoW9II2/AHiaEqvD2Fc0wAAAABJRU5ErkJggg==); |
| -webkit-mask-size: 18px 10px; |
| } |
| |
| .px { |
| color: rgb(128, 128, 128); |
| } |
| |
| #element-title { |
| position: absolute; |
| z-index: 10; |
| } |
| |
| /* Material */ |
| .hidden { |
| display: none !important; |
| } |
| |
| .tooltip-content { |
| position: absolute; |
| z-index: 10; |
| -webkit-user-select: none; |
| } |
| |
| .tooltip-content { |
| background-color: white; |
| padding: 5px 8px; |
| border-radius: 3px; |
| box-sizing: border-box; |
| max-width: calc(100% - 4px); |
| z-index: 1; |
| background-clip: padding-box; |
| will-change: transform; |
| text-rendering: optimizeLegibility; |
| pointer-events: none; |
| filter: drop-shadow(0 2px 4px rgba(0,0,0,0.35)); |
| } |
| |
| .tooltip-content::after { |
| content: ""; |
| background: white; |
| width: var(--arrow-width); |
| height: var(--arrow-height); |
| clip-path: var(--arrow); |
| position: absolute; |
| top: var(--arrow-top); |
| left: var(--arrow-left); |
| visibility: var(--arrow-visibility); |
| } |
| |
| .element-info { |
| display: flex; |
| flex-direction: column; |
| line-height: 16px; |
| } |
| |
| .element-info-header { |
| display: flex; |
| align-content: stretch; |
| line-height: 16px; |
| } |
| |
| .element-info-body { |
| display: flex; |
| flex-direction: column; |
| border-top: 1px solid #999; |
| padding-top: 2px; |
| margin-top: 2px; |
| } |
| |
| .element-info-row { |
| display: flex; |
| line-height: 19px; |
| } |
| |
| .element-info-row > * { |
| flex: none; |
| } |
| |
| .element-info-name { |
| color: #666; |
| } |
| |
| .element-info-gap { |
| flex: auto; |
| } |
| |
| .element-info-value { |
| display: flex; |
| text-align: right; |
| color: rgb(48, 57, 66); |
| margin-left: 10px; |
| align-items: baseline; |
| } |
| |
| .color-swatch { |
| display: flex; |
| margin-right: 2px; |
| width: 10px; |
| height: 10px; |
| background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); |
| line-height: 10px; |
| } |
| |
| .color-swatch-inner { |
| flex: auto; |
| border: 1px solid rgba(128, 128, 128, 0.6); |
| } |
| |
| .element-description { |
| flex: 1 1; |
| font-weight: bold; |
| word-wrap: break-word; |
| word-break: break-all; |
| } |
| |
| .dimensions { |
| color: hsl(0, 0%, 45%); |
| text-align: right; |
| margin-left: 10px; |
| } |
| |
| .material-node-width { |
| margin-right: 2px; |
| } |
| |
| .material-node-height { |
| margin-left: 2px; |
| } |
| |
| .material-tag-name { |
| /* Keep this in sync with inspectorSyntaxHighlight.css (--dom-tag-name-color) */ |
| color: rgb(136, 18, 128); |
| } |
| |
| .material-class-name, .material-node-id { |
| /* Keep this in sync with inspectorSyntaxHighlight.css (.webkit-html-attribute-value) */ |
| color: rgb(26, 26, 166); |
| } |
| |
| .contrast-text { |
| border-radius: 3px; |
| width: 20px; |
| height: 16px; |
| text-align: center; |
| line-height: 16px; |
| margin-right: 9px; |
| border: 1px solid #ccc; |
| } |
| |
| </style> |
| <script> |
| const lightGridColor = "rgba(0,0,0,0.2)"; |
| const darkGridColor = "rgba(0,0,0,0.7)"; |
| const transparentColor = "rgba(0, 0, 0, 0)"; |
| const gridBackgroundColor = "rgba(255, 255, 255, 0.8)"; |
| |
| function drawPausedInDebuggerMessage(message) |
| { |
| window._controlsVisible = true; |
| document.querySelector(".controls-line").style.visibility = "visible"; |
| document.getElementById("paused-in-debugger").textContent = message; |
| document.body.classList.add("dimmed"); |
| } |
| |
| function _drawGrid(context, rulerAtRight, rulerAtBottom) |
| { |
| if (window._gridPainted) |
| return; |
| window._gridPainted = true; |
| |
| context.save(); |
| |
| var pageFactor = pageZoomFactor * pageScaleFactor; |
| var scrollX = window.scrollX * pageScaleFactor; |
| var scrollY = window.scrollY * pageScaleFactor; |
| function zoom(x) |
| { |
| return Math.round(x * pageFactor); |
| } |
| function unzoom(x) |
| { |
| return Math.round(x / pageFactor); |
| } |
| |
| var width = canvasWidth / pageFactor; |
| var height = canvasHeight / pageFactor; |
| |
| const gridSubStep = 5; |
| const gridStep = 50; |
| |
| { |
| // Draw X grid background |
| context.save(); |
| context.fillStyle = gridBackgroundColor; |
| if (rulerAtBottom) |
| context.fillRect(0, zoom(height) - 15, zoom(width), zoom(height)); |
| else |
| context.fillRect(0, 0, zoom(width), 15); |
| |
| // Clip out backgrounds intersection |
| context.globalCompositeOperation = "destination-out"; |
| context.fillStyle = "red"; |
| if (rulerAtRight) |
| context.fillRect(zoom(width) - 15, 0, zoom(width), zoom(height)); |
| else |
| context.fillRect(0, 0, 15, zoom(height)); |
| context.restore(); |
| |
| // Draw Y grid background |
| context.fillStyle = gridBackgroundColor; |
| if (rulerAtRight) |
| context.fillRect(zoom(width) - 15, 0, zoom(width), zoom(height)); |
| else |
| context.fillRect(0, 0, 15, zoom(height)); |
| } |
| |
| context.lineWidth = 1; |
| context.strokeStyle = darkGridColor; |
| context.fillStyle = darkGridColor; |
| { |
| // Draw labels. |
| context.save(); |
| context.translate(-scrollX, 0.5 - scrollY); |
| var maxY = height + unzoom(scrollY); |
| for (var y = 2 * gridStep; y < maxY; y += 2 * gridStep) { |
| context.save(); |
| context.translate(scrollX, zoom(y)); |
| context.rotate(-Math.PI / 2); |
| context.fillText(y, 2, rulerAtRight ? zoom(width) - 7 : 13); |
| context.restore(); |
| } |
| context.translate(0.5, -0.5); |
| var maxX = width + unzoom(scrollX); |
| for (var x = 2 * gridStep; x < maxX; x += 2 * gridStep) { |
| context.save(); |
| context.fillText(x, zoom(x) + 2, rulerAtBottom ? scrollY + zoom(height) - 7 : scrollY + 13); |
| context.restore(); |
| } |
| context.restore(); |
| } |
| |
| { |
| // Draw vertical grid |
| context.save(); |
| if (rulerAtRight) { |
| context.translate(zoom(width), 0); |
| context.scale(-1, 1); |
| } |
| context.translate(-scrollX, 0.5 - scrollY); |
| var maxY = height + unzoom(scrollY); |
| for (var y = gridStep; y < maxY; y += gridStep) { |
| context.beginPath(); |
| context.moveTo(scrollX, zoom(y)); |
| var markLength = (y % (gridStep * 2)) ? 5 : 8; |
| context.lineTo(scrollX + markLength, zoom(y)); |
| context.stroke(); |
| } |
| context.strokeStyle = lightGridColor; |
| for (var y = gridSubStep; y < maxY; y += gridSubStep) { |
| if (!(y % gridStep)) |
| continue; |
| context.beginPath(); |
| context.moveTo(scrollX, zoom(y)); |
| context.lineTo(scrollX + gridSubStep, zoom(y)); |
| context.stroke(); |
| } |
| context.restore(); |
| } |
| |
| { |
| // Draw horizontal grid |
| context.save(); |
| if (rulerAtBottom) { |
| context.translate(0, zoom(height)); |
| context.scale(1, -1); |
| } |
| context.translate(0.5 - scrollX, -scrollY); |
| var maxX = width + unzoom(scrollX); |
| for (var x = gridStep; x < maxX; x += gridStep) { |
| context.beginPath(); |
| context.moveTo(zoom(x), scrollY); |
| var markLength = (x % (gridStep * 2)) ? 5 : 8; |
| context.lineTo(zoom(x), scrollY + markLength); |
| context.stroke(); |
| } |
| context.strokeStyle = lightGridColor; |
| for (var x = gridSubStep; x < maxX; x += gridSubStep) { |
| if (!(x % gridStep)) |
| continue; |
| context.beginPath(); |
| context.moveTo(zoom(x), scrollY); |
| context.lineTo(zoom(x), scrollY + gridSubStep); |
| context.stroke(); |
| } |
| context.restore(); |
| } |
| |
| context.restore(); |
| } |
| |
| function drawViewSize() |
| { |
| var text = viewportSize.width + "px \u00D7 " + viewportSize.height + "px"; |
| context.save(); |
| context.font = "20px "; |
| switch (platform) { |
| case "windows": context.font = "14px Consolas, Lucida Console"; break; |
| case "mac": context.font = "14px Menlo, Monaco"; break; |
| case "linux": context.font = "14px dejavu sans mono"; break; |
| } |
| |
| var frameWidth = canvasWidth; |
| var textWidth = context.measureText(text).width; |
| context.fillStyle = gridBackgroundColor; |
| context.fillRect(frameWidth - textWidth - 12, 0, frameWidth, 25); |
| context.fillStyle = darkGridColor; |
| context.fillText(text, frameWidth - textWidth - 6, 18); |
| context.restore(); |
| } |
| |
| function resetCanvas(canvasElement) |
| { |
| canvasElement.width = deviceScaleFactor * viewportSize.width; |
| canvasElement.height = deviceScaleFactor * viewportSize.height; |
| canvasElement.style.width = viewportSize.width + "px"; |
| canvasElement.style.height = viewportSize.height + "px"; |
| var context = canvasElement.getContext("2d"); |
| context.scale(deviceScaleFactor, deviceScaleFactor); |
| } |
| |
| function reset(resetData) |
| { |
| window.viewportSize = resetData.viewportSize; |
| window.deviceScaleFactor = resetData.deviceScaleFactor; |
| window.pageScaleFactor = resetData.pageScaleFactor; |
| window.pageZoomFactor = resetData.pageZoomFactor; |
| window.scrollX = Math.round(resetData.scrollX); |
| window.scrollY = Math.round(resetData.scrollY); |
| |
| window.canvas = document.getElementById("canvas"); |
| window.context = canvas.getContext("2d"); |
| resetCanvas(canvas); |
| |
| window.canvasWidth = viewportSize.width; |
| window.canvasHeight = viewportSize.height; |
| |
| window._controlsVisible = false; |
| document.querySelector(".controls-line").style.visibility = "hidden"; |
| document.getElementById("tooltip-container").removeChildren(); |
| |
| document.body.classList.remove("dimmed"); |
| |
| window._gridPainted = false; |
| } |
| |
| /** |
| * @param {!String} hexa |
| * @return {!Array<number>} |
| */ |
| function parseHexa(hexa) { |
| return hexa.match(/#(\w\w)(\w\w)(\w\w)(\w\w)/).slice(1).map(c => parseInt(c, 16) / 255); |
| } |
| |
| /** |
| * @param {!String} hexa |
| * @return {!Array<number>} |
| */ |
| function normalizeColorString(hexa) { |
| if (hexa.endsWith("FF")) |
| return hexa.substr(0, 7); |
| const [r, g, b, a] = parseHexa(hexa); |
| return `rgba(${(r * 255).toFixed()}, ${(g * 255).toFixed()}, ${(b * 255).toFixed()}, ${Math.round(a * 100) / 100})`; |
| } |
| |
| /** |
| * Calculate the contrast ratio between a foreground and a background color. |
| * Returns the ratio to 1, for example for two colors with a contrast ratio of 21:1, |
| * this function will return 21. |
| * See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef |
| * @param {!Array<number>} fgRGBA |
| * @param {!Array<number>} bgRGBA |
| * @return {number} |
| */ |
| function contrastRatio(fgRGBA, bgRGBA) { |
| // If we have a semi-transparent background color over an unknown |
| // background, draw the line for the "worst case" scenario: where |
| // the unknown background is the same color as the text. |
| bgRGBA = blendColors(bgRGBA, fgRGBA); |
| const fgLuminance = luminance(blendColors(fgRGBA, bgRGBA)); |
| const bgLuminance = luminance(bgRGBA); |
| const result = (Math.max(fgLuminance, bgLuminance) + 0.05) / (Math.min(fgLuminance, bgLuminance) + 0.05); |
| return result.toFixed(2); |
| |
| /** |
| * Calculate the luminance of this color using the WCAG algorithm. |
| * See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef |
| * @param {!Array<number>} rgba |
| * @return {number} |
| */ |
| function luminance(rgba) { |
| const rSRGB = rgba[0]; |
| const gSRGB = rgba[1]; |
| const bSRGB = rgba[2]; |
| |
| const r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055) / 1.055), 2.4); |
| const g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055) / 1.055), 2.4); |
| const b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055) / 1.055), 2.4); |
| |
| return 0.2126 * r + 0.7152 * g + 0.0722 * b; |
| } |
| |
| /** |
| * Combine the two given color according to alpha blending. |
| * @param {!Array<number>} fgRGBA |
| * @param {!Array<number>} bgRGBA |
| * @return {!Array<number>} |
| */ |
| function blendColors(fgRGBA, bgRGBA) { |
| const alpha = fgRGBA[3]; |
| return [ |
| ((1 - alpha) * bgRGBA[0]) + (alpha * fgRGBA[0]), |
| ((1 - alpha) * bgRGBA[1]) + (alpha * fgRGBA[1]), |
| ((1 - alpha) * bgRGBA[2]) + (alpha * fgRGBA[2]), |
| alpha + (bgRGBA[3] * (1 - alpha)) |
| ]; |
| } |
| } |
| |
| function computeIsLargeFont(contrast) { |
| const boldWeights = new Set(['bold', 'bolder', '600', '700', '800', '900']); |
| |
| const fontSizePx = parseFloat(contrast.fontSize.replace('px', '')); |
| const isBold = boldWeights.has(contrast.fontWeight); |
| |
| const fontSizePt = fontSizePx * 72 / 96; |
| if (isBold) |
| return fontSizePt >= 14; |
| else |
| return fontSizePt >= 18; |
| } |
| |
| function _createElementDescription(elementInfo) |
| { |
| const elementInfoElement = createElement("div", "element-info"); |
| const elementInfoHeaderElement = elementInfoElement.createChild("div", "element-info-header"); |
| const descriptionElement = elementInfoHeaderElement.createChild("div", "element-description monospace"); |
| const tagNameElement = descriptionElement.createChild("span", "material-tag-name"); |
| tagNameElement.textContent = elementInfo.tagName; |
| const nodeIdElement = descriptionElement.createChild("span", "material-node-id"); |
| nodeIdElement.textContent = elementInfo.idValue ? "#" + elementInfo.idValue : ""; |
| nodeIdElement.classList.toggle("hidden", !elementInfo.idValue); |
| |
| const classNameElement = descriptionElement.createChild("span", "material-class-name"); |
| classNameElement.textContent = (elementInfo.className || "").trimEnd(50); |
| classNameElement.classList.toggle("hidden", !elementInfo.className); |
| const dimensionsElement = elementInfoHeaderElement.createChild("div", "dimensions"); |
| dimensionsElement.createChild("span", "material-node-width").textContent = Math.round(elementInfo.nodeWidth * 100) / 100; |
| dimensionsElement.createTextChild("\u00d7"); |
| dimensionsElement.createChild("span", "material-node-height").textContent = Math.round(elementInfo.nodeHeight * 100) / 100; |
| |
| const style = elementInfo.style || {}; |
| let elementInfoBodyElement; |
| |
| const color = style["color"]; |
| if (color && color !== "#00000000") |
| addColorRow("Color", color); |
| |
| const fontFamily = style["font-family"]; |
| const fontSize = style["font-size"]; |
| if (fontFamily && fontSize !== "0px") |
| addTextRow("Font", `${fontSize} ${fontFamily}`.trimEnd(20)); |
| |
| const bgcolor = style["background-color"]; |
| if (bgcolor && bgcolor !== "#00000000") |
| addColorRow("Background", bgcolor); |
| |
| const margin = style["margin"]; |
| if (margin && margin !== "0px") |
| addTextRow("Margin", margin); |
| |
| const padding = style["padding"]; |
| if (padding && padding !== "0px") |
| addTextRow("Padding", padding); |
| |
| const cbgColor = elementInfo.contrast ? elementInfo.contrast.backgroundColor : null; |
| if (color && color !== "#00000000" && cbgColor && cbgColor !== "#00000000") |
| addContrastRow(style["color"], elementInfo.contrast); |
| |
| function addRow(name) { |
| if (!elementInfoBodyElement) |
| elementInfoBodyElement = elementInfoElement.createChild("div", "element-info-body") |
| const rowElement = elementInfoBodyElement.createChild("div", "element-info-row"); |
| const nameElement = rowElement.createChild("div", "element-info-name"); |
| nameElement.textContent = name; |
| rowElement.createChild("div", "element-info-gap"); |
| return rowElement.createChild("div", "element-info-value"); |
| } |
| |
| function addTextRow(name, value) { |
| addRow(name).createTextChild(value); |
| } |
| |
| function addColorRow(name, color) { |
| const valueElement = addRow(name); |
| const swatch = valueElement.createChild("div", "color-swatch"); |
| const inner = swatch.createChild("div", "color-swatch-inner"); |
| inner.style.backgroundColor = color; |
| valueElement.createTextChild(normalizeColorString(color)); |
| } |
| |
| function addContrastRow(fgColor, contrast) { |
| const ratio = contrastRatio(parseHexa(fgColor), parseHexa(contrast.backgroundColor)); |
| const threshold = computeIsLargeFont(contrast) ? 3.0 : 4.5; |
| const valueElement = addRow("Contrast"); |
| const sampleText = valueElement.createChild("div", "contrast-text"); |
| sampleText.style.color = fgColor; |
| sampleText.style.backgroundColor = contrast.backgroundColor; |
| sampleText.textContent = "Aa"; |
| const valueSpan = valueElement.createChild("span"); |
| valueSpan.textContent = Math.round(ratio * 100) / 100; |
| if (ratio < threshold) { |
| valueSpan.style.color = "red"; |
| valueSpan.style.paddingRight = "5px"; |
| valueElement.createTextChild(` < ${threshold.toFixed(1)}`); |
| } |
| } |
| |
| return elementInfoElement; |
| } |
| |
| function _drawElementTitle(elementInfo, bounds) |
| { |
| const tooltipContainer = document.getElementById("tooltip-container"); |
| tooltipContainer.removeChildren(); |
| _createMaterialTooltip(tooltipContainer, bounds, _createElementDescription(elementInfo), true); |
| } |
| |
| function _createMaterialTooltip(parentElement, bounds, contentElement, withArrow) |
| { |
| const tooltipContainer = parentElement.createChild("div"); |
| const tooltipContent = tooltipContainer.createChild("div", "tooltip-content"); |
| tooltipContent.appendChild(contentElement); |
| |
| const titleWidth = tooltipContent.offsetWidth; |
| const titleHeight = tooltipContent.offsetHeight; |
| const arrowHalfWidth = 8; |
| const pageMargin = 2; |
| const arrowWidth = arrowHalfWidth * 2; |
| const arrowInset = arrowHalfWidth + 2; |
| |
| const containerMinX = pageMargin + arrowInset; |
| const containerMaxX = canvasWidth - pageMargin - arrowInset - arrowWidth; |
| |
| // Left align arrow to the tooltip but ensure it is pointing to the element. |
| // Center align arrow if the inspected element bounds are too narrow. |
| const boundsAreTooNarrow = bounds.maxX - bounds.minX < arrowWidth + 2 * arrowInset; |
| let arrowX; |
| if (boundsAreTooNarrow) { |
| arrowX = (bounds.minX + bounds.maxX) * 0.5 - arrowHalfWidth; |
| } else { |
| const xFromLeftBound = bounds.minX + arrowInset; |
| const xFromRightBound = bounds.maxX - arrowInset - arrowWidth; |
| if (xFromLeftBound > containerMinX && xFromLeftBound < containerMaxX) |
| arrowX = xFromLeftBound; |
| else |
| arrowX = Number.constrain(containerMinX, xFromLeftBound, xFromRightBound); |
| } |
| // Hide arrow if element is completely off the sides of the page. |
| const arrowHidden = !withArrow || arrowX < containerMinX || arrowX > containerMaxX; |
| |
| let boxX = arrowX - arrowInset; |
| boxX = Number.constrain(boxX, pageMargin, canvasWidth - titleWidth - pageMargin); |
| |
| let boxY = bounds.minY - arrowHalfWidth - titleHeight; |
| let onTop = true; |
| if (boxY < 0) { |
| boxY = Math.min(canvasHeight - titleHeight, bounds.maxY + arrowHalfWidth); |
| onTop = false; |
| } else if (bounds.minY > canvasHeight) { |
| boxY = canvasHeight - arrowHalfWidth - titleHeight; |
| } |
| |
| tooltipContent.style.top = boxY + "px"; |
| tooltipContent.style.left = boxX + "px"; |
| tooltipContent.style.setProperty('--arrow-visibility', arrowHidden ? 'hidden' : 'visible'); |
| if (arrowHidden) |
| return; |
| |
| tooltipContent.style.setProperty('--arrow', onTop ? 'var(--arrow-up)' : 'var(--arrow-down)'); |
| tooltipContent.style.setProperty('--shadow-direction', onTop ? 'var(--shadow-up)' : 'var(--shadow-down)'); |
| tooltipContent.style.setProperty('--arrow-top', (onTop ? titleHeight : -arrowHalfWidth) + 'px'); |
| tooltipContent.style.setProperty('--arrow-left', (arrowX - boxX) + 'px'); |
| } |
| |
| function _drawRulers(context, bounds, rulerAtRight, rulerAtBottom) |
| { |
| context.save(); |
| var width = canvasWidth; |
| var height = canvasHeight; |
| context.strokeStyle = "rgba(128, 128, 128, 0.3)"; |
| context.lineWidth = 1; |
| context.translate(0.5, 0.5); |
| |
| if (rulerAtRight) { |
| for (var y in bounds.rightmostXForY) { |
| context.beginPath(); |
| context.moveTo(width, y); |
| context.lineTo(bounds.rightmostXForY[y], y); |
| context.stroke(); |
| } |
| } else { |
| for (var y in bounds.leftmostXForY) { |
| context.beginPath(); |
| context.moveTo(0, y); |
| context.lineTo(bounds.leftmostXForY[y], y); |
| context.stroke(); |
| } |
| } |
| |
| if (rulerAtBottom) { |
| for (var x in bounds.bottommostYForX) { |
| context.beginPath(); |
| context.moveTo(x, height); |
| context.lineTo(x, bounds.topmostYForX[x]); |
| context.stroke(); |
| } |
| } else { |
| for (var x in bounds.topmostYForX) { |
| context.beginPath(); |
| context.moveTo(x, 0); |
| context.lineTo(x, bounds.topmostYForX[x]); |
| context.stroke(); |
| } |
| } |
| |
| context.restore(); |
| } |
| |
| function drawPath(context, commands, fillColor, outlineColor, bounds) |
| { |
| var commandsIndex = 0; |
| |
| function extractPoints(count) |
| { |
| var points = []; |
| |
| for (var i = 0; i < count; ++i) { |
| var x = Math.round(commands[commandsIndex++]); |
| bounds.maxX = Math.max(bounds.maxX, x); |
| bounds.minX = Math.min(bounds.minX, x); |
| |
| var y = Math.round(commands[commandsIndex++]); |
| bounds.maxY = Math.max(bounds.maxY, y); |
| bounds.minY = Math.min(bounds.minY, y); |
| |
| bounds.leftmostXForY[y] = Math.min(bounds.leftmostXForY[y] || Number.MAX_VALUE, x); |
| bounds.rightmostXForY[y] = Math.max(bounds.rightmostXForY[y] || Number.MIN_VALUE, x); |
| bounds.topmostYForX[x] = Math.min(bounds.topmostYForX[x] || Number.MAX_VALUE, y); |
| bounds.bottommostYForX[x] = Math.max(bounds.bottommostYForX[x] || Number.MIN_VALUE, y); |
| points.push(x, y); |
| } |
| return points; |
| } |
| |
| context.save(); |
| var commandsLength = commands.length; |
| var path = new Path2D(); |
| while (commandsIndex < commandsLength) { |
| switch (commands[commandsIndex++]) { |
| case "M": |
| path.moveTo.apply(path, extractPoints(1)); |
| break; |
| case "L": |
| path.lineTo.apply(path, extractPoints(1)); |
| break; |
| case "C": |
| path.bezierCurveTo.apply(path, extractPoints(3)); |
| break; |
| case "Q": |
| path.quadraticCurveTo.apply(path, extractPoints(2)); |
| break; |
| case "Z": |
| path.closePath(); |
| break; |
| } |
| } |
| path.closePath(); |
| context.lineWidth = 0; |
| context.fillStyle = fillColor; |
| context.fill(path); |
| |
| if (outlineColor) { |
| context.lineWidth = 2; |
| context.strokeStyle = outlineColor; |
| context.stroke(path); |
| } |
| |
| context.restore(); |
| return path; |
| } |
| |
| function emptyBounds() |
| { |
| var bounds = { |
| minX: Number.MAX_VALUE, |
| minY: Number.MAX_VALUE, |
| maxX: Number.MIN_VALUE, |
| maxY: Number.MIN_VALUE, |
| leftmostXForY: {}, |
| rightmostXForY: {}, |
| topmostYForX: {}, |
| bottommostYForX: {} |
| }; |
| return bounds; |
| } |
| |
| function _drawLayoutGridHighlight(highlight, context) |
| { |
| var originX = highlight.columns.origin; |
| var originY = highlight.rows.origin; |
| |
| var columnGap = highlight.columns.gap; |
| var rowGap = highlight.rows.gap; |
| |
| var rows = highlight.rows.positions; |
| var columns = highlight.columns.positions; |
| |
| if (!rows.length || !columns.length) |
| return; |
| |
| context.save(); |
| context.translate(originX + 0.5, originY + 0.5); |
| context.scale(pageScaleFactor, pageScaleFactor) |
| |
| if (columnGap > 1 && highlight.isPrimaryGrid) { |
| var top = rows[0]; |
| var height = rows[rows.length - 1] - rows[0]; |
| for (var i = 1; i < columns.length - 1; i++) |
| context.clearRect(columns[i] - columnGap, top, columnGap, height); |
| } |
| |
| if (rowGap > 1 && highlight.isPrimaryGrid) { |
| var left = columns[0]; |
| var width = columns[columns.length - 1] - columns[0]; |
| for (var i = 1; i < rows.length - 1; i++) |
| context.clearRect(left, rows[i] - rowGap, width, rowGap); |
| } |
| |
| context.setLineDash([3, 3]); |
| context.strokeStyle = highlight.color; |
| |
| for (var column = 1; column < columns.length; column++) { |
| var x = columns[column - 1]; |
| var width = columns[column] - columns[column - 1]; |
| if (column !== columns.length - 1) |
| width -= columnGap; |
| for (var row = 1; row < rows.length; row++) { |
| var y = rows[row - 1]; |
| var height = rows[row] - rows[row - 1]; |
| if (row !== rows.length - 1) |
| height -= rowGap; |
| context.strokeRect(x, y, width, height); |
| } |
| } |
| context.restore(); |
| } |
| |
| function drawHighlight(highlight, context) |
| { |
| context = context || window.context; |
| context.save(); |
| |
| var bounds = emptyBounds(); |
| |
| for (var paths = highlight.paths.slice(); paths.length;) { |
| var path = paths.pop(); |
| drawPath(context, path.path, path.fillColor, path.outlineColor, bounds); |
| if (paths.length) { |
| context.save(); |
| context.globalCompositeOperation = "destination-out"; |
| drawPath(context, paths[paths.length - 1].path, "red", null, bounds); |
| context.restore(); |
| } |
| } |
| |
| var rulerAtRight = highlight.paths.length && highlight.showRulers && bounds.minX < 20 && bounds.maxX + 20 < canvasWidth; |
| var rulerAtBottom = highlight.paths.length && highlight.showRulers && bounds.minY < 20 && bounds.maxY + 20 < canvasHeight; |
| |
| if (highlight.showRulers) |
| _drawGrid(context, rulerAtRight, rulerAtBottom); |
| |
| if (highlight.paths.length) { |
| if (highlight.showExtensionLines) |
| _drawRulers(context, bounds, rulerAtRight, rulerAtBottom); |
| |
| if (highlight.elementInfo) |
| _drawElementTitle(highlight.elementInfo, bounds); |
| } |
| if (highlight.gridInfo) { |
| for (var grid of highlight.gridInfo) |
| _drawLayoutGridHighlight(grid, context); |
| } |
| context.restore(); |
| |
| return { bounds: bounds }; |
| } |
| |
| function drawScreenshotBorder(rect) |
| { |
| var context = window.context; |
| context.save(); |
| context.fillStyle = '#0003'; |
| context.strokeStyle = '#fffd'; |
| context.lineWidth = 1; |
| context.rect(rect.x1 - 0.5, rect.y1 - 0.5, rect.x2 - rect.x1 + 1, rect.y2 - rect.y1 + 1); |
| context.fill(); |
| context.stroke(); |
| context.restore(); |
| } |
| |
| function setPlatform(platform) |
| { |
| window.platform = platform; |
| document.body.classList.add("platform-" + platform); |
| } |
| |
| function dispatch(message) |
| { |
| var functionName = message.shift(); |
| window[functionName].apply(null, message); |
| } |
| |
| function log(text) |
| { |
| document.getElementById("log").createChild("div").textContent = text; |
| } |
| |
| function onResumeClick() |
| { |
| InspectorOverlayHost.resume(); |
| } |
| |
| function onStepOverClick() |
| { |
| InspectorOverlayHost.stepOver(); |
| } |
| |
| function onLoaded() |
| { |
| document.getElementById("resume-button").addEventListener("click", onResumeClick); |
| document.getElementById("step-over-button").addEventListener("click", onStepOverClick); |
| } |
| |
| function eventHasCtrlOrMeta(event) |
| { |
| return window.platform == "mac" ? (event.metaKey && !event.ctrlKey) : (event.ctrlKey && !event.metaKey); |
| } |
| |
| function onDocumentKeyDown(event) |
| { |
| if (!window._controlsVisible) |
| return; |
| if (event.key == "F8" || eventHasCtrlOrMeta(event) && event.keyCode == 220 /* backslash */) |
| InspectorOverlayHost.resume(); |
| else if (event.key == "F10" || eventHasCtrlOrMeta(event) && event.keyCode == 222 /* single quote */) |
| InspectorOverlayHost.stepOver(); |
| } |
| |
| Element.prototype.createChild = function(tagName, className) |
| { |
| var element = createElement(tagName, className); |
| element.addEventListener("click", function(e) { e.stopPropagation(); }, false); |
| this.appendChild(element); |
| return element; |
| } |
| |
| Element.prototype.createTextChild = function(text) |
| { |
| var element = document.createTextNode(text); |
| this.appendChild(element); |
| return element; |
| } |
| |
| Element.prototype.removeChildren = function() |
| { |
| if (this.firstChild) |
| this.textContent = ""; |
| } |
| |
| function createElement(tagName, className) |
| { |
| var element = document.createElement(tagName); |
| if (className) |
| element.className = className; |
| return element; |
| } |
| |
| String.prototype.trimEnd = function(maxLength) |
| { |
| if (this.length <= maxLength) |
| return String(this); |
| return this.substr(0, maxLength - 1) + "\u2026"; |
| } |
| |
| /** |
| * @param {number} num |
| * @param {number} min |
| * @param {number} max |
| * @return {number} |
| */ |
| Number.constrain = function(num, min, max) { |
| if (num < min) |
| num = min; |
| else if (num > max) |
| num = max; |
| return num; |
| }; |
| |
| function test() { |
| document.body.classList.add("platform-mac"); |
| reset({ |
| viewportSize: {width: 800, height: 600}, |
| deviceScaleFactor: 1, |
| pageScaleFactor: 1, |
| pageZoomFactor: 1, |
| scrollX: 0, |
| scrollY: 0 |
| }); |
| drawHighlight( |
| {"paths":[{"path":["M",122,133.796875,"L",822,133.796875,"L",822,208.796875,"L",122,208.796875,"Z"], "fillColor":"rgba(111, 168, 220, 0.658823529411765)","name":"content"}, |
| {"path":["M",122,113.796875,"L",822,113.796875,"L",822,228.796875,"L",122,228.796875,"Z"],"fillColor":"rgba(246, 178, 107, 0.66)","name":"margin"}],"showRulers":false,"showExtensionLines":false, |
| "elementInfo":{"tagName":"p","idValue":"","nodeWidth":"700","nodeHeight":"75","style":{"color":"#FFFFFFFF","font-family":"\"Product Sans\", \"Open Sans\", Roboto, Arial","font-size":"20px","line-height":"25px","padding":"0px","margin":"20px 0px","background-color":"#00000000"},"contrast":{"fontSize":"20px","fontWeight":"400","backgroundColor":"#F9B826BF"}}}); |
| } |
| |
| window.addEventListener("DOMContentLoaded", onLoaded); |
| document.addEventListener("keydown", onDocumentKeyDown); |
| </script> |
| </head> |
| <body class="fill"> |
| </body> |
| <canvas id="canvas" class="fill"></canvas> |
| <div id="tooltip-container"></div> |
| <div class="controls-line"> |
| <div class="message-box"><div id="paused-in-debugger"></div></div> |
| <div class="button" id="resume-button" title="Resume script execution (F8)."><div class="glyph"></div></div> |
| <div class="button" id="step-over-button" title="Step over next function call (F10)."><div class="glyph"></div></div> |
| </div> |
| <div id="log"></div> |
| </html> |