Skip to content

Commit

Permalink
Add export/import support to InkingManager (#499)
Browse files Browse the repository at this point in the history
Co-authored-by: Ryan Bliss <[email protected]>
  • Loading branch information
dclaux and ryanbliss committed Apr 13, 2023
1 parent a0cbb6c commit cc85792
Show file tree
Hide file tree
Showing 10 changed files with 438 additions and 94 deletions.
37 changes: 19 additions & 18 deletions lerna-package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/live-share-canvas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Easily add collaborative inking to your Teams meeting app, powered by [Fluid Fra

This package is an extension of Microsoft Live Share, and requires the `@microsoft/live-share` package. You can find it on [NPM](https://www.npmjs.com/package/@microsoft/live-share).

You can find our API reference documentation at [aka.ms/livesharedocs](https://aka.ms/livesharedocs).
You can find our documentation at [aka.ms/livesharecanvas](https://aka.ms/livesharecanvas).

## Installing

Expand Down Expand Up @@ -84,7 +84,7 @@ document.getElementById("btnPenBrushBlue").onclick = () => {
inkingManager.penBrush.color = { r: 0, g: 0, b: 255, a: 1};
}

// Other tools and brush settings are available, please refer to the documentation
// Other tools and brush settings are available, please refer to the documentation at https://aka.ms/livesharecanvas
```

## Code sample
Expand Down
72 changes: 7 additions & 65 deletions packages/live-share-canvas/src/canvas/DryWetCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@

import { InkingCanvas } from "./InkingCanvas";
import {
getPressureAdjustedSize,
IPointerPoint,
DefaultPenBrush,
IBrush,
toCssRgbaColor,
} from "../core";
import {
computeQuadBetweenTwoCircles,
computeQuadBetweenTwoRectangles,
IQuadPathSegment,
} from "../core/Internals";
import { computeQuadPath, IQuadPathSegment } from "../core/Internals";

/**
* Represents the base class from wet and dry canvases, implementing the common rendering logic.
Expand All @@ -26,65 +21,12 @@ export abstract class DryWetCanvas extends InkingCanvas {
private _points: IPointerPoint[] = [];

private computeQuadPath(tipSize: number): IQuadPathSegment[] {
const result: IQuadPathSegment[] = [];
const tipHalfSize = tipSize / 2;

if (this._pendingPointsStartIndex < this._points.length) {
let previousPoint: IPointerPoint | undefined = undefined;
let previousPointPressureAdjustedTip = 0;

if (this._pendingPointsStartIndex > 0) {
previousPoint = this._points[this._pendingPointsStartIndex - 1];
previousPointPressureAdjustedTip = getPressureAdjustedSize(
tipHalfSize,
previousPoint.pressure
);
}

for (
let i = this._pendingPointsStartIndex;
i < this._points.length;
i++
) {
const p = this._points[i];

let pressureAdjustedTip = getPressureAdjustedSize(
tipHalfSize,
p.pressure
);

const segment: IQuadPathSegment = {
endPoint: p,
tipSize: pressureAdjustedTip,
};

if (previousPoint !== undefined) {
segment.quad =
this.brush.tip === "ellipse"
? computeQuadBetweenTwoCircles(
p,
pressureAdjustedTip,
previousPoint,
previousPointPressureAdjustedTip
)
: computeQuadBetweenTwoRectangles(
p,
pressureAdjustedTip,
pressureAdjustedTip,
previousPoint,
previousPointPressureAdjustedTip,
previousPointPressureAdjustedTip
);
}

result.push(segment);

previousPoint = p;
previousPointPressureAdjustedTip = pressureAdjustedTip;
}
}

return result;
return computeQuadPath(
this._points,
this._pendingPointsStartIndex,
this.brush.tip,
tipSize
);
}

private renderQuadPath(
Expand Down
9 changes: 9 additions & 0 deletions packages/live-share-canvas/src/core/Geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ export function expandRect(rect: IRect, point: IPoint): IRect {
};
}

export function combineRects(rect1: IRect, rect2: IRect): IRect {
return {
left: Math.min(rect1.left, rect2.left),
top: Math.min(rect1.top, rect2.top),
right: Math.max(rect1.right, rect2.right),
bottom: Math.max(rect1.bottom, rect2.bottom),
};
}

/**
* Computes the distance between two points.
* @param p1 The first point.
Expand Down
114 changes: 107 additions & 7 deletions packages/live-share-canvas/src/core/InkingManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
IPointerPoint,
IRect,
screenToViewport,
combineRects,
viewportToScreen,
} from "./Geometry";
import {
Expand All @@ -24,6 +25,7 @@ import {
IStrokeCreationOptions,
StrokeType,
StrokeMode,
IRawStroke,
} from "./Stroke";
import {
InputFilter,
Expand All @@ -46,6 +48,8 @@ import {
generateUniqueId,
computeEndArrow,
isPointInsideRectangle,
computeQuadPath,
renderQuadPathToSVG,
} from "./Internals";

/**
Expand Down Expand Up @@ -493,6 +497,7 @@ export class InkingManager extends EventEmitter {
string,
EphemeralCanvas
>();
private _lastPointerMovePosition?: IPoint;

private onHostResized = (
entries: ResizeObserverEntry[],
Expand Down Expand Up @@ -889,15 +894,15 @@ export class InkingManager extends EventEmitter {
}

private queuePointerMovedNotification(position?: IPoint) {
if (this._pointerMovedNotificationTimeout !== undefined) {
window.clearTimeout(this._pointerMovedNotificationTimeout);
}
this._lastPointerMovePosition = position;

this._pointerMovedNotificationTimeout = window.setTimeout(() => {
this.notifyPointerMoved(position);
if (this._pointerMovedNotificationTimeout === undefined) {
this._pointerMovedNotificationTimeout = window.setTimeout(() => {
this.notifyPointerMoved(this._lastPointerMovePosition);

this._pointerMovedNotificationTimeout = undefined;
}, InkingManager.pointerMovedNotificationDelay);
this._pointerMovedNotificationTimeout = undefined;
}, InkingManager.pointerMovedNotificationDelay);
}
}

private notifyPointerMoved(position?: IPoint) {
Expand Down Expand Up @@ -1065,6 +1070,98 @@ export class InkingManager extends EventEmitter {
this.notifyClear();
}

/**
* Exports the current drawing as a collection of raw strokes.
* @returns A collection or raw strokes.
*/
public exportRaw(): IRawStroke[] {
const result: IRawStroke[] = [];

this._strokes.forEach((stroke: IStroke) => {
result.push({
points: stroke.getAllPoints(),
brush: { ...stroke.brush },
});
});

return result;
}

/**
* Exports the current drawing to an SVG.
* @returns A serialized SVG string.
*/
public exportSVG(): string {
let shapes = "";
let viewBox: IRect | undefined;
let largestTipSize = 0;

this._strokes.forEach((stroke: IStroke) => {
const boundingRect = stroke.getBoundingRect();

viewBox = viewBox
? combineRects(viewBox, boundingRect)
: boundingRect;

if (stroke.brush.tipSize > largestTipSize) {
largestTipSize = stroke.brush.tipSize;
}

const quadPath = computeQuadPath(
stroke.getAllPoints(),
0,
stroke.brush.tip,
stroke.brush.tipSize
);

let path = renderQuadPathToSVG(
quadPath,
stroke.brush.tip,
stroke.brush.color
);

if (stroke.brush.type === "highlighter") {
path = `<g opacity="${InkingCanvas.highlighterOpacity}">${path}</g>`;
}

shapes += path;
});

let svgViewBox = "";

if (viewBox) {
const halfTipSize = largestTipSize / 2;

// Add half the maximum brush tip size on each side
// of the view box, otherwise parts of some strokes
// would be clipped.
viewBox.left -= halfTipSize;
viewBox.top -= halfTipSize;
const viewBoxWidth = viewBox.right - viewBox.left + largestTipSize;
const viewBoxHeight = viewBox.bottom - viewBox.top + largestTipSize;

svgViewBox = `viewBox="${viewBox.left} ${viewBox.top} ${viewBoxWidth} ${viewBoxHeight}"`;
}

return `<svg ${svgViewBox} xmlns="http://www.w3.org/2000/svg">${shapes}</svg>`;
}

/**
* Imports (adds) the specified strokes into the drawing.
* @param rawStrokes The strokes to import.
*/
public importRaw(rawStrokes: IRawStroke[]) {
for (const rawStroke of rawStrokes) {
const stroke = new Stroke({
brush: rawStroke.brush,
points: rawStroke.points,
clientId: InkingManager.localClientId,
});

this.addStroke(stroke);
}
}

/**
* Starts a new wet stroke which will be drawn progressively on the canvas. Multiple wet strokes
* can be created at the same time and will not interfere with each other.
Expand All @@ -1087,6 +1184,7 @@ export class InkingManager extends EventEmitter {
canvas.resize(this.clientWidth, this.clientHeight);
canvas.offset = this.offset;
canvas.scale = this.scale;
canvas.referencePoint = this.referencePoint;

let stroke: WetStroke;

Expand Down Expand Up @@ -1400,6 +1498,8 @@ export class InkingManager extends EventEmitter {
if (this._referencePoint !== value) {
this._referencePoint = value;

this._dryCanvas.referencePoint = value;

this.reRender();
}
}
Expand Down
Loading

0 comments on commit cc85792

Please sign in to comment.