UNPKG

mylingo3d

Version:

Lingo3D is a React/Vue 3d game development framework that ships with a complete visual editor

401 lines 16.7 kB
import { CanvasTexture, LinearFilter, Mesh, MeshBasicMaterial, PlaneGeometry, Color, Sprite, SpriteMaterial } from "three"; import { scaleDown } from "../../engine/constants"; export class HTMLMesh extends Mesh { constructor(dom) { const texture = new HTMLTexture(dom); const geometry = new PlaneGeometry(texture.image.width * scaleDown, texture.image.height * scaleDown); const material = new MeshBasicMaterial({ map: texture, toneMapped: false, transparent: true }); super(geometry, material); function onEvent(event) { material.map.dispatchDOMEvent(event); } this.addEventListener("mousedown", onEvent); this.addEventListener("mousemove", onEvent); this.addEventListener("mouseup", onEvent); this.addEventListener("click", onEvent); this.dispose = function () { geometry.dispose(); material.dispose(); material.map.dispose(); this.removeEventListener("mousedown", onEvent); this.removeEventListener("mousemove", onEvent); this.removeEventListener("mouseup", onEvent); this.removeEventListener("click", onEvent); }; this.update = () => texture.update(); } } export class HTMLSprite extends Sprite { constructor(dom) { const texture = new HTMLTexture(dom); const material = new SpriteMaterial({ map: texture, toneMapped: false, transparent: true }); super(material); this.scale.set(texture.image.width * scaleDown, texture.image.height * scaleDown, 0); function onEvent(event) { material.map.dispatchDOMEvent(event); } this.addEventListener("mousedown", onEvent); this.addEventListener("mousemove", onEvent); this.addEventListener("mouseup", onEvent); this.addEventListener("click", onEvent); this.dispose = function () { material.dispose(); material.map.dispose(); this.removeEventListener("mousedown", onEvent); this.removeEventListener("mousemove", onEvent); this.removeEventListener("mouseup", onEvent); this.removeEventListener("click", onEvent); }; this.update = () => texture.update(); } } class HTMLTexture extends CanvasTexture { constructor(dom) { super(html2canvas(dom)); this.dom = dom; this.anisotropy = 16; this.minFilter = LinearFilter; this.magFilter = LinearFilter; // Create an observer on the DOM, and run html2canvas update in the next loop const observer = new MutationObserver(() => { this.update(); }); const config = { attributes: true, childList: true, subtree: true, characterData: true }; observer.observe(dom, config); this.observer = observer; } dispatchDOMEvent(event) { if (event.data) { htmlevent(this.dom, event.type, event.data.x, event.data.y); } } update() { this.image = html2canvas(this.dom); this.needsUpdate = true; } dispose() { if (this.observer) { this.observer.disconnect(); } super.dispose(); } } // const canvases = new WeakMap(); function html2canvas(element) { const range = document.createRange(); const color = new Color(); function Clipper(context) { const clips = []; let isClipping = false; function doClip() { if (isClipping) { isClipping = false; context.restore(); } if (clips.length === 0) return; let minX = -Infinity, minY = -Infinity; let maxX = Infinity, maxY = Infinity; for (let i = 0; i < clips.length; i++) { const clip = clips[i]; minX = Math.max(minX, clip.x); minY = Math.max(minY, clip.y); maxX = Math.min(maxX, clip.x + clip.width); maxY = Math.min(maxY, clip.y + clip.height); } context.save(); context.beginPath(); context.rect(minX, minY, maxX - minX, maxY - minY); context.clip(); isClipping = true; } return { add: function (clip) { clips.push(clip); doClip(); }, remove: function () { clips.pop(); doClip(); } }; } function drawText(style, x, y, string) { if (string !== "") { if (style.textTransform === "uppercase") { string = string.toUpperCase(); } context.font = style.fontWeight + " " + style.fontSize + " " + style.fontFamily; context.textBaseline = "top"; context.fillStyle = style.color; context.fillText(string, x, y + parseFloat(style.fontSize) * 0.1); } } function buildRectPath(x, y, w, h, r) { if (w < 2 * r) r = w / 2; if (h < 2 * r) r = h / 2; context.beginPath(); context.moveTo(x + r, y); context.arcTo(x + w, y, x + w, y + h, r); context.arcTo(x + w, y + h, x, y + h, r); context.arcTo(x, y + h, x, y, r); context.arcTo(x, y, x + w, y, r); context.closePath(); } function drawBorder(style, which, x, y, width, height) { const borderWidth = style[which + "Width"]; const borderStyle = style[which + "Style"]; const borderColor = style[which + "Color"]; if (borderWidth !== "0px" && borderStyle !== "none" && borderColor !== "transparent" && borderColor !== "rgba(0, 0, 0, 0)") { context.strokeStyle = borderColor; context.lineWidth = parseFloat(borderWidth); context.beginPath(); context.moveTo(x, y); context.lineTo(x + width, y + height); context.stroke(); } } function drawElement(element, style) { let x = 0, y = 0, width = 0, height = 0; if (element.nodeType === Node.TEXT_NODE) { // text range.selectNode(element); const rect = range.getBoundingClientRect(); x = rect.left - offset.left - 0.5; y = rect.top - offset.top - 0.5; width = rect.width; height = rect.height; drawText(style, x, y, element.nodeValue.trim()); } else if (element.nodeType === Node.COMMENT_NODE) { return; } else if (element instanceof HTMLCanvasElement) { // Canvas element if (element.style.display === "none") return; context.save(); const dpr = window.devicePixelRatio; context.scale(1 / dpr, 1 / dpr); context.drawImage(element, 0, 0); context.restore(); } else { if (element.style.display === "none") return; const rect = element.getBoundingClientRect(); x = rect.left - offset.left - 0.5; y = rect.top - offset.top - 0.5; width = rect.width; height = rect.height; style = window.getComputedStyle(element); // Get the border of the element used for fill and border buildRectPath(x, y, width, height, parseFloat(style.borderRadius)); const backgroundColor = style.backgroundColor; if (backgroundColor !== "transparent" && backgroundColor !== "rgba(0, 0, 0, 0)") { context.fillStyle = backgroundColor; context.fill(); } // If all the borders match then stroke the round rectangle const borders = [ "borderTop", "borderLeft", "borderBottom", "borderRight" ]; let match = true; let prevBorder = null; for (const border of borders) { if (prevBorder !== null) { match = style[border + "Width"] === style[prevBorder + "Width"] && style[border + "Color"] === style[prevBorder + "Color"] && style[border + "Style"] === style[prevBorder + "Style"]; } if (match === false) break; prevBorder = border; } if (match === true) { // They all match so stroke the rectangle from before allows for border-radius const width = parseFloat(style.borderTopWidth); if (style.borderTopWidth !== "0px" && style.borderTopStyle !== "none" && style.borderTopColor !== "transparent" && style.borderTopColor !== "rgba(0, 0, 0, 0)") { context.strokeStyle = style.borderTopColor; context.lineWidth = width; context.stroke(); } } else { // Otherwise draw individual borders drawBorder(style, "borderTop", x, y, width, 0); drawBorder(style, "borderLeft", x, y, 0, height); drawBorder(style, "borderBottom", x, y + height, width, 0); drawBorder(style, "borderRight", x + width, y, 0, height); } if (element instanceof HTMLInputElement) { let accentColor = style.accentColor; if (accentColor === undefined || accentColor === "auto") accentColor = style.color; color.set(accentColor); const luminance = Math.sqrt(0.299 * color.r ** 2 + 0.587 * color.g ** 2 + 0.114 * color.b ** 2); const accentTextColor = luminance < 0.5 ? "white" : "#111111"; if (element.type === "radio") { buildRectPath(x, y, width, height, height); context.fillStyle = "white"; context.strokeStyle = accentColor; context.lineWidth = 1; context.fill(); context.stroke(); if (element.checked) { buildRectPath(x + 2, y + 2, width - 4, height - 4, height); context.fillStyle = accentColor; context.strokeStyle = accentTextColor; context.lineWidth = 2; context.fill(); context.stroke(); } } if (element.type === "checkbox") { buildRectPath(x, y, width, height, 2); context.fillStyle = element.checked ? accentColor : "white"; context.strokeStyle = element.checked ? accentTextColor : accentColor; context.lineWidth = 1; context.stroke(); context.fill(); if (element.checked) { const currentTextAlign = context.textAlign; context.textAlign = "center"; const properties = { color: accentTextColor, fontFamily: style.fontFamily, fontSize: height + "px", fontWeight: "bold" }; drawText(properties, x + width / 2, y, "✔"); context.textAlign = currentTextAlign; } } if (element.type === "range") { const [min, max, value] = ["min", "max", "value"].map((property) => parseFloat(element[property])); const position = ((value - min) / (max - min)) * (width - height); buildRectPath(x, y + height / 4, width, height / 2, height / 4); context.fillStyle = accentTextColor; context.strokeStyle = accentColor; context.lineWidth = 1; context.fill(); context.stroke(); buildRectPath(x, y + height / 4, position + height / 2, height / 2, height / 4); context.fillStyle = accentColor; context.fill(); buildRectPath(x + position, y, height, height, height / 2); context.fillStyle = accentColor; context.fill(); } if (element.type === "color" || element.type === "text" || element.type === "number") { clipper.add({ x: x, y: y, width: width, height: height }); drawText(style, x + parseInt(style.paddingLeft), y + parseInt(style.paddingTop), element.value); clipper.remove(); } } } /* // debug context.strokeStyle = '#' + Math.random().toString( 16 ).slice( - 3 ); context.strokeRect( x - 0.5, y - 0.5, width + 1, height + 1 ); */ const isClipping = style.overflow === "auto" || style.overflow === "hidden"; if (isClipping) clipper.add({ x: x, y: y, width: width, height: height }); for (let i = 0; i < element.childNodes.length; i++) { drawElement(element.childNodes[i], style); } if (isClipping) clipper.remove(); } const offset = element.getBoundingClientRect(); let canvas; if (canvases.has(element)) { canvas = canvases.get(element); } else { canvas = document.createElement("canvas"); canvas.width = offset.width; canvas.height = offset.height; } const context = canvas.getContext("2d" /*, { alpha: false }*/); const clipper = new Clipper(context); // console.time( 'drawElement' ); drawElement(element); // console.timeEnd( 'drawElement' ); return canvas; } function htmlevent(element, event, x, y) { const mouseEventInit = { clientX: x * element.offsetWidth + element.offsetLeft, clientY: y * element.offsetHeight + element.offsetTop, view: element.ownerDocument.defaultView }; window.dispatchEvent(new MouseEvent(event, mouseEventInit)); const rect = element.getBoundingClientRect(); x = x * rect.width + rect.left; y = y * rect.height + rect.top; function traverse(element) { if (element.nodeType !== Node.TEXT_NODE && element.nodeType !== Node.COMMENT_NODE) { const rect = element.getBoundingClientRect(); if (x > rect.left && x < rect.right && y > rect.top && y < rect.bottom) { element.dispatchEvent(new MouseEvent(event, mouseEventInit)); if (element instanceof HTMLInputElement && element.type === "range" && (event === "mousedown" || event === "click")) { const [min, max] = ["min", "max"].map((property) => parseFloat(element[property])); const width = rect.width; const offsetX = x - rect.x; const proportion = offsetX / width; element.value = min + (max - min) * proportion; element.dispatchEvent(new InputEvent("input", { bubbles: true })); } } for (let i = 0; i < element.childNodes.length; i++) { traverse(element.childNodes[i]); } } } traverse(element); } //# sourceMappingURL=HTMLMesh.js.map