work on labels

This commit is contained in:
Josh de Leeuw 2022-03-10 13:22:33 -05:00
parent 919f7e63db
commit 83b3b965fa
2 changed files with 234 additions and 110 deletions

View File

@ -15,7 +15,9 @@
prompt: '<p>Label the face.</p>', prompt: '<p>Label the face.</p>',
labels: [ labels: [
'Mouth', 'Mouth',
'Eye' 'Eye',
'Nose',
'Ear'
], ],
regions: [ regions: [
{left: 150, right: 225, top:180, bottom:200}, {left: 150, right: 225, top:180, bottom:200},

View File

@ -58,8 +58,8 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
private display_element: HTMLElement; private display_element: HTMLElement;
private deselect_all_flag = true; private deselect_all_flag = true;
private categories = []; private categories = [];
private active_category = { id: null, label: "?", color: "#444" };
private palette: Array<string>; private palette: Array<string>;
private labelDialog: LabelDialog;
constructor(private jsPsych: JsPsych) {} constructor(private jsPsych: JsPsych) {}
@ -88,6 +88,11 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
} }
} }
showLabelDialog(target: AnnotationBox, x, y) {
this.labelDialog.setTarget(target);
this.labelDialog.show(x, y);
}
private renderDisplay(trial) { private renderDisplay(trial) {
let html = `<div id="jspsych-annotation-display">`; let html = `<div id="jspsych-annotation-display">`;
if (trial.prompt !== null) { if (trial.prompt !== null) {
@ -98,37 +103,19 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
<div id='annotated-image-container'> <div id='annotated-image-container'>
<img src="${trial.image}" draggable="false"></img> <img src="${trial.image}" draggable="false"></img>
</div> </div>
<div id='annotation-options'>
`;
let i = 0;
for (const l of trial.labels) {
html += `<div style="--main-color:${trial.colors[i]}"><input type="radio" id="opt${i}" name="annotate_label" value="${l}"><label for="opt${i}">${l}</label></div>`;
i++;
}
// html += `
// <div><input type="radio" id="opt${i}" name="annotate_label" value=""><label for="opt${i}"><input id="opt${i}_text" type="text"></label></div>
// `;
html += `
</div>
</div> </div>
`; `;
html += `</div>`; html += `</div>`;
this.display_element.innerHTML = html; this.display_element.innerHTML = html;
this.add_new_label();
this.img_container = this.display_element.querySelector("#annotated-image-container"); this.img_container = this.display_element.querySelector("#annotated-image-container");
this.labelDialog = new LabelDialog(true, this.categories, this.img_container);
} }
private addEvents(trial) { private addEvents(trial) {
this.img_container.addEventListener("mousedown", this.start_box); this.img_container.addEventListener("mousedown", this.start_box);
const radios = this.display_element.querySelectorAll('input[type="radio"]');
for (const r of Array.from(radios)) {
r.addEventListener("change", this.handle_radio_change);
}
this.img_container.addEventListener("mousemove", this.sort_boxes); this.img_container.addEventListener("mousemove", this.sort_boxes);
document.addEventListener("mousedown", () => { document.addEventListener("mousedown", () => {
@ -145,8 +132,46 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
display: flex; display: flex;
} }
#jspsych-annotation-display #annotation-options div { #annotation-dialog {
margin-bottom: 0.5em; position: absolute;
z-index: 999;
border-radius: 2px;
box-shadow: 2px 2px 4px rgba(0,0,0,0.5);
background: white;
transform: scale(1);
transform-origin: top left;
transition: transform 0.3s, top 0.3s, left 0.3s;
display: block;
cursor: initial;
}
#annotation-dialog.hidden {
transform: scale(0);
}
#annotation-dialog-close {
text-align: right;
}
#annotation-dialog-close-btn {
border: 0;
background-color: white;
color: #999;
font-size: 20px;
cursor: pointer;
}
#annotation-dialog-close-btn:hover {
color: black;
}
#annotation-options {
text-align: left;
display: flex;
row-gap: 0.25em;
flex-wrap: wrap;
margin-bottom: 0.25em;
padding: 0px 10px;
} }
#jspsych-annotation-display input[type="radio"] { #jspsych-annotation-display input[type="radio"] {
@ -158,32 +183,37 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
background-color: var(--main-color); background-color: var(--main-color);
color: white; color: white;
padding: 0.35em 0.5em; padding: 0.35em 0.5em;
margin-bottom: 0.25em; margin-right: 0.25em;
cursor: pointer; cursor: pointer;
display: block; display: block;
line-height: normal; line-height: normal;
transition: margin-right 0.2s; font-size:12px;
} }
#jspsych-annotation-display input[type="radio"] + label::before { .annotation-label-container {
content: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="white"><path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"/></svg>'); flex-shrink: 0;
}
#jspsych-annotation-display input[type="radio"] + label::after {
content: "";
border: 1px solid white;
border-radius: 20px;
height: 1em;
width: 1em;
margin-left: 0.25em;
vertical-align: middle;
display: inline-block; display: inline-block;
padding-right: 0.5em;
position: relative;
top: 0.15em;
transform: scale(0);
transition: transform 0.2s;
} }
#jspsych-annotation-display input[type="radio"]:checked + label::before { #jspsych-annotation-display input[type="radio"]:checked + label::after {
transform: scale(1); content: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="white"><path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"/></svg>');
} }
#jspsych-annotation-display label input[type="text"] { #jspsych-annotation-display label input[type="text"] {
background: none; background: none;
border: none; border: none;
color: white; color: white;
font-size: 18px; font-size: 12px;
margin: 0; margin: 0;
padding:0; padding:0;
} }
@ -209,16 +239,22 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
display: block; display: block;
} }
#jspsych-annotation-display #annotated-image-container .annotation-box { .annotation-box {
border: 1px solid var(--main-color); border: 1px solid var(--main-color);
position: absolute; position: absolute;
color: var(--main-color); color: var(--main-color);
user-select: none; user-select: none;
background-color: var(--very-transparent-color); background-color: var(--very-transparent-color);
cursor: pointer;
} }
#jspsych-annotation-display #annotated-image-container .annotation-box:hover { .annotation-box::before {
content: "";
position: absolute;
height: calc(100% + 8px);
width: calc(100% + 8px);
top: -4px;
left: -4px;
} }
.annotation-box-label { .annotation-box-label {
@ -248,14 +284,7 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
border: 1px solid white; border: 1px solid white;
} }
.annotation-box::before {
content: "";
position: absolute;
height: calc(100% + 8px);
width: calc(100% + 8px);
top: -4px;
left: -4px;
}
.annotation-box-remove { .annotation-box-remove {
visibility: hidden; visibility: hidden;
@ -358,10 +387,6 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
visibility: visible; visibility: visible;
} }
#jspsych-annotation-display #annotation-options {
text-align: left;
padding-left: 24px;
}
</style>` </style>`
); );
} }
@ -391,7 +416,7 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
if (this.is_drawing) { if (this.is_drawing) {
this.active_box.finishDrawing(); this.active_box.finishDrawing();
this.active_box.setCategory(this.active_category); // this.active_box.setCategory(this.active_category);
this.active_box.select(); this.active_box.select();
@ -415,27 +440,6 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
} }
} }
select_label(label: string) {
const radio: HTMLFormElement = this.display_element.querySelector(`input[value='${label}']`);
if (radio) {
radio.checked = true;
} else {
const radios = this.display_element.querySelectorAll('input[type="radio"]');
for (const r of Array.from(radios)) {
(r as HTMLFormElement).checked = false;
}
}
}
private handle_radio_change(e) {
this.active_category = this.categories[(e.target as HTMLFormElement).id.substring(3, 4)];
for (const b of this.boxes) {
if (b.isSelected()) {
b.setCategory(this.active_category);
}
}
}
private deselect_all(e) { private deselect_all(e) {
if (this.deselect_all_flag && !["RADIO", "LABEL", "INPUT"].includes(e.target.tagName)) { if (this.deselect_all_flag && !["RADIO", "LABEL", "INPUT"].includes(e.target.tagName)) {
for (const b of this.boxes) { for (const b of this.boxes) {
@ -443,19 +447,103 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
} }
} }
} }
}
class LabelDialog {
private element: HTMLElement;
private container: HTMLElement;
private target: AnnotationBox;
private categories = [];
private allow_user_generated_labels: boolean;
constructor(allow_user_generated_labels, categories, container) {
autoBind(this);
this.allow_user_generated_labels = allow_user_generated_labels;
this.container = container;
this.categories = categories;
this.initRender();
}
initRender() {
const el = document.createElement("div");
el.id = "annotation-dialog";
el.className = "hidden";
let html = `
<div id='annotation-dialog-close'>
<button id='annotation-dialog-close-btn'>&times;</button>
</div>
`;
html += `<div id='annotation-options'>`;
let i = 0;
for (const cat of this.categories) {
html += `<div class="annotation-label-container" style="--main-color:${cat.color}"><input type="radio" id="opt${i}" name="annotate_label" value="${cat.label}"><label for="opt${i}">${cat.label}</label></div>`;
i++;
}
html += `</div>`;
html += `
<div id='annotation-dialog-remove'>
<button id='annotation-dialog-remove-btn'>Remove</button>
</div>
`;
el.innerHTML = html;
this.element = el;
this.container.appendChild(this.element);
this.addEvents();
if (this.allow_user_generated_labels) {
this.add_new_label();
}
}
private addEvents() {
const radios = this.element.querySelectorAll('input[type="radio"]');
for (const r of Array.from(radios)) {
r.addEventListener("change", this.handle_radio_change);
}
this.element.querySelector("#annotation-dialog-close-btn").addEventListener("click", (e) => {
e.stopPropagation();
this.hide();
});
this.element.addEventListener("mousedown", (e) => {
e.stopPropagation();
});
}
show(x, y) {
requestAnimationFrame(() => {});
this.element.style.top = `${y}px`;
this.element.style.left = `${x}px`;
this.element.classList.remove("hidden");
}
hide() {
this.element.classList.add("hidden");
}
setTarget(box: AnnotationBox) {
this.target = box;
}
private add_new_label(e?) { private add_new_label(e?) {
const container = this.display_element.querySelector("#annotation-options");
const category_id = this.categories.length; const category_id = this.categories.length;
const container = this.element.querySelector("#annotation-options");
this.categories.push({ this.categories.push({
id: category_id, id: category_id,
label: "", label: "",
color: this.palette[category_id], color: "#ab37f2",
}); });
const html = ` const html = `
<div style="--main-color: ${this.palette[category_id]}"><input type="radio" id="opt${category_id}" name="annotate_label" value=""><label for="opt${category_id}"><input id="opt${category_id}_text" type="text"></label></div> <div class="annotation-label-container" style="--main-color: ${"#ab37f2"}"><input type="radio" id="opt${category_id}" name="annotate_label" value=""><label for="opt${category_id}"><input id="opt${category_id}_text" type="text" size="10"></label></div>
`; `;
container.insertAdjacentHTML("beforeend", html); container.insertAdjacentHTML("beforeend", html);
container container
@ -478,6 +566,11 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
.addEventListener("change", this.handle_radio_change); .addEventListener("change", this.handle_radio_change);
} }
private handle_radio_change(e) {
const category = this.categories[(e.target as HTMLFormElement).id.substring(3, 4)];
this.target.setCategory(category);
}
private update_labels(e) { private update_labels(e) {
const text = e.target as HTMLFormElement; const text = e.target as HTMLFormElement;
const radio = text.parentElement.parentElement.querySelector( const radio = text.parentElement.parentElement.querySelector(
@ -487,19 +580,19 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin<Info> {
const old_label = radio.value; const old_label = radio.value;
const new_label = text.value; const new_label = text.value;
for (const b of this.boxes) { // for (const b of this.boxes) {
if (b.getLabel() == old_label) { // if (b.getLabel() == old_label) {
b.setLabel(new_label); // b.setLabel(new_label);
} // }
} // }
radio.value = new_label; radio.value = new_label;
} }
} }
class AnnotationBox { class AnnotationBox {
private label = "?";
private element: HTMLElement; private element: HTMLElement;
private label = "?";
private start_x; private start_x;
private start_y; private start_y;
private end_x; private end_x;
@ -513,6 +606,7 @@ class AnnotationBox {
private modifiable = true; private modifiable = true;
private color = "#444444"; private color = "#444444";
private category = null; private category = null;
private showDialogFlag = false;
constructor(x, y, box_list, container, plugin) { constructor(x, y, box_list, container, plugin) {
autoBind(this); autoBind(this);
@ -560,7 +654,7 @@ class AnnotationBox {
if (label) { if (label) {
this.label = label; this.label = label;
} }
this.element.querySelector(".annotation-box-label").innerHTML = this.label; //this.element.querySelector(".annotation-box-label").innerHTML = this.label;
} }
getLabel() { getLabel() {
@ -611,9 +705,10 @@ class AnnotationBox {
} }
finishDrawing() { finishDrawing() {
// <span class="annotation-box-label"></span>
// <span class="annotation-box-remove">X</span>
this.element.innerHTML = ` this.element.innerHTML = `
<span class="annotation-box-label"></span>
<span class="annotation-box-remove">X</span>
<div class="annotation-box-resize top left"></div> <div class="annotation-box-resize top left"></div>
<div class="annotation-box-resize top right"></div> <div class="annotation-box-resize top right"></div>
<div class="annotation-box-resize bottom left"></div> <div class="annotation-box-resize bottom left"></div>
@ -626,16 +721,46 @@ class AnnotationBox {
} }
addEvents() { addEvents() {
this.element.querySelector(".annotation-box-remove").addEventListener("mousedown", (e) => { // this.element.querySelector(".annotation-box-remove").addEventListener("mousedown", (e) => {
// e.stopPropagation();
// });
// this.element.querySelector(".annotation-box-remove").addEventListener("mouseup", (e) => {
// e.stopPropagation();
// });
// this.element.querySelector(".annotation-box-remove").addEventListener("click", (e) => {
// e.preventDefault();
// this.remove();
// });
this.element.addEventListener("mousedown", (e) => {
e.stopPropagation();
this.showDialogFlag = true;
if (this.modifiable) {
this.startDrag(e);
}
});
this.element.addEventListener("mouseup", (e) => {
this.stopDrag();
this.stopMove();
if (this.showDialogFlag) {
this.plugin.showLabelDialog(
this,
e.clientX - this.container.getBoundingClientRect().left,
e.clientY - this.container.getBoundingClientRect().top
);
}
e.stopPropagation(); e.stopPropagation();
}); });
this.element.querySelector(".annotation-box-remove").addEventListener("mouseup", (e) => {
e.stopPropagation(); // this.container.addEventListener("mouseup", (e)=>{
}); // this.stopMove();
this.element.querySelector(".annotation-box-remove").addEventListener("click", (e) => { // e.stopPropagation();
e.preventDefault(); // })
this.remove();
});
this.element this.element
.querySelector(".annotation-box-resize.bottom.right") .querySelector(".annotation-box-resize.bottom.right")
@ -677,21 +802,17 @@ class AnnotationBox {
this.startMove(); this.startMove();
}); });
this.container.addEventListener("mouseup", () => { // this.element.querySelector(".annotation-box-label").addEventListener("click", (e) => {
this.stopMove(); // e.stopPropagation();
}); // this.select();
// });
this.element.querySelector(".annotation-box-label").addEventListener("click", (e) => { // this.element.querySelector(".annotation-box-label").addEventListener("mousedown", (e) => {
e.stopPropagation(); // if (this.modifiable) {
this.select(); // this.startDrag(e);
}); // }
// e.stopPropagation();
this.element.querySelector(".annotation-box-label").addEventListener("mousedown", (e) => { // });
if (this.modifiable) {
this.startDrag(e);
}
e.stopPropagation();
});
} }
startMove() { startMove() {
@ -731,6 +852,8 @@ class AnnotationBox {
} }
dragHandler(e) { dragHandler(e) {
this.showDialogFlag = false;
const box = this.element.getBoundingClientRect(); const box = this.element.getBoundingClientRect();
const container = this.container.getBoundingClientRect(); const container = this.container.getBoundingClientRect();
@ -775,13 +898,12 @@ class AnnotationBox {
b.deselect(); b.deselect();
} }
this.selected = true; this.selected = true;
this.element.querySelector(".annotation-box-label").classList.add("selected"); //this.element.querySelector(".annotation-box-label").classList.add("selected");
this.plugin.select_label(this.label);
} }
deselect() { deselect() {
this.selected = false; this.selected = false;
this.element.querySelector(".annotation-box-label").classList.remove("selected"); //this.element.querySelector(".annotation-box-label").classList.remove("selected");
} }
isSelected() { isSelected() {