Skip to content

Commit

Permalink
Merge pull request #32 from projectstorm/feature_qol2
Browse files Browse the repository at this point in the history
Even more quality of life improvements
  • Loading branch information
dylanvorster authored Jul 8, 2023
2 parents b62f1f3 + 81dc9f5 commit 1486c45
Show file tree
Hide file tree
Showing 17 changed files with 146 additions and 16 deletions.
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Concept and image reference board software for ambitious creatives 🎨

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

![](./images/example1.png)
![](./images/example2.png)

## What

Tornado is self-hosted software for the web (currently in development) that provides digital media artists with the ability to create concept and reference boards.
Expand All @@ -21,14 +24,17 @@ You can simply paste images you have copied in your clipboard, and then arrange
* Multiple Users
* Light and dark mode
* Email + password authentication
* Image resizing on the server
* Image resizing on the server (4 sizes based on the image zoom)
* Uses window DPI to determine which image variant to serve
* Canvas zoom and translate
* 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
* Resize images on any corner
* Initial widths are computed based on the average sizes of the other images
* Lock mode to prevent editing, auto unlock on image paste

![](./images/screenshot1.png)
![](./images/screenshot2.png)
Expand Down Expand Up @@ -139,5 +145,20 @@ _Note: Pull requests must have the appropriate label (eg 'feature') to be includ

## About

Author: Dylan Vorster (dylanvorster.com)

Project Author: Dylan Vorster (dylanvorster.com)

Links to a few of the awesome artists featured in the example images:

