Skip to content

Commit 46f39d1

Browse files
committed
hackweek annotation code
1 parent 801d2b8 commit 46f39d1

File tree

9 files changed

+1406
-0
lines changed

9 files changed

+1406
-0
lines changed

packages/feedback/src/screenshot/components/ScreenshotEditor.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DOCUMENT, WINDOW } from '../../constants';
77
import type { Dialog } from '../../types';
88
import { createScreenshotInputStyles } from './ScreenshotInput.css';
99
import { useTakeScreenshot } from './useTakeScreenshot';
10+
import { ImageEditorWrapper } from './imageEditorWrapper';
1011

1112
interface FactoryParams {
1213
h: typeof hType;
@@ -64,6 +65,7 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
6465
const croppingRef = useRef<HTMLCanvasElement>(null);
6566
const [croppingRect, setCroppingRect] = useState<Box>({ startx: 0, starty: 0, endx: 0, endy: 0 });
6667
const [confirmCrop, setConfirmCrop] = useState(false);
68+
const [isAnnotating, setIsAnnotating] = useState(false);
6769

6870
useEffect(() => {
6971
WINDOW.addEventListener('resize', resizeCropper, false);
@@ -226,6 +228,20 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
226228
<div class="editor">
227229
<style dangerouslySetInnerHTML={styles} />
228230
<div class="canvasContainer" ref={canvasContainerRef}>
231+
{isAnnotating && (
232+
<ImageEditorWrapper
233+
src={imageBuffer}
234+
onCancel={() => setIsAnnotating(false)}
235+
onSubmit={annotatedImage => {
236+
setIsAnnotating(false);
237+
const ctx = imageBuffer.getContext('2d');
238+
if (ctx && annotatedImage) {
239+
ctx.clearRect(0, 0, imageBuffer.width, imageBuffer.height);
240+
ctx.drawImage(annotatedImage, 0, 0);
241+
}
242+
}}
243+
></ImageEditorWrapper>
244+
)}
229245
<div class="cropButtonContainer" style={{ position: 'absolute' }} ref={cropContainerRef}>
230246
<canvas style={{ position: 'absolute' }} ref={croppingRef}></canvas>
231247
<CropCorner
@@ -290,6 +306,7 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
290306
</div>
291307
</div>
292308
</div>
309+
<button onClick={() => setIsAnnotating(true)}>Annotate</button>
293310
</div>
294311
);
295312
};
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { DOCUMENT } from '../../constants';
2+
3+
/**
4+
* Creates <style> element for widget dialog
5+
*/
6+
export function createScreenshotAnnotateStyles(): HTMLStyleElement {
7+
const style = DOCUMENT.createElement('style');
8+
9+
const surface200 = '#FAF9FB';
10+
const gray100 = '#F0ECF3';
11+
12+
style.textContent = `
13+
.canvas {
14+
cursor: crosshair;
15+
max-width: 100vw;
16+
max-height: 100vh;
17+
}
18+
19+
.container {
20+
position: fixed;
21+
z-index: 10000;
22+
height: 100vh;
23+
width: 100vw;
24+
top: 0;
25+
left: 0;
26+
background-color: rgba(240, 236, 243, 1);
27+
background-image: repeating-linear-gradient(
28+
45deg,
29+
transparent,
30+
transparent 5px,
31+
rgba(0, 0, 0, 0.03) 5px,
32+
rgba(0, 0, 0, 0.03) 10px
33+
);
34+
}
35+
36+
.canvasWrapper {
37+
position: relative;
38+
width: 100%;
39+
margin-top: 32px;
40+
height: calc(100% - 96px);
41+
display: flex;
42+
align-items: center;
43+
justify-content: center;
44+
}
45+
46+
.toolbarGroup {
47+
display: flex;
48+
flex-direction: row;
49+
height: 42px;
50+
background-color: white;
51+
border: rgba(58, 17, 95, 0.14) 1px solid;
52+
border-radius: 10px;
53+
padding: 4px;
54+
overflow: hidden;
55+
gap: 4px;
56+
box-shadow: 0px 1px 2px 1px rgba(43, 34, 51, 0.04);
57+
}
58+
59+
.toolbar {
60+
position: absolute;
61+
width: 100%;
62+
bottom: 0px;
63+
padding: 12px 16px;
64+
display: flex;
65+
gap: 12px;
66+
flex-direction: row;
67+
justify-content: center;
68+
}
69+
70+
.flexSpacer {
71+
flex: 1;
72+
}
73+
74+
.toolButton {
75+
width: 32px;
76+
height: 32px;
77+
border-radius: 6px;
78+
border: none;
79+
background-color: white;
80+
color: rgba(43, 34, 51, 1);
81+
font-size: 16px;
82+
font-weight: 600;
83+
display: flex;
84+
align-items: center;
85+
justify-content: center;
86+
cursor: pointer;
87+
&:hover {
88+
background-color: rgba(43, 34, 51, 0.06);
89+
}
90+
&:active {
91+
background-color: rgba(108, 95, 199, 1) !important;\
92+
color: white;
93+
}
94+
}
95+
96+
.cancelButton {
97+
height: 40px;
98+
width: 84px;
99+
border: rgba(58, 17, 95, 0.14) 1px solid;
100+
background-color: #fff;
101+
color: rgba(43, 34, 51, 1);
102+
font-size: 14px;
103+
font-weight: 500;
104+
cursor: pointer;
105+
border-radius: 10px;
106+
&:hover {
107+
background-color: #eee;
108+
}
109+
}
110+
111+
.submitButton {
112+
height: 40px;
113+
width: 84px;
114+
border: none;
115+
background-color: rgba(108, 95, 199, 1);
116+
color: #fff;
117+
font-size: 14px;
118+
font-weight: 500;
119+
cursor: pointer;
120+
border-radius: 10px;
121+
&:hover {
122+
background-color: rgba(88, 74, 192, 1);
123+
}
124+
}
125+
126+
.colorInput {
127+
position: relative;
128+
display: flex;
129+
width: 32px;
130+
height: 32px;
131+
align-items: center;
132+
justify-content: center;
133+
margin: 0;
134+
cursor: pointer;
135+
& input[type='color'] {
136+
position: absolute;
137+
top: 0;
138+
left: 0;
139+
opacity: 0;
140+
width: 0;
141+
height: 0;
142+
}
143+
}
144+
145+
.colorDisplay {
146+
width: 16px;
147+
height: 16px;
148+
border-radius: 4px;
149+
background-color: var(--annotateColor);
150+
}
151+
`;
152+
153+
return style;
154+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
2+
import { h } from 'preact';
3+
import type { ToolKey } from './useImageEditor';
4+
import { Tools, useImageEditor } from './useImageEditor';
5+
import { ArrowIcon, HandIcon, PenIcon, RectangleIcon } from './useImageEditor/icons';
6+
import { WINDOW } from './../../constants';
7+
import type { ComponentType } from 'preact';
8+
import { createScreenshotAnnotateStyles } from './imageEditorWrapper.css';
9+
10+
export interface Rect {
11+
height: number;
12+
width: number;
13+
x: number;
14+
y: number;
15+
}
16+
interface ImageEditorWrapperProps {
17+
onCancel: () => void;
18+
onSubmit: (screenshot: HTMLCanvasElement | null) => void;
19+
src: HTMLCanvasElement;
20+
}
21+
22+
const iconMap: Record<ToolKey, ComponentType> = {
23+
arrow: ArrowIcon,
24+
pen: PenIcon,
25+
rectangle: RectangleIcon,
26+
select: HandIcon,
27+
};
28+
29+
const getCanvasRenderSize = (canvas: HTMLCanvasElement, containerElement: HTMLDivElement) => {
30+
const canvasWidth = canvas.width;
31+
const canvasHeight = canvas.height;
32+
const maxWidth = containerElement.getBoundingClientRect().width;
33+
const maxHeight = containerElement.getBoundingClientRect().height;
34+
// fit canvas to window
35+
let width = canvasWidth;
36+
let height = canvasHeight;
37+
const canvasRatio = canvasWidth / canvasHeight;
38+
const windowRatio = maxWidth / maxHeight;
39+
40+
if (canvasRatio > windowRatio && canvasWidth > maxWidth) {
41+
height = (maxWidth / canvasWidth) * canvasHeight;
42+
width = maxWidth;
43+
}
44+
45+
if (canvasRatio < windowRatio && canvasHeight > maxHeight) {
46+
width = (maxHeight / canvasHeight) * canvasWidth;
47+
height = maxHeight;
48+
}
49+
50+
return { width, height };
51+
};
52+
53+
const srcToImage = (src: string): HTMLImageElement => {
54+
const image = new Image();
55+
image.src = src;
56+
return image;
57+
};
58+
59+
function ToolIcon({ tool }: { tool: ToolKey }) {
60+
const Icon = tool ? iconMap[tool] : HandIcon;
61+
return <Icon />;
62+
}
63+
64+
export function ImageEditorWrapper({ src, onCancel, onSubmit }: ImageEditorWrapperProps) {
65+
const wrapperRef = useRef<HTMLDivElement>(null);
66+
const [canvas, setCanvas] = useState<HTMLCanvasElement | null>(null);
67+
const styles = useMemo(() => ({ __html: createScreenshotAnnotateStyles().innerText }), []);
68+
69+
const resizeCanvas = useCallback(() => {
70+
if (!canvas || !wrapperRef.current) {
71+
return;
72+
}
73+
// fit canvas to window
74+
const { width, height } = getCanvasRenderSize(canvas, wrapperRef.current);
75+
canvas.style.width = `${width}px`;
76+
canvas.style.height = `${height}px`;
77+
}, [canvas]);
78+
79+
// const image = useMemo(() => srcToImage(src), [src]);
80+
const { selectedTool, setSelectedTool, selectedColor, setSelectedColor, getBlob } = useImageEditor({
81+
canvas,
82+
image: src,
83+
onLoad: resizeCanvas,
84+
});
85+
86+
useEffect(() => {
87+
resizeCanvas();
88+
WINDOW.addEventListener('resize', resizeCanvas);
89+
return () => {
90+
WINDOW.removeEventListener('resize', resizeCanvas);
91+
};
92+
}, [resizeCanvas]);
93+
94+
return (
95+
<div>
96+
<style dangerouslySetInnerHTML={styles} />
97+
<div class="container">
98+
<div class="canvasWrapper" ref={wrapperRef}>
99+
<canvas class="canvas" ref={setCanvas} />
100+
</div>
101+
<div class="toolbar">
102+
<button class="cancelButton" onClick={() => onCancel()}>
103+
Cancel
104+
</button>
105+
<div class="flexSpacer" />
106+
<div class="toolbarGroup">
107+
{Tools.map(tool => (
108+
<button
109+
class="toolButton"
110+
key={tool}
111+
// active={selectedTool === tool}
112+
onClick={() => setSelectedTool(tool)}
113+
>
114+
<ToolIcon tool={tool} />
115+
</button>
116+
))}
117+
</div>
118+
<div class="toolbarGroup">
119+
<label class="colorInput">
120+
<div
121+
class="colorDisplay"
122+
// color={selectedColor}
123+
/>
124+
<input
125+
type="color"
126+
value={selectedColor}
127+
// onChange={e => setSelectedColor(e?.target?.value)}
128+
/>
129+
</label>
130+
</div>
131+
<div />
132+
<button class="submitButton" onClick={async () => onSubmit(canvas)}>
133+
Save
134+
</button>
135+
</div>
136+
</div>
137+
</div>
138+
);
139+
}

0 commit comments

Comments
 (0)