Skip to content

Commit

Permalink
fullscreen and canvas cache support
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanvorster committed Jul 4, 2023
1 parent e80d6aa commit ee886b1
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 14 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ You can simply paste images you have copied in your clipboard, and then arrange
* Image paste from clipboard and translate
* Name and rename boards
* Crop images + the ability to re-crop the original image at any stage
* Double click to focus images in the center of the screen

![](./images/screenshot.png)
![](./images/screenshot2.png)
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

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

3 changes: 2 additions & 1 deletion tornado-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"react-cropper": "^2.3.3",
"react-router-dom": "^6.12.1",
"uuid": "^9.0.0",
"cropperjs": "^1.5.13"
"cropperjs": "^1.5.13",
"screenfull": "^6.0.2"
},
"devDependencies": {
"@types/lodash": "^4.14.195",
Expand Down
14 changes: 12 additions & 2 deletions tornado-frontend/src/fonts.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import '@fontsource/open-sans';
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCrop, faMoon, faPlus, faSignOut, faSpinner, faSun, faTrash } from '@fortawesome/free-solid-svg-icons';
import {
faCrop,
faExpand,
faMinimize,
faMoon,
faPlus,
faSignOut,
faSpinner,
faSun,
faTrash
} from '@fortawesome/free-solid-svg-icons';
import { css } from '@emotion/react';

library.add(faPlus, faSpinner, faSignOut, faSun, faMoon, faCrop, faTrash);
library.add(faPlus, faSpinner, faSignOut, faSun, faMoon, faCrop, faTrash, faExpand, faMinimize);

export const FONT = css`
font-family: 'Open sans', sans-serif;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import * as React from 'react';
import {
BaseModel,
BasePositionModel,
CanvasEngine,
CanvasModel,
FactoryBank,
SelectionBoxLayerFactory,
Toolkit
} from '@projectstorm/react-canvas-core';
import * as _ from 'lodash';
import { ImageElementFactory } from './image-element/ImageElementFactory';
import { ImageLayerFactory } from './image-layer/ImageLayerFactory';
import { DefaultCanvasState } from './DefaultCanvasState';
import { CanvasEngineListener } from '@projectstorm/react-canvas-core';
import { ConceptCanvasModel } from './ConceptCanvasModel';
import { ControlsLayerFactory } from './controls-layer/ControlsLayerFactory';
import { boundingBoxFromPolygons, Rectangle } from '@projectstorm/geometry';

export class ConceptCanvasEngine extends CanvasEngine<CanvasEngineListener, ConceptCanvasModel> {
elementBank: FactoryBank<ImageElementFactory>;
Expand All @@ -22,8 +25,8 @@ export class ConceptCanvasEngine extends CanvasEngine<CanvasEngineListener, Conc
registerDefaultDeleteItemsAction: true
});
this.elementBank = new FactoryBank();
this.elementBank.registerFactory(new ImageElementFactory());
this.registerFactoryBank(this.elementBank);
this.elementBank.registerFactory(new ImageElementFactory());

this.getLayerFactories().registerFactory(new SelectionBoxLayerFactory());
this.getLayerFactories().registerFactory(new ImageLayerFactory());
Expand All @@ -43,4 +46,56 @@ export class ConceptCanvasEngine extends CanvasEngine<CanvasEngineListener, Conc

return super.getMouseElement(event);
}

getBoundingNodesRect(nodes: BasePositionModel[]): Rectangle {
if (nodes.length === 0) {
return new Rectangle();
}
return new Rectangle(boundingBoxFromPolygons(nodes.map((node) => node.getBoundingBox())));
}

/**
* FIXME this should go back into react-diagrams along with the dependent functions (the CanvasEngine)
*/
zoomToFitElements(options: { margin: number; elements: BasePositionModel[]; maxZoom: number }) {
const { margin, elements, maxZoom } = options;
const nodesRect = this.getBoundingNodesRect(elements);
// there is something we should zoom on
let canvasRect = this.canvas.getBoundingClientRect();

const calculate = (margin: number = 0) => {
// work out zoom
const xFactor = this.canvas.clientWidth / (nodesRect.getWidth() + margin * 2);
const yFactor = this.canvas.clientHeight / (nodesRect.getHeight() + margin * 2);

let zoomFactor = xFactor < yFactor ? xFactor : yFactor;
if (maxZoom && zoomFactor > maxZoom) {
zoomFactor = maxZoom;
}

return {
zoom: zoomFactor,
x:
canvasRect.width / 2 -
((nodesRect.getWidth() + margin * 2) / 2 + nodesRect.getTopLeft().x) * zoomFactor +
margin,
y:
canvasRect.height / 2 -
((nodesRect.getHeight() + margin * 2) / 2 + nodesRect.getTopLeft().y) * zoomFactor +
margin
};
};

let params = calculate(0);
if (margin) {
if (params.x < margin || params.y < margin) {
params = calculate(margin);
}
}

// apply
this.model.setZoomLevel(params.zoom * 100);
this.model.setOffset(params.x, params.y);
this.repaintCanvas();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import * as _ from 'lodash';
import { Action, CanvasWidget, InputType } from '@projectstorm/react-canvas-core';
import { ConceptBoardModel } from '../../../../stores/ConceptsStore';
import { ConceptCanvasEngine } from './ConceptCanvasEngine';
import { ConceptCanvasModel } from './ConceptCanvasModel';
import { usePasteMedia } from '../../../../hooks/usePasteMedia';
import { useSystem } from '../../../../hooks/useSystem';
import { ImageElement } from './image-element/ImageElementFactory';

export interface ConceptCanvasWidgetProps {
board: ConceptBoardModel;
Expand Down Expand Up @@ -53,6 +55,19 @@ export const ConceptCanvasWidget: React.FC<ConceptCanvasWidgetProps> = (props) =
const model = new ConceptCanvasModel(props.board);
model.load(engine);
engine.setModel(model);

if (!props.board.canvasTranslateCache) {
engine.zoomToFitElements({
maxZoom: 2,
elements: _.values(model.getLayers()[0].getModels()) as unknown as ImageElement[],
margin: 10
});
} else {
model.setOffsetX(props.board.canvasTranslateCache.offsetX);
model.setOffsetY(props.board.canvasTranslateCache.offsetY);
model.setZoomLevel(props.board.canvasTranslateCache.zoom);
}
props.board.canvasTranslateCache = null;
}, [props.board]);

if (!engine) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export const ControlsElementWidget: React.FC<ControlsElementWidgetProps> = (prop
icon="crop"
instant={true}
action={async () => {
const canvasModel = props.model.getCanvasModel();
canvasModel.model.canvasTranslateCache = {
zoom: canvasModel.getZoomLevel(),
offsetX: canvasModel.getOffsetX(),
offsetY: canvasModel.getOffsetY()
};
navigate(
generatePath(Routing.CONCEPTS_BOARD_IMAGE_CROP, {
board: `${props.model.getCanvasModel().model.id}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
import { ImageElementWidget } from './ImageElementWidget';
import { ImageLayerModel } from '../image-layer/ImageLayerFactory';
import { FileData } from '@projectstorm/tornado-common';
import { ConceptCanvasEngine } from '../ConceptCanvasEngine';
import { Rectangle } from '@projectstorm/geometry';