* https://twitter.com/KilluKaela
* https://twitter.com/IndigoBeatss
* https://twitter.com/the_aftrmrkt
* https://twitter.com/Xezeno1
* https://twitter.com/for_riner
* https://twitter.com/fwflunky
* https://twitter.com/Dev_Voxy
* https://twitter.com/_rat_riot
* https://twitter.com/Frayvuir
* https://twitter.com/DJayjesse
* https://twitter.com/bl_s21
* https://twitter.com/yumesan_yume
* https://twitter.com/MegamanUMX
Binary file added images/example1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/example2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions tornado-frontend/src/System.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ export class System {
@observable
theme?: TornadoTheme;

@observable
title: string;

constructor() {
this.title = null;

if (window.localStorage.getItem(System.LIGHT_KEY) === 'true') {
this.theme = ThemeLight;
} else {
Expand Down Expand Up @@ -61,6 +66,7 @@ export class System {
}

updateTitle(title: string) {
this.title = title;
document.title = `Tornado${title ? ` | ${title}` : ''}`;
}

Expand Down
10 changes: 9 additions & 1 deletion tornado-frontend/src/client/MediaClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,17 @@ export class MediaObject {
(data) =>
new Promise((resolve) => {
const url = window.URL.createObjectURL(data);

// this wacky code sort-of hydrates the rendering buffer
// which greatly reduces initial flickering of the Image blob for the first time
// it is rendered in the DOM
const img = document.createElement('img');
img.src = url;
resolve(url);

// this kind of also helps a bit
_.defer(() => {
resolve(url);
});
})
)
);
Expand Down
6 changes: 5 additions & 1 deletion tornado-frontend/src/fonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import {
faCrop,
faExpand,
faLock,
faLockOpen,
faMagnifyingGlassMinus,
faMagnifyingGlassPlus,
faMinimize,
Expand All @@ -28,7 +30,9 @@ library.add(
faMinimize,
faUpRightAndDownLeftFromCenter,
faMagnifyingGlassPlus,
faMagnifyingGlassMinus
faMagnifyingGlassMinus,
faLock,
faLockOpen
);

export const FONT = css`
Expand Down
12 changes: 12 additions & 0 deletions tornado-frontend/src/hooks/useTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useEffect } from 'react';
import { autorun } from 'mobx';
import { useSystem } from './useSystem';

export const useTitle = (cb: () => string) => {
const system = useSystem();
useEffect(() => {
return autorun(() => {
system.updateTitle(cb());
});
}, []);
};
14 changes: 11 additions & 3 deletions tornado-frontend/src/routes/content/ConceptBoardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,25 @@ import { useSystem } from '../../hooks/useSystem';
import { ConceptCanvasWidget } from './widgets/react-canvas/ConceptCanvasWidget';
import { useParams } from 'react-router-dom';
import { observer } from 'mobx-react';
import { useTitle } from '../../hooks/useTitle';

export const ConceptBoardPage: React.FC = observer((props) => {
useAuthenticated();
const system = useSystem();
const { board } = useParams<{ board: string }>();

useEffect(() => {
system.conceptStore?.loadConcept(parseInt(board)).then((concept) => {
system.updateTitle(`Concept ${concept.board.name}`);
});
system.conceptStore?.loadConcept(parseInt(board));
}, [board]);

useTitle(() => {
const concept = system.conceptStore?.getConcept(parseInt(board));
if (!concept) {
return 'Loading...';
}
return `${concept.board.name}`;
});

const concept = system.conceptStore?.getConcept(parseInt(board));
if (!concept) {
return null;
Expand Down
10 changes: 10 additions & 0 deletions tornado-frontend/src/routes/content/ImageCropPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Routing } from '../routes';
import * as _ from 'lodash';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { styled } from '../../theme/theme';
import { useTitle } from '../../hooks/useTitle';

export const ImageCropPage: React.FC = observer((props) => {
useAuthenticated();
Expand All @@ -22,6 +23,14 @@ export const ImageCropPage: React.FC = observer((props) => {
const cropperRef = createRef<ReactCropperElement>();
const [mediaUrl, setMedia] = useState(null);

useTitle(() => {
const concept = system.conceptStore?.getConcept(parseInt(board));
if (!concept) {
return 'Loading...';
}
return `${concept.board.name} - Crop image: ${image}`;
});

useEffect(() => {
const media = system.clientMedia.getMediaObject(parseInt(image));
media.getURL(MediaSize.ORIGINAL).then((url) => {
Expand Down Expand Up @@ -150,5 +159,6 @@ namespace S {
height: 100%;
width: 100%;
flex-grow: 1;
overflow: hidden;
`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ import { ControlsLayerFactory } from './controls-layer/ControlsLayerFactory';
import { boundingBoxFromPolygons, Rectangle } from '@projectstorm/geometry';
import { CustomZoomAction } from './CustomZoomAction';

export interface ConceptCanvasEngineOptions {
isLocked: () => boolean;
}

export class ConceptCanvasEngine extends CanvasEngine<CanvasEngineListener, ConceptCanvasModel> {
elementBank: FactoryBank<ImageElementFactory>;

constructor() {
constructor(options: ConceptCanvasEngineOptions) {
super({
registerDefaultDeleteItemsAction: true
});
Expand All @@ -33,7 +37,7 @@ export class ConceptCanvasEngine extends CanvasEngine<CanvasEngineListener, Conc
this.getLayerFactories().registerFactory(new ImageLayerFactory());
this.getLayerFactories().registerFactory(new ControlsLayerFactory());

this.getStateMachine().pushState(new DefaultCanvasState());
this.getStateMachine().pushState(new DefaultCanvasState(options));

this.getActionEventBus()
.getActionsForType(InputType.MOUSE_WHEEL)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled';
import * as _ from 'lodash';
import { Action, CanvasWidget, InputType } from '@projectstorm/react-canvas-core';
Expand All @@ -18,8 +18,13 @@ export interface ConceptCanvasWidgetProps {

export const ConceptCanvasWidget: React.FC<ConceptCanvasWidgetProps> = observer((props) => {
const system = useSystem();
const [locked, setLocked] = useState(true);
const ref = useRef<boolean>();
ref.current = locked;
const [engine] = useState(() => {
return new ConceptCanvasEngine();
return new ConceptCanvasEngine({
isLocked: () => ref.current
});
});
const [position, setPosition] = useState<React.MouseEvent>(null);
const [ready, setReady] = useState(false);
Expand Down Expand Up @@ -87,6 +92,8 @@ export const ConceptCanvasWidget: React.FC<ConceptCanvasWidgetProps> = observer(
// paste handler
usePasteMedia({
gotMedia: (files) => {
setLocked(false);

files.forEach(async (file) => {
const media = await system.clientMedia.uploadMedia(file);
const element = engine.getModel().addImage();
Expand All @@ -113,6 +120,13 @@ export const ConceptCanvasWidget: React.FC<ConceptCanvasWidgetProps> = observer(
<S.Parent>
<S.Container engine={engine} />
<S.Controls>
<S.Button
type={ButtonType.DISCRETE}
icon={locked ? 'lock' : 'lock-open'}
action={async () => {
setLocked(!locked);
}}
/>
<S.Button
type={ButtonType.DISCRETE}
icon="magnifying-glass-minus"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ import { ResizeElementState } from './ResizeElementState';
import { CornerPosition } from './controls-layer/ControlsElementWidget';
import { ImageElement } from './image-element/ImageElementFactory';

export interface DefaultCanvasStateOptions {
isLocked: () => boolean;
}

export class DefaultCanvasState extends State<CanvasEngine> {
dragCanvas: DragCanvasState;
resizeElement: ResizeElementState;
dragItems: MoveItemsState;

constructor() {
constructor(options: DefaultCanvasStateOptions) {
super({
name: 'default-diagrams'
});
Expand All @@ -32,8 +36,13 @@ export class DefaultCanvasState extends State<CanvasEngine> {
new Action({
type: InputType.MOUSE_DOWN,
fire: (event: ActionEvent<MouseEvent>) => {
const element = this.engine.getActionEventBus().getModelForEvent(event);
const locked = options.isLocked();
if (locked) {
this.transitionWithEvent(this.dragCanvas, event);
return;
}

const element = this.engine.getActionEventBus().getModelForEvent(event);
if ((event.event.target as HTMLDivElement).dataset.anchorposition) {
this.resizeElement.setup(
(event.event.target as HTMLDivElement).dataset.anchorposition as CornerPosition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,19 @@ export class ImageElement extends BasePositionModel<BasePositionModelGenerics &
}

update(data: FileData) {
const size = 500;
// figure out the average sizes are of the other elements for when we paste
let widths = this.getCanvasModel()
.getImageElements()
.filter((i) => i !== this)
.map((i) => i.width);

if (widths.length === 0) {
widths = [500];
}

let totalWidth = widths.reduce((prev, cur) => prev + cur, 0);

const size = totalWidth / widths.length;
this.imageID = data.id;
this.setSize(size, (size / data.width) * data.height);
}
Expand Down
5 changes: 4 additions & 1 deletion tornado-frontend/src/routes/manage/ConceptBoardsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { observer } from 'mobx-react';
import { TableRowActionsWidget } from '../../widgets/table/TableRowActionsWidget';
import { ConceptBoardModel } from '../../stores/ConceptsStore';
import { RelativeDateCellWidget } from '../../widgets/table/RelativeDateCellWidget';
import { useTitle } from '../../hooks/useTitle';

export interface ConceptBoardRow extends TableRow {
board: ConceptBoardModel;
Expand All @@ -22,8 +23,10 @@ export const ConceptBoardsPage: React.FC = observer((props) => {
const system = useSystem();
useEffect(() => {
system.conceptStore.loadConcepts();
system.updateTitle('Concepts');
}, []);
useTitle(() => {
return 'Concepts';
});
return (
<S.Container>
<S.Buttons>
Expand Down
4 changes: 4 additions & 0 deletions tornado-frontend/src/routes/welcome/SignInPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import { FormikFieldWidget } from '../../widgets/forms/FormikFieldWidget';
import { ButtonType, ButtonWidget } from '../../widgets/forms/ButtonWidget';
import { useSystem } from '../../hooks/useSystem';
import { useUnAuthenticated } from '../../hooks/useAuthenticated';
import { useTitle } from '../../hooks/useTitle';

export interface SignInPageProps {}

export const SignInPage: React.FC<SignInPageProps> = (props) => {
useUnAuthenticated();
useTitle(() => {
return 'Sign in';
});
const system = useSystem();
return (
<S.Container>
Expand Down
14 changes: 14 additions & 0 deletions tornado-frontend/src/widgets/header/HeaderWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom';
import { Routing } from '../../routes/routes';
import { observer } from 'mobx-react';
import { useSystem } from '../../hooks/useSystem';
import { FONT } from '../../fonts';

const logo_light = require('../../../media/logo-small-light.png');
const logo_dark = require('../../../media/logo-small-dark.png');
Expand All @@ -25,6 +26,8 @@ export const HeaderWidget: React.FC<HeaderWidgetProps> = observer((props) => {
}}
src={system.theme.light ? logo_dark : logo_light}
></S.Logo>
<S.Title>{system.title}</S.Title>
<S.Spacer></S.Spacer>
<HeaderUserWidget bodyRef={props.bodyRef} />
</S.Container>
);
Expand All @@ -40,6 +43,17 @@ namespace S {
padding-right: 50px;
`;

export const Title = styled.div`
font-size: 16px;
color: ${(p) => p.theme.text.description};
margin-left: 20px;
${FONT};
`;

export const Spacer = styled.div`
flex-grow: 1;
`;

export const Logo = styled.img`
height: 40px;
cursor: pointer;
Expand Down
1 change: 1 addition & 0 deletions tornado-frontend/src/widgets/layout/RootWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ namespace S {
* {
margin: 0;
padding: 0;
user-select: none;
}
html {
Expand Down

0 comments on commit 1486c45

Please sign in to comment.