UNPKG

@lightningtv/renderer

Version:
376 lines 13.5 kB
import { CoreNode, } from '../core/CoreNode.js'; import {} from './Renderer.js'; import { isProductionEnvironment } from '../utils.js'; const stylePropertyMap = { alpha: (v) => { if (v === 1) { return null; } return { prop: 'opacity', value: `${v}` }; }, x: (x) => { return { prop: 'left', value: `${x}px` }; }, y: (y) => { return { prop: 'top', value: `${y}px` }; }, width: (w) => { if (w === 0) { return null; } return { prop: 'width', value: `${w}px` }; }, height: (h) => { if (h === 0) { return null; } return { prop: 'height', value: `${h}px` }; }, fontSize: (fs) => { if (fs === 0) { return null; } return { prop: 'font-size', value: `${fs}px` }; }, lineHeight: (lh) => { if (lh === 0) { return null; } return { prop: 'line-height', value: `${lh}px` }; }, zIndex: () => 'z-index', fontFamily: () => 'font-family', fontStyle: () => 'font-style', fontWeight: () => 'font-weight', fontStretch: () => 'font-stretch', letterSpacing: () => 'letter-spacing', textAlign: () => 'text-align', overflowSuffix: () => 'overflow-suffix', maxLines: () => 'max-lines', contain: () => 'contain', verticalAlign: () => 'vertical-align', clipping: (v) => { if (v === false) { return null; } return { prop: 'overflow', value: v ? 'hidden' : 'visible' }; }, rotation: (v) => { if (v === 0) { return null; } return { prop: 'transform', value: `rotate(${v}rad)` }; }, scale: (v) => { if (v === 1) { return null; } return { prop: 'transform', value: `scale(${v})` }; }, scaleX: (v) => { if (v === 1) { return null; } return { prop: 'transform', value: `scaleX(${v})` }; }, scaleY: (v) => { if (v === 1) { return null; } return { prop: 'transform', value: `scaleY(${v})` }; }, color: (v) => { if (v === 0) { return null; } return { prop: 'color', value: convertColorToRgba(v) }; }, }; const convertColorToRgba = (color) => { const a = (color & 0xff) / 255; const b = (color >> 8) & 0xff; const g = (color >> 16) & 0xff; const r = (color >> 24) & 0xff; return `rgba(${r},${g},${b},${a})`; }; const domPropertyMap = { id: 'test-id', }; const gradientColorPropertyMap = [ 'colorTop', 'colorBottom', 'colorLeft', 'colorRight', 'colorTl', 'colorTr', 'colorBl', 'colorBr', ]; const knownProperties = new Set([ ...Object.keys(stylePropertyMap), ...Object.keys(domPropertyMap), // ...gradientColorPropertyMap, 'src', 'parent', 'data', ]); export class Inspector { root = null; canvas = null; height = 1080; width = 1920; scaleX = 1; scaleY = 1; constructor(canvas, settings) { if (isProductionEnvironment()) return; if (!settings) { throw new Error('settings is required'); } // calc dimensions based on the devicePixelRatio this.height = Math.ceil(settings.appHeight ?? 1080 / (settings.deviceLogicalPixelRatio ?? 1)); this.width = Math.ceil(settings.appWidth ?? 1920 / (settings.deviceLogicalPixelRatio ?? 1)); this.scaleX = settings.deviceLogicalPixelRatio ?? 1; this.scaleY = settings.deviceLogicalPixelRatio ?? 1; this.canvas = canvas; this.root = document.createElement('div'); this.setRootPosition(); document.body.appendChild(this.root); //listen for changes on canvas const mutationObserver = new MutationObserver(this.setRootPosition.bind(this)); mutationObserver.observe(canvas, { attributes: true, childList: false, subtree: false, }); // Create a ResizeObserver to watch for changes in the element's size const resizeObserver = new ResizeObserver(this.setRootPosition.bind(this)); resizeObserver.observe(canvas); //listen for changes on window window.addEventListener('resize', this.setRootPosition.bind(this)); console.warn('Inspector is enabled, this will impact performance'); } setRootPosition() { if (this.root === null || this.canvas === null) { return; } // get the world position of the canvas object, so we can match the inspector to it const rect = this.canvas.getBoundingClientRect(); const top = document.documentElement.scrollTop + rect.top; const left = document.documentElement.scrollLeft + rect.left; this.root.id = 'root'; this.root.style.left = `${left}px`; this.root.style.top = `${top}px`; this.root.style.width = `${this.width}px`; this.root.style.height = `${this.height}px`; this.root.style.position = 'absolute'; this.root.style.transformOrigin = '0 0 0'; this.root.style.transform = `scale(${this.scaleX}, ${this.scaleY})`; this.root.style.overflow = 'hidden'; this.root.style.zIndex = '65534'; } createDiv(id, properties) { const div = document.createElement('div'); div.style.position = 'absolute'; div.id = id.toString(); // set initial properties for (const key in properties) { this.updateNodeProperty(div, // really typescript? really? key, properties[key], properties); } return div; } createNode(node) { const div = this.createDiv(node.id, node.props); div.node = node; node.div = div; node.on('inViewport', () => div.setAttribute('state', 'inViewport')); node.on('inBounds', () => div.setAttribute('state', 'inBounds')); node.on('outOfBounds', () => div.setAttribute('state', 'outOfBounds')); // Monitor only relevant properties by trapping with selective assignment return this.createProxy(node, div); } createTextNode(node) { const div = this.createDiv(node.id, node.props); div.node = node; node.div = div; return this.createProxy(node, div); } createProxy(node, div) { // Define traps for each property in knownProperties knownProperties.forEach((property) => { let originalProp = Object.getOwnPropertyDescriptor(node, property); if (originalProp === undefined) { // Search the prototype chain for the property descriptor const proto = Object.getPrototypeOf(node); originalProp = Object.getOwnPropertyDescriptor(proto, property); } if (originalProp === undefined) { return; } Object.defineProperty(node, property, { get() { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return originalProp?.get?.call(node); }, set: (value) => { originalProp?.set?.call(node, value); this.updateNodeProperty(div, property, value, node.props); }, configurable: true, enumerable: true, }); }); const originalDestroy = node.destroy; Object.defineProperty(node, 'destroy', { value: () => { this.destroyNode(node.id); originalDestroy.call(node); }, }); const originalAnimate = node.animate; Object.defineProperty(node, 'animate', { value: (props, settings) => { const animationController = originalAnimate.call(node, props, settings); const originalStart = animationController.start.bind(animationController); animationController.start = () => { this.animateNode(div, props, settings); return originalStart(); }; return animationController; }, }); return node; } destroyNode(id) { const div = document.getElementById(id.toString()); div?.remove(); } updateNodeProperty(div, property, // eslint-disable-next-line @typescript-eslint/no-explicit-any value, props) { if (this.root === null || value === undefined || value === null) { return; } /** * Special case for parent property */ if (property === 'parent') { const parentId = value.id; // only way to detect if the parent is the root node // if you are reading this and have a better way, please let me know if (parentId === 1) { this.root.appendChild(div); return; } const parent = document.getElementById(parentId.toString()); parent?.appendChild(div); return; } // special case for text if (property === 'text') { div.innerHTML = String(value); // hide text because we can't render SDF fonts // it would look weird and obstruct the WebGL rendering div.style.visibility = 'hidden'; return; } // special case for images // we're not setting any CSS properties to avoid images getting loaded twice // as the renderer will handle the loading of the image. Setting it to `data-src` if (property === 'src' && value) { div.setAttribute(`data-src`, String(value)); return; } // special case for color gradients (normal colors are handled by the stylePropertyMap) // FIXME the renderer seems to return the same number for all colors // if (gradientColorPropertyMap.includes(property as string)) { // const color = convertColorToRgba(value as number); // div.setAttribute(`data-${property}`, color); // return; // } if (property === 'rtt' && value) { div.setAttribute('data-rtt', String(value)); return; } // CSS mappable attribute if (stylePropertyMap[property]) { const mappedStyleResponse = stylePropertyMap[property]?.(value); if (mappedStyleResponse === null) { return; } if (typeof mappedStyleResponse === 'string') { div.style.setProperty(mappedStyleResponse, String(value)); return; } if (typeof mappedStyleResponse === 'object') { let value = mappedStyleResponse.value; if (property === 'x') { const mount = props.mountX; const width = props.width; if (mount) { value = `${parseInt(value) - width * mount}px`; } } else if (property === 'y') { const mount = props.mountY; const height = props.height; if (mount) { value = `${parseInt(value) - height * mount}px`; } } div.style.setProperty(mappedStyleResponse.prop, value); } return; } // DOM properties if (domPropertyMap[property]) { const domProperty = domPropertyMap[property]; if (!domProperty) { return; } div.setAttribute(String(domProperty), String(value)); return; } // custom data properties if (property === 'data') { for (const key in value) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const keyValue = value[key]; if (keyValue === undefined) { div.removeAttribute(`data-${key}`); } else { div.setAttribute(`data-${key}`, String(keyValue)); } } return; } } // simple animation handler animateNode(div, props, settings) { const { duration = 1000, delay = 0, // easing = 'linear', // repeat = 0, // loop = false, // stopMethod = false, } = settings; const { x, y, width, height, alpha = 1, rotation = 0, scale = 1, color, mountX, mountY, } = props; // ignoring loops and repeats for now, as that might be a bit too much for the inspector function animate() { setTimeout(() => { div.style.top = `${y - height * mountY}px`; div.style.left = `${x - width * mountX}px`; div.style.width = `${width}px`; div.style.height = `${height}px`; div.style.opacity = `${alpha}`; div.style.rotate = `${rotation}rad`; div.style.scale = `${scale}`; div.style.color = convertColorToRgba(color); }, duration); } setTimeout(animate, delay); } } //# sourceMappingURL=Inspector.js.map