export class ImageElement extends BasePositionModel {
public width: number;
Expand All @@ -35,6 +37,10 @@ export class ImageElement extends BasePositionModel {
this.height = (size / data.width) * data.height;
}

getBoundingBox(): Rectangle {
return Rectangle.fromPointAndSize(this.position, this.width, this.height);
}

serialize() {
return {
...super.serialize(),
Expand All @@ -53,7 +59,7 @@ export class ImageElement extends BasePositionModel {
}
}

export class ImageElementFactory extends AbstractReactFactory<BasePositionModel> {
export class ImageElementFactory extends AbstractReactFactory<BasePositionModel, ConceptCanvasEngine> {
static TYPE = 'concept/image';

constructor() {
Expand All @@ -65,6 +71,18 @@ export class ImageElementFactory extends AbstractReactFactory<BasePositionModel>
}

generateReactWidget(event: GenerateWidgetEvent<ImageElement>): JSX.Element {
return <ImageElementWidget key={event.model.getID()} model={event.model} />;
return (
<ImageElementWidget
focus={() => {
this.engine.zoomToFitElements({
margin: 10,
elements: [event.model],
maxZoom: 2
});
}}
key={event.model.getID()}
model={event.model}
/>
);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import * as React from 'react';
import { useEffect } from 'react';
import { ImageElement } from './ImageElementFactory';
import { ResponseImageWidget } from './ResponseImageWidget';
import { useForceUpdate } from '../../../../../hooks/useForceUpdate';
import { styled } from '../../../../../theme/theme';

export interface ImageElementWidgetProps {
model: ImageElement;
focus: () => any;
}

export const ImageElementWidget: React.FC<ImageElementWidgetProps> = (props) => {
return (
<S.Container
onDoubleClick={() => {
props.focus?.();
}}
data-imageid={props.model.getID()}
style={{
left: props.model.getX(),
Expand Down
7 changes: 7 additions & 0 deletions tornado-frontend/src/stores/ConceptsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,16 @@ export class ConceptBoardModel extends BaseObserver<ConceptBoardModelListener> {
};
};

canvasTranslateCache: {
offsetX: number;
offsetY: number;
zoom: number;
};

constructor(protected options: ConceptBoardModelOptions) {
super();
this.board = options.board;
this.canvasTranslateCache = null;
makeObservable(this);
}

Expand Down
25 changes: 25 additions & 0 deletions tornado-frontend/src/widgets/header/HeaderUserWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { useEffect } from 'react';
import { observer } from 'mobx-react';
import { useSystem } from '../../hooks/useSystem';
import { FONT } from '../../fonts';
Expand All @@ -7,14 +8,27 @@ import Avatar from 'react-avatar';
import { ButtonType, ButtonWidget } from '../forms/ButtonWidget';
import ReactSwitch from 'react-switch';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import screenfull from 'screenfull';
import { useForceUpdate } from '../../hooks/useForceUpdate';

export interface HeaderUserWidgetProps {
className?: any;
bodyRef: React.RefObject<HTMLDivElement>;
}

export const HeaderUserWidget: React.FC<HeaderUserWidgetProps> = observer((props) => {
const system = useSystem();
const forceUpdate = useForceUpdate();
const user = system.userStore.authenticatedUser;
useEffect(() => {
const h = () => {
forceUpdate();
};
screenfull.on('change', h);
return () => {
screenfull.off('change', h);
};
}, []);
return (
<S.Container className={props.className}>
<S.Switch
Expand Down Expand Up @@ -42,6 +56,17 @@ export const HeaderUserWidget: React.FC<HeaderUserWidgetProps> = observer((props
color={system.theme.controls.button.primary.backgroundHover}
fgColor={system.theme.controls.button.primary.colorHover}
/>
<S.Button
type={ButtonType.NORMAL}
icon={screenfull.isFullscreen ? 'minimize' : 'expand'}
action={async () => {
if (screenfull.isFullscreen) {
screenfull.exit();
} else {
screenfull.request(props.bodyRef.current);
}
}}
/>
<S.Button
type={ButtonType.NORMAL}
icon="sign-out"
Expand Down
7 changes: 4 additions & 3 deletions tornado-frontend/src/widgets/header/HeaderWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { useSystem } from '../../hooks/useSystem';
const logo_light = require('../../../media/logo-small-light.png');
const logo_dark = require('../../../media/logo-small-dark.png');

export interface HeaderWidgetProps {}
export interface HeaderWidgetProps {
bodyRef: React.RefObject<HTMLDivElement>;
}

export const HeaderWidget: React.FC<HeaderWidgetProps> = observer((props) => {
const navigate = useNavigate();
Expand All @@ -23,8 +25,7 @@ export const HeaderWidget: React.FC<HeaderWidgetProps> = observer((props) => {
}}
src={system.theme.light ? logo_dark : logo_light}
></S.Logo>

<HeaderUserWidget />
<HeaderUserWidget bodyRef={props.bodyRef} />
</S.Container>
);
});
Expand Down
8 changes: 5 additions & 3 deletions tornado-frontend/src/widgets/layout/RootWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,26 @@ import { System } from '../../System';
import { SystemContext } from '../../hooks/useSystem';
import { LayersWidget } from './LayersWidget';
import { observer } from 'mobx-react';
import { useRef } from 'react';

export interface RootWidgetProps {
system: System;
}

export const RootWidget: React.FC<RootWidgetProps> = observer((props) => {
const bodyRef = useRef<HTMLDivElement>();
return (
<>
<Global styles={S.Global} />
<ThemeProvider theme={props.system.theme}>
<BrowserRouter>
<SystemContext.Provider value={props.system}>
<S.Container>
<HeaderWidget />
<S.Container ref={bodyRef}>
<HeaderWidget bodyRef={bodyRef} />
<S.Body />
<FooterWidget />
<S.Layers />
</S.Container>
<S.Layers />
</SystemContext.Provider>
</BrowserRouter>
</ThemeProvider>
Expand Down

0 comments on commit ee886b1

Please sign in to comment.