From b0efa4a390415072eaeb06c8719ea57d73e10466 Mon Sep 17 00:00:00 2001 From: vkalev <50213748+vkalev@users.noreply.github.com> Date: Wed, 30 Jun 2021 12:52:52 -0500 Subject: ink Bézier handle movement fixed + small visual changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/util/InteractionUtils.tsx | 2 +- src/client/views/InkStrokeProperties.ts | 119 ++++++++++++++++++++++++-------- src/client/views/InkingStroke.scss | 18 ++--- src/client/views/InkingStroke.tsx | 93 ++++++++++++++----------- src/fields/InkField.ts | 24 +++++++ 5 files changed, 177 insertions(+), 79 deletions(-) (limited to 'src') diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 01d00db30..ba935e3bf 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -208,7 +208,7 @@ export namespace InteractionUtils { (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt); @@ -79,10 +78,6 @@ export class InkStrokeProperties { }); } - constructor() { - InkStrokeProperties.Instance = this; - } - /** * Adds a new control point to the ink instance when editing its format. * @param x The x-coordinate of the current new point. @@ -213,35 +208,99 @@ export class InkStrokeProperties { } /** - * Handles the movement / scaling of control points of an ink instance. - * @param xDiff The movement of the control point's x-coordinate. - * @param yDiff The movement of the control point's y-coordinate. - * @param controlNum The index of the current control point selected. - * @returns The changed x- and y-coordinates of the control points. + * Handles the movement/scaling of a control point. */ @undoBatch @action - control = (xDiff: number, yDiff: number, controlNum: number) => - this.applyFunction((doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { + moveControl = (deltaX: number, deltaY: number, controlIndex: number) => + this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const newPoints: { X: number, Y: number }[] = []; - const order = controlNum % 4; + const order = controlIndex % 4; for (var i = 0; i < ink.length; i++) { - newPoints.push( - (controlNum === i || - (order === 0 && i === controlNum + 1) || - (order === 0 && controlNum !== 0 && i === controlNum - 2) || - (order === 0 && controlNum !== 0 && i === controlNum - 1) || - (order === 3 && i === controlNum - 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) || - ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) - ) ? - { X: ink[i].X - xDiff / ptsXscale, Y: ink[i].Y - yDiff / ptsYscale } : - { X: ink[i].X, Y: ink[i].Y }); + const leftHandlePoint = order === 0 && i === controlIndex + 1; + const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2; + if (controlIndex === i || + leftHandlePoint || + rightHandlePoint || + (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || + (order === 3 && i === controlIndex - 1) || + (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) || + (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) || + ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))) { + newPoints.push({ X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }); + } else { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } } return newPoints; }); + /** + * Rotates the target point about the origin point for a given angle (radians). + */ + @action + rotatePoint = (target: PointData, origin: PointData, angle: number) => { + target.X -= origin.X; + target.Y -= origin.Y; + const newX = Math.cos(angle) * target.X - Math.sin(angle) * target.Y; + const newY = Math.sin(angle) * target.X + Math.cos(angle) * target.Y; + target.X = newX + origin.X; + target.Y = newY + origin.Y; + return target + } + + /** + * Finds the angle difference (in radians) between two vectors relative to an arbitrary origin. + */ + angleChange = (a: PointData, b: PointData, origin: PointData) => { + // Finding vector representation of inputted points relative to new origin. + let vectorA = { X: a.X - origin.X, Y: a.Y - origin.Y }; + let vectorB = { X: b.X - origin.X, Y: b.Y - origin.Y }; + const crossProduct = vectorB.X * vectorA.Y - vectorB.Y * vectorA.X; + // Determining whether rotation is clockwise or counterclockwise. + const sign = crossProduct < 0 ? 1 : -1; + const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y); + const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y); + // Normalizing the vectors. + vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA }; + vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB }; + const dotProduct = vectorB.X * vectorA.X + vectorB.Y * vectorA.Y; + const theta = Math.acos(dotProduct); + return sign * theta; + } + + /** + * Handles the movement/scaling of a handle point. + */ + @undoBatch + @action + moveHandle = (deltaX: number, deltaY: number, handleIndex: number) => + this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { + const newPoints: { X: number, Y: number }[] = []; + const order = handleIndex % 4; + let newHandlePoint = { X: 0, Y: 0 }; + + for (var i = 0; i < ink.length; i++) { + if (handleIndex === i) { + newHandlePoint = { X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }; + newPoints.push({ X: newHandlePoint.X, Y: newHandlePoint.Y }); + } else { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + } + + if (handleIndex !== 1 && handleIndex !== ink.length - 2) { + const oldHandlePoint = ink[handleIndex]; + let oppositeHandlePoint = order === 1 ? ink[handleIndex - 3] : ink[handleIndex + 3]; + const controlPoint = order === 1 ? ink[handleIndex - 1] : ink[handleIndex + 1]; + const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); + oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); + order === 1 ? newPoints[handleIndex - 3] = oppositeHandlePoint : newPoints[handleIndex + 3] = oppositeHandlePoint; + } + + return newPoints; + }); + /** * Changes the color of the border of the ink instance. * @param color The new hex value to change the border to. diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss index 30ab1967e..f67b1779d 100644 --- a/src/client/views/InkingStroke.scss +++ b/src/client/views/InkingStroke.scss @@ -1,11 +1,11 @@ .inkingStroke { - mix-blend-mode: multiply; - stroke-linejoin: round; - stroke-linecap: round; - overflow: visible !important; - transform-origin: top left; + mix-blend-mode: multiply; + stroke-linejoin: round; + stroke-linecap: round; + overflow: visible !important; + transform-origin: top left; - svg:not(:root) { - overflow: visible !important; - } -} \ No newline at end of file + svg:not(:root) { + overflow: visible !important; + } +} diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 859e53b97..163eb05b0 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -2,7 +2,7 @@ import { action } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../fields/Doc"; import { documentSchema } from "../../fields/documentSchemas"; -import { InkData, InkField, InkTool } from "../../fields/InkField"; +import { InkData, InkField, InkTool, ControlPoint, HandlePoint, HandleLine } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; @@ -28,33 +28,51 @@ export class InkingStroke extends ViewBoxBaseComponent { + analyzeStrokes = () => { const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]); } - public static toggleMask = action((inkDoc: Doc) => { + @action + public static toggleMask = (inkDoc: Doc) => { inkDoc.isInkMask = !inkDoc.isInkMask; inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined; inkDoc.mixBlendMode = inkDoc.isInkMask ? "hard-light" : undefined; inkDoc.color = "#9b9b9bff"; inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined; - }); + }; /** * Handles the movement of a selected control point when the user clicks and drags. - * @param e React Pointer Event. - * @param controlNum The number of the currently selected control point. + * @param controlNum The index of the currently selected control point. */ @action onControlDown = (e: React.PointerEvent, controlNum: number): void => { if (InkStrokeProperties.Instance) { - InkStrokeProperties.Instance.control(0, 0, 1); + InkStrokeProperties.Instance.moveControl(0, 0, 1); + const controlUndo = UndoManager.StartBatch("DocDecs set radius"); + const screenScale = this.props.ScreenToLocalTransform().Scale; + setupMoveUpEvents(this, e, + (e: PointerEvent, down: number[], delta: number[]) => { + InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlNum); + return false; + }, + () => controlUndo?.end(), emptyFunction); + } + } + + /** + * Handles the movement of a selected handle point when the user clicks and drags. + * @param controlNum The index of the currently selected handle point. + */ + onHandleDown = (e: React.PointerEvent, handleNum: number): void => { + if (InkStrokeProperties.Instance) { + InkStrokeProperties.Instance.moveControl(0, 0, 1); const controlUndo = UndoManager.StartBatch("DocDecs set radius"); const screenScale = this.props.ScreenToLocalTransform().Scale; setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { - InkStrokeProperties.Instance?.control(-delta[0] * screenScale, -delta[1] * screenScale, controlNum); + InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum); return false; }, () => controlUndo?.end(), emptyFunction); @@ -69,7 +87,7 @@ export class InkingStroke extends ViewBoxBaseComponent { if (InkStrokeProperties.Instance) { InkStrokeProperties.Instance._currPoint = i; - document.addEventListener("keydown", this.delPts, true); + document.addEventListener("keydown", this.onDelete, true); } } @@ -78,7 +96,7 @@ export class InkingStroke extends ViewBoxBaseComponent { + onDelete = (e: KeyboardEvent) => { if (["-", "Backspace", "Delete"].includes(e.key)) { if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation(); } @@ -101,7 +119,6 @@ export class InkingStroke extends ViewBoxBaseComponent p.X); const ys = data.map(p => p.Y); @@ -118,12 +135,18 @@ export class InkingStroke extends ViewBoxBaseComponent 1 && lineRgt - lineLft > 1, false); + + const selectedLine = InteractionUtils.CreatePolyline(data, lineLft - strokeWidth * 3, lineTop - strokeWidth * 3, "#1F85DE", strokeWidth / 6, strokeWidth / 6, + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), + StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), + StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); // Invisible polygonal line that enables the ink to be selected by the user. const hpoints = InteractionUtils.CreatePolyline(data, left, top, @@ -136,10 +159,13 @@ export class InkingStroke extends ViewBoxBaseComponent= 4) { + + // create separate functions for these for (var i = 0; i <= data.length - 4; i += 4) { controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); controlPoints.push({ X: data[i + 3].X, Y: data[i + 3].Y, I: i + 3 }); @@ -147,29 +173,21 @@ export class InkingStroke extends ViewBoxBaseComponent @@ -178,27 +196,24 @@ export class InkingStroke extends ViewBoxBaseComponent { formatInstance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} pointerEvents="all" cursor="all-scroll" /> ); - // Green circles that allow the user to edit the curvature of the line using the selected point as the anchor. + // Blue circles that allow the user to edit the curvature of the line using the selected control point as the anchor. const handles = handlePoints.map((pts, i) => - this.onControlDown(e, pts.I)} pointerEvents="all" cursor="default" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> + this.onHandleDown(e, pts.I)} pointerEvents="all" cursor="default" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> ); - // Points (red circles) of the ink that are made visible to user when editing its format. + // Control points of the ink (blue outlined squares) that are made visible to user when editing its format. const controls = controlPoints.map((pts, i) => - { this.changeCurrPoint(pts.I); this.onControlDown(e, pts.I); }} pointerEvents="all" cursor="default" /> ); - // Set of two green lines (each with a handle at the end) that are rendered perpendicular to the current selected point while editing. + // Set of two blue lines (each with a handle at the end) that are rendered perpendicular to the current selected point while editing. const handleLines = handleLine.map((pts, i) => - + - ); @@ -227,8 +242,8 @@ export class InkingStroke extends ViewBoxBaseComponent ); } diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index b79a03146..c158dac42 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -13,6 +13,7 @@ export enum InkTool { Stamp = "stamp" } + // Defines a point in an ink as a pair of x- and y-coordinates. export interface PointData { X: number; @@ -22,6 +23,29 @@ export interface PointData { // Defines an ink as an array of points. export type InkData = Array; +export interface ControlPoint { + X: number; + Y: number; + I: number; +} + +export interface HandlePoint { + X: number; + Y: number; + I: number; + dot1: number; + dot2: number; +} + +export interface HandleLine { + X1: number; + Y1: number; + X2: number; + Y2: number; + dot1: number; + dot2: number; +} + const pointSchema = createSimpleSchema({ X: true, Y: true }); -- cgit v1.2.3-70-g09d2