<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="preload" href="/clay/fonts/Calistoga-Regular.ttf" as="font" type="font/ttf" crossorigin> <link rel="preload" href="/clay/fonts/Quicksand-Semibold.ttf" as="font" type="font/ttf" crossorigin> <title>Clay - UI Layout Library</title> <style> html, body { width: 100%; height: 100%; overflow: hidden; padding: 0; margin: 0; pointer-events: none; background: rgb(244, 235, 230); } /* Import the font using @font-face */ @font-face { font-family: 'Calistoga'; font-style: normal; font-weight: 400; src: url('/clay/fonts/Calistoga-Regular.ttf') format('truetype'); } @font-face { font-family: 'Quicksand'; font-style: normal; font-weight: 400; src: url('/clay/fonts/Quicksand-Semibold.ttf') format('truetype'); } body > canvas { width: 100%; height: 100%; touch-action: none; } div, a, img { position: absolute; box-sizing: border-box; -webkit-backface-visibility: hidden; pointer-events: none; } a { cursor: pointer; pointer-events: all; } .text { pointer-events: all; white-space: pre; } /* TODO special exception for text selection in debug tools */ [id='2067877626'] > * { pointer-events: none !important; } </style> </head> <script type="module"> const CLAY_RENDER_COMMAND_TYPE_NONE = 0; const CLAY_RENDER_COMMAND_TYPE_RECTANGLE = 1; const CLAY_RENDER_COMMAND_TYPE_BORDER = 2; const CLAY_RENDER_COMMAND_TYPE_TEXT = 3; const CLAY_RENDER_COMMAND_TYPE_IMAGE = 4; const CLAY_RENDER_COMMAND_TYPE_SCISSOR_START = 5; const CLAY_RENDER_COMMAND_TYPE_SCISSOR_END = 6; const CLAY_RENDER_COMMAND_TYPE_CUSTOM = 7; const GLOBAL_FONT_SCALING_FACTOR = 0.8; let renderCommandSize = 0; let scratchSpaceAddress = 8; let heapSpaceAddress = 0; let memoryDataView; let textDecoder = new TextDecoder("utf-8"); let previousFrameTime; let fontsById = [ 'Quicksand', 'Calistoga', 'Quicksand', 'Quicksand', 'Quicksand', ]; let elementCache = {}; let imageCache = {}; let dimensionsDefinition = { type: 'struct', members: [ {name: 'width', type: 'float'}, {name: 'height', type: 'float'}, ]}; let colorDefinition = { type: 'struct', members: [ {name: 'r', type: 'float' }, {name: 'g', type: 'float' }, {name: 'b', type: 'float' }, {name: 'a', type: 'float' }, ]}; let stringDefinition = { type: 'struct', members: [ {name: 'length', type: 'uint32_t' }, {name: 'chars', type: 'uint32_t' }, ]}; let stringSliceDefinition = { type: 'struct', members: [ {name: 'length', type: 'uint32_t' }, {name: 'chars', type: 'uint32_t' }, {name: 'baseChars', type: 'uint32_t' }, ]}; let borderWidthDefinition = { type: 'struct', members: [ {name: 'left', type: 'uint16_t'}, {name: 'right', type: 'uint16_t'}, {name: 'top', type: 'uint16_t'}, {name: 'bottom', type: 'uint16_t'}, {name: 'betweenChildren', type: 'uint16_t'}, ]}; let cornerRadiusDefinition = { type: 'struct', members: [ {name: 'topLeft', type: 'float'}, {name: 'topRight', type: 'float'}, {name: 'bottomLeft', type: 'float'}, {name: 'bottomRight', type: 'float'}, ]}; let textConfigDefinition = { name: 'text', type: 'struct', members: [ { name: 'userData', type: 'uint32_t' }, { name: 'textColor', ...colorDefinition }, { name: 'fontId', type: 'uint16_t' }, { name: 'fontSize', type: 'uint16_t' }, { name: 'letterSpacing', type: 'uint16_t' }, { name: 'lineSpacing', type: 'uint16_t' }, { name: 'wrapMode', type: 'uint8_t' }, { name: 'disablePointerEvents', type: 'uint8_t' }, { name: '_padding', type: 'uint16_t' }, ]}; let textRenderDataDefinition = { type: 'struct', members: [ { name: 'stringContents', ...stringSliceDefinition }, { name: 'textColor', ...colorDefinition }, { name: 'fontId', type: 'uint16_t' }, { name: 'fontSize', type: 'uint16_t' }, { name: 'letterSpacing', type: 'uint16_t' }, { name: 'lineHeight', type: 'uint16_t' }, ]}; let rectangleRenderDataDefinition = { type: 'struct', members: [ { name: 'backgroundColor', ...colorDefinition }, { name: 'cornerRadius', ...cornerRadiusDefinition }, ]}; let imageRenderDataDefinition = { type: 'struct', members: [ { name: 'backgroundColor', ...colorDefinition }, { name: 'cornerRadius', ...cornerRadiusDefinition }, { name: 'sourceDimensions', ...dimensionsDefinition }, { name: 'imageData', type: 'uint32_t' }, ]}; let customRenderDataDefinition = { type: 'struct', members: [ { name: 'backgroundColor', ...colorDefinition }, { name: 'cornerRadius', ...cornerRadiusDefinition }, { name: 'customData', type: 'uint32_t' }, ]}; let borderRenderDataDefinition = { type: 'struct', members: [ { name: 'color', ...colorDefinition }, { name: 'cornerRadius', ...cornerRadiusDefinition }, { name: 'width', ...borderWidthDefinition }, { name: 'padding', type: 'uint16_t'} ]}; let scrollRenderDataDefinition = { type: 'struct', members: [ { name: 'horizontal', type: 'bool' }, { name: 'vertical', type: 'bool' }, ]}; let customHTMLDataDefinition = { type: 'struct', members: [ { name: 'link', ...stringDefinition }, { name: 'cursorPointer', type: 'uint8_t' }, { name: 'disablePointerEvents', type: 'uint8_t' }, ]}; let renderCommandDefinition = { name: 'CLay_RenderCommand', type: 'struct', members: [ { name: 'boundingBox', type: 'struct', members: [ { name: 'x', type: 'float' }, { name: 'y', type: 'float' }, { name: 'width', type: 'float' }, { name: 'height', type: 'float' }, ]}, { name: 'renderData', type: 'union', members: [ { name: 'rectangle', ...rectangleRenderDataDefinition }, { name: 'text', ...textRenderDataDefinition }, { name: 'image', ...imageRenderDataDefinition }, { name: 'custom', ...customRenderDataDefinition }, { name: 'border', ...borderRenderDataDefinition }, { name: 'scroll', ...scrollRenderDataDefinition }, ]}, { name: 'userData', type: 'uint32_t'}, { name: 'id', type: 'uint32_t' }, { name: 'zIndex', type: 'int16_t' }, { name: 'commandType', type: 'uint8_t' }, { name: '_padding', type: 'uint8_t' }, ] }; function getStructTotalSize(definition) { switch(definition.type) { case 'union': case 'struct': { let totalSize = 0; for (const member of definition.members) { let result = getStructTotalSize(member); if (definition.type === 'struct') { totalSize += result; } else { totalSize = Math.max(totalSize, result); } } return totalSize; } case 'float': return 4; case 'uint32_t': return 4; case 'int32_t': return 4; case 'uint16_t': return 2; case 'int16_t': return 2; case 'uint8_t': return 1; case 'bool': return 1; default: { throw "Unimplemented C data type " + definition.type } } } function readStructAtAddress(address, definition) { switch(definition.type) { case 'union': case 'struct': { let struct = { __size: 0 }; for (const member of definition.members) { let result = readStructAtAddress(address, member); struct[member.name] = result; if (definition.type === 'struct') { struct.__size += result.__size; address += result.__size; } else { struct.__size = Math.max(struct.__size, result.__size); } } return struct; } case 'float': return { value: memoryDataView.getFloat32(address, true), __size: 4 }; case 'uint32_t': return { value: memoryDataView.getUint32(address, true), __size: 4 }; case 'int32_t': return { value: memoryDataView.getUint32(address, true), __size: 4 }; case 'uint16_t': return { value: memoryDataView.getUint16(address, true), __size: 2 }; case 'int16_t': return { value: memoryDataView.getInt16(address, true), __size: 2 }; case 'uint8_t': return { value: memoryDataView.getUint8(address, true), __size: 1 }; case 'bool': return { value: memoryDataView.getUint8(address, true), __size: 1 }; default: { throw "Unimplemented C data type " + definition.type } } } function getTextDimensions(text, font) { // re-use canvas object for better performance window.canvasContext.font = font; let metrics = window.canvasContext.measureText(text); return { width: metrics.width, height: metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent }; } function createMainArena(arenaStructAddress, arenaMemoryAddress) { let memorySize = instance.exports.Clay_MinMemorySize(); // Last arg is address to store return value instance.exports.Clay_CreateArenaWithCapacityAndMemory(arenaStructAddress, memorySize, arenaMemoryAddress); } async function init() { await Promise.all(fontsById.map(f => document.fonts.load(`12px "${f}"`))); window.htmlRoot = document.body.appendChild(document.createElement('div')); window.canvasRoot = document.body.appendChild(document.createElement('canvas')); window.canvasContext = window.canvasRoot.getContext("2d"); window.mousePositionXThisFrame = 0; window.mousePositionYThisFrame = 0; window.mouseWheelXThisFrame = 0; window.mouseWheelYThisFrame = 0; window.touchDown = false; window.arrowKeyDownPressedThisFrame = false; window.arrowKeyUpPressedThisFrame = false; let zeroTimeout = null; document.addEventListener("wheel", (event) => { window.mouseWheelXThisFrame = event.deltaX * -0.1; window.mouseWheelYThisFrame = event.deltaY * -0.1; clearTimeout(zeroTimeout); zeroTimeout = setTimeout(() => { window.mouseWheelXThisFrame = 0; window.mouseWheelYThisFrame = 0; }, 10); }); function handleTouch (event) { if (event.touches.length === 1) { window.touchDown = true; let target = event.target; let scrollTop = 0; let scrollLeft = 0; let activeRendererIndex = memoryDataView.getUint32(instance.exports.ACTIVE_RENDERER_INDEX.value, true); while (activeRendererIndex !== 1 && target) { scrollLeft += target.scrollLeft; scrollTop += target.scrollTop; target = target.parentElement; } window.mousePositionXThisFrame = event.changedTouches[0].pageX + scrollLeft; window.mousePositionYThisFrame = event.changedTouches[0].pageY + scrollTop; } } document.addEventListener("touchstart", handleTouch); document.addEventListener("touchmove", handleTouch); document.addEventListener("touchend", () => { window.touchDown = false; window.mousePositionXThisFrame = 0; window.mousePositionYThisFrame = 0; }) document.addEventListener("mousemove", (event) => { let target = event.target; let scrollTop = 0; let scrollLeft = 0; let activeRendererIndex = memoryDataView.getUint32(instance.exports.ACTIVE_RENDERER_INDEX.value, true); while (activeRendererIndex !== 1 && target) { scrollLeft += target.scrollLeft; scrollTop += target.scrollTop; target = target.parentElement; } window.mousePositionXThisFrame = event.x + scrollLeft; window.mousePositionYThisFrame = event.y + scrollTop; }); document.addEventListener("mousedown", (event) => { window.mouseDown = true; window.mouseDownThisFrame = true; }); document.addEventListener("mouseup", (event) => { window.mouseDown = false; }); document.addEventListener("keydown", (event) => { if (event.key === "ArrowDown") { window.arrowKeyDownPressedThisFrame = true; } if (event.key === "ArrowUp") { window.arrowKeyUpPressedThisFrame = true; } if (event.key === "d") { window.dKeyPressedThisFrame = true; } }); const importObject = { clay: { measureTextFunction: (addressOfDimensions, textToMeasure, addressOfConfig, userData) => { let stringLength = memoryDataView.getUint32(textToMeasure, true); let pointerToString = memoryDataView.getUint32(textToMeasure + 4, true); let textConfig = readStructAtAddress(addressOfConfig, textConfigDefinition); let textDecoder = new TextDecoder("utf-8"); let text = textDecoder.decode(memoryDataView.buffer.slice(pointerToString, pointerToString + stringLength)); let sourceDimensions = getTextDimensions(text, `${Math.round(textConfig.fontSize.value * GLOBAL_FONT_SCALING_FACTOR)}px ${fontsById[textConfig.fontId.value]}`); memoryDataView.setFloat32(addressOfDimensions, sourceDimensions.width, true); memoryDataView.setFloat32(addressOfDimensions + 4, sourceDimensions.height, true); }, queryScrollOffsetFunction: (addressOfOffset, elementId) => { let container = document.getElementById(elementId.toString()); if (container) { memoryDataView.setFloat32(addressOfOffset, -container.scrollLeft, true); memoryDataView.setFloat32(addressOfOffset + 4, -container.scrollTop, true); } }, }, }; const { instance } = await WebAssembly.instantiateStreaming( fetch("/clay/index.wasm"), importObject ); memoryDataView = new DataView(new Uint8Array(instance.exports.memory.buffer).buffer); scratchSpaceAddress = instance.exports.__heap_base.value; let clayScratchSpaceAddress = instance.exports.__heap_base.value + 1024; heapSpaceAddress = instance.exports.__heap_base.value + 2048; let arenaAddress = scratchSpaceAddress + 8; window.instance = instance; createMainArena(arenaAddress, heapSpaceAddress); memoryDataView.setFloat32(instance.exports.__heap_base.value, window.innerWidth, true); memoryDataView.setFloat32(instance.exports.__heap_base.value + 4, window.innerHeight, true); instance.exports.Clay_Initialize(arenaAddress, instance.exports.__heap_base.value); instance.exports.SetScratchMemory(arenaAddress, clayScratchSpaceAddress); renderCommandSize = getStructTotalSize(renderCommandDefinition); renderLoop(); } function MemoryIsDifferent(one, two, length) { for (let i = 0; i < length; i++) { if (one[i] !== two[i]) { return true; } } return false; } function SetElementBackgroundColorAndRadius(element, cornerRadius, backgroundColor) { element.style.backgroundColor = `rgba(${backgroundColor.r.value}, ${backgroundColor.g.value}, ${backgroundColor.b.value}, ${backgroundColor.a.value / 255})`; if (cornerRadius.topLeft.value > 0) { element.style.borderTopLeftRadius = cornerRadius.topLeft.value + 'px'; } if (cornerRadius.topRight.value > 0) { element.style.borderTopRightRadius = cornerRadius.topRight.value + 'px'; } if (cornerRadius.bottomLeft.value > 0) { element.style.borderBottomLeftRadius = cornerRadius.bottomLeft.value + 'px'; } if (cornerRadius.bottomRight.value > 0) { element.style.borderBottomRightRadius = cornerRadius.bottomRight.value + 'px'; } } function renderLoopHTML() { let capacity = memoryDataView.getInt32(scratchSpaceAddress, true); let length = memoryDataView.getInt32(scratchSpaceAddress + 4, true); let arrayOffset = memoryDataView.getUint32(scratchSpaceAddress + 8, true); let scissorStack = [{ nextAllocation: { x: 0, y: 0 }, element: htmlRoot, nextElementIndex: 0 }]; let previousId = 0; for (let i = 0; i < length; i++, arrayOffset += renderCommandSize) { let entireRenderCommandMemory = new Uint8Array(memoryDataView.buffer.slice(arrayOffset, arrayOffset + renderCommandSize)); let renderCommand = readStructAtAddress(arrayOffset, renderCommandDefinition); let parentElement = scissorStack[scissorStack.length - 1]; let element = null; let isMultiConfigElement = previousId === renderCommand.id.value; if (!elementCache[renderCommand.id.value]) { let elementType = 'div'; switch (renderCommand.commandType.value & 0xff) { case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: { // if (readStructAtAddress(renderCommand.renderData.rectangle.value, rectangleRenderDataDefinition).link.length.value > 0) { TODO reimplement links // elementType = 'a'; // } break; } case CLAY_RENDER_COMMAND_TYPE_IMAGE: { elementType = 'img'; break; } default: break; } element = document.createElement(elementType); element.id = renderCommand.id.value; if (renderCommand.commandType.value === CLAY_RENDER_COMMAND_TYPE_SCISSOR_START) { element.style.overflow = 'hidden'; } elementCache[renderCommand.id.value] = { exists: true, element: element, previousMemoryCommand: new Uint8Array(0), previousMemoryConfig: new Uint8Array(0), previousMemoryText: new Uint8Array(0) }; } let elementData = elementCache[renderCommand.id.value]; element = elementData.element; if (!isMultiConfigElement && Array.prototype.indexOf.call(parentElement.element.children, element) !== parentElement.nextElementIndex) { if (parentElement.nextElementIndex === 0) { parentElement.element.insertAdjacentElement('afterbegin', element); } else { parentElement.element.childNodes[Math.min(parentElement.nextElementIndex - 1, parentElement.element.childNodes.length - 1)].insertAdjacentElement('afterend', element); } } elementData.exists = true; // Don't get me started. Cheaper to compare the render command memory than to update HTML elements let dirty = MemoryIsDifferent(elementData.previousMemoryCommand, entireRenderCommandMemory, renderCommandSize) && !isMultiConfigElement; if (!isMultiConfigElement) { parentElement.nextElementIndex++; } previousId = renderCommand.id.value; elementData.previousMemoryCommand = entireRenderCommandMemory; let offsetX = scissorStack.length > 0 ? scissorStack[scissorStack.length - 1].nextAllocation.x : 0; let offsetY = scissorStack.length > 0 ? scissorStack[scissorStack.length - 1].nextAllocation.y : 0; if (dirty) { element.style.transform = `translate(${Math.round(renderCommand.boundingBox.x.value - offsetX)}px, ${Math.round(renderCommand.boundingBox.y.value - offsetY)}px)` element.style.width = Math.round(renderCommand.boundingBox.width.value) + 'px'; element.style.height = Math.round(renderCommand.boundingBox.height.value) + 'px'; } // note: commandType is packed to uint8_t and has 3 garbage bytes of padding switch(renderCommand.commandType.value & 0xff) { case (CLAY_RENDER_COMMAND_TYPE_NONE): { break; } case (CLAY_RENDER_COMMAND_TYPE_RECTANGLE): { let config = renderCommand.renderData.rectangle; let configMemory = JSON.stringify(config); if (configMemory === elementData.previousMemoryConfig) { break; } SetElementBackgroundColorAndRadius(element, config.cornerRadius, config.backgroundColor); if (renderCommand.userData.value !== 0) { let customData = readStructAtAddress(renderCommand.userData.value, customHTMLDataDefinition); let linkContents = customData.link.length.value > 0 ? textDecoder.decode(new Uint8Array(memoryDataView.buffer.slice(customData.link.chars.value, customData.link.chars.value + customData.link.length.value))) : 0; memoryDataView.setUint32(0, renderCommand.id.value, true); if (linkContents.length > 0 && (window.mouseDownThisFrame || window.touchDown) && instance.exports.Clay_PointerOver(0)) { window.location.href = linkContents; } if (linkContents.length > 0) { element.href = linkContents; } if (linkContents.length > 0 || customData.cursorPointer.value) { element.style.pointerEvents = 'all'; element.style.cursor = 'pointer'; } } elementData.previousMemoryConfig = configMemory; break; } case (CLAY_RENDER_COMMAND_TYPE_BORDER): { let config = renderCommand.renderData.border; let configMemory = JSON.stringify(config); if (configMemory === elementData.previousMemoryConfig) { break; } let color = config.color; elementData.previousMemoryConfig = configMemory; if (config.width.left.value > 0) { element.style.borderLeft = `${config.width.left.value}px solid rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})` } if (config.width.right.value > 0) { element.style.borderRight = `${config.width.right.value}px solid rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})` } if (config.width.top.value > 0) { element.style.borderTop = `${config.width.top.value}px solid rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})` } if (config.width.bottom.value > 0) { element.style.borderBottom = `${config.width.bottom.value}px solid rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})` } if (config.cornerRadius.topLeft.value > 0) { element.style.borderTopLeftRadius = config.cornerRadius.topLeft.value + 'px'; } if (config.cornerRadius.topRight.value > 0) { element.style.borderTopRightRadius = config.cornerRadius.topRight.value + 'px'; } if (config.cornerRadius.bottomLeft.value > 0) { element.style.borderBottomLeftRadius = config.cornerRadius.bottomLeft.value + 'px'; } if (config.cornerRadius.bottomRight.value > 0) { element.style.borderBottomRightRadius = config.cornerRadius.bottomRight.value + 'px'; } break; } case (CLAY_RENDER_COMMAND_TYPE_TEXT): { let config = renderCommand.renderData.text; let customData = readStructAtAddress(renderCommand.userData.value, customHTMLDataDefinition); let configMemory = JSON.stringify(config); let stringContents = new Uint8Array(memoryDataView.buffer.slice(config.stringContents.chars.value, config.stringContents.chars.value + config.stringContents.length.value)); if (configMemory !== elementData.previousMemoryConfig) { element.className = 'text'; let textColor = config.textColor; let fontSize = Math.round(config.fontSize.value * GLOBAL_FONT_SCALING_FACTOR); element.style.color = `rgba(${textColor.r.value}, ${textColor.g.value}, ${textColor.b.value}, ${textColor.a.value})`; element.style.fontFamily = fontsById[config.fontId.value]; element.style.fontSize = fontSize + 'px'; element.style.pointerEvents = customData.disablePointerEvents.value ? 'none' : 'all'; elementData.previousMemoryConfig = configMemory; } if (stringContents.length !== elementData.previousMemoryText.length || MemoryIsDifferent(stringContents, elementData.previousMemoryText, stringContents.length)) { element.innerHTML = textDecoder.decode(stringContents); } elementData.previousMemoryText = stringContents; break; } case (CLAY_RENDER_COMMAND_TYPE_SCISSOR_START): { scissorStack.push({ nextAllocation: { x: renderCommand.boundingBox.x.value, y: renderCommand.boundingBox.y.value }, element, nextElementIndex: 0 }); let config = renderCommand.renderData.scroll; let configMemory = JSON.stringify(config); if (configMemory === elementData.previousMemoryConfig) { break; } if (config.horizontal.value) { element.style.overflowX = 'scroll'; element.style.pointerEvents = 'auto'; } if (config.vertical.value) { element.style.overflowY = 'scroll'; element.style.pointerEvents = 'auto'; } elementData.previousMemoryConfig = configMemory; break; } case (CLAY_RENDER_COMMAND_TYPE_SCISSOR_END): { scissorStack.splice(scissorStack.length - 1, 1); break; } case (CLAY_RENDER_COMMAND_TYPE_IMAGE): { let config = renderCommand.renderData.image; let imageURL = readStructAtAddress(config.imageData.value, stringDefinition); let srcContents = new Uint8Array(memoryDataView.buffer.slice(imageURL.chars.value, imageURL.chars.value + imageURL.length.value)); if (srcContents.length !== elementData.previousMemoryText.length || MemoryIsDifferent(srcContents, elementData.previousMemoryText, srcContents.length)) { element.src = textDecoder.decode(srcContents); } elementData.previousMemoryText = srcContents; break; } case (CLAY_RENDER_COMMAND_TYPE_CUSTOM): break; default: { console.log("Error: unhandled render command"); } } } for (const key of Object.keys(elementCache)) { if (elementCache[key].exists) { elementCache[key].exists = false; } else { elementCache[key].element.remove(); delete elementCache[key]; } } } function renderLoopCanvas() { // Note: Rendering to canvas needs to be scaled up by window.devicePixelRatio in both width and height. // e.g. if we're working on a device where devicePixelRatio is 2, we need to render // everything at width^2 x height^2 resolution, then scale back down with css to get the correct pixel density. let capacity = memoryDataView.getUint32(scratchSpaceAddress, true); let length = memoryDataView.getUint32(scratchSpaceAddress + 4, true); let arrayOffset = memoryDataView.getUint32(scratchSpaceAddress + 8, true); window.canvasRoot.width = window.innerWidth * window.devicePixelRatio; window.canvasRoot.height = window.innerHeight * window.devicePixelRatio; window.canvasRoot.style.width = window.innerWidth + 'px'; window.canvasRoot.style.height = window.innerHeight + 'px'; let ctx = window.canvasContext; let scale = window.devicePixelRatio; for (let i = 0; i < length; i++, arrayOffset += renderCommandSize) { let renderCommand = readStructAtAddress(arrayOffset, renderCommandDefinition); let boundingBox = renderCommand.boundingBox; // note: commandType is packed to uint8_t and has 3 garbage bytes of padding switch(renderCommand.commandType.value & 0xff) { case (CLAY_RENDER_COMMAND_TYPE_NONE): { break; } case (CLAY_RENDER_COMMAND_TYPE_RECTANGLE): { let config = renderCommand.renderData.rectangle; let color = config.backgroundColor; ctx.beginPath(); window.canvasContext.fillStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`; window.canvasContext.roundRect( boundingBox.x.value * scale, // x boundingBox.y.value * scale, // y boundingBox.width.value * scale, // width boundingBox.height.value * scale, [config.cornerRadius.topLeft.value * scale, config.cornerRadius.topRight.value * scale, config.cornerRadius.bottomRight.value * scale, config.cornerRadius.bottomLeft.value * scale]) // height; ctx.fill(); ctx.closePath(); // Handle link clicks if (renderCommand.userData.value !== 0) { let customData = readStructAtAddress(renderCommand.userData.value, customHTMLDataDefinition); let linkContents = customData.link.length.value > 0 ? textDecoder.decode(new Uint8Array(memoryDataView.buffer.slice(customData.link.chars.value, customData.link.chars.value + customData.link.length.value))) : 0; memoryDataView.setUint32(0, renderCommand.id.value, true); if (linkContents.length > 0 && (window.mouseDownThisFrame || window.touchDown) && instance.exports.Clay_PointerOver(0)) { window.location.href = linkContents; } } break; } case (CLAY_RENDER_COMMAND_TYPE_BORDER): { let config = renderCommand.renderData.border; let color = config.color; ctx.beginPath(); ctx.moveTo(boundingBox.x.value * scale, boundingBox.y.value * scale); // Top Left Corner if (config.cornerRadius.topLeft.value > 0) { let lineWidth = config.width.top.value; let halfLineWidth = lineWidth / 2; ctx.moveTo((boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + config.cornerRadius.topLeft.value + halfLineWidth) * scale); ctx.lineWidth = lineWidth * scale; ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`; ctx.arcTo((boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale, (boundingBox.x.value + config.cornerRadius.topLeft.value + halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale, config.cornerRadius.topLeft.value * scale); ctx.stroke(); } // Top border if (config.width.top.value > 0) { let lineWidth = config.width.top.value; let halfLineWidth = lineWidth / 2; ctx.lineWidth = lineWidth * scale; ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`; ctx.moveTo((boundingBox.x.value + config.cornerRadius.topLeft.value + halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale); ctx.lineTo((boundingBox.x.value + boundingBox.width.value - config.cornerRadius.topRight.value - halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale); ctx.stroke(); } // Top Right Corner if (config.cornerRadius.topRight.value > 0) { let lineWidth = config.width.top.value; let halfLineWidth = lineWidth / 2; ctx.moveTo((boundingBox.x.value + boundingBox.width.value - config.cornerRadius.topRight.value - halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale); ctx.lineWidth = lineWidth * scale; ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`; ctx.arcTo((boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale, (boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + config.cornerRadius.topRight.value + halfLineWidth) * scale, config.cornerRadius.topRight.value * scale); ctx.stroke(); } // Right border if (config.width.right.value > 0) { let lineWidth = config.width.right.value; let halfLineWidth = lineWidth / 2; ctx.lineWidth = lineWidth * scale; ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`; ctx.moveTo((boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + config.cornerRadius.topRight.value + halfLineWidth) * scale); ctx.lineTo((boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - config.cornerRadius.topRight.value - halfLineWidth) * scale); ctx.stroke(); } // Bottom Right Corner if (config.cornerRadius.bottomRight.value > 0) { let lineWidth = config.width.top.value; let halfLineWidth = lineWidth / 2; ctx.moveTo((boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - config.cornerRadius.bottomRight.value - halfLineWidth) * scale); ctx.lineWidth = lineWidth * scale; ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`; ctx.arcTo((boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale, (boundingBox.x.value + boundingBox.width.value - config.cornerRadius.bottomRight.value - halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale, config.cornerRadius.bottomRight.value * scale); ctx.stroke(); } // Bottom Border if (config.width.bottom.value > 0) { let lineWidth = config.width.bottom.value; let halfLineWidth = lineWidth / 2; ctx.lineWidth = lineWidth * scale; ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`; ctx.moveTo((boundingBox.x.value + config.cornerRadius.bottomLeft.value + halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale); ctx.lineTo((boundingBox.x.value + boundingBox.width.value - config.cornerRadius.bottomRight.value - halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale); ctx.stroke(); } // Bottom Left Corner if (config.cornerRadius.bottomLeft.value > 0) { let lineWidth = config.width.bottom.value; let halfLineWidth = lineWidth / 2; ctx.moveTo((boundingBox.x.value + config.cornerRadius.bottomLeft.value + halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale); ctx.lineWidth = lineWidth * scale; ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`; ctx.arcTo((boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale, (boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - config.cornerRadius.bottomLeft.value - halfLineWidth) * scale, config.cornerRadius.bottomLeft.value * scale); ctx.stroke(); } // Left Border if (config.width.left.value > 0) { let lineWidth = config.width.left.value; let halfLineWidth = lineWidth / 2; ctx.lineWidth = lineWidth * scale; ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`; ctx.moveTo((boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - config.cornerRadius.bottomLeft.value - halfLineWidth) * scale); ctx.lineTo((boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + config.cornerRadius.bottomRight.value + halfLineWidth) * scale); ctx.stroke(); } ctx.closePath(); break; } case (CLAY_RENDER_COMMAND_TYPE_TEXT): { let config = renderCommand.renderData.text; let textContents = config.stringContents; let stringContents = new Uint8Array(memoryDataView.buffer.slice(textContents.chars.value, textContents.chars.value + textContents.length.value)); let fontSize = config.fontSize.value * GLOBAL_FONT_SCALING_FACTOR * scale; ctx.font = `${fontSize}px ${fontsById[config.fontId.value]}`; let color = config.textColor; ctx.textBaseline = 'middle'; ctx.fillStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`; ctx.fillText(textDecoder.decode(stringContents), boundingBox.x.value * scale, (boundingBox.y.value + boundingBox.height.value / 2 + 1) * scale); break; } case (CLAY_RENDER_COMMAND_TYPE_SCISSOR_START): { window.canvasContext.save(); window.canvasContext.beginPath(); window.canvasContext.rect(boundingBox.x.value * scale, boundingBox.y.value * scale, boundingBox.width.value * scale, boundingBox.height.value * scale); window.canvasContext.clip(); window.canvasContext.closePath(); break; } case (CLAY_RENDER_COMMAND_TYPE_SCISSOR_END): { window.canvasContext.restore(); break; } case (CLAY_RENDER_COMMAND_TYPE_IMAGE): { let config = renderCommand.renderData.image; let imageURL = readStructAtAddress(config.imageData.value, stringDefinition); let src = textDecoder.decode(new Uint8Array(memoryDataView.buffer.slice(imageURL.chars.value, imageURL.chars.value + imageURL.length.value))); if (!imageCache[src]) { imageCache[src] = { image: new Image(), loaded: false, } imageCache[src].image.onload = () => imageCache[src].loaded = true; imageCache[src].image.src = src; } else if (imageCache[src].loaded) { ctx.drawImage(imageCache[src].image, boundingBox.x.value * scale, boundingBox.y.value * scale, boundingBox.width.value * scale, boundingBox.height.value * scale); } break; } case (CLAY_RENDER_COMMAND_TYPE_CUSTOM): break; } } } function renderLoop(currentTime) { const elapsed = currentTime - previousFrameTime; previousFrameTime = currentTime; let activeRendererIndex = memoryDataView.getUint32(instance.exports.ACTIVE_RENDERER_INDEX.value, true); if (activeRendererIndex === 0) { instance.exports.UpdateDrawFrame(scratchSpaceAddress, window.innerWidth, window.innerHeight, 0, 0, window.mousePositionXThisFrame, window.mousePositionYThisFrame, window.touchDown, window.mouseDown, 0, 0, window.dKeyPressedThisFrame, elapsed / 1000); } else { instance.exports.UpdateDrawFrame(scratchSpaceAddress, window.innerWidth, window.innerHeight, window.mouseWheelXThisFrame, window.mouseWheelYThisFrame, window.mousePositionXThisFrame, window.mousePositionYThisFrame, window.touchDown, window.mouseDown, window.arrowKeyDownPressedThisFrame, window.arrowKeyUpPressedThisFrame, window.dKeyPressedThisFrame, elapsed / 1000); } let rendererChanged = activeRendererIndex !== window.previousActiveRendererIndex; switch (activeRendererIndex) { case 0: { renderLoopHTML(); if (rendererChanged) { window.htmlRoot.style.display = 'block'; window.canvasRoot.style.display = 'none'; } break; } case 1: { renderLoopCanvas(); if (rendererChanged) { window.htmlRoot.style.display = 'none'; window.canvasRoot.style.display = 'block'; } break; } } window.previousActiveRendererIndex = activeRendererIndex; requestAnimationFrame(renderLoop); window.mouseDownThisFrame = false; window.arrowKeyUpPressedThisFrame = false; window.arrowKeyDownPressedThisFrame = false; window.dKeyPressedThisFrame = false; } init(); </script> <body> </body> </html>