aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortschicke-brown <tyler_schicke@brown.edu>2019-03-08 04:58:58 +0000
committerGitHub <noreply@github.com>2019-03-08 04:58:58 +0000
commit5a1da11a5767899aac2f1bfac6d33e0ee5d47c9e (patch)
tree303cc06880b6c5e0cc644f559c9548f95790423e /src
parent9940924f361f1b9d65d7cc0ad1e4f6f1f4d5d318 (diff)
parent889407852167dece0160127817e390336697e3a6 (diff)
Merge pull request #45 from browngraphicslab/inking
Inking
Diffstat (limited to 'src')
-rw-r--r--src/client/views/InkingCanvas.scss32
-rw-r--r--src/client/views/InkingCanvas.tsx161
-rw-r--r--src/client/views/InkingControl.tsx77
-rw-r--r--src/client/views/InkingStroke.tsx66
-rw-r--r--src/client/views/Main.tsx2
-rw-r--r--src/client/views/collections/CollectionFreeFormView.tsx6
-rw-r--r--src/fields/InkField.ts47
-rw-r--r--src/fields/KeyStore.ts1
-rw-r--r--src/server/Message.ts2
-rw-r--r--src/server/ServerUtil.ts3
10 files changed, 395 insertions, 2 deletions
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
new file mode 100644
index 000000000..f654b194b
--- /dev/null
+++ b/src/client/views/InkingCanvas.scss
@@ -0,0 +1,32 @@
+.inking-canvas {
+ position: fixed;
+ top: -50000px;
+ left: -50000px; // z-index: 99; //overlays ink on top of everything
+ svg {
+ width: 100000px;
+ height: 100000px;
+ .highlight {
+ mix-blend-mode: multiply;
+ }
+ }
+}
+
+.inking-control {
+ position: absolute;
+ right: 0;
+ bottom: 75px;
+ text-align: right;
+ .ink-panel {
+ margin-top: 12px;
+ &:first {
+ margin-top: 0;
+ }
+ }
+ .ink-size {
+ display: flex;
+ justify-content: space-between;
+ input {
+ width: 85%;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
new file mode 100644
index 000000000..baf1567b7
--- /dev/null
+++ b/src/client/views/InkingCanvas.tsx
@@ -0,0 +1,161 @@
+import { observer } from "mobx-react";
+import { action } from "mobx";
+import { InkingControl } from "./InkingControl";
+import React = require("react");
+import { Transform } from "../util/Transform";
+import { Document } from "../../fields/Document";
+import { KeyStore } from "../../fields/KeyStore";
+import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField";
+import { JsxArgs } from "./nodes/DocumentView";
+import { InkingStroke } from "./InkingStroke";
+import "./InkingCanvas.scss"
+import { CollectionDockingView } from "./collections/CollectionDockingView";
+import { Utils } from "../../Utils";
+
+
+interface InkCanvasProps {
+ getScreenTransform: () => Transform;
+ Document: Document;
+}
+
+@observer
+export class InkingCanvas extends React.Component<InkCanvasProps> {
+
+ private _isDrawing: boolean = false;
+ private _idGenerator: string = "";
+
+ constructor(props: Readonly<InkCanvasProps>) {
+ super(props);
+ }
+
+ get inkData(): StrokeMap {
+ return new Map(this.props.Document.GetData(KeyStore.Ink, InkField, new Map));
+ }
+
+ set inkData(value: StrokeMap) {
+ this.props.Document.SetData(KeyStore.Ink, value, InkField);
+ }
+
+ componentDidMount() {
+ document.addEventListener("mouseup", this.handleMouseUp);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("mouseup", this.handleMouseUp);
+ }
+
+
+ @action
+ handleMouseDown = (e: React.PointerEvent): void => {
+ if (e.button != 0 ||
+ InkingControl.Instance.selectedTool === InkTool.None) {
+ return;
+ }
+ e.stopPropagation()
+ if (InkingControl.Instance.selectedTool === InkTool.Eraser) {
+ return
+ }
+ e.stopPropagation()
+ const point = this.relativeCoordinatesForEvent(e);
+
+ // start the new line, saves a uuid to represent the field of the stroke
+ this._idGenerator = Utils.GenerateGuid();
+ let data = this.inkData;
+ data.set(this._idGenerator,
+ {
+ pathData: [point],
+ color: InkingControl.Instance.selectedColor,
+ width: InkingControl.Instance.selectedWidth,
+ tool: InkingControl.Instance.selectedTool
+ });
+ this.inkData = data;
+ this._isDrawing = true;
+ }
+
+ @action
+ handleMouseMove = (e: React.PointerEvent): void => {
+ if (!this._isDrawing ||
+ InkingControl.Instance.selectedTool === InkTool.None) {
+ return;
+ }
+ e.stopPropagation()
+ if (InkingControl.Instance.selectedTool === InkTool.Eraser) {
+ return
+ }
+ const point = this.relativeCoordinatesForEvent(e);
+
+ // add points to new line as it is being drawn
+ let data = this.inkData;
+ let strokeData = data.get(this._idGenerator);
+ if (strokeData) {
+ strokeData.pathData.push(point);
+ data.set(this._idGenerator, strokeData);
+ }
+
+ this.inkData = data;
+ }
+
+ @action
+ handleMouseUp = (e: MouseEvent): void => {
+ this._isDrawing = false;
+ }
+
+ relativeCoordinatesForEvent = (e: React.MouseEvent): { x: number, y: number } => {
+ let [x, y] = this.props.getScreenTransform().transformPoint(e.clientX, e.clientY);
+ x += 50000
+ y += 50000
+ return { x, y };
+ }
+
+ @action
+ removeLine = (id: string): void => {
+ let data = this.inkData;
+ data.delete(id);
+ this.inkData = data;
+ }
+
+ render() {
+ // styling for cursor
+ let canvasStyle = {};
+ if (InkingControl.Instance.selectedTool === InkTool.None) {
+ canvasStyle = { pointerEvents: "none" };
+ } else {
+ canvasStyle = { pointerEvents: "auto", cursor: "crosshair" };
+ }
+
+ // get data from server
+ // let inkField = this.props.Document.GetT(KeyStore.Ink, InkField);
+ // if (!inkField || inkField == "<Waiting>") {
+ // return (<div className="inking-canvas" style={canvasStyle}
+ // onMouseDown={this.handleMouseDown} onMouseMove={this.handleMouseMove} >
+ // <svg>
+ // </svg>
+ // </div >)
+ // }
+
+ let lines = this.inkData;
+
+ // parse data from server
+ let paths: Array<JSX.Element> = []
+ Array.from(lines).map(item => {
+ let id = item[0];
+ let strokeData = item[1];
+ paths.push(<InkingStroke key={id} id={id}
+ line={strokeData.pathData}
+ color={strokeData.color}
+ width={strokeData.width}
+ tool={strokeData.tool}
+ deleteCallback={this.removeLine} />)
+ })
+
+ return (
+
+ <div className="inking-canvas" style={canvasStyle}
+ onPointerDown={this.handleMouseDown} onPointerMove={this.handleMouseMove} >
+ <svg>
+ {paths}
+ </svg>
+ </div >
+ )
+ }
+} \ No newline at end of file
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
new file mode 100644
index 000000000..929fb42a1
--- /dev/null
+++ b/src/client/views/InkingControl.tsx
@@ -0,0 +1,77 @@
+import { observable, action, computed } from "mobx";
+import { CirclePicker, ColorResult } from 'react-color'
+import React = require("react");
+import "./InkingCanvas.scss"
+import { InkTool } from "../../fields/InkField";
+import { observer } from "mobx-react";
+
+@observer
+export class InkingControl extends React.Component {
+ static Instance: InkingControl = new InkingControl({});
+ @observable private _selectedTool: InkTool = InkTool.None;
+ @observable private _selectedColor: string = "#f44336";
+ @observable private _selectedWidth: string = "25";
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ InkingControl.Instance = this
+ }
+
+ @action
+ switchTool = (tool: InkTool): void => {
+ this._selectedTool = tool;
+ }
+
+ @action
+ switchColor = (color: ColorResult): void => {
+ this._selectedColor = color.hex;
+ }
+
+ @action
+ switchWidth = (width: string): void => {
+ this._selectedWidth = width;
+ }
+
+ @computed
+ get selectedTool() {
+ return this._selectedTool;
+ }
+
+ @computed
+ get selectedColor() {
+ return this._selectedColor;
+ }
+
+ @computed
+ get selectedWidth() {
+ return this._selectedWidth;
+ }
+
+ selected = (tool: InkTool) => {
+ if (this._selectedTool === tool) {
+ return { backgroundColor: "black", color: "white" }
+ }
+ return {}
+ }
+
+ render() {
+ return (
+ <div className="inking-control">
+ <div className="ink-tools ink-panel">
+ <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}>Pen</button>
+ <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}>Highlighter</button>
+ <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}>Eraser</button>
+ <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}> None</button>
+ </div>
+ <div className="ink-size ink-panel">
+ <label htmlFor="stroke-width">Size</label>
+ <input type="range" min="1" max="100" defaultValue="25" name="stroke-width"
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} />
+ </div>
+ <div className="ink-color ink-panel">
+ <CirclePicker onChange={this.switchColor} />
+ </div>
+ </div>
+ )
+ }
+} \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
new file mode 100644
index 000000000..d724421d3
--- /dev/null
+++ b/src/client/views/InkingStroke.tsx
@@ -0,0 +1,66 @@
+import { observer } from "mobx-react";
+import { observable } from "mobx";
+import { InkingControl } from "./InkingControl";
+import { InkTool } from "../../fields/InkField";
+import React = require("react");
+
+
+interface StrokeProps {
+ id: string;
+ line: Array<{ x: number, y: number }>;
+ color: string;
+ width: string;
+ tool: InkTool;
+ deleteCallback: (index: string) => void;
+}
+
+@observer
+export class InkingStroke extends React.Component<StrokeProps> {
+
+ @observable private _strokeTool: InkTool = this.props.tool;
+ @observable private _strokeColor: string = this.props.color;
+ @observable private _strokeWidth: string = this.props.width;
+
+ private _canvasColor: string = "#cdcdcd";
+
+ deleteStroke = (e: React.MouseEvent): void => {
+ if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) {
+ this.props.deleteCallback(this.props.id);
+ }
+ }
+
+ parseData = (line: Array<{ x: number, y: number }>): string => {
+ if (line.length === 0) {
+ return "";
+ }
+ const pathData = "M " +
+ line.map(p => {
+ return p.x + " " + p.y;
+ }).join(" L ");
+ return pathData;
+ }
+
+ createStyle() {
+ switch (this._strokeTool) {
+ // add more tool styles here
+ default:
+ return {
+ fill: "none",
+ stroke: this._strokeColor,
+ strokeWidth: this._strokeWidth + "px",
+ }
+ }
+ }
+
+
+ render() {
+ let pathStyle = this.createStyle();
+ let pathData = this.parseData(this.props.line);
+
+ return (
+ <path className={(this._strokeTool === InkTool.Highlighter) ? "highlight" : ""}
+ d={pathData} style={pathStyle} strokeLinejoin="round" strokeLinecap="round"
+ onMouseOver={this.deleteStroke} onMouseDown={this.deleteStroke} />
+ )
+ }
+} \ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index d845fa7a3..7aca6d88f 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -17,6 +17,7 @@ import { ContextMenu } from './ContextMenu';
import { DocumentDecorations } from './DocumentDecorations';
import { DocumentView } from './nodes/DocumentView';
import "./Main.scss";
+import { InkingControl } from './InkingControl';
configure({ enforceActions: "observed" }); // causes errors to be generated when modifying an observable outside of an action
@@ -96,6 +97,7 @@ Documents.initProtos(mainDocId, (res?: Document) => {
<button onClick={clearDatabase}>Clear Database</button></div>
<button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button>
<button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button>
+ <InkingControl />
</div>),
document.getElementById('root'));
})
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 1a7349201..aac0153dd 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -21,6 +21,9 @@ import "./CollectionFreeFormView.scss";
import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
import { CollectionViewBase } from "./CollectionViewBase";
import { Documents } from "../../documents/Documents";
+import { InkingCanvas } from "../InkingCanvas";
+import { InkingControl } from "../InkingControl";
+import { InkTool } from "../../../fields/InkField";
import React = require("react");
const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
@@ -137,7 +140,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
let localTransform = this.getLocalTransform()
localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y)
- console.log(localTransform)
+ // console.log(localTransform)
this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale);
this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale);
@@ -305,6 +308,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
style={{ transformOrigin: "left top", transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}
ref={this._canvasRef}>
{this.backgroundView}
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
{cursor}
{this.views}
</div>
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
new file mode 100644
index 000000000..a475e2aae
--- /dev/null
+++ b/src/fields/InkField.ts
@@ -0,0 +1,47 @@
+import { BasicField } from "./BasicField";
+import { Types } from "../server/Message";
+import { FieldId } from "./Field";
+
+export enum InkTool {
+ None,
+ Pen,
+ Highlighter,
+ Eraser
+}
+export interface StrokeData {
+ pathData: Array<{ x: number, y: number }>;
+ color: string;
+ width: string;
+ tool: InkTool;
+}
+export type StrokeMap = Map<string, StrokeData>;
+
+export class InkField extends BasicField<StrokeMap> {
+ constructor(data: StrokeMap = new Map, id?: FieldId, save: boolean = true) {
+ super(data, save, id);
+ }
+
+ ToScriptString(): string {
+ return `new InkField("${this.Data}")`;
+ }
+
+ Copy() {
+ return new InkField(this.Data);
+ }
+
+ ToJson(): { _id: string; type: Types; data: any; } {
+ return {
+ type: Types.Ink,
+ data: this.Data,
+ _id: this.Id,
+ }
+ }
+
+ static FromJson(id: string, data: any): InkField {
+ let map = new Map<string, StrokeData>();
+ Object.keys(data).forEach(key => {
+ map.set(key, data[key]);
+ });
+ return new InkField(map, id, false);
+ }
+} \ No newline at end of file
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
index a3b39735d..9cdd18f4e 100644
--- a/src/fields/KeyStore.ts
+++ b/src/fields/KeyStore.ts
@@ -26,4 +26,5 @@ export namespace KeyStore {
export const Caption = new Key("Caption");
export const ActiveFrame = new Key("ActiveFrame");
export const DocumentText = new Key("DocumentText");
+ export const Ink = new Key("Ink");
}
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 148e6e723..fc07ef89b 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -45,7 +45,7 @@ export class GetFieldArgs {
}
export enum Types {
- Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html
+ Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Ink
}
export class DocumentTransfer implements Transferable {
diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts
index a53fb5d2b..f5734a86c 100644
--- a/src/server/ServerUtil.ts
+++ b/src/server/ServerUtil.ts
@@ -11,6 +11,7 @@ import { Types } from './Message';
import { Utils } from '../Utils';
import { HtmlField } from '../fields/HtmlField';
import { WebField } from '../fields/WebField';
+import { InkField } from '../fields/InkField';
export class ServerUtils {
public static FromJson(json: any): Field {
@@ -41,6 +42,8 @@ export class ServerUtils {
return new ImageField(new URL(data), id, false)
case Types.List:
return ListField.FromJson(id, data)
+ case Types.Ink:
+ return InkField.FromJson(id, data);
case Types.Document:
let doc: Document = new Document(id, false)
let fields: [string, string][] = data as [string, string][]