Skip to content
Snippets Groups Projects
Verified Commit ecf8aea1 authored by Loïs Poujade's avatar Loïs Poujade
Browse files

Allow to add annotations on video

- if no Openseadragon instance, use a VideoViewerReference (based on
  OSDReferences) to get canvas size & DOM element
- add 2 number fields to specify annotation start/end
- speed up build by removing umd (not used in our dev setup) and demo build
- remove conflicting dependencies
  Both removed dependencies are installed at a lower version by another
  dependence (immutable is installed by draft-js-*)
parent 4f136781
No related branches found
No related tags found
1 merge request!1Allow to add annotations on video
module.exports = { module.exports = {
plugins: [
// TODO loose: which options is ignored in depencies ?
['@babel/plugin-proposal-private-methods', { loose: true }],
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
],
presets: [ presets: [
[ [
'@babel/preset-env', '@babel/preset-env',
......
...@@ -4,12 +4,12 @@ module.exports = { ...@@ -4,12 +4,12 @@ module.exports = {
type: 'react-component', type: 'react-component',
npm: { npm: {
esModules: true, esModules: true,
umd: { // umd: {
global: 'MiradorAnnotation', // global: 'MiradorAnnotation',
externals: { // externals: {
react: 'React', // react: 'React',
}, // },
}, // },
}, },
webpack: { webpack: {
aliases: { aliases: {
......
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
"umd" "umd"
], ],
"scripts": { "scripts": {
"build": "nwb build-react-component", "build": "nwb build-react-component --no-demo",
"clean": "nwb clean-module && nwb clean-demo", "clean": "nwb clean-module",
"lint": "eslint ./src ./__tests__", "lint": "eslint ./src ./__tests__",
"prepublishOnly": "npm run build", "prepublishOnly": "npm run build",
"start": "nwb serve-react-demo", "start": "nwb serve-react-demo",
...@@ -26,7 +26,6 @@ ...@@ -26,7 +26,6 @@
"draft-js": "^0.11.6", "draft-js": "^0.11.6",
"draft-js-export-html": "^1.4.1", "draft-js-export-html": "^1.4.1",
"draft-js-import-html": "^1.4.1", "draft-js-import-html": "^1.4.1",
"immutable": "^4.0.0-rc.12",
"material-ui-color-components": "^0.3.0", "material-ui-color-components": "^0.3.0",
"paper": "^0.12.11", "paper": "^0.12.11",
"react-color": "^2.18.1" "react-color": "^2.18.1"
...@@ -61,7 +60,6 @@ ...@@ -61,7 +60,6 @@
"eslint-plugin-jest": "^23.18.0", "eslint-plugin-jest": "^23.18.0",
"eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.20.3", "eslint-plugin-react": "^7.20.3",
"eslint-plugin-react-hooks": "^4.0.6",
"jest": "^26.1.0", "jest": "^26.1.0",
"jest-canvas-mock": "^2.2.0", "jest-canvas-mock": "^2.2.0",
"jest-localstorage-mock": "^2.4.2", "jest-localstorage-mock": "^2.4.2",
......
...@@ -31,6 +31,27 @@ import TextEditor from './TextEditor'; ...@@ -31,6 +31,27 @@ import TextEditor from './TextEditor';
import WebAnnotation from './WebAnnotation'; import WebAnnotation from './WebAnnotation';
import CursorIcon from './icons/Cursor'; import CursorIcon from './icons/Cursor';
/** Extract time information from annotation target */
function timeFromAnnoTarget(annotarget) {
// TODO w3c media fragments: t=,10 t=5,
const r = /t=([0-9]+),([0-9]+)/.exec(annotarget);
if (!r || r.length !== 3) {
return ['', ''];
}
return [r[1], r[2]];
}
/** Extract xywh from annotation target */
function geomFromAnnoTarget(annotarget) {
console.warn('TODO proper extraction');
const r = /xywh=((-?[0-9]+,?)+)/.exec(annotarget);
console.info('extracted from ', annotarget, r);
if (!r || r.length !== 3) {
return ['', ''];
}
return [r[1], r[2]];
}
/** */ /** */
class AnnotationCreation extends Component { class AnnotationCreation extends Component {
/** */ /** */
...@@ -38,6 +59,8 @@ class AnnotationCreation extends Component { ...@@ -38,6 +59,8 @@ class AnnotationCreation extends Component {
super(props); super(props);
const annoState = {}; const annoState = {};
if (props.annotation) { if (props.annotation) {
//
// annotation body
if (Array.isArray(props.annotation.body)) { if (Array.isArray(props.annotation.body)) {
annoState.tags = []; annoState.tags = [];
props.annotation.body.forEach((body) => { props.annotation.body.forEach((body) => {
...@@ -50,19 +73,27 @@ class AnnotationCreation extends Component { ...@@ -50,19 +73,27 @@ class AnnotationCreation extends Component {
} else { } else {
annoState.annoBody = props.annotation.body.value; annoState.annoBody = props.annotation.body.value;
} }
//
// drawing position
if (props.annotation.target.selector) { if (props.annotation.target.selector) {
if (Array.isArray(props.annotation.target.selector)) { if (Array.isArray(props.annotation.target.selector)) {
props.annotation.target.selector.forEach((selector) => { props.annotation.target.selector.forEach((selector) => {
if (selector.type === 'SvgSelector') { if (selector.type === 'SvgSelector') {
annoState.svg = selector.value; annoState.svg = selector.value;
} else if (selector.type === 'FragmentSelector') { } else if (selector.type === 'FragmentSelector') {
annoState.xywh = selector.value.replace('xywh=', ''); // TODO proper fragment selector extraction
annoState.xywh = geomFromAnnoTarget(selector.value);
[annoState.tstart, annoState.tend] = timeFromAnnoTarget(selector.value);
} }
}); });
} else { } else {
annoState.svg = props.annotation.target.selector.value; annoState.svg = props.annotation.target.selector.value;
// eslint-disable-next-line max-len
[annoState.tstart, annoState.tend] = timeFromAnnoTarget(props.annotation.target.selector.value);
} }
} }
//
// start/end time
} }
this.state = { this.state = {
activeTool: 'cursor', activeTool: 'cursor',
...@@ -77,12 +108,16 @@ class AnnotationCreation extends Component { ...@@ -77,12 +108,16 @@ class AnnotationCreation extends Component {
strokeColor: '#00BFFF', strokeColor: '#00BFFF',
strokeWidth: 1, strokeWidth: 1,
svg: null, svg: null,
tend: '',
tstart: '',
xywh: null, xywh: null,
...annoState, ...annoState,
}; };
this.submitForm = this.submitForm.bind(this); this.submitForm = this.submitForm.bind(this);
this.updateBody = this.updateBody.bind(this); this.updateBody = this.updateBody.bind(this);
this.updateTstart = this.updateTstart.bind(this);
this.updateTend = this.updateTend.bind(this);
this.updateGeometry = this.updateGeometry.bind(this); this.updateGeometry = this.updateGeometry.bind(this);
this.changeTool = this.changeTool.bind(this); this.changeTool = this.changeTool.bind(this);
this.changeClosedMode = this.changeClosedMode.bind(this); this.changeClosedMode = this.changeClosedMode.bind(this);
...@@ -152,8 +187,12 @@ class AnnotationCreation extends Component { ...@@ -152,8 +187,12 @@ class AnnotationCreation extends Component {
annotation, canvases, closeCompanionWindow, receiveAnnotation, config, annotation, canvases, closeCompanionWindow, receiveAnnotation, config,
} = this.props; } = this.props;
const { const {
annoBody, tags, xywh, svg, annoBody, tags, xywh, svg, tstart, tend,
} = this.state; } = this.state;
let fsel = xywh;
if (tstart && tend) {
fsel = `${xywh || ''}&t=${tstart},${tend}`;
}
canvases.forEach((canvas) => { canvases.forEach((canvas) => {
const storageAdapter = config.annotation.adapter(canvas.id); const storageAdapter = config.annotation.adapter(canvas.id);
const anno = new WebAnnotation({ const anno = new WebAnnotation({
...@@ -163,7 +202,7 @@ class AnnotationCreation extends Component { ...@@ -163,7 +202,7 @@ class AnnotationCreation extends Component {
manifestId: canvas.options.resource.id, manifestId: canvas.options.resource.id,
svg, svg,
tags, tags,
xywh, xywh: fsel,
}).toJson(); }).toJson();
if (annotation) { if (annotation) {
storageAdapter.update(anno).then((annoPage) => { storageAdapter.update(anno).then((annoPage) => {
...@@ -200,6 +239,12 @@ class AnnotationCreation extends Component { ...@@ -200,6 +239,12 @@ class AnnotationCreation extends Component {
this.setState({ annoBody }); this.setState({ annoBody });
} }
/** update annotation start time */
updateTstart(ev) { this.setState({ tstart: ev.target.value }); }
/** update annotation end time */
updateTend(ev) { this.setState({ tend: ev.target.value }); }
/** */ /** */
updateGeometry({ svg, xywh }) { updateGeometry({ svg, xywh }) {
this.setState({ this.setState({
...@@ -216,7 +261,9 @@ class AnnotationCreation extends Component { ...@@ -216,7 +261,9 @@ class AnnotationCreation extends Component {
const { const {
activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl, strokeColor, activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl, strokeColor,
popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode, annoBody, svg, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode, annoBody, svg,
tstart, tend,
} = this.state; } = this.state;
return ( return (
<CompanionWindow <CompanionWindow
title={annotation ? 'Edit annotation' : 'New annotation'} title={annotation ? 'Edit annotation' : 'New annotation'}
...@@ -342,6 +389,15 @@ class AnnotationCreation extends Component { ...@@ -342,6 +389,15 @@ class AnnotationCreation extends Component {
</Grid> </Grid>
</Grid> </Grid>
<Grid container> <Grid container>
<Grid item xs={12}>
<Typography variant="overline">
Duration
</Typography>
</Grid>
<Grid item xs={12}>
<input name="tstart" type="number" step="1" value={tstart} onChange={this.updateTstart} />
<input name="tend" type="number" step="1" value={tend} onChange={this.updateTend} />
</Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="overline"> <Typography variant="overline">
Content Content
...@@ -419,6 +475,7 @@ const styles = (theme) => ({ ...@@ -419,6 +475,7 @@ const styles = (theme) => ({
}); });
AnnotationCreation.propTypes = { AnnotationCreation.propTypes = {
// TODO proper web annotation type ?
annotation: PropTypes.object, // eslint-disable-line react/forbid-prop-types annotation: PropTypes.object, // eslint-disable-line react/forbid-prop-types
canvases: PropTypes.arrayOf( canvases: PropTypes.arrayOf(
PropTypes.shape({ id: PropTypes.string, index: PropTypes.number }), PropTypes.shape({ id: PropTypes.string, index: PropTypes.number }),
......
...@@ -2,6 +2,7 @@ import React, { Component } from 'react'; ...@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences'; import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences';
import { VideoViewersReferences } from 'mirador/dist/es/src/plugins/VideoViewersReferences';
import { renderWithPaperScope, PaperContainer } from '@psychobolt/react-paperjs'; import { renderWithPaperScope, PaperContainer } from '@psychobolt/react-paperjs';
import import
{ {
...@@ -16,6 +17,17 @@ import flatten from 'lodash/flatten'; ...@@ -16,6 +17,17 @@ import flatten from 'lodash/flatten';
import EditTool from './EditTool'; import EditTool from './EditTool';
import { mapChildren } from './utils'; import { mapChildren } from './utils';
/** Use a canvas "like a OSD viewport" (temporary) */
function viewportFromAnnotationOverlayVideo(annotationOverlayVideo) {
const { canvas } = annotationOverlayVideo;
return {
getCenter: () => ({ x: canvas.getWidth() / 2, y: canvas.getHeight() / 2 }),
getFlip: () => false,
getRotation: () => false,
getZoom: () => 1,
};
}
/** */ /** */
class AnnotationDrawing extends Component { class AnnotationDrawing extends Component {
/** */ /** */
...@@ -25,12 +37,6 @@ class AnnotationDrawing extends Component { ...@@ -25,12 +37,6 @@ class AnnotationDrawing extends Component {
this.addPath = this.addPath.bind(this); this.addPath = this.addPath.bind(this);
} }
/** */
componentDidMount() {
const { windowId } = this.props;
this.OSDReference = OSDReferences.get(windowId);
}
/** */ /** */
addPath(path) { addPath(path) {
const { closed, strokeWidth, updateGeometry } = this.props; const { closed, strokeWidth, updateGeometry } = this.props;
...@@ -61,23 +67,32 @@ class AnnotationDrawing extends Component { ...@@ -61,23 +67,32 @@ class AnnotationDrawing extends Component {
/** */ /** */
paperThing() { paperThing() {
const { windowId } = this.props;
let viewport = null;
let img = null;
if (OSDReferences.get(windowId)) {
console.debug('[annotation-plugin] OSD reference: ', OSDReferences.get(windowId));
viewport = OSDReferences.get(windowId).current.viewport;
img = OSDReferences.get(windowId).current.world.getItemAt(0);
} else if (VideoViewersReferences.get(windowId)) {
console.debug('[annotation-plugin] VideoViewers reference: ', VideoViewersReferences.get(windowId));
viewport = viewportFromAnnotationOverlayVideo(VideoViewersReferences.get(windowId).props);
}
const { const {
activeTool, fillColor, strokeColor, strokeWidth, svg, activeTool, fillColor, strokeColor, strokeWidth, svg,
} = this.props; } = this.props;
if (!activeTool || activeTool === 'cursor') return null; if (!activeTool || activeTool === 'cursor') return null;
// Setup Paper View to have the same center and zoom as the OSD Viewport // Setup Paper View to have the same center and zoom as the OSD Viewport/video canvas
const viewportZoom = this.OSDReference.viewport.getZoom(true); const center = img
const image1 = this.OSDReference.world.getItemAt(0); ? img.viewportToImageCoordinates(viewport.getCenter(true))
const center = image1.viewportToImageCoordinates( : viewport.getCenter();
this.OSDReference.viewport.getCenter(true), const flipped = viewport.getFlip();
);
const flipped = this.OSDReference.viewport.getFlip();
const viewProps = { const viewProps = {
center: new Point(center.x, center.y), center: new Point(center.x, center.y),
rotation: this.OSDReference.viewport.getRotation(), rotation: viewport.getRotation(),
scaling: new Point(flipped ? -1 : 1, 1), scaling: new Point(flipped ? -1 : 1, 1),
zoom: image1.viewportToImageZoom(viewportZoom), zoom: img ? img.viewportToImageZoom(viewport.getZoom()) : viewport.getZoom(),
}; };
let ActiveTool = RectangleTool; let ActiveTool = RectangleTool;
...@@ -141,9 +156,12 @@ class AnnotationDrawing extends Component { ...@@ -141,9 +156,12 @@ class AnnotationDrawing extends Component {
/** */ /** */
render() { render() {
const { windowId } = this.props; const { windowId } = this.props;
this.OSDReference = OSDReferences.get(windowId).current; const container = OSDReferences.get(windowId)
? OSDReferences.get(windowId).current.element
: VideoViewersReferences.get(windowId).apiRef.current;
return ( return (
ReactDOM.createPortal(this.paperThing(), this.OSDReference.element) ReactDOM.createPortal(this.paperThing(), container)
); );
} }
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment