diff --git a/packages/plugin-image-text-annotation/example.html b/packages/plugin-image-text-annotation/example.html index a3b68c70..7941ed50 100644 --- a/packages/plugin-image-text-annotation/example.html +++ b/packages/plugin-image-text-annotation/example.html @@ -15,7 +15,9 @@ prompt: '

Label the face.

', labels: [ 'Mouth', - 'Eye' + 'Eye', + 'Nose', + 'Ear' ], regions: [ {left: 150, right: 225, top:180, bottom:200}, diff --git a/packages/plugin-image-text-annotation/src/index.ts b/packages/plugin-image-text-annotation/src/index.ts index 5aa5b9e0..33598eb6 100644 --- a/packages/plugin-image-text-annotation/src/index.ts +++ b/packages/plugin-image-text-annotation/src/index.ts @@ -58,8 +58,8 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { private display_element: HTMLElement; private deselect_all_flag = true; private categories = []; - private active_category = { id: null, label: "?", color: "#444" }; private palette: Array; + private labelDialog: LabelDialog; constructor(private jsPsych: JsPsych) {} @@ -88,6 +88,11 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { } } + showLabelDialog(target: AnnotationBox, x, y) { + this.labelDialog.setTarget(target); + this.labelDialog.show(x, y); + } + private renderDisplay(trial) { let html = `
`; if (trial.prompt !== null) { @@ -98,37 +103,19 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin {
-
- `; - let i = 0; - for (const l of trial.labels) { - html += `
`; - i++; - } - // html += ` - //
- // `; - html += ` -
`; html += ``; this.display_element.innerHTML = html; - this.add_new_label(); - this.img_container = this.display_element.querySelector("#annotated-image-container"); + + this.labelDialog = new LabelDialog(true, this.categories, this.img_container); } private addEvents(trial) { 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); document.addEventListener("mousedown", () => { @@ -145,8 +132,46 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { display: flex; } - #jspsych-annotation-display #annotation-options div { - margin-bottom: 0.5em; + #annotation-dialog { + 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"] { @@ -158,32 +183,37 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { background-color: var(--main-color); color: white; padding: 0.35em 0.5em; - margin-bottom: 0.25em; + margin-right: 0.25em; cursor: pointer; display: block; line-height: normal; - transition: margin-right 0.2s; + font-size:12px; } - #jspsych-annotation-display input[type="radio"] + label::before { - content: url('data:image/svg+xml; utf8, '); + .annotation-label-container { + 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; - 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 { - transform: scale(1); + #jspsych-annotation-display input[type="radio"]:checked + label::after { + content: url('data:image/svg+xml; utf8, '); } #jspsych-annotation-display label input[type="text"] { background: none; border: none; color: white; - font-size: 18px; + font-size: 12px; margin: 0; padding:0; } @@ -209,16 +239,22 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { display: block; } - #jspsych-annotation-display #annotated-image-container .annotation-box { + .annotation-box { border: 1px solid var(--main-color); position: absolute; color: var(--main-color); user-select: none; 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 { @@ -248,14 +284,7 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { 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 { visibility: hidden; @@ -358,10 +387,6 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { visibility: visible; } - #jspsych-annotation-display #annotation-options { - text-align: left; - padding-left: 24px; - } ` ); } @@ -391,7 +416,7 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { if (this.is_drawing) { this.active_box.finishDrawing(); - this.active_box.setCategory(this.active_category); + // this.active_box.setCategory(this.active_category); this.active_box.select(); @@ -415,27 +440,6 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { } } - 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) { if (this.deselect_all_flag && !["RADIO", "LABEL", "INPUT"].includes(e.target.tagName)) { for (const b of this.boxes) { @@ -443,19 +447,103 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { } } } +} + +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 = ` +
+ +
+ `; + html += `
`; + let i = 0; + for (const cat of this.categories) { + html += `
`; + i++; + } + html += `
`; + + html += ` +
+ +
+ `; + 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?) { - const container = this.display_element.querySelector("#annotation-options"); const category_id = this.categories.length; + const container = this.element.querySelector("#annotation-options"); this.categories.push({ id: category_id, label: "", - color: this.palette[category_id], + color: "#ab37f2", }); const html = ` -
+
`; container.insertAdjacentHTML("beforeend", html); container @@ -478,6 +566,11 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { .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) { const text = e.target as HTMLFormElement; const radio = text.parentElement.parentElement.querySelector( @@ -487,19 +580,19 @@ class ImageTextAnnotationPlugin implements JsPsychPlugin { const old_label = radio.value; const new_label = text.value; - for (const b of this.boxes) { - if (b.getLabel() == old_label) { - b.setLabel(new_label); - } - } + // for (const b of this.boxes) { + // if (b.getLabel() == old_label) { + // b.setLabel(new_label); + // } + // } radio.value = new_label; } } class AnnotationBox { - private label = "?"; private element: HTMLElement; + private label = "?"; private start_x; private start_y; private end_x; @@ -513,6 +606,7 @@ class AnnotationBox { private modifiable = true; private color = "#444444"; private category = null; + private showDialogFlag = false; constructor(x, y, box_list, container, plugin) { autoBind(this); @@ -560,7 +654,7 @@ class AnnotationBox { if (label) { this.label = label; } - this.element.querySelector(".annotation-box-label").innerHTML = this.label; + //this.element.querySelector(".annotation-box-label").innerHTML = this.label; } getLabel() { @@ -611,9 +705,10 @@ class AnnotationBox { } finishDrawing() { + // + // X + this.element.innerHTML = ` - - X
@@ -626,16 +721,46 @@ class AnnotationBox { } 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(); }); - 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.container.addEventListener("mouseup", (e)=>{ + // this.stopMove(); + // e.stopPropagation(); + // }) this.element .querySelector(".annotation-box-resize.bottom.right") @@ -677,21 +802,17 @@ class AnnotationBox { this.startMove(); }); - this.container.addEventListener("mouseup", () => { - this.stopMove(); - }); + // this.element.querySelector(".annotation-box-label").addEventListener("click", (e) => { + // e.stopPropagation(); + // this.select(); + // }); - this.element.querySelector(".annotation-box-label").addEventListener("click", (e) => { - e.stopPropagation(); - this.select(); - }); - - this.element.querySelector(".annotation-box-label").addEventListener("mousedown", (e) => { - if (this.modifiable) { - this.startDrag(e); - } - e.stopPropagation(); - }); + // this.element.querySelector(".annotation-box-label").addEventListener("mousedown", (e) => { + // if (this.modifiable) { + // this.startDrag(e); + // } + // e.stopPropagation(); + // }); } startMove() { @@ -731,6 +852,8 @@ class AnnotationBox { } dragHandler(e) { + this.showDialogFlag = false; + const box = this.element.getBoundingClientRect(); const container = this.container.getBoundingClientRect(); @@ -775,13 +898,12 @@ class AnnotationBox { b.deselect(); } this.selected = true; - this.element.querySelector(".annotation-box-label").classList.add("selected"); - this.plugin.select_label(this.label); + //this.element.querySelector(".annotation-box-label").classList.add("selected"); } deselect() { this.selected = false; - this.element.querySelector(".annotation-box-label").classList.remove("selected"); + //this.element.querySelector(".annotation-box-label").classList.remove("selected"); } isSelected() {