const stampTool = () => toolbar.registerTool( "/res/icons/file-up.svg", "Stamp Image", (state, opt) => { state.loaded = true; // Draw new cursor immediately ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); state.movecb({...mouse.coords.world.pos}); // Start Listeners mouse.listen.world.onmousemove.on(state.movecb); mouse.listen.world.btn.left.onclick.on(state.drawcb); mouse.listen.world.btn.right.onclick.on(state.cancelcb); // For calls from other tools to paste image if (opt && opt.image) { state.addResource( opt.name || "Clipboard", opt.image, opt.temporary === undefined ? true : opt.temporary, false ); state.ctxmenu.uploadButton.disabled = true; state.back = opt.back || null; toolbar.lock(); } else if (opt) { throw Error( "Pasting from other tools must be in format {image, name?, temporary?, back?}" ); } else { state.ctxmenu.uploadButton.disabled = ""; } }, (state, opt) => { state.loaded = false; // Clear Listeners mouse.listen.world.onmousemove.clear(state.movecb); mouse.listen.world.btn.left.onclick.clear(state.drawcb); mouse.listen.world.btn.right.onclick.clear(state.cancelcb); // Deselect state.selected = null; Array.from(state.ctxmenu.resourceList.children).forEach((child) => { child.classList.remove("selected"); }); ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); }, { init: (state) => { state.loaded = false; state.snapToGrid = true; state.resources = []; state.selected = null; state.back = null; state.lastMouseMove = {x: 0, y: 0}; state.selectResource = (resource, nolock = true) => { if (nolock && state.ctxmenu.uploadButton.disabled) return; console.debug( `[stamp] Selecting Resource '${resource && resource.name}'[${ resource && resource.id }]` ); const resourceWrapper = resource && resource.dom.wrapper; const wasSelected = resourceWrapper && resourceWrapper.classList.contains("selected"); Array.from(state.ctxmenu.resourceList.children).forEach((child) => { child.classList.remove("selected"); }); // Select if (!wasSelected) { resourceWrapper && resourceWrapper.classList.add("selected"); state.selected = resource; } // If already selected, clear selection else { resourceWrapper.classList.remove("selected"); state.selected = null; } ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); if (state.loaded) state.movecb(state.lastMouseMove); }; // Synchronizes resources array with the DOM and Local Storage const syncResources = () => { // Saves to local storage try { localStorage.setItem( "tools.stamp.resources", JSON.stringify( state.resources .filter((resource) => !resource.temporary) .map((resource) => { const canvas = document.createElement("canvas"); canvas.width = resource.image.width; canvas.height = resource.image.height; const ctx = canvas.getContext("2d"); ctx.drawImage(resource.image, 0, 0); return { id: resource.id, name: resource.name, src: canvas.toDataURL(), }; }) ) ); } catch (e) { console.warn( "[stamp] Failed to synchronize resources with local storage" ); console.warn(e); } // Creates DOM elements when needed state.resources.forEach((resource) => { if ( !state.ctxmenu.resourceList.querySelector( `#resource-${resource.id}` ) ) { console.debug( `[stamp] Creating Resource Element [resource-${resource.id}]` ); const resourceWrapper = document.createElement("div"); resourceWrapper.id = `resource-${resource.id}`; resourceWrapper.classList.add("resource"); const resourceTitle = document.createElement("span"); resourceTitle.textContent = resource.name; resourceTitle.classList.add("resource-title"); resourceWrapper.appendChild(resourceTitle); resourceWrapper.addEventListener("click", () => state.selectResource(resource) ); resourceWrapper.addEventListener("mouseover", () => { state.ctxmenu.previewPane.style.display = "block"; state.ctxmenu.previewPane.style.backgroundImage = `url(${resource.image.src})`; }); resourceWrapper.addEventListener("mouseleave", () => { state.ctxmenu.previewPane.style.display = "none"; }); // Add action buttons const actionArray = document.createElement("div"); actionArray.classList.add("actions"); const renameButton = document.createElement("button"); renameButton.addEventListener( "click", (evn) => { evn.stopPropagation(); const name = prompt("Rename your resource:", resource.name); if (name) { resource.name = name; resourceTitle.textContent = name; syncResources(); } }, {passive: false} ); renameButton.title = "Rename Resource"; renameButton.appendChild(document.createElement("div")); renameButton.classList.add("rename-btn"); const trashButton = document.createElement("button"); trashButton.addEventListener( "click", (evn) => { evn.stopPropagation(); state.ctxmenu.previewPane.style.display = "none"; state.deleteResource(resource.id); }, {passive: false} ); trashButton.title = "Delete Resource"; trashButton.appendChild(document.createElement("div")); trashButton.classList.add("delete-btn"); actionArray.appendChild(renameButton); actionArray.appendChild(trashButton); resourceWrapper.appendChild(actionArray); state.ctxmenu.resourceList.appendChild(resourceWrapper); resource.dom = {wrapper: resourceWrapper}; } }); // Removes DOM elements when needed const elements = Array.from(state.ctxmenu.resourceList.children); if (elements.length > state.resources.length) elements.forEach((element) => { let remove = true; state.resources.some((resource) => { if (element.id.endsWith(resource.id)) { remove = false; } }); if (remove) { console.debug(`[stamp] Sync Removing Element [${element.id}]`); state.ctxmenu.resourceList.removeChild(element); } }); }; // Adds a image resource (temporary allows only one draw, used for pasting) state.addResource = (name, image, temporary = false, nolock = true) => { const id = guid(); const resource = { id, name, image, temporary, }; console.info(`[stamp] Adding Resource '${name}'[${id}]`); state.resources.push(resource); syncResources(); // Select this resource state.selectResource(resource, nolock); return resource; }; // Used for temporary images too state.deleteResource = (id) => { const resourceIndex = state.resources.findIndex((v) => v.id === id); const resource = state.resources[resourceIndex]; if (state.selected === resource) state.selected = null; console.info( `[stamp] Deleting Resource '${resource.name}'[${resource.id}]` ); state.resources.splice(resourceIndex, 1); syncResources(); }; state.movecb = (evn) => { let x = evn.x; let y = evn.y; if (state.snapToGrid) { x += snap(evn.x, 0, 64); y += snap(evn.y, 0, 64); } state.lastMouseMove = evn; ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); // Draw selected image if (state.selected) { ovCtx.drawImage(state.selected.image, x, y); } // Draw current cursor location ovCtx.lineWidth = 3; ovCtx.strokeStyle = "#FFF"; ovCtx.beginPath(); ovCtx.moveTo(x, y + 10); ovCtx.lineTo(x, y - 10); ovCtx.moveTo(x + 10, y); ovCtx.lineTo(x - 10, y); ovCtx.stroke(); }; state.drawcb = (evn) => { let x = evn.x; let y = evn.y; if (state.snapToGrid) { x += snap(evn.x, 0, 64); y += snap(evn.y, 0, 64); } const resource = state.selected; if (resource) { commands.runCommand("drawImage", "Image Stamp", { image: resource.image, x, y, }); if (resource.temporary) { state.deleteResource(resource.id); } } if (state.back) { toolbar.unlock(); const backfn = state.back; state.back = null; backfn({message: "Returning from stamp", pasted: true}); } }; state.cancelcb = (evn) => { state.selectResource(null); if (state.back) { toolbar.unlock(); const backfn = state.back; state.back = null; backfn({message: "Returning from stamp", pasted: false}); } }; /** * Creates context menu */ if (!state.ctxmenu) { state.ctxmenu = {}; // Snap To Grid Checkbox state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox( state, "snapToGrid", "Snap To Grid" ).label; // Create resource list const uploadButtonId = `upload-btn-${guid()}`; const resourceManager = document.createElement("div"); resourceManager.classList.add("resource-manager"); const resourceList = document.createElement("div"); resourceList.classList.add("resource-list"); const previewPane = document.createElement("div"); previewPane.classList.add("preview-pane"); const uploadLabel = document.createElement("label"); uploadLabel.classList.add("upload-button"); uploadLabel.classList.add("button"); uploadLabel.classList.add("tool"); uploadLabel.textContent = "Upload Image"; uploadLabel.htmlFor = uploadButtonId; const uploadButton = document.createElement("input"); uploadButton.id = uploadButtonId; uploadButton.type = "file"; uploadButton.accept = "image/*"; uploadButton.multiple = true; uploadButton.style.display = "none"; uploadButton.addEventListener("change", (evn) => { [...uploadButton.files].forEach((file) => { if (file.type.startsWith("image/")) { console.info("Uploading Image " + file.name); const url = window.URL || window.webkitURL; const image = document.createElement("img"); image.src = url.createObjectURL(file); image.onload = () => state.addResource(file.name, image, false); } }); uploadButton.value = null; }); uploadLabel.appendChild(uploadButton); resourceManager.appendChild(resourceList); resourceManager.appendChild(uploadLabel); resourceManager.appendChild(previewPane); resourceManager.addEventListener( "drop", (evn) => { evn.preventDefault(); resourceManager.classList.remove("dragging"); if (evn.dataTransfer.items) { Array.from(evn.dataTransfer.items).forEach((item) => { if (item.kind === "file" && item.type.startsWith("image/")) { const file = item.getAsFile(); const url = window.URL || window.webkitURL; const image = document.createElement("img"); image.src = url.createObjectURL(file); state.addResource(file.name, image, false); } }); } }, {passive: false} ); resourceManager.addEventListener( "dragover", (evn) => { evn.preventDefault(); }, {passive: false} ); resourceManager.addEventListener("dragover", (evn) => { resourceManager.classList.add("dragging"); }); resourceManager.addEventListener("dragover", (evn) => { resourceManager.classList.remove("dragging"); }); state.ctxmenu.uploadButton = uploadButton; state.ctxmenu.previewPane = previewPane; state.ctxmenu.resourceManager = resourceManager; state.ctxmenu.resourceList = resourceList; // Performs resource fetch from local storage (async () => { const storageResources = localStorage.getItem( "tools.stamp.resources" ); if (storageResources) { const parsed = JSON.parse(storageResources); state.resources.push( ...(await Promise.all( parsed.map((resource) => { const image = document.createElement("img"); image.src = resource.src; return new Promise((resolve, reject) => { image.onload = () => resolve({id: resource.id, name: resource.name, image}); }); }) )) ); syncResources(); } })(); } }, populateContextMenu: (menu, state) => { menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(state.ctxmenu.resourceManager); }, shortcut: "U", } );