Skip to content

Commit

Permalink
Merge pull request #20 from projectstorm/feature_crop
Browse files Browse the repository at this point in the history
Ability to crop images + floating image controls
  • Loading branch information
dylanvorster authored Jul 2, 2023
2 parents f4f8ba0 + 56b737e commit 48f68d9
Show file tree
Hide file tree
Showing 25 changed files with 324 additions and 75 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ You can simply paste images you have copied in your clipboard, and then arrange
* 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

![](./images/screenshot.png)
![](./images/screenshot2.png)
![](./images/screenshot3.png)


## Requirements
Expand Down
Binary file modified images/screenshot.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 modified images/screenshot2.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/screenshot3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

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

11 changes: 10 additions & 1 deletion tornado-common/src/api/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ export enum MediaSize {
SMALL = 0,
MEDIUM = 1,
LARGE = 2,
ORIGINAL = 3
X_LARGE = 3,
ORIGINAL = 4
}

export interface GetMediaRequest {
image: number;
size: MediaSize;
}

export interface MediaCropRequest {
image: number;
top: number;
left: number;
width: number;
height: number;
}
3 changes: 2 additions & 1 deletion tornado-common/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum Routes {
CONCEPT_DELETE = '/concept/delete',
CONCEPT_UPDATE = '/concept/update',
UPLOAD_MEDIA = '/media/upload',
GET_MEDIA = '/media/get',
MEDIA_GET = '/media/get',
MEDIA_CROP = '/media/crop',
CONCEPT = '/concept'
}
4 changes: 3 additions & 1 deletion tornado-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
"react-avatar": "^5.0.3",
"react-dom": "^18.2.0",
"react-switch": "^7.0.0",
"react-cropper": "^2.3.3",
"react-router-dom": "^6.12.1",
"uuid": "^9.0.0"
"uuid": "^9.0.0",
"cropperjs": "^1.5.13"
},
"devDependencies": {
"@types/lodash": "^4.14.195",
Expand Down
11 changes: 10 additions & 1 deletion tornado-frontend/src/client/MediaClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BaseObserver, FileData, GetMediaRequest, MediaSize, Routes } from '@projectstorm/tornado-common';
import * as _ from 'lodash';

