Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fullscreen toggle, double click focus and cache canvas zoom + translate after crop, Fit all images on board mount #22

Merged
merged 2 commits into from
Jul 4, 2023
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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
## Tornado (WIP)
## Tornado

[![Build](https://github.com/projectstorm/tornado/actions/workflows/test.yml/badge.svg)](https://github.com/projectstorm/tornado/actions/workflows/test.yml)
[![Docker](https://img.shields.io/docker/pulls/projectstorm/tornado.svg)](https://hub.docker.com/r/projectstorm/tornado)
[![pnpm](https://img.shields.io/badge/maintained%20with-pnpm-f9ad00.svg)](https://pnpm.io/)


Concept image-board software for ambitious creatives 🎨
Concept and image reference board software for ambitious creatives 🎨

(Inspired by the awesome https://www.pureref.com/ ref and https://vizref.com/)

## What

Expand All @@ -24,6 +26,8 @@ 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
* Fullscreen toggle

![](./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
Loading