"use strict" var selectedExport; if (location.pathname.includes("/page/")) { //don't init if not in the right path //this is to stop code running from comfyUI directly $(initPaint); } var initialPalette = [ "#000000","#ffffff", "#202020","#404040", "#606060","#a0a0a0", "#808080","#a0a0a0", "#c0c060","#e0e0e0", "#ff0000","#ff8000", "#ffff00","#008000", "#00ff00","#00ff80", "#0080ff","#50C0ff", "#0000ff","#8080ff", "#a00060","#ff00ff", "#ffa0a0","#a06040"]; var tip = { lastX : 0, lastY : 0, x : 0.0, y : 0.0, color : "black", size : 5, drawOperation : feltTip, } var pen=feltTip; var brushSizeControl = brushDiameterControl("tip_diameter"); var picStack = []; var activePic; function setActivePic(newValue) { activePic=newValue; updateLayerList() if (!activePic) return; activePic.bringToFront(); } var dragging = false; var dragStartX; var dragStartY; var draggingElement = null; function setExportPic(pic) { selectedExport=pic; $(".sidebutton").removeClass("output") const button=pic.element.querySelector(".sidebutton"); button.classList.add("output"); pic.commit(); transmitMask(pic.mask.canvas); } function closePic(pic) { pic.element.parentElement.removeChild(pic.element); if (pic === activePic) { const newActive = document.querySelector("#workspace .pic"); setActivePic(newActive.pic); } } var lastUsedMaskColor = "#402040"; class Layer { canvas = document.createElement("canvas"); ctx = this.canvas.getContext("2d",{willReadFrequently:true}); mask = false; visible = true; compositeOperation = "source-over"; opacity = 1; _maskColor = lastUsedMaskColor; constructor (title, {width,height}, mask = false) { this.canvas.width=width; this.canvas.height=height; this.mask = mask; this.title= title; if (mask) { this.compositeOperation="source-over"; } } get maskColor() { return this._maskColor; } set maskColor(newValue) { this._maskColor=newValue; if (this.mask) { this.ctx.save(); this.ctx.globalCompositeOperation="source-atop"; this.ctx.fillStyle=newValue; this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height); this.ctx.restore(); } } } function updateMirrorGridButtonImage() { const columns = $(".grid_mirrors #repeat_x").val(); const rows = $(".grid_mirrors #repeat_y").val(); const image=gridImage(rows,columns,48); $("#mirror_grid").css('background-image',`url(${image.toDataURL()})`); } function updateMirrorRotationButtonImage() { const fins = $(".rotational_mirrors input").val(); const image=spiralImage(fins,36); $("#mirror_rotational").css('background-image',`url(${image.toDataURL()})`); } var mirrorFunction = a=>a; function updateMirrors() { let result = a=>a; if($("#mirror_rotational.mirror.button").hasClass("down")) { const ways = $(".rotational_mirrors input").val(); const rotationFunction = rotationalSymmetry(ways); result = composeFunction(result,rotationFunction); } if($("#mirror_grid.mirror.button").hasClass("down")) { const columns = $(".grid_mirrors #repeat_x").val(); const rows = $(".grid_mirrors #repeat_y").val(); const gridFunction = cells(columns,rows); result = composeFunction(result,gridFunction); } if($("#mirror_x.mirror.button").hasClass("down")) result = composeFunction(result,mirrorX); if($("#mirror_y.mirror.button").hasClass("down")) result = composeFunction(result,mirrorY); if($("#mirror_tlbr.mirror.button").hasClass("down")) result = composeFunction(result,mirrorDiagonalXY); if($("#mirror_trbl.mirror.button").hasClass("down")) result = composeFunction(result,mirrorDiagonalY1MinusX); mirrorFunction=result; } function createDrawArea(canvas = blankCanvas()) { const undoDepth = 10; const element = document.createElement("div") const ctx=canvas.getContext("2d"); const eventOverlay = document.createElement("div"); const sidebar = document.createElement("div"); const closeButton = document.createElement("div"); var updatingStroke=false; sidebar.className = "sidebutton"; closeButton.className = "closebutton"; element.className="pic"; eventOverlay.className="eventoverlay fillparent"; element.appendChild(sidebar); element.appendChild(canvas); element.appendChild(eventOverlay); element.appendChild(closeButton); canvas.ctx=ctx; const activeOperationCanvas = document.createElement("canvas"); activeOperationCanvas.width = canvas.width; activeOperationCanvas.height = canvas.height; activeOperationCanvas.ctx = activeOperationCanvas.getContext("2d",{willReadFrequently:true}); const image = canvas.ctx.getAllImageData(); const undoStack = []; const redoStack = []; const layers = []; layers.push( new Layer("base", canvas)); const mask =new Layer("mask", canvas,true); layers.push(mask); layers[0].ctx.putImageData(image,0,0); activeOperationCanvas.ctx.putImageData(image,0,0); eventOverlay.addEventListener("mousedown", handleMouseDown); eventOverlay.addEventListener("mouseup", handleMouseUp); eventOverlay.addEventListener("mousemove", handleMouseMove); eventOverlay.addEventListener("contextmenu",function(e){e.preventDefault(); return false;}); const result = {element,eventOverlay,image,layers,canvas,mask, scale:1, scalefactor:0, offsetX:0, offsetY:0, isDrawing:false, activeLayer:layers[0], strokeCoordinates :[], strokeModifier: a=>a, setTransform( ) { const {element,scale,offsetX,offsetY} = this; element.style.setProperty("--scalefactor",scale) element.style.setProperty("--translateX",offsetX+"px") element.style.setProperty("--translateY",offsetY+"px") //var trans =`translate(${offsetX}px,${offsetY}px) scale(${scale},${scale})`; //$(element).css("transform",trans); }, bringToFront() { const oldPos = picStack.indexOf(this); if (oldPos >= 0) { picStack.splice(oldPos,1); } picStack.unshift(this); let z = 100; for (const {element} of picStack) { element.style.zIndex=z; element.setAttribute("data-z",z); z-=1; } }, composite(suppressMask=false) { canvas.ctx.save(); canvas.ctx.clearRect(0,0,canvas.width,canvas.height) for (const layer of this.layers) { if (suppressMask && this.mask==layer) continue; if (!layer.visible) continue; canvas.ctx.globalAlpha = layer.opacity; canvas.ctx.globalCompositeOperation=layer.compositeOperation; if (this.isDrawing && layer===this.activeLayer) { canvas.ctx.drawImage(activeOperationCanvas,0,0); } else { canvas.ctx.drawImage(layer.canvas,0,0); } } canvas.ctx.restore(); }, commit() { let data = activeOperationCanvas.ctx.getAllImageData() const undoRecord = { layer: this.activeLayer, data: this.activeLayer.ctx.getAllImageData() } undoStack.push(undoRecord); if (undoStack.length > undoDepth) undoStack.shift(); redoStack.length=0; this.activeLayer.ctx.putImageData(data,0,0); this.isDrawing = false; if (selectedExport===this) { if (this.activeLayer===this.mask) { transmitMask(this.mask.canvas); } else { this.composite(true); transmitCanvas(canvas); } } if (activePic===this) { updateLayerList(); //inefficient to remake all controls on edit, fix this } this.updateVisualRepresentation(); }, updateVisualRepresentation(transmit=false) { this.composite(); if (transmit && this===selectedExport) transmitCanvas(canvas); }, startDraw(x,y) { if (!this.activeLayer.visible) return; //don't draw on hidden layers. this.strokeModifier=mirrorFunction; //let data = this.activeLayer.ctx.getAllImageData() //activeOperationCanvas.ctx.putImageData(data,0,0); this.isDrawing=true; this.strokeCoordinates=[{x:x+0.001,y}]; tip.x=x; tip.y=y; this.draw(x,y); }, stopDraw(x,y) { const self=this; if (updatingStroke) { setTimeout(_=>self.stopDraw(x,y),1) } else { this.isDrawing=false; this.commit(); } }, draw(x,y) { this.strokeCoordinates.push({x,y}); //This is done in a timeout to allow multiple draw movements //to accumulate rather than slowing things down if (!updatingStroke) { updatingStroke=true; setTimeout(_=>{ activeOperationCanvas.ctx.clearRect(0,0,activeOperationCanvas.width,activeOperationCanvas.height); activeOperationCanvas.ctx.drawImage(this.activeLayer.canvas,0,0); const unitRange=this.strokeCoordinates.map(({x,y})=>({x:x/canvas.width,y:y/canvas.height})); const strokes = this.strokeModifier([unitRange]) for (const unitStroke of strokes) { const stroke=unitStroke.map(({x,y})=>({x:x*canvas.width,y:y*canvas.height})); tip.drawOperation(activeOperationCanvas.ctx,tip,stroke) } if (this.activeLayer.mask) { const ctx=activeOperationCanvas.ctx; ctx.save(); ctx.globalCompositeOperation="source-atop"; ctx.fillStyle = this.activeLayer.maskColor; ctx.fillRect(0,0,activeOperationCanvas.width,activeOperationCanvas.height); ctx.restore(); } this.composite(); updatingStroke=false; },1) } }, removeLayer(layer=this.activeLayer) { const i = layers.indexOf(layer); if (i !==-1) { layers.splice(i,1); if (layer === this.activeLayer) { this.activeLayer = layers[Math.min(layers.length-1,i)]; } } this.updateVisualRepresentation(true); }, addEmptyLayer() { let result = new Layer("new layer", canvas) layers.push(result); return result; }, insertLayer(layer, below = null) { const indexExisting = layers.indexOf(layer); if (indexExisting !== -1) { layers.splice(indexExisting, 1); } const indexBelow = layers.indexOf(below); if (indexBelow !== -1) { layers.splice(indexBelow, 0, layer); } else { layers.push(layer); } } } eventOverlay.pic = result; sidebar.addEventListener("mousedown",_=> setExportPic(result)) closeButton.addEventListener("click",_=> closePic(result)) result.setTransform(); setActivePic(result) return result; } function initPaint(){ var palette=$("#palette"); for (let i of initialPalette) { const e=`
`; palette.append($(e).data("colour",i)); } palette.append($(`
Erase
`).data("colour","#0000")) poulateLayerControl(); $(".background")[0].addEventListener("wheel",handleMouseWheel); $(".paletteentry").on("mousedown", function(e) { if (![feltTip,pixelTip].includes(pen)) { pen=feltTip; } let eraser=false; let c= $(e.currentTarget).data("colour"); if (c==="#0000") { c="#ffffff"; eraser = true; } if (e.which == 1) { $("#foreground").val(c).data("eraser",eraser); } if (e.which == 3) { $("#background").val(c).data("eraser",eraser); } }).on("contextmenu", function(){return false;} ); $("#pixels").on("click",function(e) {pen=pixelTip;}); $("#tip1").on("click",function(e) {tip.size=1; pen=feltTip; }); $("#tip2").on("click",function(e) {tip.size=2; pen=feltTip;}); $("#tip3").on("click",function(e) {tip.size=3; pen=feltTip;}); $("#tip5").on("click",function(e) {tip.size=5; pen=feltTip;}); $("#tip9").on("click",function(e) {tip.size=9; pen=feltTip;}); $("#eraser").on("click",function(e) {tip.size=9; pen=eraserTip;}); $("#fine_eraser").on("click",function(e) {tip.size=9; pen=pixelClear;}); $(".pen.button").on("click", function(e) { $(".pen.button").removeClass("down"); $(e.currentTarget).addClass("down"); brushSizeControl.diameter=tip.size; }); brushSizeControl.addEventListener("changed", e=> { tip.size=brushSizeControl.diameter; for (let p of $(".pic")) { updateBrushCursor(p); } }); $(".panel").append(brushSizeControl).append(`
`); $('.grid_mirrors input').on("change",_=>{updateMirrorGridButtonImage(); updateMirrors()}) $('.rotational_mirrors input').on("change",_=>{updateMirrorRotationButtonImage(); updateMirrors()}) $('.mirror.button').on("mousedown", e=>{ e.currentTarget.classList.toggle('down'); updateMirrors(); }) updateMirrorGridButtonImage(); updateMirrorRotationButtonImage(); window.test1=createDrawArea(); window.test2=createDrawArea(); $("#workspace").append(test1.element); $("#workspace").append(test2.element); brushSizeControl.diameter=tip.size; setExportPic(test2); } function addNewImage(image) { console.log( `addNewImage(${image.width},${image.height})`) const target=blankCanvas(image.width,image.height) var ctx = target.getContext('2d'); ctx.drawImage(image, 0, 0); let result =createDrawArea(target); $("#workspace").append(result.element); //transmitCanvas(target); return(result) } function addNewLayer(image) { if (!selectedExport) { setExportPic(addNewImage(image)); return; } let pic = selectedExport; if (pic.canvas.width === image.width && pic.canvas.height==image.height) { const layer = new Layer("generation",pic.canvas); layer.ctx.drawImage(image,0,0); console.log("new layer", layer) console.log(layer.canvas.width, layer.canvas.height) pic.layers.push(layer); pic.activeLayer=layer; updateLayerList(); pic.updateVisualRepresentation(); } } function handleMouseDown(e) { //console.log("mousedown on pic",e); let pic = e.currentTarget.pic; setActivePic(pic); const maskLayer = activePic.activeLayer.mask switch (e.button) { case 0: tip.drawOperation = $("#foreground").data("eraser")?eraserTip:pen; tip.colour = maskLayer?"#000":$("#foreground").val(); pic.startDraw(e.offsetX,e.offsetY); createCaptureOverlay(e.currentTarget) break; case 1: e.preventDefault(); dragStartX=pic.offsetX; dragStartY=pic.offsetY; mouseDownX=e.clientX; mouseDownY=e.clientY; $(e.currentTarget.pic.element).css("transition" , "none"); dragging = true; createCaptureOverlay(e.currentTarget) break; case 2: tip.colour = $("#background").val(); tip.drawOperation = (maskLayer || $("#background").data("eraser"))?eraserTip:pen; pic.startDraw(e.offsetX,e.offsetY); createCaptureOverlay(e.currentTarget) e.preventDefault(); break; } } function handleMouseLeave(e) { const bounds=e.currentTarget.getBoundingClientRect(); let pic = e.currentTarget.pic; if (dragging) { console.log("mouseleave on pic",e); let cx = e.pageX-bounds.left; let cy = e.pageY-bounds.top; console.log({cx,cy}) if (e.buttons!==4) { stopDragging(); return; } var dx = e.clientX-mouseDownX; var dy = e.clientY-mouseDownY; console.log({dx,dy}) pic.offsetX=dragStartX+dx; pic.offsetY=dragStartY+dy; pic.setTransform(); } } function handleMouseUp(e) { let pic = e.currentTarget.pic; activePic = pic; if (dragging) { if (e.buttons!==4) { stopDragging(); removeCaptureOverlay(); return; } } if (pic.isDrawing && e.buttons === 0) { removeCaptureOverlay(); pic.stopDraw(e.offsetX,e.offsetY) } } function handleMouseMove(e) { let pic = e.currentTarget.pic; if (pic!==activePic) return; if (dragging) { if (e.buttons!==4) { stopDragging(); removeCaptureOverlay(); return; } var dx = e.clientX-mouseDownX; var dy = e.clientY-mouseDownY; pic.offsetX=dragStartX+dx; pic.offsetY=dragStartY+dy; //console.log({dx,dy}); pic.setTransform(); } if (pic.isDrawing && e.buttons === 0) { removeCaptureOverlay(); pic.stopDraw(e.offsetX,e.offsetY) } if (pic.isDrawing) pic.draw(e.offsetX,e.offsetY); } function handleMouseWheel(e) { let direction = -Math.sign(e.deltaY); const scaleAround= {x:e.pageX,y:e.pageY}; setScale(activePic.scalefactor+direction,scaleAround); activePic.setTransform(); } function scalefactorToSize(n) { return (Math.pow(2,(n/2))); } function setScale(newfactor, around) { const pic = activePic const bounds = pic.element.getBoundingClientRect(); const parentBounds = pic.element.offsetParent.getBoundingClientRect(); const center = {x : bounds.x+bounds.width/2, y:bounds.y+bounds.width/2}; if (!insideBounds(around,bounds)) around = center; const px = (around.x - bounds.left) /bounds.width; const py = (around.y - bounds.top) / bounds.height; const newScale = scalefactorToSize(newfactor); const expectedWidth = pic.element.clientWidth*newScale const expectedHeight = pic.element.clientHeight*newScale const candidateX = around.x - expectedWidth *px - parentBounds.left; const candidateY = around.y - expectedHeight *py - parentBounds.top; pic.offsetX = candidateX pic.offsetY = candidateY pic.scale=newScale; pic.scalefactor=newfactor; pic.setTransform(); updateBrushCursor(pic.element); } function pixelTip(ctx,penInfo,strokePath) { console.log(penInfo) ctx.fillStyle=penInfo.colour; for (let {x,y} of strokePath) { x=Math.floor(x-0.25); y=Math.floor(y-0.25); ctx.fillRect(x,y,1,1); }; } function feltTip(ctx,penInfo,strokePath) { ctx.lineWidth=penInfo.size; ctx.strokeStyle=penInfo.colour; ctx.lineCap="round"; ctx.lineJoin="round"; ctx.beginPath(); for (let {x,y} of strokePath) { ctx.lineTo(x,y); } ctx.stroke(); } function eraserTip(ctx,penInfo,strokePath) { ctx.save(); ctx.lineWidth=penInfo.size; ctx.strokeStyle="white"; ctx.lineCap="round"; ctx.lineJoin="round"; ctx.globalCompositeOperation="destination-out"; ctx.beginPath(); for (let {x,y} of strokePath) { ctx.lineTo(x,y); } ctx.stroke(); ctx.restore(); } function pixelClear(ctx,penInfo,strokePath) { let x=Math.floor(penInfo.x-0.25); let y=Math.floor(penInfo.y-0.25); ctx.clearRect(x,y,1,1); for (let {x,y} of strokePath) { x=Math.floor(x-0.25); y=Math.floor(y-0.25); ctx.clearRect(x,y,1,1); }; } var mouseDownX=0; var mouseDownY=0; function stopDragging() { dragging=false; //console.log("stop dragging"); $(activePic.element).css("transition" , ""); } function brushDiameterControl(id="diameter") { const element = document.createElement("canvas"); element.className="diameter_control" element.id=id; const ctx = element.getContext("2d"); const max_diameter = 48; element.width=192; element.height=max_diameter; //radius divider removes one radius for the half circle at the end of the control const radiusDivider = (element.width/(max_diameter/2))-1; let diameter = 15; function redraw() { let radius = diameter/2; let max_radius = max_diameter/2; ctx.clearRect(0,0,element.width,element.height); ctx.beginPath(); ctx.arc(element.width-max_radius,max_radius,max_radius,-Math.PI/2,Math.PI/2); ctx.lineTo(0,max_radius); ctx.fillStyle="#8888"; ctx.fill(); ctx.beginPath(); ctx.arc(radius*radiusDivider,max_radius,radius,0,Math.PI*2); ctx.fillStyle="#000"; ctx.fill(); ctx.fillText(diameter,8,10); } Object.defineProperty(element, 'radius', { get() { return this.diameter/2; }, set(value) {this.diameter=value*2} }); Object.defineProperty(element, 'diameter', { get() { return diameter; }, set(value) { value=Math.round(value); if (value<1) value=1; if (value>max_diameter) value=max_diameter; if (value !== diameter) { diameter=value; element.dispatchEvent(new Event("changed")); } redraw(); } }); let dragging = false; function handleMouseDown(e) { if (e.button !==0 ) return let {x,y} = containerToCanvas(element,e.clientX,e.clientY) e.stopPropagation(); element.radius=x/radiusDivider; dragging=(y>0) && (y
`) $(layer_control).append(content); $('select.composite-mode').on("change", e=>{ const mode = e.currentTarget.value; activePic.activeLayer.compositeOperation=mode; activePic.updateVisualRepresentation(); }) $("input.maskColor").on("change", e=>{ lastUsedMaskColor = e.currentTarget.value; activePic.activeLayer.maskColor=lastUsedMaskColor; activePic.updateVisualRepresentation(); updateLayerList(); }); $("input.opacity").on("input", e=> { activePic.activeLayer.opacity = e.currentTarget.value/100; activePic.updateVisualRepresentation(true); //shouldn't need this updateLayerList(); }); $(".add_layer").on("click",_=>{ activePic.activeLayer = activePic.addEmptyLayer(); updateLayerList(); }); $(".remove_layer").on("click",_=>{ activePic.removeLayer(activePic.activeLayer) updateLayerList(); }); let dropzone =$(".layer_list.layer_dropzone"); dropzone.on("dragover",e=>{ dropzone.children().removeClass("insert_after"); dropzone.removeClass("insert_top") const insertPoint=findInsertPoint(dropzone[0],e); if (insertPoint !== draggingElement) { if (insertPoint) { insertPoint.classList.add("insert_after"); } else { dropzone.addClass("insert_top") } e.preventDefault(); } }); dropzone.on("drop", e=>{ dropzone.removeClass("insert_top") const insertPoint=findInsertPoint(dropzone[0],e); if (insertPoint !== draggingElement) { if (draggingElement) { const layer = draggingElement.layer; const below = insertPoint?.layer; activePic.insertLayer(layer,below); updateLayerList(); activePic.updateVisualRepresentation(); } else { //handle file drop maybe? } } }) } function findInsertPoint(dropzone, event) { const dropzoneRect = dropzone.getBoundingClientRect(); const children = Array.from(dropzone.children); // Normalize the event clientY coordinate to the dropzone's coordinate space const eventClientY = event.clientY - dropzoneRect.top; for (let i = 0; i < children.length; i++) { const child = children[i]; const childRect = child.getBoundingClientRect(); const childTop = childRect.top - dropzoneRect.top; const childBottom = childTop + childRect.height; if (eventClientY >= childTop && eventClientY <= childBottom) { if (child === draggingElement) { return draggingElement; } if (eventClientY < childTop + childRect.height / 2) { // Event is in the top half of the child element return i === 0 ? null : children[i - 1]; } else { // Event is in the bottom half of the child element return child; } } } return children.length > 0 ? children[children.length - 1] : null; } function updateLayerList() { function makeLayerWidget(layer) { const pic=activePic; const result = $(`
${layer.title}
${layer.mask?`
`:''}
`)[0]; window.dummyGlobal=result; const canvas = result.querySelector(".thumbnail"); const ctx=canvas.getContext("2d"); ctx.drawImage(layer.canvas,0,0,canvas.width,canvas.height); result.layer=layer; result.querySelector(".visibilitybox").onmousedown = e => { e.stopPropagation(); layer.visible=!layer.visible; pic.updateVisualRepresentation(true); updateLayerList(); } if (layer.mask) { result.querySelector(".checkbox").onmousedown = e => { e.stopPropagation(); pic.mask= (pic.mask===layer) ? null : layer; updateLayerList(); } } result.onmousedown = e=> { if (e.button==0) { if (pic.activeLayer !== layer) { pic.activeLayer=layer; updateLayerList() } } } result.ondrag = e=>{ draggingElement=e.currentTarget; } result.ondragend = e=> { if (draggingElement === e.currentTarget ) draggingElement=null; } return result; }; const layer_control=document.querySelector("#layer_control") const layer_list =layer_control.querySelector(".layer_list") while(layer_list.firstChild) layer_list.removeChild(layer_list.firstChild) if (!activePic) return; const newControls = activePic.layers.map(makeLayerWidget); newControls.reverse().forEach(element=>layer_list.appendChild(element)) $(".layer-attributes").toggleClass("mask", activePic.activeLayer.mask) lastUsedMaskColor=activePic.activeLayer.maskColor; $("input.maskColor").val(lastUsedMaskColor) $("input.opacity").val((activePic.activeLayer.opacity*100)|0) $('select.composite-mode').val(activePic.activeLayer.compositeOperation); } function updateBrushCursor(picElement) { const p=picElement; let scale = parseFloat(p.style.getPropertyValue("--scalefactor")); let newCursor = circleImage(tip.size * scale); let offset = (newCursor.width/2)|0; let value = `url(${newCursor.toDataURL()}) ${offset} ${offset}, auto `; if ((newCursor.width < 2) || (newCursor.width>120)) value="crosshair"; p.style.setProperty("cursor",value); } function cloneEvent(event, modifications = {}) { let eventProperties = { "bubbles": event.bubbles, "cancelBubble": event.cancelBubble, "cancelable": event.cancelable, "composed": event.composed, "currentTarget": event.currentTarget, "defaultPrevented": event.defaultPrevented, "eventPhase": event.eventPhase, "isTrusted": event.isTrusted, "target": event.target, "timeStamp": event.timeStamp, "type": event.type, }; // Checking if the event is a MouseEvent, add mouse event properties if(event instanceof MouseEvent) { eventProperties = { ...eventProperties, "altKey": event.altKey, "button": event.button, "buttons": event.buttons, "clientX": event.clientX, "clientY": event.clientY, "ctrlKey": event.ctrlKey, "metaKey": event.metaKey, "movementX": event.movementX, "movementY": event.movementY, "offsetX": event.offsetX, "offsetY": event.offsetY, "pageX": event.pageX, "pageY": event.pageY, "relatedTarget": event.relatedTarget, "screenX": event.screenX, "screenY": event.screenY, "shiftKey": event.shiftKey, }; } return { ...modifications, ...eventProperties }; } function forwardEvent(mouseEvent, recipient) { var rect = recipient.getBoundingClientRect(); const options = cloneEvent(mouseEvent,{ clientX:mouseEvent.clientX - rect.left, clientY:mouseEvent.clientY - rect.top, }); var newEvent = new MouseEvent(mouseEvent.type, options ) recipient.dispatchEvent(newEvent); } function createCaptureOverlay(element) { var overlay = document.createElement('div'); overlay.id = 'global_overlay'; overlay.className = 'fillparent'; document.body.appendChild(overlay); var forwardMouseEvent = function(e) { if (e.buttons==0) removeCaptureOverlay(); forwardEvent(e, element); }; ['mousedown', 'mouseup', 'mousemove', 'click', 'dblclick'].forEach(function(eventName) { overlay.addEventListener(eventName, forwardMouseEvent); }); overlay.addEventListener("contextmenu",function(e){e.preventDefault(); return false;}); } function removeCaptureOverlay() { $('#global_overlay').remove(); }