Annotate WIP

This commit is contained in:
psjostrom 2020-11-12 13:42:06 +01:00
parent 8ea3d6ae74
commit 7becbbed94
5 changed files with 235 additions and 74 deletions

View File

@ -441,7 +441,7 @@ export class WindowHandler {
} else { } else {
this.isMana = false; this.isMana = false;
} }
logger.info('window-handler: isMana: ' + this.isMana); logger.info('window-handler: isMana: ' + this.isMana);
// Injects custom title bar and snack bar css into the webContents // Injects custom title bar and snack bar css into the webContents
await injectStyles(this.mainWindow, this.isCustomTitleBar); await injectStyles(this.mainWindow, this.isCustomTitleBar);
@ -963,14 +963,14 @@ export class WindowHandler {
} }
const parentWindow = BrowserWindow.getFocusedWindow(); const parentWindow = BrowserWindow.getFocusedWindow();
const MIN_HEIGHT = 312; const MIN_HEIGHT = 320;
const MIN_WIDTH = 320; const MIN_WIDTH = 312;
const CONTAINER_HEIGHT = 124; const CONTAINER_HEIGHT = 120;
let windowHeight = dimensions?.height let windowHeight = dimensions?.height
? dimensions.height + CONTAINER_HEIGHT ? dimensions.height + CONTAINER_HEIGHT
: 600; : 320;
let windowWidth = dimensions?.width || 800; let windowWidth = dimensions?.width || 312;
if (dimensions && dimensions.height && dimensions.height < MIN_HEIGHT) { if (dimensions && dimensions.height && dimensions.height < MIN_HEIGHT) {
windowHeight = MIN_HEIGHT + CONTAINER_HEIGHT; windowHeight = MIN_HEIGHT + CONTAINER_HEIGHT;

View File

@ -38,7 +38,7 @@ const ColorPickerPill = (props: IColorPickerPillProps) => {
return ( return (
<div <div
key={color.rgbaColor} key={color.rgbaColor}
className='enclosingCircle' className='EnclosingCircle'
onClick={chooseColor} onClick={chooseColor}
data-testid={'colorDot ' + color.rgbaColor} data-testid={'colorDot ' + color.rgbaColor}
> >
@ -50,14 +50,14 @@ const ColorPickerPill = (props: IColorPickerPillProps) => {
cursor: 'pointer', cursor: 'pointer',
border: hasOutline ? border : undefined, border: hasOutline ? border : undefined,
}} }}
className='colorDot' className='ColorDot'
/> />
</div> </div>
); );
}; };
return ( return (
<div className='colorPicker'> <div className='ColorPicker'>
{props.availableColors.map((color) => ColorDot(color))} {props.availableColors.map((color) => ColorDot(color))}
</div> </div>
); );

View File

