Skip to content

Commit cdb175a

Browse files
authored
Make Doodles Clickable (#15)
* add tag info to doodles * add click handling to Doodles * those darn tests * fix comments
1 parent 63ee8f3 commit cdb175a

File tree

12 files changed

+206
-46
lines changed

12 files changed

+206
-46
lines changed

src/annotator/guest.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ export default class Guest extends Delegator {
802802
*/
803803
clearDoodleCanvas() {
804804
if (this.doodleCanvasController) {
805-
this.doodleCanvasController.saveLines();
805+
this.doodleCanvasController.newLines = [];
806806
}
807807
}
808808

@@ -820,9 +820,12 @@ export default class Guest extends Delegator {
820820
}
821821
}
822822
}
823-
this.doodleCanvasController.savedLines = [
824-
...this.doodleCanvasController.savedLines,
825-
...newLines,
823+
this.doodleCanvasController.savedDoodles = [
824+
...this.doodleCanvasController.savedDoodles,
825+
{
826+
$tag: doodleAnnotation.$tag,
827+
lines: newLines,
828+
},
826829
];
827830
}
828831
}

src/annotator/sidebar.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ export default class Sidebar extends Guest {
148148
tool: 'pen',
149149
size: 5,
150150
color: 'red',
151+
},
152+
tag => {
153+
this.crossframe?.call('showAnnotations', [tag]);
154+
this.crossframe?.call('showSidebar');
151155
}
152156
);
153157

src/doodle/canvas.js

Lines changed: 106 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createElement } from 'preact';
1+
import { createElement, Fragment } from 'preact';
22
import { useRef, useEffect } from 'preact/hooks';
33
import propTypes from 'prop-types';
44

