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; | |
}); |