@ -1,9 +1,10 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { LazyBrush } from 'lazy-brush';
import * as React from 'react'; import * as React from 'react';
import { i18n } from '../../common/i18n-preload'; import { i18n } from '../../common/i18n-preload';
import ColorPickerPill, { IColor } from './color-picker-pill'; import ColorPickerPill, { IColor } from './color-picker-pill';
const { useState, useCallback, useRef, useEffect } = React; const { useState, useRef, useEffect } = React;
enum Tool { enum Tool {
pen = 'PEN', pen = 'PEN',
@ -32,6 +33,16 @@ export interface ISvgPath {
shouldShow: boolean; shouldShow: boolean;
} }
const lazy = new LazyBrush({
radius: 3,
enabled: true,
initialPoint: { x: 0, y: 0 },
});
const TOP_MENU_HEIGHT = 48;
const MIN_ANNOTATE_AREA_HEIGHT = 200;
const MIN_ANNOTATE_AREA_WIDTH = 312;
const PEN_WIDTH = 5;
const HIGHLIGHT_WIDTH = 28;
const availablePenColors: IColor[] = [ const availablePenColors: IColor[] = [
{ rgbaColor: 'rgba(0, 0, 40, 1)' }, { rgbaColor: 'rgba(0, 0, 40, 1)' },
{ rgbaColor: 'rgba(0, 142, 255, 1)' }, { rgbaColor: 'rgba(0, 142, 255, 1)' },
@ -55,12 +66,9 @@ const SnippingTool = () => {
height: 600, height: 600,
width: 800, width: 800,
}); });
const [isDrawing, setIsDrawing] = useState(false);
const [paths, setPaths] = useState<IPath[]>([]); const [paths, setPaths] = useState<IPath[]>([]);
const [chosenTool, setChosenTool] = useState(Tool.pen); const [chosenTool, setChosenTool] = useState(Tool.pen);
const [annotateAreaLocation, setAnnotateAreaLocation] = useState({
left: 0,
top: 0,
});
const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)'); const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)');
const [highlightColor, setHighlightColor] = useState( const [highlightColor, setHighlightColor] = useState(
'rgba(0, 142, 255, 0.64)', 'rgba(0, 142, 255, 0.64)',
@ -86,12 +94,6 @@ const SnippingTool = () => {
}; };
}, []); }, []);
const annotateRef = useCallback((domNode) => {
if (domNode) {
setAnnotateAreaLocation(domNode.getBoundingClientRect());
}
}, []);
// Hook that alerts clicks outside of the passed ref // Hook that alerts clicks outside of the passed ref
const useClickOutsideExaminer = ( const useClickOutsideExaminer = (
colorPickerRf: React.RefObject<HTMLDivElement>, colorPickerRf: React.RefObject<HTMLDivElement>,
@ -151,21 +153,27 @@ const SnippingTool = () => {
}; };
const clear = () => { const clear = () => {
const updPaths = [...paths]; // Clear logic here
updPaths.map((p) => { };
p.shouldShow = false;
return p; const maybeErasePath = (key: string) => {
}); // erase logic here
setPaths(updPaths); return key;
};
const stopDrawing = () => {
if (isDrawing) {
setIsDrawing(false);
}
}; };
// Utility functions // Utility functions
const getMousePosition = (e: React.MouseEvent) => { const getMousePosition = (e: React.MouseEvent) => {
return { // We need to offset for elements in the window that is not the annotate area
x: e.pageX - annotateAreaLocation.left, const x = imageDimensions.width >= MIN_ANNOTATE_AREA_WIDTH ? e.pageX : e.pageX - (MIN_ANNOTATE_AREA_WIDTH - imageDimensions.width) / 2;
y: e.pageY - annotateAreaLocation.top, const y = imageDimensions.height >= MIN_ANNOTATE_AREA_HEIGHT ? (e.pageY - TOP_MENU_HEIGHT) : (e.pageY - ((MIN_ANNOTATE_AREA_HEIGHT - imageDimensions.height) / 2) - TOP_MENU_HEIGHT);
}; return { x, y };
}; };
const markChosenColor = (colors: IColor[], chosenColor: string) => { const markChosenColor = (colors: IColor[], chosenColor: string) => {
@ -192,11 +200,153 @@ const SnippingTool = () => {
return undefined; return undefined;
}; };
const done = (e) => { const done = () => {
getMousePosition(e);
ipcRenderer.send('upload-snippet', screenSnippet); ipcRenderer.send('upload-snippet', screenSnippet);
}; };
// Render and preparing render functions
const addHighlightPoint = (paths: IPath[], point: IPoint) => {
const activePath = paths[paths.length - 1];
const shouldShow = true;
const key = 'path' + paths.length;
if (!isDrawing) {
paths.push({
points: [point],
color: highlightColor,
strokeWidth: HIGHLIGHT_WIDTH,
shouldShow,
key,
});
} else {
activePath.points.push(point);
}
return paths;
};
const addPenPoint = (paths: IPath[], point: IPoint) => {
const activePath = paths[paths.length - 1];
const shouldShow = true;
const key = 'path' + paths.length;
if (!isDrawing) {
paths.push({
points: [point],
color: penColor,
strokeWidth: PEN_WIDTH,
shouldShow,
key,
});
} else {
activePath.points.push(point);
}
return paths;
};
const addPathPoint = (e: React.MouseEvent) => {
const p = [...paths];
const mousePos = getMousePosition(e);
lazy.update({ x: mousePos.x, y: mousePos.y });
const point: IPoint = lazy.getBrushCoordinates();
if (chosenTool === Tool.highlight) {
setPaths(addHighlightPoint(p, point));
} else {
setPaths(addPenPoint(p, point));
}
if (!isDrawing) {
setIsDrawing(true);
}
};
const renderPath = (path: ISvgPath) => {
return (
<path
pointerEvents={path.shouldShow ? 'visiblePainted' : 'none'}
style={{ display: path.shouldShow ? 'block' : 'none' }}
key={path.key}
stroke={path.color}
strokeLinecap='round'
strokeWidth={path.strokeWidth || 5}
d={path.svgPath}
fill='none'
onClick={() => maybeErasePath(path.key)}
/>
);
};
const renderPaths = (paths: ISvgPath[]) => {
return paths.map((path) => renderPath(path));
};
const getSvgDot = (point: IPoint) => {
const { x, y } = point;
// This is the SVG path data for a dot at the location of x, y
return (
'M ' +
x +
' ' +
y +
' m -0.1, 0 a 0.1,0.1 0 1,0 0.2,0 a 0.1,0.1 0 1,0 -0.2,0'
);
};
const getSvgPath = (points: IPoint[]) => {
let stroke = '';
if (points && points.length > 0) {
// Start point of path
stroke = `M ${points[0].x} ${points[0].y}`;
let p1: IPoint;
let p2: IPoint;
let end: IPoint;
// Adding points from points array to SVG curve path
for (let i = 1; i < points.length - 2; i += 2) {
p1 = points[i];
p2 = points[i + 1];
end = points[i + 2];
stroke += ` C ${p1.x} ${p1.y}, ${p2.x} ${p2.y}, ${end.x} ${end.y}`;
}
}
return stroke;
};
const getSvgPathData = (path: IPath) => {
const points = path.points;
const x = points[0].x;
const y = points[0].y;
let data: string;
// Since a path must got from point A to point B, we need at least two X and Y pairs to render something.
// Therefore we start with render a dot, so that the user gets visual feedback from only one X and Y pair.
data = getSvgDot({ x, y });
data += getSvgPath(points);
return {
svgPath: data,
key: path && path.key,
strokeWidth: path && path.strokeWidth,
color: path && path.color,
shouldShow: path && path.shouldShow,
};
};
const getSvgPathsData = (paths: IPath[]) => {
return paths.map((path) => getSvgPathData(path));
};
// Mouse tracking functions
const handleMouseDown = (e: React.MouseEvent) => {
if (chosenTool === Tool.eraser) {
return;
}
addPathPoint(e);
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDrawing || chosenTool === Tool.eraser) {
return;
}
addPathPoint(e);
};
return ( return (
<div className='SnippingTool' lang={i18n.getLocale()}> <div className='SnippingTool' lang={i18n.getLocale()}>
<header> <header>
@ -205,6 +355,7 @@ const SnippingTool = () => {
style={getBorderStyle(Tool.pen)} style={getBorderStyle(Tool.pen)}
className='ActionButton' className='ActionButton'
onClick={usePen} onClick={usePen}
title={i18n.t('Pen', SNIPPING_TOOL_NAMESPACE)()}
> >
<img src='../renderer/assets/snip-draw.svg' /> <img src='../renderer/assets/snip-draw.svg' />
</button> </button>
@ -212,6 +363,7 @@ const SnippingTool = () => {
style={getBorderStyle(Tool.highlight)} style={getBorderStyle(Tool.highlight)}
className='ActionButton' className='ActionButton'
onClick={useHighlight} onClick={useHighlight}
title={i18n.t('Highlight', SNIPPING_TOOL_NAMESPACE)()}
> >
<img src='../renderer/assets/snip-highlight.svg' /> <img src='../renderer/assets/snip-highlight.svg' />
</button> </button>
@ -219,6 +371,7 @@ const SnippingTool = () => {
style={getBorderStyle(Tool.eraser)} style={getBorderStyle(Tool.eraser)}
className='ActionButton' className='ActionButton'
onClick={useEraser} onClick={useEraser}
title={i18n.t('Erase', SNIPPING_TOOL_NAMESPACE)()}
> >
<img src='../renderer/assets/snip-erase.svg' /> <img src='../renderer/assets/snip-erase.svg' />
</button> </button>
@ -228,7 +381,7 @@ const SnippingTool = () => {
{i18n.t('Clear', SNIPPING_TOOL_NAMESPACE)()} {i18n.t('Clear', SNIPPING_TOOL_NAMESPACE)()}
</button> </button>
</div> </div>
</header>; </header>
{ {
shouldRenderPenColorPicker && ( shouldRenderPenColorPicker && (
@ -258,20 +411,34 @@ const SnippingTool = () => {
) )
} }
<main> <main style={{ minHeight: MIN_ANNOTATE_AREA_HEIGHT }}>
<div ref={annotateRef}> <div>
<img <svg
src={screenSnippet} style={{ cursor: 'crosshair' }}
id='annotate'
width={imageDimensions.width} width={imageDimensions.width}
height={imageDimensions.height} height={imageDimensions.height}
className='SnippetImage' onMouseDown={handleMouseDown}
alt={i18n.t('Screen snippet', SNIPPING_TOOL_NAMESPACE)()} onMouseUp={stopDrawing}
onMouseMove={handleMouseMove}
onMouseLeave={stopDrawing}
>
<image
x={0}
y={0}
id='screenSnippet'
xlinkHref={screenSnippet}
width={imageDimensions.width}
height={imageDimensions.height}
className='SnippetImage'
/> />
{renderPaths(getSvgPathsData(paths))}
</svg>
</div> </div>
</main> </main>
<footer> <footer>
<button onClick={done}> <button className='DoneButton' onClick={done}>
{i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()} {i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
</button> </button>
</footer> </footer>

View File

@ -15,6 +15,13 @@ body {
width: 100%; width: 100%;
} }
button {
&:focus {
user-select: none;
outline: none;
}
}
.SnippingTool:lang(ja-JP) { .SnippingTool:lang(ja-JP) {
font-family: @font-family-ja; font-family: @font-family-ja;
@ -43,8 +50,8 @@ body {
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
text-align: center; text-align: center;
padding: 4px 0; align-items: center;
max-height: 48px; height: 48px;
.ActionButton { .ActionButton {
width: 24px; width: 24px;
@ -65,25 +72,21 @@ body {
&:first-child { &:first-child {
margin-left: 0; margin-left: 0;
} }
&:focus {
outline: none;
}
} }
.ClearButton { .ClearButton {
display: block; display: block;
padding: 4px 10px;
border: 2px solid #7c7f86; border: 2px solid #7c7f86;
border-radius: 16px; border-radius: 16px;
color: #7c7f86; color: #7c7f86;
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 16px;
margin-left: 24px;
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;
background-color: #ffffff; background-color: #ffffff;
height: 24px;
width: 68px;
} }
.DrawActions { .DrawActions {
@ -95,22 +98,20 @@ body {
.ClearActions { .ClearActions {
display: flex; display: flex;
flex: none;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
text-align: center; text-align: center;
position: absolute; position: absolute;
right: 24px; right: 20px;
align-items: center; align-items: center;
.ActionButton {
img {
width: 16px;
}
}
} }
} }
main { main {
display: flex;
align-items: center;
justify-content: center;
text-align: center; text-align: center;
margin: 0; margin: 0;
background: linear-gradient( background: linear-gradient(
@ -119,6 +120,8 @@ body {
rgba(255, 255, 255, 0.96) rgba(255, 255, 255, 0.96)
), ),
#525760; #525760;
height: 100%;
width: 100%;
.SnippetImage { .SnippetImage {
width: 100%; width: 100%;
@ -129,18 +132,17 @@ body {
footer { footer {
display: flex; display: flex;
align-items: center;
justify-content: flex-end; justify-content: flex-end;
padding: 2px 32px 4px 0; height: 72px;
max-height: 72px;
button { .DoneButton {
box-shadow: none; box-shadow: none;
border: none; border: none;
border-radius: 16px; border-radius: 16px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
padding: 8px 24px;
display: inline-block; display: inline-block;
text-decoration: none; text-decoration: none;
line-height: 16px; line-height: 16px;
@ -148,27 +150,18 @@ body {
color: rgba(255, 255, 255, 0.96); color: rgba(255, 255, 255, 0.96);
cursor: pointer; cursor: pointer;
text-transform: uppercase; text-transform: uppercase;
margin: 10px 30px 4px 0; margin-right: 32px;
height: 32px;
width: 80px;
&:focus { &:focus {
box-shadow: 0 0 10px rgba(61, 162, 253, 1); box-shadow: 0 0 10px rgba(61, 162, 253, 1);
outline: none;
}
}
}
@media only screen and (max-width: 400px) {
header {
.ClearActions {
position: relative;
right: auto;
margin-left: 24px;
} }
} }
} }
} }
.colorPicker { .ColorPicker {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin: 4px; margin: 4px;
@ -181,7 +174,7 @@ body {
border-radius: 24px; border-radius: 24px;
} }
.enclosingCircle { .EnclosingCircle {
width: 24px; width: 24px;
height: 24px; height: 24px;
background: #FFFFFF; background: #FFFFFF;
@ -196,7 +189,7 @@ body {
display: flex; display: flex;
} }
.colorDot { .ColorDot {
border-radius: 50%; border-radius: 50%;
flex: none; flex: none;
order: 0; order: 0;

View File

@ -23,6 +23,7 @@
true, true,
650 650
], ],
"no-shadowed-variable": false,
"no-trailing-whitespace": true, "no-trailing-whitespace": true,
"no-duplicate-variable": true, "no-duplicate-variable": true,
"no-var-keyword": true, "no-var-keyword": true,