export interface MediaUploadListener {
progressChanged: (percent: number) => any;
Expand Down Expand Up @@ -45,6 +46,14 @@ export class MediaObject {
this.cache = new Map();
}

clearCachesSizes() {
_.forEach(MediaSize, (m) => {
if (m !== MediaSize.ORIGINAL) {
this.cache.delete(m);
}
});
}

async getURL(size: MediaSize) {
if (!this.cache.has(size)) {
this.cache.set(
Expand Down Expand Up @@ -81,7 +90,7 @@ export class MediaClient {
}

async getMedia(image: number, size: MediaSize) {
const res = await fetch(`${this.options.baseURL}${Routes.GET_MEDIA}`, {
const res = await fetch(`${this.options.baseURL}${Routes.MEDIA_GET}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
Expand Down
3 changes: 3 additions & 0 deletions tornado-frontend/src/client/TornadoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DeleteConceptsResponse,
LoginRequest,
LoginResponse,
MediaCropRequest,
Routes,
UpdateConceptRequest
} from '@projectstorm/tornado-common';
Expand Down Expand Up @@ -74,4 +75,6 @@ export class TornadoClient {
deleteConcept = this.createRoute<DeleteConceptsRequest, DeleteConceptsResponse>(Routes.CONCEPT_DELETE);

updateConcept = this.createRoute<UpdateConceptRequest, void>(Routes.CONCEPT_UPDATE);

mediaCrop = this.createRoute<MediaCropRequest, void>(Routes.MEDIA_CROP);
}
4 changes: 2 additions & 2 deletions tornado-frontend/src/fonts.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import '@fontsource/open-sans';
import { library } from '@fortawesome/fontawesome-svg-core';
import { faMoon, faPlus, faSignOut, faSpinner, faSun } from '@fortawesome/free-solid-svg-icons';
import { faCrop, faMoon, faPlus, faSignOut, faSpinner, faSun, faTrash } from '@fortawesome/free-solid-svg-icons';
import { css } from '@emotion/react';

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

export const FONT = css`
font-family: 'Open sans', sans-serif;
Expand Down
5 changes: 3 additions & 2 deletions tornado-frontend/src/hooks/useButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
export interface UseButtonOptions {
action: (event: MouseEvent) => Promise<any>;
disabled?: boolean;
instant?: boolean;
}

export const useButton = (props: UseButtonOptions, ref: React.RefObject<HTMLDivElement>) => {
Expand All @@ -26,9 +27,9 @@ export const useButton = (props: UseButtonOptions, ref: React.RefObject<HTMLDivE
setLoading(false);
});
};
ref.current.addEventListener('click', l);
ref.current.addEventListener(props.instant ? 'mousedown' : 'click', l, { capture: true });
return () => {
ref.current?.removeEventListener('click', l);
ref.current?.removeEventListener(props.instant ? 'mousedown' : 'click', l, { capture: true });
};
}, [props.action, props.disabled]);
return { loading };
Expand Down
19 changes: 6 additions & 13 deletions tornado-frontend/src/routes/content/ConceptBoardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,18 @@ import { useSystem } from '../../hooks/useSystem';
import { ConceptCanvasWidget } from './widgets/react-canvas/ConceptCanvasWidget';
import { useParams } from 'react-router-dom';
import { observer } from 'mobx-react';
import { autorun } from 'mobx';

export interface ConceptBoardPageProps {}

export const ConceptBoardPage: React.FC<ConceptBoardPageProps> = observer((props) => {
export const ConceptBoardPage: React.FC = observer((props) => {
const system = useSystem();
const { id } = useParams<{ id: string }>();
const { board } = useParams<{ board: string }>();
useAuthenticated();
useEffect(() => {
system.conceptStore.loadConcepts();
return autorun(() => {
const concept = system.conceptStore.getConcept(parseInt(id));
if (concept) {
system.updateTitle(`Concept ${concept.board.name}`);
}
system.conceptStore.loadConcept(parseInt(board)).then((concept) => {
system.updateTitle(`Concept ${concept.board.name}`);
});
}, [id]);
}, [board]);

const concept = system.conceptStore.getConcept(parseInt(id));
const concept = system.conceptStore.getConcept(parseInt(board));
if (!concept) {
return null;
}
Expand Down
128 changes: 128 additions & 0 deletions tornado-frontend/src/routes/content/ImageCropPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as React from 'react';
import { createRef, useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { useAuthenticated } from '../../hooks/useAuthenticated';
import { useSystem } from '../../hooks/useSystem';
import { generatePath, useNavigate, useParams } from 'react-router-dom';
import { observer } from 'mobx-react';
import Cropper, { ReactCropperElement } from 'react-cropper';
import { MediaSize } from '@projectstorm/tornado-common';
import 'cropperjs/dist/cropper.css';
import { ButtonType, ButtonWidget } from '../../widgets/forms/ButtonWidget';
import { Routing } from '../routes';
import * as _ from 'lodash';

export const ImageCropPage: React.FC = observer((props) => {
const system = useSystem();
useAuthenticated();
const navigate = useNavigate();
const { board, image } = useParams<{ board: string; image: string }>();

const [mediaUrl, setMedia] = useState(null);
useEffect(() => {
const media = system.clientMedia.getMediaObject(parseInt(image));
media.getURL(MediaSize.ORIGINAL).then((url) => {
setMedia(url);
});
}, [board, image]);

const cropperRef = createRef<ReactCropperElement>();

if (!mediaUrl) {
return null;
}

return (
<S.Container>
<S.Buttons>
<S.Button
type={ButtonType.NORMAL}
label="Cancel"
action={async () => {
navigate(generatePath(Routing.CONCEPTS_BOARD, { board: board }));
}}
/>
<S.Button
type={ButtonType.NORMAL}
label="Reset to full size"
action={async () => {
const data = cropperRef.current.cropper.getImageData();
cropperRef.current.cropper.setData({
x: 0,
y: 0,
width: data.naturalWidth,
height: data.naturalHeight
});
}}
/>
<S.Button
type={ButtonType.PRIMARY}
label="Crop"
action={async () => {
const data = cropperRef.current.cropper.getData();
await system.client.mediaCrop({
image: parseInt(image),
top: data.y,
left: data.x,
width: data.width,
height: data.height
});

// clear the URL caches
system.clientMedia.getMediaObject(parseInt(image)).clearCachesSizes();

// update the image sizes on the board
const concept = await system.conceptStore.loadConcept(parseInt(board));
_.forEach(concept.board.data.layers[0].models, (m) => {
if (m.image_id === parseInt(image)) {
m.height = (m.width / data.width) * data.height;
}
});
await concept.setCanvasData(concept.board.data);
navigate(generatePath(Routing.CONCEPTS_BOARD, { board: board }));
}}
/>
</S.Buttons>
<S.Crop
ref={cropperRef}
zoomTo={0.5}
initialAspectRatio={1}
src={mediaUrl}
viewMode={2}
minCropBoxHeight={10}
minCropBoxWidth={10}
background={false}
responsive={true}
autoCropArea={1}
checkOrientation={false} // https://github.com/fengyuanchen/cropperjs/issues/671
guides={true}
/>
</S.Container>
);
});
namespace S {
export const Container = styled.div`
box-sizing: border-box;
padding: 10px;
height: 100%;
display: flex;
flex-direction: column;
`;

export const Buttons = styled.div`
padding-top: 10px;
padding-bottom: 10px;
justify-content: flex-end;
display: flex;
`;

export const Button = styled(ButtonWidget)`
margin-left: 5px;
`;

export const Crop = styled(Cropper)`
height: 100%;
width: 100%;
flex-grow: 1;
`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class ConceptCanvasModel extends CanvasModel {
if (!this.model.board.data) {
return;
}
// @ts-ignore
this.deserializeModel(this.model.board.data, engine);
}

Expand Down
Loading

0 comments on commit 48f68d9

Please sign in to comment.