Annotate using tags.json
This commit is contained in:
parent
438c09a48f
commit
4a0d605595
3 changed files with 207 additions and 96 deletions
|
@ -124,7 +124,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags li {
|
.tags .tag {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border: solid 1px darkgray;
|
border: solid 1px darkgray;
|
||||||
|
@ -132,16 +132,30 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags li:hover {
|
.tags li{
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.tags .subtags{
|
||||||
|
padding:0;
|
||||||
|
font-size: 80%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tags .subtags .tag{
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags .tag:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: darkgray;
|
background: darkgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags li.selected {
|
.tags .tag.selected {
|
||||||
background: lightsteelblue;
|
background: #3FB8AF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags li.annotation-rm {
|
.tags .tag.annotation-rm {
|
||||||
/* display: none; */
|
/* display: none; */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: red;
|
color: red;
|
||||||
|
@ -156,14 +170,14 @@
|
||||||
/* hide behind bar, instead into nothing */
|
/* hide behind bar, instead into nothing */
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-annotation .tags li.annotation-rm {
|
.selected-annotation .tags .tag.annotation-rm {
|
||||||
color: red;
|
color: red;
|
||||||
display: block;
|
display: block;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags li span {
|
.tags .tag span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
@ -171,9 +185,15 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
.tags .subtags .tag span{
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin-right: 2px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.annotations {
|
.annotations {
|
||||||
height: 10px;
|
height: 30px;
|
||||||
/* border: solid 1px darkgray; */
|
/* border: solid 1px darkgray; */
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -193,33 +213,6 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotation-test {
|
|
||||||
background-color: red !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.annotation-another {
|
|
||||||
background-color: blue !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.annotation-google {
|
|
||||||
background-color: blueviolet !important;
|
|
||||||
}
|
|
||||||
.annotation-map {
|
|
||||||
background-color: red !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.annotation-relation {
|
|
||||||
background-color: blue !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.annotation-text {
|
|
||||||
background-color: blueviolet !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.annotation-figure {
|
|
||||||
background-color: pink !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unsaved::before {
|
.unsaved::before {
|
||||||
content: '*';
|
content: '*';
|
||||||
color: red;
|
color: red;
|
||||||
|
@ -296,7 +289,7 @@
|
||||||
if (location.search) {
|
if (location.search) {
|
||||||
ann = new Annotator(
|
ann = new Annotator(
|
||||||
document.getElementById("interface"),
|
document.getElementById("interface"),
|
||||||
["map", "text", "relation", "figure"],
|
"tags.json",
|
||||||
location.search.substring(1)
|
location.search.substring(1)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -110,10 +110,9 @@ class StrokeSlice {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Annotator extends EventTarget {
|
class Annotator extends EventTarget {
|
||||||
constructor(wrapperEl, tags, fileurl) {
|
constructor(wrapperEl, tagFile, fileurl) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|
||||||
this.formatter = wNumb({
|
this.formatter = wNumb({
|
||||||
decimals: 2,
|
decimals: 2,
|
||||||
edit: (time) => {
|
edit: (time) => {
|
||||||
|
@ -181,61 +180,94 @@ class Annotator extends EventTarget {
|
||||||
this.scrubberEl.classList.add('scrubber')
|
this.scrubberEl.classList.add('scrubber')
|
||||||
this.controlsEl.appendChild(this.scrubberEl);
|
this.controlsEl.appendChild(this.scrubberEl);
|
||||||
|
|
||||||
this.tagsEl = document.createElement('ul');
|
|
||||||
this.tagsEl.classList.add('tags');
|
|
||||||
for (let tag of tags) {
|
|
||||||
let tagEl = document.createElement('li');
|
|
||||||
tagEl.classList.add('tag');
|
|
||||||
tagEl.dataset.tag = tag;
|
|
||||||
tagEl.innerText = tag;
|
|
||||||
tagEl.addEventListener('click', (e) => {
|
|
||||||
this.addTag(tag, this.inPointPosition, this.outPointPosition);
|
|
||||||
})
|
|
||||||
|
|
||||||
let signEl = document.createElement('span');
|
|
||||||
signEl.classList.add('annotation-' + tag);
|
|
||||||
tagEl.prepend(signEl);
|
|
||||||
this.tagsEl.appendChild(tagEl);
|
|
||||||
}
|
|
||||||
let tagEl = document.createElement('li');
|
|
||||||
tagEl.classList.add('tag');
|
|
||||||
tagEl.classList.add('annotation-rm');
|
|
||||||
tagEl.dataset.tag = 'rm';
|
|
||||||
tagEl.title = "Remove annotation";
|
|
||||||
tagEl.innerHTML = "🚮"; // ×
|
|
||||||
tagEl.addEventListener('click', (e) => {
|
|
||||||
if (this.selectedAnnotation) {
|
|
||||||
this.removeAnnotation(this.selectedAnnotationI);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.tagsEl.appendChild(tagEl);
|
|
||||||
|
|
||||||
this.controlsEl.appendChild(this.tagsEl);
|
|
||||||
|
|
||||||
this.annotationsEl = document.createElement('div');
|
this.annotationsEl = document.createElement('div');
|
||||||
this.annotationsEl.classList.add('annotations')
|
this.annotationsEl.classList.add('annotations')
|
||||||
this.controlsEl.appendChild(this.annotationsEl);
|
this.controlsEl.appendChild(this.annotationsEl);
|
||||||
|
|
||||||
|
this.loadTags(tagFile).then(() => {
|
||||||
|
this.tagsEl = document.createElement('ul');
|
||||||
|
this.tagsEl.classList.add('tags');
|
||||||
|
const addTags = (tags, tagsEl) => {
|
||||||
|
Object.entries(tags).forEach(([tag, tagData]) => {
|
||||||
|
let tagLiEl = document.createElement('li');
|
||||||
|
let tagEl = document.createElement('div');
|
||||||
|
|
||||||
this.inPointPosition = [0, 0];
|
tagEl.classList.add('tag');
|
||||||
this.inPointTimeMs = null;
|
tagEl.dataset.tag = tag;
|
||||||
this.outPointPosition = null;
|
tagEl.innerText = tagData.hasOwnProperty('fullname') ? tagData.fullname : tag;
|
||||||
this.outPointTimeMs = null;
|
tagEl.addEventListener('click', (e) => {
|
||||||
this._currentTimeMs = 0;
|
this.addTag(tag, this.inPointPosition, this.outPointPosition);
|
||||||
this.videoIsPlaying = false;
|
});
|
||||||
|
|
||||||
const groups = ['before', 'annotation', 'after']
|
tagEl.title = tagData.hasOwnProperty('description') ? tagData.description : "";
|
||||||
this.strokeGroups = {};
|
|
||||||
groups.forEach(group => {
|
let signEl = document.createElement('span');
|
||||||
let groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
signEl.classList.add('annotation-' + tag);
|
||||||
groupEl.classList.add(group)
|
signEl.style.backgroundColor = this.getColorForTag(tag);
|
||||||
this.svgEl.appendChild(groupEl);
|
tagEl.prepend(signEl);
|
||||||
this.strokeGroups[group] = new StrokeGroup(groupEl, this);
|
|
||||||
|
tagLiEl.appendChild(tagEl);
|
||||||
|
|
||||||
|
if(tagData.hasOwnProperty('sub')){
|
||||||
|
const subEl = document.createElement('ul');
|
||||||
|
subEl.classList.add('subtags');
|
||||||
|
addTags(tagData.sub, subEl);
|
||||||
|
tagLiEl.appendChild(subEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsEl.appendChild(tagLiEl);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
addTags(this.tags, this.tagsEl);
|
||||||
|
|
||||||
|
let tagEl = document.createElement('li');
|
||||||
|
tagEl.classList.add('tag');
|
||||||
|
tagEl.classList.add('annotation-rm');
|
||||||
|
tagEl.dataset.tag = 'rm';
|
||||||
|
tagEl.title = "Remove annotation";
|
||||||
|
tagEl.innerHTML = "🚮"; // ×
|
||||||
|
tagEl.addEventListener('click', (e) => {
|
||||||
|
if (this.selectedAnnotation) {
|
||||||
|
this.removeAnnotation(this.selectedAnnotationI);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.tagsEl.appendChild(tagEl);
|
||||||
|
|
||||||
|
this.controlsEl.appendChild(this.tagsEl);
|
||||||
|
|
||||||
|
this.inPointPosition = [0, 0];
|
||||||
|
this.inPointTimeMs = null;
|
||||||
|
this.outPointPosition = null;
|
||||||
|
this.outPointTimeMs = null;
|
||||||
|
this._currentTimeMs = 0;
|
||||||
|
this.videoIsPlaying = false;
|
||||||
|
|
||||||
|
const groups = ['before', 'annotation', 'after']
|
||||||
|
this.strokeGroups = {};
|
||||||
|
groups.forEach(group => {
|
||||||
|
let groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||||
|
groupEl.classList.add(group)
|
||||||
|
this.svgEl.appendChild(groupEl);
|
||||||
|
this.strokeGroups[group] = new StrokeGroup(groupEl, this);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.annotations = [];
|
||||||
|
|
||||||
|
this.load(fileurl);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.annotations = [];
|
getColorForTag(tag) {
|
||||||
|
const tagData = this.tagMap[tag];
|
||||||
this.load(fileurl);
|
console.log(tag, tagData);
|
||||||
|
if (tagData && tagData.hasOwnProperty('color')) {
|
||||||
|
return tagData.color;
|
||||||
|
}
|
||||||
|
if (tagData && tagData.hasOwnProperty('parent')) {
|
||||||
|
return this.getColorForTag(tagData['parent']);
|
||||||
|
}
|
||||||
|
return 'black';
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAnnotations(save) {
|
updateAnnotations(save) {
|
||||||
|
@ -251,6 +283,8 @@ class Annotator extends EventTarget {
|
||||||
this.annotationEl.style.left = left + '%';
|
this.annotationEl.style.left = left + '%';
|
||||||
this.annotationEl.style.right = right + '%';
|
this.annotationEl.style.right = right + '%';
|
||||||
|
|
||||||
|
this.annotationEl.style.backgroundColor = this.getColorForTag(annotation.tag);
|
||||||
|
|
||||||
this.annotationEl.classList.add('annotation-' + annotation.tag);
|
this.annotationEl.classList.add('annotation-' + annotation.tag);
|
||||||
if (this.selectedAnnotationI == annotation_i) {
|
if (this.selectedAnnotationI == annotation_i) {
|
||||||
this.annotationEl.classList.add('selected');
|
this.annotationEl.classList.add('selected');
|
||||||
|
@ -274,7 +308,7 @@ class Annotator extends EventTarget {
|
||||||
this.annotationsEl.appendChild(this.annotationEl);
|
this.annotationsEl.appendChild(this.annotationEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tagsEl.childNodes.forEach(tagEl => {
|
this.tagsEl.querySelectorAll('.tag').forEach(tagEl => {
|
||||||
if (this.selectedAnnotation && this.selectedAnnotation.tag == tagEl.dataset.tag) {
|
if (this.selectedAnnotation && this.selectedAnnotation.tag == tagEl.dataset.tag) {
|
||||||
tagEl.classList.add('selected')
|
tagEl.classList.add('selected')
|
||||||
} else {
|
} else {
|
||||||
|
@ -464,7 +498,7 @@ class Annotator extends EventTarget {
|
||||||
'min': sliderMin,
|
'min': sliderMin,
|
||||||
'max': sliderMax,
|
'max': sliderMax,
|
||||||
},
|
},
|
||||||
keyboardDefaultStep: (sliderMax-sliderMin) / 1000,
|
keyboardDefaultStep: (sliderMax - sliderMin) / 1000,
|
||||||
tooltips: [
|
tooltips: [
|
||||||
this.formatter,
|
this.formatter,
|
||||||
this.formatter
|
this.formatter
|
||||||
|
@ -519,7 +553,7 @@ class Annotator extends EventTarget {
|
||||||
};
|
};
|
||||||
ttInputEl.addEventListener('keydown', (keyE) => {
|
ttInputEl.addEventListener('keydown', (keyE) => {
|
||||||
keyE.stopPropagation(); //prevent movement of tooltip
|
keyE.stopPropagation(); //prevent movement of tooltip
|
||||||
if(keyE.key == "Enter") {
|
if (keyE.key == "Enter") {
|
||||||
submit();
|
submit();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -568,8 +602,6 @@ class Annotator extends EventTarget {
|
||||||
this.nextViewboxTimeout = null;
|
this.nextViewboxTimeout = null;
|
||||||
this._setPausedFlag(true);
|
this._setPausedFlag(true);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.setupAudioConfig().then(() => {
|
this.setupAudioConfig().then(() => {
|
||||||
// this.setUpAnnotator()
|
// this.setUpAnnotator()
|
||||||
this.updateAnnotations(false);
|
this.updateAnnotations(false);
|
||||||
|
@ -591,6 +623,28 @@ class Annotator extends EventTarget {
|
||||||
// this.playStrokePosition(0, 1);
|
// this.playStrokePosition(0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
loadTags(tagFile) {
|
||||||
|
// tags config
|
||||||
|
const request = new Request(tagFile);
|
||||||
|
return fetch(request)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(tags => {
|
||||||
|
this.tags = tags;
|
||||||
|
this.tagMap = {};
|
||||||
|
const addTagsToMap = (tags, parent) => {
|
||||||
|
Object.entries(tags).forEach(([tag, tagData]) => {
|
||||||
|
tagData['parent'] = typeof parent != "undefined" ? parent : null;
|
||||||
|
this.tagMap[tag] = tagData;
|
||||||
|
if (tagData.hasOwnProperty("sub")) {
|
||||||
|
addTagsToMap(tagData.sub, tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
addTagsToMap(this.tags);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setupAudioConfig() {
|
setupAudioConfig() {
|
||||||
// audio config
|
// audio config
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -799,7 +853,7 @@ class Annotator extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
setViewboxPosition(box_i) {
|
setViewboxPosition(box_i) {
|
||||||
if(this.currentViewboxI == box_i){
|
if (this.currentViewboxI == box_i) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentViewboxI = box_i
|
this.currentViewboxI = box_i
|
||||||
|
@ -864,14 +918,14 @@ class Annotator extends EventTarget {
|
||||||
console.log('not playing because of interrupt');
|
console.log('not playing because of interrupt');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// else {
|
// else {
|
||||||
// this.videoIsPlaying = true;
|
// this.videoIsPlaying = true;
|
||||||
// }
|
// }
|
||||||
this.setViewboxPosition(box_i);
|
this.setViewboxPosition(box_i);
|
||||||
|
|
||||||
const next_box_i = box_i+1;
|
const next_box_i = box_i + 1;
|
||||||
if(this.viewboxes.length <= next_box_i){
|
if (this.viewboxes.length <= next_box_i) {
|
||||||
console.debug('done playing viewbox');
|
console.debug('done playing viewbox');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -969,7 +1023,7 @@ class Annotator extends EventTarget {
|
||||||
this.playheadEl.value = this._currentTimeMs;
|
this.playheadEl.value = this._currentTimeMs;
|
||||||
this.timeCodeEl.value = this.formatter.to(this._currentTimeMs);
|
this.timeCodeEl.value = this.formatter.to(this._currentTimeMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_animationFrame(timestamp) {
|
_animationFrame(timestamp) {
|
||||||
// TODO, move time at end of playStrokePosition to here
|
// TODO, move time at end of playStrokePosition to here
|
||||||
|
@ -1033,7 +1087,7 @@ class Annotator extends EventTarget {
|
||||||
this.dispatchEvent(new CustomEvent('seeking', { detail: time }));
|
this.dispatchEvent(new CustomEvent('seeking', { detail: time }));
|
||||||
this._currentTimeMs = Number.parseFloat(time) * 1000;
|
this._currentTimeMs = Number.parseFloat(time) * 1000;
|
||||||
[this.currentPathI, this.currentPointI] = this.findPositionForTime(this._currentTimeMs);
|
[this.currentPathI, this.currentPointI] = this.findPositionForTime(this._currentTimeMs);
|
||||||
|
|
||||||
this._updatePlayhead();
|
this._updatePlayhead();
|
||||||
this._updateFrame();
|
this._updateFrame();
|
||||||
this.dispatchEvent(new CustomEvent('seeked', { detail: this.currentTime }));
|
this.dispatchEvent(new CustomEvent('seeked', { detail: this.currentTime }));
|
||||||
|
@ -1104,7 +1158,7 @@ class Annotator extends EventTarget {
|
||||||
return [path_i, point_i];
|
return [path_i, point_i];
|
||||||
}
|
}
|
||||||
|
|
||||||
findViewboxForTime(ms){
|
findViewboxForTime(ms) {
|
||||||
ms = Math.min(Math.max(ms, 0), this.lastFrameTime);
|
ms = Math.min(Math.max(ms, 0), this.lastFrameTime);
|
||||||
// console.log('scrub to', ms)
|
// console.log('scrub to', ms)
|
||||||
let box_i = 0;
|
let box_i = 0;
|
||||||
|
|
64
app/www/tags.json
Normal file
64
app/www/tags.json
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"human-machine": {
|
||||||
|
"fullname": "Human/machine Entanglements",
|
||||||
|
"color": "orange",
|
||||||
|
"sub": {
|
||||||
|
"vision": {
|
||||||
|
"fullname": "Vision",
|
||||||
|
"color": "orange"
|
||||||
|
},
|
||||||
|
"sound": {},
|
||||||
|
"other-senses": {
|
||||||
|
"fullname": "Other senses"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tensions": {
|
||||||
|
"fullname": "Tensions, contestations & problems",
|
||||||
|
"description" : "Which problems are identified?, when do they become problems?",
|
||||||
|
"color": "gray"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"color": "blue",
|
||||||
|
"fullname": "Security & types of data",
|
||||||
|
"sub": {
|
||||||
|
"definitions": {
|
||||||
|
"description": "e.g. domain knowledge"
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"description": "e.g. fake data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actants":{
|
||||||
|
"color": "pink",
|
||||||
|
"sub":{
|
||||||
|
"algorithm": {},
|
||||||
|
"technologies": {},
|
||||||
|
"entities":{
|
||||||
|
"fullname": "Entities: people, institutions etc."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relations":{
|
||||||
|
"color": "red",
|
||||||
|
"sub": {
|
||||||
|
"inside-outside":{},
|
||||||
|
"public-private":{}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"consequences":{
|
||||||
|
"color": "green",
|
||||||
|
"sub":{
|
||||||
|
"effects":{},
|
||||||
|
"future-imaginaries":{},
|
||||||
|
"speculations": {
|
||||||
|
"description": "what is & what will/can be done."
|
||||||
|
},
|
||||||
|
"innovations": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue