Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/data-objects/canvas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@
"webpack:dev": "webpack --env.development"
},
"dependencies": {
"@fluid-example/example-utils": "^0.45.0",
"@fluidframework/aqueduct": "^0.45.0",
"@fluidframework/common-definitions": "^0.20.1",
"@fluidframework/core-interfaces": "^0.39.7",
"@fluidframework/ink": "^0.45.0",
"@fluidframework/view-interfaces": "^0.45.0"
"react": "^16.10.2"
},
"devDependencies": {
"@fluidframework/build-common": "^0.22.0",
Expand All @@ -53,6 +54,7 @@
"@types/jest-environment-puppeteer": "2.2.0",
"@types/node": "^12.19.0",
"@types/puppeteer": "1.3.0",
"@types/react": "^16.9.15",
"@typescript-eslint/eslint-plugin": "~4.14.0",
"@typescript-eslint/parser": "~4.14.0",
"concurrently": "^5.2.0",
Expand Down
126 changes: 11 additions & 115 deletions examples/data-objects/canvas/src/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,129 +5,25 @@

import { DataObject } from "@fluidframework/aqueduct";
import { IFluidHandle } from "@fluidframework/core-interfaces";
import { IColor, IInk, Ink, InkCanvas } from "@fluidframework/ink";
import { IFluidHTMLOptions, IFluidHTMLView } from "@fluidframework/view-interfaces";
// eslint-disable-next-line import/no-unassigned-import
import "./style.less";
import { IInk, Ink } from "@fluidframework/ink";

const colorPickerColors: IColor[] = [
{ r: 253, g: 0, b: 12, a: 1 },
{ r: 134, g: 0, b: 56, a: 1 },
{ r: 253, g: 187, b: 48, a: 1 },
{ r: 255, g: 255, b: 81, a: 1 },
{ r: 0, g: 45, b: 98, a: 1 },
{ r: 255, g: 255, b: 255, a: 1 },
{ r: 246, g: 83, b: 20, a: 1 },
{ r: 0, g: 161, b: 241, a: 1 },
{ r: 124, g: 187, b: 0, a: 1 },
{ r: 8, g: 170, b: 51, a: 1 },
{ r: 0, g: 0, b: 0, a: 1 },
];
export class Canvas extends DataObject {
private _ink: IInk | undefined;

export class Canvas extends DataObject implements IFluidHTMLView {
public get IFluidHTMLView() { return this; }

private ink: IInk;
private inkCanvas: InkCanvas;
private inkColorPicker: HTMLDivElement;

public render(elm: HTMLElement, options?: IFluidHTMLOptions): void {
elm.appendChild(this.createCanvasDom());
this.sizeCanvas();

window.addEventListener("resize", this.sizeCanvas.bind(this));
public get ink() {
if (this._ink === undefined) {
throw new Error("Ink should be defined before access");
}
return this._ink;
}

protected async initializingFirstTime() {
this.root.set("pageInk", Ink.create(this.runtime).handle);
this.root.set("ink", Ink.create(this.runtime).handle);
}

protected async hasInitialized() {
// Wait here for the ink
const handle = await this.root.wait<IFluidHandle<IInk>>("pageInk");
this.ink = await handle.get();
}

private createCanvasDom() {
const inkComponentRoot = document.createElement("div");
inkComponentRoot.classList.add("ink-component-root");

const inkSurface = document.createElement("div");
inkSurface.classList.add("ink-surface");

const canvasElement = document.createElement("canvas");
canvasElement.classList.add("ink-canvas");

this.inkCanvas = new InkCanvas(canvasElement, this.ink);

const inkToolbar = this.createToolbar();

inkComponentRoot.appendChild(inkSurface);
inkSurface.appendChild(canvasElement);
inkSurface.appendChild(inkToolbar);

this.inkColorPicker = this.createColorPicker();

inkComponentRoot.appendChild(this.inkColorPicker);

return inkComponentRoot;
}

private createToolbar() {
const inkToolbar = document.createElement("div");
inkToolbar.classList.add("ink-toolbar");

const colorButton = document.createElement("button");
colorButton.classList.add("ink-toolbar-button", "fluid-icon-pencil");
colorButton.setAttribute("title", "Change Color");
colorButton.addEventListener("click", this.toggleColorPicker.bind(this));

const replayButton = document.createElement("button");
replayButton.classList.add("ink-toolbar-button", "fluid-icon-replay");
replayButton.setAttribute("title", "Replay");
replayButton.addEventListener("click", this.inkCanvas.replay.bind(this.inkCanvas));

const clearButton = document.createElement("button");
clearButton.classList.add("ink-toolbar-button", "fluid-icon-cross");
clearButton.setAttribute("title", "Clear");
clearButton.addEventListener("click", this.inkCanvas.clear.bind(this.inkCanvas));

inkToolbar.appendChild(colorButton);
inkToolbar.appendChild(replayButton);
inkToolbar.appendChild(clearButton);

return inkToolbar;
}

private createColorPicker() {
const inkColorPicker = document.createElement("div");
inkColorPicker.classList.add("ink-color-picker");

for (const color of colorPickerColors) {
inkColorPicker.appendChild(this.createColorOption(color));
}

return inkColorPicker;
}

private createColorOption(color: IColor) {
const inkColorOption = document.createElement("button");
inkColorOption.classList.add("ink-color-option");
inkColorOption.style.backgroundColor = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;

inkColorOption.addEventListener("click", () => {
this.inkCanvas.setPenColor(color);
this.toggleColorPicker();
});

return inkColorOption;
}

private toggleColorPicker() {
this.inkColorPicker.classList.toggle("show");
}

private sizeCanvas() {
this.inkCanvas.sizeCanvasBackingStore();
const handle = await this.root.wait<IFluidHandle<IInk>>("ink");
this._ink = await handle.get();
}
}
13 changes: 6 additions & 7 deletions examples/data-objects/canvas/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
* Licensed under the MIT License.
*/

import { ContainerViewRuntimeFactory } from "@fluid-example/example-utils";
import {
ContainerRuntimeFactoryWithDefaultDataStore,
DataObjectFactory,
} from "@fluidframework/aqueduct";
import { IEvent } from "@fluidframework/common-definitions";
import { Ink } from "@fluidframework/ink";
import React from "react";
import { Canvas } from "./canvas";
import { CanvasView } from "./view";

export const CanvasInstantiationFactory =
new DataObjectFactory<Canvas, undefined, undefined, IEvent>(
Expand All @@ -21,9 +23,6 @@ export const CanvasInstantiationFactory =
{},
);

export const fluidExport = new ContainerRuntimeFactoryWithDefaultDataStore(
CanvasInstantiationFactory,
new Map([
[CanvasInstantiationFactory.type, Promise.resolve(CanvasInstantiationFactory)],
]),
);
const canvasViewCallback = (canvas: Canvas) => React.createElement(CanvasView, { canvas });

export const fluidExport = new ContainerViewRuntimeFactory<Canvas>(CanvasInstantiationFactory, canvasViewCallback);
147 changes: 147 additions & 0 deletions examples/data-objects/canvas/src/view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { IColor, InkCanvas } from "@fluidframework/ink";

import React, { useEffect, useRef, useState } from "react";

import { Canvas } from "./canvas";
// eslint-disable-next-line import/no-unassigned-import
import "./style.less";

const colorPickerColors: IColor[] = [
{ r: 253, g: 0, b: 12, a: 1 },
{ r: 134, g: 0, b: 56, a: 1 },
{ r: 253, g: 187, b: 48, a: 1 },
{ r: 255, g: 255, b: 81, a: 1 },
{ r: 0, g: 45, b: 98, a: 1 },
{ r: 255, g: 255, b: 255, a: 1 },
{ r: 246, g: 83, b: 20, a: 1 },
{ r: 0, g: 161, b: 241, a: 1 },
{ r: 124, g: 187, b: 0, a: 1 },
{ r: 8, g: 170, b: 51, a: 1 },
{ r: 0, g: 0, b: 0, a: 1 },
];

interface IToolbarProps {
toggleColorPicker: () => void;
replayInk: () => void;
clearInk: () => void;
}

const Toolbar: React.FC<IToolbarProps> = (props: IToolbarProps) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant typing

Suggested change
const Toolbar: React.FC<IToolbarProps> = (props: IToolbarProps) => {
const Toolbar: React.FC<IToolbarProps> = (props) => {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally yes, but unfortunately eslint doesn't detect this correctly and errors:

jsx-eslint/eslint-plugin-react#2353

const { toggleColorPicker, replayInk, clearInk } = props;
return (
<div className="ink-toolbar">
<button
className="ink-toolbar-button fluid-icon-pencil"
title="Change Color"
onClick={toggleColorPicker}
></button>
<button
className="ink-toolbar-button fluid-icon-replay"
title="Replay"
onClick={replayInk}
></button>
<button
className="ink-toolbar-button fluid-icon-cross"
title="Clear"
onClick={clearInk}
></button>
</div>
);
};

interface IColorOptionProps {
color: IColor;
choose: () => void;
}

const ColorOption: React.FC<IColorOptionProps> = (props: IColorOptionProps) => {
const { color, choose } = props;
return (
<button
className="ink-color-option"
onClick={ choose }
style={{ backgroundColor: `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})` }}
></button>
);
};

interface IColorPickerProps {
show: boolean;
choose: (color: IColor) => void;
}

const ColorPicker: React.FC<IColorPickerProps> = (props: IColorPickerProps) => {
const { show, choose } = props;
return (
<div className={`ink-color-picker${show ? " show" : ""}`}>
{
colorPickerColors.map((color, index) => {
const pickColor = () => {
choose(color);
};
return <ColorOption key={index} color={color} choose={pickColor} />;
})
}
</div>
);
};

interface ICanvasViewProps {
canvas: Canvas;
}

export const CanvasView: React.FC<ICanvasViewProps> = (props: ICanvasViewProps) => {
const { canvas } = props;
const [inkCanvas, setInkCanvas] = useState<InkCanvas | undefined>(undefined);
const [showColorPicker, setShowColorPicker] = useState<boolean>(false);
// eslint-disable-next-line no-null/no-null
const canvasRef = useRef<HTMLCanvasElement>(null);

useEffect(() => {
// eslint-disable-next-line no-null/no-null
if (canvasRef.current !== null && inkCanvas === undefined) {
setInkCanvas(new InkCanvas(canvasRef.current, canvas.ink));
}
}, [canvas, canvasRef.current]);

useEffect(() => {
if (inkCanvas !== undefined) {
const resizeHandler = () => {
inkCanvas.sizeCanvasBackingStore();
};
window.addEventListener("resize", resizeHandler);
inkCanvas.sizeCanvasBackingStore();
return () => {
window.removeEventListener("resize", resizeHandler);
};
}
}, [inkCanvas]);

const toggleColorPicker = () => {
setShowColorPicker(!showColorPicker);
};
const replayInk = inkCanvas?.replay.bind(inkCanvas) ?? (() => {});
const clearInk = inkCanvas?.clear.bind(inkCanvas) ?? (() => {});
const chooseColor = (color: IColor) => {
inkCanvas?.setPenColor(color);
setShowColorPicker(false);
};
return (
<div className="ink-component-root">
<div className="ink-surface">
<canvas className="ink-canvas" ref={ canvasRef }></canvas>
<Toolbar
toggleColorPicker={ toggleColorPicker }
replayInk={ replayInk }
clearInk={ clearInk }
/>
</div>
<ColorPicker show={ showColorPicker } choose={ chooseColor } />
</div>
);
};
19 changes: 16 additions & 3 deletions examples/data-objects/canvas/test/canvas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Licensed under the MIT License.
*/

import { ElementHandle } from "puppeteer";
import { globals } from "../jest.config";

describe("canvas", () => {
Expand All @@ -21,8 +20,16 @@ describe("canvas", () => {
it("can be drawn upon with a computer mouse input peripheral", async () => {
// draw on the canvas
await page.waitForSelector("canvas");
const canvas: ElementHandle = await page.$("canvas");
const canvas = await page.$("canvas");
expect(canvas).not.toBe(null);
if (canvas === null) {
throw new Error("Canvas not found");
}
const boundingBox = await canvas.boundingBox();
expect(boundingBox).not.toBe(null);
if (boundingBox === null) {
throw new Error("Bounding box not defined");
}
await page.mouse.move(boundingBox.x + boundingBox.width / 2, boundingBox.y + boundingBox.height / 2);
await page.mouse.down();
await page.mouse.move(126, 19);
Expand All @@ -37,7 +44,13 @@ describe("canvas", () => {
const width = Math.min(...canvases.map((c) => c.width));
const height = Math.min(...canvases.map((c) => c.height));

const imgs = canvases.map((c) => c.getContext("2d").getImageData(0, 0, width, height).data);
const imgs = canvases.map((c) => {
const context = c.getContext("2d");
if (context === null) {
throw new Error("Failed to get 2d context");
}
return context.getImageData(0, 0, width, height).data;
});
if (imgs[0].length == 0) {
return "Canvas 1 doesn't have any pixels";
}
Expand Down
2 changes: 1 addition & 1 deletion examples/data-objects/canvas/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
],
"compilerOptions": {
"outDir": "dist",
"strict": false,
"jsx": "react",
"types": ["jest", "puppeteer", "jest-environment-puppeteer", "expect-puppeteer", "react", "react-dom"],
},
"include": [
Expand Down