@@ -9,15 +9,18 @@ const Canvas = ({
99
handleMouseUp,
1010
handleMouseMove,
1111
handleMouseLeave,
12-
lines,
12+
doodles,
13+
handleDoodleClick,
1314
}) => {
1415
const canvasRef = useRef(null);
16+
const hitCanvasRef = useRef(null);
1517

16-
const drawLine = (ctx, line) => {
18+
const drawLine = (ctx, hitctx, line, colorHash) => {
1719
if (line.points.length <= 1) {
1820
return;
1921
}
2022
const [[startX, startY], ...rest] = line.points;
23+
// Draw each line twice, once on the visible canvas, once on the 'hit' canvas
2124
// Move to the first point, begin the line
2225
ctx.beginPath();
2326
ctx.moveTo(startX, startY);
@@ -37,29 +40,113 @@ const Canvas = ({
3740

3841
ctx.stroke();
3942
ctx.closePath();
43+
44+
// now draw the same line, but on our hit canvas
45+
hitctx.beginPath();
46+
hitctx.moveTo(startX, startY);
47+
hitctx.lineWidth = line.size;
48+
49+
if (line.tool === 'pen') {
50+
hitctx.globalCompositeOperation = 'source-over';
51+
hitctx.strokeStyle = colorHash;
52+
} else {
53+
hitctx.globalCompositeOperation = 'destination-out';
54+
}
55+
56+
// Draw the rest of the lines
57+
for (let [x, y] of rest) {
58+
hitctx.lineTo(x, y);
59+
}
60+
61+
hitctx.stroke();
62+
hitctx.closePath();
63+
};
64+
65+
const colorToTag = (r, g, b) => {
66+
const tagNum = ((r << 16) | (g << 8) | b) - 1;
67+
return `t${tagNum}`;
68+
};
69+
70+
const tagToColor = tag => {
71+
if (tag === 'none') {
72+
// this is an untagged (in progress) doodle
73+
return '#000000';
74+
}
75+
const tagNum = parseInt(tag.substring(1), 10) + 1;
76+
return `#${tagNum.toString(16).padStart(6, '0')}`;
4077
};
4178

79+
// have to add event listener this way because the canvas has pointerEvents = None
80+
useEffect(() => {
81+
const handleClick = e => {
82+
const canvas = canvasRef.current;
83+
const hitCanvas = hitCanvasRef.current;
84+
const hitCtx = hitCanvas.getContext('2d');
85+
86+
// get the click coordinates
87+
const boundingBox = canvas.getBoundingClientRect();
88+
const xPos = e.clientX - boundingBox.left;
89+
const yPos = e.clientY - boundingBox.top;
90+
91+
// make sure that this click happened inside the canvas
92+
if (
93+
xPos > 0 &&
94+
xPos < boundingBox.width &&
95+
yPos > 0 &&
96+
yPos > boundingBox.height
97+
) {
98+
// get the color of the pixel clicked ony
99+
const hitColor = hitCtx.getImageData(xPos, yPos, 1, 1).data;
100+
// convert the color to a tag
101+
const tag = colorToTag(hitColor[0], hitColor[1], hitColor[2]);
102+
// call the click function with that tag. We offset tags to colors by 1 so that t-1 means there is no tag.
103+
if (tag !== 't-1') {
104+
handleDoodleClick(tag);
105+
}
106+
}
107+
};
108+
109+
document.addEventListener('click', function (e) {
110+
handleClick(e);
111+
});
112+
}, [handleDoodleClick]);
113+
42114
useEffect(() => {
43115
const canvas = canvasRef.current;
44116
const ctx = canvas.getContext('2d');
45-
ctx.clearRect(0, 0, canvas.width, canvas.height);
117+
const hitCanvas = hitCanvasRef.current;
118+
const hitctx = hitCanvas.getContext('2d');
46119

47-
// Draw all of the lines (reverse order so that erasing works)
48-
for (let i = lines.length - 1; i >= 0; i--) {
49-
drawLine(ctx, lines[i]);
120+
ctx.clearRect(0, 0, canvas.width, canvas.height);
121+
// Draw all of the doodles
122+
for (let d = 0; d < doodles.length; d++) {
123+
const doodle = doodles[d];
124+
let colorHash = tagToColor(doodle.$tag);
125+
// Draw all of the lines (reverse order so that erasing works)
126+
for (let i = doodle.lines.length - 1; i >= 0; i--) {
127+
drawLine(ctx, hitctx, doodle.lines[i], colorHash);
128+
}
50129
}
51-
}, [lines]);
130+
}, [doodles]);
52131

53132
return (
54-
<canvas
55-
width={width}
56-
height={height}
57-
ref={canvasRef}
58-
onMouseDown={handleMouseDown}
59-
onMouseMove={handleMouseMove}
60-
onMouseUp={handleMouseUp}
61-
onMouseLeave={handleMouseLeave}
62-
/>
133+
<Fragment>
134+
<canvas
135+
width={width}
136+
height={height}
137+
ref={canvasRef}
138+
onMouseDown={handleMouseDown}
139+
onMouseMove={handleMouseMove}
140+
onMouseUp={handleMouseUp}
141+
onMouseLeave={handleMouseLeave}
142+
/>
143+
<canvas
144+
width={width}
145+
height={height}
146+
ref={hitCanvasRef}
147+
style={{ visibility: 'hidden' }}
148+
/>
149+
</Fragment>
63150
);
64151
};
65152

@@ -70,7 +157,8 @@ Canvas.propTypes = {
70157
handleMouseUp: propTypes.func.isRequired,
71158
handleMouseMove: propTypes.func.isRequired,
72159
handleMouseLeave: propTypes.func.isRequired,
73-
lines: propTypes.array.isRequired,
160+
handleDoodleClick: propTypes.func.isRequired,
161+
doodles: propTypes.array.isRequired,
74162
};
75163

76164
export { Canvas };

src/doodle/displayCanvas.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
import { createElement } from 'preact';
22
import { Canvas } from './canvas';
33
import propTypes from 'prop-types';
4+
/**
5+
* @typedef DisplayCanvasProps
6+
* @prop {HTMLElement} container - Which element the DisplayCanvas should cover.
7+
* @prop {Array<import('../types/api').Doodle>} doodles - An array of Doodles to render
8+
* @prop {(String) => any} handleDoodleClick - What to do when a doodle is clicked? Accepts the Doodle's `tag` as a prop
9+
*/
410

5-
const DisplayCanvas = ({ container, lines }) => {
11+
/**
12+
* Component that renders saved Doodle annotations
13+
*
14+
* @param {DisplayCanvasProps} props
15+
*/
16+
const DisplayCanvas = ({ container, doodles, handleDoodleClick }) => {
617
const boundingRect = container.getBoundingClientRect();
718
return (
819
<div
@@ -26,15 +37,17 @@ const DisplayCanvas = ({ container, lines }) => {
2637
handleMouseUp={() => {}}
2738
handleMouseLeave={() => {}}
2839
handleMouseMove={() => {}}
29-
lines={lines}
40+
doodles={doodles}
41+
handleDoodleClick={handleDoodleClick}
3042
/>
3143
</div>
3244
);
3345
};
3446

3547
DisplayCanvas.propTypes = {
36-
lines: propTypes.array.isRequired,
48+
doodles: propTypes.array.isRequired,
3749
container: propTypes.object.isRequired,
50+
handleDoodleClick: propTypes.func.isRequired,
3851
};
3952

4053
export { DisplayCanvas };

src/doodle/doodleCanvas.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ import propTypes from 'prop-types';
1515
*/
1616

1717
/**
18-
* Component that renders icons using inline `<svg>` elements.
19-
* This enables their appearance to be customized via CSS.
20-
*
21-
* This matches the way we do icons on the website, see
22-
* https://github.com/hypothesis/h/pull/3675
18+
* Component that renders a canvas for users to draw Doodles on.
2319
*
2420
* @param {DoodleCanvasProps} props
2521
*/
@@ -33,6 +29,11 @@ const DoodleCanvas = ({
3329
setLines,
3430
}) => {
3531
const [isDrawing, setIsDrawing] = useState(false);
32+
const [everActive, setEverActive] = useState(false);
33+
34+
if (active && !everActive) {
35+
setEverActive(true);
36+
}
3637

3738
useEffect(() => {
3839
if (lines.length === 0) {
@@ -93,6 +94,10 @@ const DoodleCanvas = ({
9394
setLines([newLine, ...rest]);
9495
};
9596

97+
if (!everActive) {
98+
return null;
99+
}
100+
96101
return (
97102
<div
98103
style={{
@@ -116,7 +121,8 @@ const DoodleCanvas = ({
116121
handleMouseUp={handleMouseUp}
117122
handleMouseLeave={handleMouseLeave}
118123
handleMouseMove={handleMouseMove}
119-
lines={lines}
124+
doodles={[{ $tag: 'none', lines: lines }]}
125+
handleDoodleClick={() => {}}
120126
/>
121127
</div>
122128
);
@@ -126,9 +132,9 @@ DoodleCanvas.propTypes = {
126132
tool: propTypes.string.isRequired,
127133
size: propTypes.number.isRequired,
128134
active: propTypes.bool.isRequired,
135+
color: propTypes.string.isRequired,
129136
lines: propTypes.array.isRequired,
130137
setLines: propTypes.func.isRequired,
131-
color: propTypes.string.isRequired,
132138
attachedElement: propTypes.any.isRequired,
133139
};
134140

src/doodle/doodleController.js

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ export class DoodleController {
77
* @param {HTMLElement | null} container - Element into which the toolbar is rendered
88
* @param {any} options
99
*/
10-
constructor(container, options) {
10+
constructor(container, options, handleDoodleClick) {
1111
const { tool, size, color } = options;
1212
this._lines = [];
13-
this._savedLines = [];
13+
this._savedDoodles = [];
1414
this._newLines = [];
1515

1616
this._container = container === null ? document.body : container;
@@ -19,6 +19,7 @@ export class DoodleController {
1919
this._color = color;
2020

2121
this._doodleable = false;
22+
this._handleDoodleClick = handleDoodleClick;
2223

2324
// create a new element to render into, to avoid overwriting the main page content.
2425
this.target = document.body.appendChild(document.createElement('div'));
@@ -41,13 +42,13 @@ export class DoodleController {
4142
/**
4243
* Update the lines and re-render on change
4344
*/
44-
set savedLines(lines) {
45-
this._savedLines = lines;
45+
set savedDoodles(lines) {
46+
this._savedDoodles = lines;
4647
this.render();
4748
}
4849

49-
get savedLines() {
50-
return this._savedLines;
50+
get savedDoodles() {
51+
return this._savedDoodles;
5152
}
5253

5354
set newLines(lines) {
@@ -90,12 +91,6 @@ export class DoodleController {
9091
return this._doodleable;
9192
}
9293

93-
saveLines() {
94-
this._savedLines = [...this._newLines, ...this._savedLines];
95-
this._newLines = [];
96-
this.render();
97-
}
98-
9994
render() {
10095
const setLines = lines => {
10196
this.newLines = lines;
@@ -111,7 +106,11 @@ export class DoodleController {
111106
setLines={setLines}
112107
color={this._color}
113108
/>
114-
<DisplayCanvas lines={this.savedLines} container={this._container} />
109+
<DisplayCanvas
110+
handleDoodleClick={this._handleDoodleClick}
111+
doodles={this.savedDoodles}
112+
container={this._container}
113+
/>
115114
</Fragment>,
116115
this.target
117116
);

src/sidebar/components/SidebarView.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ function SidebarView({
5656
const searchUris = store.searchUris();
5757
const sidebarHasOpened = store.hasSidebarOpened();
5858
const userId = store.profile().userid;
59+
//WILLNOTE we can use this to hide/show for this user!
5960

6061
// The local `$tag` of a direct-linked annotation; populated once it
6162
// has anchored: meaning that it's ready to be focused and scrolled to

src/sidebar/services/frame-sync.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export default function FrameSync(annotationsService, bridge, store) {
166166

167167
bridge.on('showAnnotations', function (tags) {
168168
store.selectAnnotations(store.findIDsForTags(tags));
169-
store.selectTab('annotation');
169+
store.selectTab(store.findTypeForTags(tags));
170170
});
171171

172172
bridge.on('focusAnnotations', function (tags) {

0 commit comments

Comments
 (0)