A tiny tool to let you draw regions, mark positions inside the regions and export the positions as percentage coordinates.
Copy and paste all the code from poster_markup.js into the dev console, and press enter.
| // | |
| // ## Globals ## | |
| // | |
| let currentState = null; | |
| let mouse = { | |
| target: null, | |
| down: false, | |
| justPressed: false, | |
| x: 0, | |
| y: 0, | |
| startX: 0, | |
| startY: 0, | |
| }; | |
| // | |
| // ## Helper functions to create DOM elements ## | |
| // | |
| function createOverlayElement() { | |
| const overlay = document.createElement("div"); | |
| overlay.style.position = "fixed"; | |
| overlay.style.background = "rgba(200, 200, 200, 0.2)"; | |
| overlay.style.width = "100%"; | |
| overlay.style.height = "100%"; | |
| overlay.style.top = "0"; | |
| overlay.style.left = "0"; | |
| overlay.style.cursor = "crosshair"; | |
| overlay.style.fontFamily = "sans-serif"; | |
| overlay.style.zIndex = "9999"; | |
| return overlay; | |
| } | |
| function createInstructionsElement( | |
| text = "", | |
| backgroundColor = "rgba(0, 200, 0, 1)" | |
| ) { | |
| const instructions = document.createElement("div"); | |
| instructions.style.background = backgroundColor; | |
| instructions.style.position = "absolute"; | |
| instructions.style.padding = "10px"; | |
| instructions.style.width = "100%"; | |
| instructions.style.color = "white"; | |
| instructions.style.display = "flex"; | |
| instructions.style.boxSizing = "border-box"; | |
| instructions.style.justifyContent = "space-between"; | |
| instructions.textContent = text; | |
| return instructions; | |
| } | |
| function createRegionElement( | |
| bounds = { x: 0, y: 0, width: 0, height: 0 }, | |
| disablePointerEvents = true | |
| ) { | |
| const region = document.createElement("div"); | |
| region.style.position = "absolute"; | |
| region.style.boxShadow = "0 0 0 5px rgba(0, 200, 0, 0.8)"; | |
| region.style.left = bounds.x + "px"; | |
| region.style.top = bounds.y + "px"; | |
| region.style.width = bounds.width + "px"; | |
| region.style.height = bounds.height + "px"; | |
| if (disablePointerEvents) { | |
| region.style.pointerEvents = "none"; | |
| } | |
| return region; | |
| } | |
| // | |
| // ## App states ## | |
| // | |
| const initialState = { | |
| enter() { | |
| this.startButton = document.createElement("button"); | |
| this.startButton.textContent = "Start"; | |
| this.startButton.style.position = "fixed"; | |
| this.startButton.style.zIndex = "99999"; | |
| this.startButton.style.top = 0; | |
| this.startButton.style.left = 0; | |
| this.startButton.addEventListener("click", () => { | |
| transitionTo("drawRegion"); | |
| }); | |
| document.body.appendChild(this.startButton); | |
| }, | |
| exit() { | |
| document.body.removeChild(this.startButton); | |
| }, | |
| update() {}, | |
| }; | |
| const drawRegion = { | |
| // State specfic variables. | |
| showRegion: false, | |
| regionBounds: { x: 0, y: 0, width: 0, height: 0 }, | |
| enter() { | |
| this.regionBounds = { x: 0, y: 0, width: 0, height: 0 }; | |
| this.overlay = createOverlayElement(); | |
| this.region = createRegionElement(this.regionBounds, false); | |
| this.instructions = createInstructionsElement( | |
| "Click and drag to draw a region around the poster." | |
| ); | |
| this.confirmButton = document.createElement("button"); | |
| this.confirmButton.textContent = "Next"; | |
| this.region.addEventListener("click", () => { | |
| transitionTo("markPositions", { | |
| regionBounds: this.regionBounds, | |
| }); | |
| }); | |
| this.confirmButton.addEventListener("click", () => { | |
| transitionTo("markPositions", { | |
| regionBounds: this.regionBounds, | |
| }); | |
| }); | |
| this.overlay.appendChild(this.region); | |
| this.overlay.appendChild(this.instructions); | |
| this.instructions.appendChild(this.confirmButton); | |
| document.body.appendChild(this.overlay); | |
| }, | |
| exit() { | |
| document.body.removeChild(this.overlay); | |
| }, | |
| update() { | |
| const regionBounds = this.region.getBoundingClientRect(); | |
| this.showRegion = regionBounds.width > 0 && regionBounds.height > 0; | |
| this.region.style.opacity = this.showRegion ? "1" : "0"; | |
| if (mouse.down && mouse.target === this.overlay) { | |
| this.region.style.left = mouse.startX + "px"; | |
| this.region.style.top = mouse.startY + "px"; | |
| this.region.style.width = mouse.x - mouse.startX + "px"; | |
| this.region.style.height = mouse.y - mouse.startY + "px"; | |
| } | |
| this.regionBounds = regionBounds; | |
| }, | |
| }; | |
| const markPositions = { | |
| positions: [], | |
| enter(params = {}) { | |
| this.positions = []; | |
| this.overlay = createOverlayElement(); | |
| this.region = createRegionElement(params.regionBounds, false); | |
| this.instructions = createInstructionsElement( | |
| "Click inside the region to mark positions. Click marks to remove them.", | |
| "rgba(150, 150, 150, 1)" | |
| ); | |
| this.exportButton = document.createElement("button"); | |
| this.exportButton.textContent = "Export and close"; | |
| this.exportButton.addEventListener("click", () => { | |
| // TODO: Implement the format the data should be exported | |
| // to. Could be a .csv file? | |
| console.log(this.positions); | |
| transitionTo("initialState"); | |
| }); | |
| this.region.addEventListener("mousedown", (event) => { | |
| if (event.target !== this.region) { | |
| return; | |
| } | |
| const id = Date.now() + "-" + Math.round(Math.random() * 100); | |
| const x = event.offsetX; | |
| const y = event.offsetY; | |
| const percentX = x / params.regionBounds.width; | |
| const percentY = y / params.regionBounds.height; | |
| this.positions.push({ id, x: percentX, y: percentY }); | |
| const marker = document.createElement("div"); | |
| marker.style.position = "absolute"; | |
| marker.style.background = "red"; | |
| marker.style.border = "2px solid white"; | |
| marker.style.boxShadow = "0 0 0 2px red"; | |
| marker.style.borderRadius = "100px"; | |
| marker.style.width = "8px"; | |
| marker.style.height = "8px"; | |
| marker.style.transform = "translate(-50%, -50%)"; | |
| marker.style.left = percentX * 100 + "%"; | |
| marker.style.top = percentY * 100 + "%"; | |
| marker.addEventListener("click", () => { | |
| const positionToRemoveIndex = this.positions.findIndex( | |
| (position) => position.id === id | |
| ); | |
| this.positions.splice(positionToRemoveIndex, 1); | |
| this.region.removeChild(marker); | |
| }); | |
| this.region.appendChild(marker); | |
| }); | |
| this.instructions.appendChild(this.exportButton); | |
| this.overlay.appendChild(this.region); | |
| this.overlay.appendChild(this.instructions); | |
| document.body.appendChild(this.overlay); | |
| }, | |
| exit() { | |
| document.body.removeChild(this.overlay); | |
| }, | |
| update() {}, | |
| }; | |
| // | |
| // ## State machine ## | |
| // | |
| const states = { | |
| initialState, | |
| drawRegion, | |
| markPositions, | |
| }; | |
| function transitionTo(stateName, params = {}) { | |
| const nextState = states[stateName]; | |
| if (!nextState) { | |
| console.warn("No state called", stateName); | |
| return; | |
| } | |
| if (currentState && currentState["exit"]) { | |
| currentState.exit(); | |
| } | |
| currentState = states[stateName]; | |
| if (currentState && currentState["enter"]) { | |
| currentState.enter(params); | |
| } | |
| } | |
| function update() { | |
| if (!currentState || !currentState["update"]) { | |
| console.warn("No current state"); | |
| return; | |
| } | |
| currentState.update(); | |
| window.requestAnimationFrame(update); | |
| } | |
| // | |
| // ## Initialize ## | |
| // | |
| transitionTo("initialState"); | |
| update(); | |
| document.addEventListener("mousemove", (event) => { | |
| mouse.x = event.clientX; | |
| mouse.y = event.clientY; | |
| }); | |
| document.addEventListener("mousedown", (event) => { | |
| mouse.down = true; | |
| mouse.target = event.target; | |
| mouse.startX = event.clientX; | |
| mouse.startY = event.clientY; | |
| }); | |
| document.addEventListener("mouseup", () => { | |
| mouse.down = false; | |
| }); |