{
- const currentTime = () => Math.round(video.getCurrentTime());
+ const currentTime = () => Math.round(video.get('currentTime'));
const [time, setTime] = useState(currentTime());
const visible = useSelector(getVisible);
diff --git a/client/src/components/molecules/Thumbnail/Thumbnail.tsx b/client/src/components/molecules/Thumbnail/Thumbnail.tsx
index ca855bf..902e571 100644
--- a/client/src/components/molecules/Thumbnail/Thumbnail.tsx
+++ b/client/src/components/molecules/Thumbnail/Thumbnail.tsx
@@ -6,7 +6,7 @@ import { moveTo } from '@/store/currentVideo/actions';
import Slider from '@/components/atoms/Slider';
import HoverSlider from '@/components/atoms/HoverSlider';
import video from '@/video';
-import { getThumbnails } from '@/store/selectors';
+import { getThumbnails, getIsCrop } from '@/store/selectors';
import CropLayer from '@/components/molecules/CropLayer';
const StyledDiv = styled.div`
@@ -24,8 +24,9 @@ const StyledImg = styled.img`
const Thumbnail: React.FC = () => {
const thumbnails = useSelector(getThumbnails);
+ const isCrop = useSelector(getIsCrop);
+
const [time, setTime] = useState(0);
- const [position, setPosition] = useState([0, 0]);
const dispatch = useDispatch();
@@ -47,7 +48,7 @@ const Thumbnail: React.FC = () => {
const distance = mouseLocation - offset;
const width = thumbnailRef.current.clientWidth;
- const duration = video.getDuration();
+ const duration = video.get('duration');
const hoverTime = (distance / width) * duration;
@@ -71,10 +72,10 @@ const Thumbnail: React.FC = () => {
onMouseLeave={handleMouseLeave}
onMouseEnter={handleMouseEnter}
>
-
+ {isCrop &&
}
- {thumbnails.map(image => {
+ {(isCrop ? video.getThumbnails() : thumbnails).map(image => {
return
;
})}
diff --git a/client/src/components/molecules/TimeZone/TimeZone.tsx b/client/src/components/molecules/TimeZone/TimeZone.tsx
index 83e7be1..ea1feed 100644
--- a/client/src/components/molecules/TimeZone/TimeZone.tsx
+++ b/client/src/components/molecules/TimeZone/TimeZone.tsx
@@ -1,8 +1,13 @@
import React from 'react';
-import { useSelector } from 'react-redux';
+import { shallowEqual, useSelector } from 'react-redux';
import styled from 'styled-components';
-import { getDuration } from '@/store/selectors';
+import {
+ getDuration,
+ getStartEnd,
+ getIsCrop,
+ getIsCropAndDuration,
+} from '@/store/selectors';
import TimeText from '@/components/atoms/TimeText';
import color from '@/theme/colors';
@@ -23,7 +28,8 @@ const InnerDiv = styled.div`
const PART_COUNT = 6;
-const getTimes = (duration: number): number[] => {
+const getTimes = ({ start, end }): number[] => {
+ const duration: number = end - start;
if (!duration) return [];
const times: number[] = [];
@@ -40,8 +46,11 @@ const getTimes = (duration: number): number[] => {
};
const TimeZone: React.FC = () => {
- const duration: number = Math.round(useSelector(getDuration));
- const times: number[] = getTimes(duration);
+ const { isCrop, duration } = useSelector(getIsCropAndDuration, shallowEqual);
+ const { start, end } = useSelector(getStartEnd);
+
+ const parameter = isCrop ? { start: 0, end: duration } : { start, end };
+ const times: number[] = getTimes(parameter);
return (
diff --git a/client/src/components/molecules/UploadArea/UploadArea.tsx b/client/src/components/molecules/UploadArea/UploadArea.tsx
index ba46c91..1409a9a 100644
--- a/client/src/components/molecules/UploadArea/UploadArea.tsx
+++ b/client/src/components/molecules/UploadArea/UploadArea.tsx
@@ -8,6 +8,8 @@ import { setVideo } from '@/store/originalVideo/actions';
import { getName } from '@/store/selectors';
import { reset } from '@/store/actionTypes';
+import webglController from '@/webgl/webglController';
+
const StyledDiv = styled.div`
display: flex;
align-items: center;
@@ -29,11 +31,8 @@ const UploadArea: React.FC = () => {
const handleChange = () => {
const localFile: File = ref.current?.files[0];
-
- if (localFile) {
- const objectURL = URL.createObjectURL(localFile);
- dispatch(setVideo(localFile, objectURL));
- } else dispatch(reset());
+ const objectURL = URL.createObjectURL(localFile);
+ dispatch(setVideo(localFile, objectURL));
setVisible(false);
};
diff --git a/client/src/components/organisms/Tools/Tools.tsx b/client/src/components/organisms/Tools/Tools.tsx
index 7cf6b8e..8304bdc 100644
--- a/client/src/components/organisms/Tools/Tools.tsx
+++ b/client/src/components/organisms/Tools/Tools.tsx
@@ -1,145 +1,104 @@
-import React, { useState } from 'react';
-import styled from 'styled-components';
+import React, { useState, useReducer, useCallback, useMemo } from 'react';
+import styled, { keyframes } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
-import {
- BsFillSkipStartFill,
- BsFillSkipEndFill,
- BsFillPlayFill,
- BsFillPauseFill,
-} from 'react-icons/bs';
-import { RiScissorsLine } from 'react-icons/ri';
+
import webglController from '@/webgl/webglController';
import ButtonGroup from '@/components/molecules/ButtonGroup';
import UploadArea from '@/components/molecules/UploadArea';
-import size from '@/theme/sizes';
import video from '@/video';
import {
play as playAction,
pause,
moveTo,
} from '@/store/currentVideo/actions';
-import { getStartEnd } from '@/store/selectors';
+import { getStartEnd, getPlaying } from '@/store/selectors';
+import { cropStart, cropCancel, cropConfirm } from '@/store/actionTypes';
+import reducer, { initialData, ButtonTypes } from './reducer';
+import {
+ getEditToolData,
+ getSubEditToolsData,
+ getVideoToolsData,
+} from './buttonData';
+
+const UP = 'up';
+const DOWN = 'down';
const StyledDiv = styled.div`
+ position: relative;
display: flex;
justify-content: space-between;
- align-items: center;
+ align-items: flex-end;
padding: 1rem;
`;
-interface button {
- onClick: () => void;
- message: string;
- type: 'default' | 'transparent';
- children: React.ReactChild;
-}
+const slide = keyframes`
+ from {
+ transform: translate(0, 50px);
+ opacity: 0;
+ }
+ to {
+ transform: translate(0, 0);
+ opacity: 1;
+ }
+`;
+
+const StyledEditToolDiv = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
+
+const EditTool = styled(ButtonGroup)`
+ width: 20rem;
+`;
+
+const WrapperDiv = styled.div`
+ width: 25rem;
+ display: flex;
+ justify-content: center;
+ animation: ${slide} 0.5s -0.1s ease-out;
+`;
+
+const SubEditTool = styled(ButtonGroup)`
+ width: 100%;
+`;
-const getVideoToolsData = (
- backwardVideo: () => void,
- playPauseVideo: () => void,
- forwardVideo: () => void,
- play: boolean
-): button[] => [
- {
- onClick: backwardVideo,
- message: '',
- type: 'transparent',
- children: ,
- },
- {
- onClick: playPauseVideo,
- message: '',
- type: 'transparent',
- children: play ? (
-
- ) : (
-
- ),
- },
- {
- onClick: forwardVideo,
- message: '',
- type: 'transparent',
- children: ,
- },
-];
-
-const getEditToolsData = (
- rotateLeft90Degree: () => void,
- rotateRight90Degree: () => void,
- reverseUpsideDown: () => void,
- reverseSideToSide: () => void,
- enlarge: () => void,
- reduce: () => void,
- crop: () => void
-): button[] => [
- {
- onClick: rotateLeft90Degree,
- message: "Left 90'",
- type: 'transparent',
- children: null,
- },
- {
- onClick: rotateRight90Degree,
- message: "Right 90'",
- type: 'transparent',
- children: null,
- },
- {
- onClick: reverseUpsideDown,
- message: 'Up to Down',
- type: 'transparent',
- children: null,
- },
- {
- onClick: reverseSideToSide,
- message: 'Side to Side',
- type: 'transparent',
- children: null,
- },
- { onClick: enlarge, message: 'enlarge', type: 'transparent', children: null },
- { onClick: reduce, message: 'reduce', type: 'transparent', children: null },
- {
- onClick: crop,
- message: 'crop',
- type: 'transparent',
- children: ,
- },
-];
-
-const EditTool = styled(ButtonGroup)``;
const VideoTool = styled(ButtonGroup)``;
-const Tools: React.FC = () => {
- const [play, setPlay] = useState(true); // Fix 스토어로 등록
+interface props {
+ setEdit: Function;
+}
+
+const Tools: React.FC = ({ setEdit }) => {
+ const play = useSelector(getPlaying);
const dispatch = useDispatch();
+ const [toolType, setToolType] = useState(null);
+ const [buttonData, dispatchButtonData] = useReducer(reducer, initialData);
const { start, end } = useSelector(getStartEnd, shallowEqual);
const backwardVideo = () => {
- const dstTime = Math.max(video.getCurrentTime() - 10, start);
+ const dstTime = Math.max(video.get('currentTime') - 10, start);
video.setCurrentTime(dstTime);
dispatch(moveTo(dstTime));
};
const forwardVideo = () => {
- const dstTime = Math.min(video.getCurrentTime() + 10, end);
+ const dstTime = Math.min(video.get('currentTime') + 10, end);
video.setCurrentTime(dstTime);
dispatch(moveTo(dstTime));
};
const playPauseVideo = () => {
- if (play) {
+ if (!play) {
video.play();
dispatch(playAction());
} else {
video.pause();
dispatch(pause());
}
-
- setPlay(!play);
};
document.onkeydown = (event: KeyboardEvent) => {
@@ -160,14 +119,61 @@ const Tools: React.FC = () => {
}
};
- const rotateLeft90Degree = () => webglController.rotateLeft90Degree();
- const rotateRight90Degree = () => webglController.rotateRight90Degree();
- const reverseUpsideDown = () => webglController.reverseUpsideDown();
- const reverseSideToSide = () => webglController.reverseSideToSide();
- const enlarge = () => webglController.enlarge();
- const reduce = () => webglController.reduce();
- const crop = () => {
- // crop
+ const closeSubtool = () => {
+ setEdit(DOWN);
+ setToolType(null);
+ dispatchButtonData({ type: null });
+ };
+
+ const openSubtool = (type: ButtonTypes, payload: (() => void)[]) => {
+ if (type !== ButtonTypes.crop) dispatch(cropCancel());
+ setEdit(UP);
+ setToolType(type);
+ dispatchButtonData({ type, payload });
+ };
+
+ const handleCropManually = useCallback(() => {}, []); // TODO:
+ const handleCropConfirm = useCallback(() => {
+ dispatch(cropConfirm());
+ closeSubtool();
+ }, [dispatch]);
+ const handleCropCancel = useCallback(() => {
+ dispatch(cropCancel());
+ closeSubtool();
+ }, [dispatch]);
+
+ const methods = useMemo(
+ () => ({
+ rotateReverse: [
+ webglController.rotateLeft90Degree,
+ webglController.rotateRight90Degree,
+ webglController.reverseUpsideDown,
+ webglController.reverseSideToSide,
+ ],
+ ratio: [webglController.enlarge, webglController.reduce],
+ crop: [handleCropManually, handleCropConfirm, handleCropCancel],
+ }),
+ []
+ );
+
+ const handleRotateReverse = () =>
+ toolType === ButtonTypes.videoEffect
+ ? closeSubtool()
+ : openSubtool(ButtonTypes.videoEffect, methods.rotateReverse);
+
+ const handleRatio = () =>
+ toolType === ButtonTypes.ratio
+ ? closeSubtool()
+ : openSubtool(ButtonTypes.ratio, methods.ratio);
+
+ const handleCrop = () => {
+ if (toolType === ButtonTypes.crop) {
+ dispatch(cropCancel());
+ closeSubtool();
+ } else {
+ openSubtool(ButtonTypes.crop, methods.crop);
+ dispatch(cropStart());
+ }
};
return (
@@ -180,17 +186,20 @@ const Tools: React.FC = () => {
play
)}
/>
-
+ {toolType && (
+
+
+
)}
- />
+
+
);
diff --git a/client/src/components/organisms/Tools/buttonData.tsx b/client/src/components/organisms/Tools/buttonData.tsx
new file mode 100644
index 0000000..9b7241d
--- /dev/null
+++ b/client/src/components/organisms/Tools/buttonData.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+
+import {
+ BsFillSkipStartFill,
+ BsFillSkipEndFill,
+ BsFillPlayFill,
+ BsFillPauseFill,
+ BsAspectRatio,
+} from 'react-icons/bs';
+import { RiScissorsLine } from 'react-icons/ri';
+import { MdScreenRotation } from 'react-icons/md';
+
+import size from '@/theme/sizes';
+
+import { ButtonData } from './reducer';
+
+interface button {
+ onClick: () => void;
+ message: string;
+ type: 'default' | 'transparent';
+ children: React.ReactChild;
+}
+
+export const getVideoToolsData = (
+ backwardVideo: () => void,
+ playPauseVideo: () => void,
+ forwardVideo: () => void,
+ play: boolean
+): button[] => [
+ {
+ onClick: backwardVideo,
+ message: '',
+ type: 'transparent',
+ children:
,
+ },
+ {
+ onClick: playPauseVideo,
+ message: '',
+ type: 'transparent',
+ children: play ? (
+
+ ) : (
+
+ ),
+ },
+ {
+ onClick: forwardVideo,
+ message: '',
+ type: 'transparent',
+ children:
,
+ },
+];
+
+export const getEditToolData = (
+ rotateReverse: () => void,
+ ratio: () => void,
+ crop: () => void
+): button[] => [
+ {
+ onClick: rotateReverse,
+ message: '회전 / 반전',
+ type: 'transparent',
+ children:
,
+ },
+ {
+ onClick: ratio,
+ message: '비율',
+ type: 'transparent',
+ children:
,
+ },
+ {
+ onClick: crop,
+ message: '자르기',
+ type: 'transparent',
+ children:
,
+ },
+];
+
+export const getSubEditToolsData = (buttonData: ButtonData): button[] =>
+ [...Array(buttonData.onClicks?.length)].map((_, idx) => ({
+ onClick: buttonData.onClicks[idx],
+ message: buttonData.messages[idx],
+ type: buttonData.type,
+ children: buttonData.childrens[idx],
+ }));
diff --git a/client/src/components/organisms/Tools/reducer.tsx b/client/src/components/organisms/Tools/reducer.tsx
new file mode 100644
index 0000000..29062a4
--- /dev/null
+++ b/client/src/components/organisms/Tools/reducer.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { BsTerminal, BsCheck, BsX } from 'react-icons/bs';
+import {
+ MdRotateLeft,
+ MdRotateRight,
+ MdZoomIn,
+ MdZoomOut,
+} from 'react-icons/md';
+import { CgMergeHorizontal, CgMergeVertical } from 'react-icons/cg';
+
+import size from '@/theme/sizes';
+
+export enum ButtonTypes {
+ crop = 'crop',
+ videoEffect = 'videoEffect',
+ ratio = 'ratio',
+}
+
+export interface ButtonData {
+ onClicks: (() => void)[];
+ messages: string[];
+ type: 'default' | 'transparent';
+ childrens: React.ReactChild[];
+}
+
+interface ButtonDataAction {
+ type: ButtonTypes | null;
+ payload?: (() => void)[];
+}
+
+// crop
+const cropMessages = ['직접입력', '확인', '취소'];
+const cropChildrens = [
+
,
+
,
+
,
+];
+
+// videoEffect
+const rotateReverseMessages = ['왼쪽', '오른쪽', '상하 반전', '좌우 반전'];
+const rotateReverseChildrens = [
+
,
+
,
+
,
+
,
+];
+
+// ratio
+const ratioMessages = ['확대', '축소'];
+const ratioChildrens = [
+
,
+
,
+];
+
+export const initialData: ButtonData = {
+ onClicks: [],
+ messages: [],
+ type: 'transparent',
+ childrens: [],
+};
+
+export default (state: ButtonData, action: ButtonDataAction): ButtonData => {
+ switch (action.type) {
+ case ButtonTypes.crop:
+ return {
+ onClicks: action.payload,
+ messages: cropMessages,
+ type: 'transparent',
+ childrens: cropChildrens,
+ };
+ case ButtonTypes.videoEffect:
+ return {
+ onClicks: action.payload,
+ messages: rotateReverseMessages,
+ type: 'transparent',
+ childrens: rotateReverseChildrens,
+ };
+ case ButtonTypes.ratio:
+ return {
+ onClicks: action.payload,
+ messages: ratioMessages,
+ type: 'transparent',
+ childrens: ratioChildrens,
+ };
+ default:
+ return initialData;
+ }
+};
diff --git a/client/src/components/organisms/VideoContainer/VideoContainer.tsx b/client/src/components/organisms/VideoContainer/VideoContainer.tsx
index d5415fe..42628fb 100644
--- a/client/src/components/organisms/VideoContainer/VideoContainer.tsx
+++ b/client/src/components/organisms/VideoContainer/VideoContainer.tsx
@@ -1,5 +1,36 @@
import React from 'react';
-import styled from 'styled-components';
+import styled, { css, keyframes } from 'styled-components';
+
+import color from '@/theme/colors';
+
+const UP = 'up';
+const DOWN = 'down';
+
+const slideUp = keyframes`
+ from {
+ transform: translate(0, 2rem);
+ }
+ to {
+ transform: translate(0, 0);
+ }
+`;
+
+const slideDown = keyframes`
+ from {
+ transform: translate(0, -2rem);
+ }
+ to {
+ transform: translate(0, 0);
+ }
+`;
+
+const videoUp = css`
+ animation: ${slideUp} 0.3s ease-out;
+`;
+
+const videoDown = css`
+ animation: ${slideDown} 0.5s ease-out;
+`;
const StyledDiv = styled.div`
display: flex;
@@ -8,13 +39,20 @@ const StyledDiv = styled.div`
`;
const StyledCanvas = styled.canvas`
- height: 35rem;
+ height: 32rem;
+ background-color: ${color.BLACK};
+ ${({ isEdit }) => (isEdit === UP ? videoUp : '')};
+ ${({ isEdit }) => (isEdit === DOWN ? videoDown : '')};
`;
-const VideoContainer: React.FC = () => {
+interface props {
+ isEdit: string;
+}
+
+const VideoContainer: React.FC
= ({ isEdit }) => {
return (
-
+
);
};
diff --git a/client/src/index.html b/client/src/index.html
index b1bba6e..03bc745 100644
--- a/client/src/index.html
+++ b/client/src/index.html
@@ -4,7 +4,9 @@
- react-app
+
+
+ WAVE
diff --git a/client/src/pages/edit.tsx b/client/src/pages/edit.tsx
new file mode 100644
index 0000000..064edde
--- /dev/null
+++ b/client/src/pages/edit.tsx
@@ -0,0 +1,39 @@
+import React, { useState } from 'react';
+import { useSelector } from 'react-redux';
+import styled from 'styled-components';
+
+import TimeLine from '@/components/organisms/TimeLine';
+import Header from '@/components/organisms/Header';
+import Tools from '@/components/organisms/Tools';
+import VideoContainer from '@/components/organisms/VideoContainer';
+import Loading from '@/components/atoms/Loading';
+import { getMessage } from '@/store/selectors';
+
+const StyledDiv = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ height: 100vh;
+ width: 100vw;
+`;
+
+const BottomDiv = styled.div``;
+
+const EditPage: React.FC = () => {
+ const message = useSelector(getMessage);
+ const [isEdit, setEdit] = useState('');
+
+ return (
+
+ {message && }
+
+
+
+
+
+
+
+ );
+};
+
+export default React.memo(EditPage);
diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/client/src/store/actionTypes.ts b/client/src/store/actionTypes.ts
index e4a1eb2..93c9903 100644
--- a/client/src/store/actionTypes.ts
+++ b/client/src/store/actionTypes.ts
@@ -16,6 +16,16 @@ export const MOVE_TO = 'current/MOVE_TO';
export const SET_THUMBNAILS = 'current/SET_THUMBNAILS';
export const CROP = 'current/CROP';
+// crop
+export const CROP_START = 'crop/CROP_START';
+export const cropStart = () => ({ type: CROP_START });
+
+export const CROP_CANCEL = 'crop/CROP_CANCEL';
+export const cropCancel = () => ({ type: CROP_CANCEL });
+
+export const CROP_CONFIRM = 'crop/CROP_CONFIRM';
+export const cropConfirm = () => ({ type: CROP_CONFIRM });
+
// history
// global
diff --git a/client/src/store/crop/actions.ts b/client/src/store/crop/actions.ts
new file mode 100644
index 0000000..f2f9dbc
--- /dev/null
+++ b/client/src/store/crop/actions.ts
@@ -0,0 +1,32 @@
+import { CROP_START, CROP_CANCEL, CROP_CONFIRM, CROP } from '../actionTypes';
+import { CropAction } from '../currentVideo/actions';
+
+export const cropStart = () => ({
+ type: typeof CROP_START,
+});
+
+export const cropCancel = () => ({
+ type: typeof CROP_CANCEL,
+});
+
+export const cropConfirm = () => ({
+ type: typeof CROP_CONFIRM,
+});
+
+export type CropStartAction = {
+ type: typeof CROP_START;
+};
+
+export type CropCancelAction = {
+ type: typeof CROP_CANCEL;
+};
+
+export type CropConfirmAction = {
+ type: typeof CROP_CONFIRM;
+};
+
+export type CropStoreAction =
+ | CropStartAction
+ | CropCancelAction
+ | CropConfirmAction
+ | CropAction;
diff --git a/client/src/store/crop/reducer.ts b/client/src/store/crop/reducer.ts
new file mode 100644
index 0000000..70270d3
--- /dev/null
+++ b/client/src/store/crop/reducer.ts
@@ -0,0 +1,42 @@
+import { CROP_START, CROP_CANCEL, CROP_CONFIRM, CROP } from '../actionTypes';
+import { CropStoreAction } from './actions';
+
+export interface CropState {
+ isCrop: boolean;
+ isCropConfirm: boolean;
+}
+
+const initialState: CropState = {
+ isCrop: false,
+ isCropConfirm: false,
+};
+
+export default (
+ state: CropState = initialState,
+ action: CropStoreAction
+): CropState => {
+ switch (action.type) {
+ case CROP_START:
+ return {
+ ...state,
+ isCrop: true,
+ };
+ case CROP_CANCEL:
+ return {
+ ...state,
+ isCrop: false,
+ };
+ case CROP_CONFIRM:
+ return {
+ ...state,
+ isCropConfirm: true,
+ };
+ case CROP:
+ return {
+ isCrop: false,
+ isCropConfirm: false,
+ };
+ default:
+ return state;
+ }
+};
diff --git a/client/src/store/crop/sagas.ts b/client/src/store/crop/sagas.ts
new file mode 100644
index 0000000..e12cbb3
--- /dev/null
+++ b/client/src/store/crop/sagas.ts
@@ -0,0 +1,25 @@
+import { put, call, takeLatest } from 'redux-saga/effects';
+
+import video from '@/video/video';
+import webglController from '@/webgl/webglController';
+import { setThumbnails } from '../currentVideo/actions';
+import { CROP, error } from '../actionTypes';
+
+function* updateThumbnails(action) {
+ try {
+ const thumbnails: string[] = yield call(
+ video.makeThumbnails,
+ action.payload.start,
+ action.payload.end
+ );
+ yield call(webglController.main);
+ yield put(setThumbnails(thumbnails));
+ } catch (err) {
+ console.log(err);
+ yield put(error());
+ }
+}
+
+export default function* CropThumbnail() {
+ yield takeLatest(CROP, updateThumbnails);
+}
diff --git a/client/src/store/currentVideo/actions.ts b/client/src/store/currentVideo/actions.ts
index 390a2c9..738ac53 100644
--- a/client/src/store/currentVideo/actions.ts
+++ b/client/src/store/currentVideo/actions.ts
@@ -51,7 +51,7 @@ export type SetThumbnailsAction = {
};
};
-type CropAction = {
+export type CropAction = {
type: typeof CROP;
payload: {
start: number;
diff --git a/client/src/store/originalVideo/sagas.ts b/client/src/store/originalVideo/sagas.ts
index d277b48..ee70e4d 100644
--- a/client/src/store/originalVideo/sagas.ts
+++ b/client/src/store/originalVideo/sagas.ts
@@ -1,13 +1,19 @@
-import { put, call, takeLatest } from 'redux-saga/effects';
+import { put, call, takeLatest, select } from 'redux-saga/effects';
import video from '@/video/video';
-import WebglController from '@/webgl/webglController';
+import webglController from '@/webgl/webglController';
import { loadMetadata } from './actions';
import { setThumbnails } from '../currentVideo/actions';
import { SET_VIDEO, error } from '../actionTypes';
+import { getDuration } from '../selectors';
const TIMEOUT = 5_000;
+export function* deleteSrc() {
+ yield call(webglController.reset);
+ yield call(video.revoke);
+}
+
function waitMetadataLoading(objectURL) {
return new Promise((resolve, reject) => {
const timer = setTimeout(reject, TIMEOUT, 'loading metadata timeout');
@@ -33,7 +39,7 @@ function* load(action) {
const thumbnails: string[] = yield call(video.makeThumbnails, 0, duration);
- yield call(WebglController.main);
+ yield call(webglController.main);
yield put(setThumbnails(thumbnails));
} catch (err) {
console.log(err);
@@ -41,6 +47,6 @@ function* load(action) {
}
}
-export default function* watchSetVideo() {
+export function* watchSetVideo() {
yield takeLatest(SET_VIDEO, load);
}
diff --git a/client/src/store/reducer.ts b/client/src/store/reducer.ts
index 5e786ba..93013ec 100644
--- a/client/src/store/reducer.ts
+++ b/client/src/store/reducer.ts
@@ -1,15 +1,18 @@
import { combineReducers } from 'redux';
import originalVideo, { OriginalVideoState } from './originalVideo/reducer';
import currentVideo, { CurrentVideoState } from './currentVideo/reducer';
+import crop, { CropState } from './crop/reducer';
export interface RootState {
originalVideo: OriginalVideoState;
currentVideo: CurrentVideoState;
+ crop: CropState;
}
const reducers = {
originalVideo,
currentVideo,
+ crop,
};
export default combineReducers(reducers);
diff --git a/client/src/store/sagas.ts b/client/src/store/sagas.ts
index 09d4577..7fb727a 100644
--- a/client/src/store/sagas.ts
+++ b/client/src/store/sagas.ts
@@ -1,16 +1,12 @@
import { all, call, takeEvery } from 'redux-saga/effects';
-import video from '@/video';
-import watchSetVideo from '@/store/originalVideo/sagas';
+import { watchSetVideo, deleteSrc } from '@/store/originalVideo/sagas';
+import CropThumbnail from '@/store/crop/sagas';
import { RESET } from './actionTypes';
-function* deleteSrc() {
- yield call(video.revoke);
-}
-
function* watchReset() {
yield takeEvery(RESET, deleteSrc);
}
export default function* rootSaga() {
- yield all([watchSetVideo(), watchReset()]);
+ yield all([watchSetVideo(), watchReset(), CropThumbnail()]);
}
diff --git a/client/src/store/selectors.ts b/client/src/store/selectors.ts
index 336be27..934baf4 100644
--- a/client/src/store/selectors.ts
+++ b/client/src/store/selectors.ts
@@ -25,3 +25,27 @@ export const getStartEnd = (state: RootState) => {
};
export const getThumbnails = (state: RootState) =>
state.currentVideo.thumbnails;
+
+// crop
+export const getIsCrop = (state: RootState) => state.crop.isCrop;
+
+export const getIsCropConfirm = (state: RootState) => state.crop.isCropConfirm;
+
+export const getCropState = (state: RootState) => {
+ const { isCrop } = state.crop;
+ const { isCropConfirm } = state.crop;
+
+ return {
+ isCrop,
+ isCropConfirm,
+ };
+};
+export const getIsCropAndDuration = (state: RootState) => {
+ const { isCrop } = state.crop;
+ const { length } = state.originalVideo;
+
+ return {
+ isCrop,
+ duration: length,
+ };
+};
diff --git a/client/src/video/video.tsx b/client/src/video/video.tsx
index 201e2c7..5f3bbb0 100644
--- a/client/src/video/video.tsx
+++ b/client/src/video/video.tsx
@@ -1,12 +1,21 @@
class Video {
private video: HTMLVideoElement;
- private THUMNAIL_COUNT: number = 30;
-
private canvas: HTMLCanvasElement;
+ private THUMBNAIL_COUNT: number = 30;
+
private thumbnails: string[];
+ private getAllowedFields: Set = new Set([
+ 'paused',
+ 'duration',
+ 'videoWidth',
+ 'videoHeight',
+ 'src',
+ 'currentTime',
+ ]);
+
constructor() {
this.canvas = document.createElement('canvas');
this.video = document.createElement('video');
@@ -18,28 +27,13 @@ class Video {
return this.video.paused;
};
- getVideo = () => {
- return this.video;
- };
-
- getDuration = () => {
- return this.video.duration;
+ get = field => {
+ if (this.getAllowedFields.has(field)) return this.video[field];
+ return undefined;
};
- getVideoWidth = () => {
- return this.video.videoWidth;
- };
-
- getVideoHeight = () => {
- return this.video.videoHeight;
- };
-
- getSrc = () => {
- return this.video.src;
- };
-
- getCurrentTime = () => {
- return this.video.currentTime;
+ getVideo = () => {
+ return this.video;
};
getThumbnails = () => {
@@ -59,20 +53,20 @@ class Video {
return new Promise((resolve, reject) => {
try {
(async () => {
- const gap = (end - start) / (this.THUMNAIL_COUNT - 1);
- let secs = start;
+ const gap = (end - start) / (this.THUMBNAIL_COUNT - 1);
+ let secs = end;
- const images: string[] = [];
+ const images: string[] = new Array(this.THUMBNAIL_COUNT);
- for (let count = 0; count < this.THUMNAIL_COUNT; count += 1) {
+ for (let count = this.THUMBNAIL_COUNT - 1; count >= 0; count -= 1) {
this.setCurrentTime(secs);
const image: string = await this.getImageAt();
- secs += gap;
- images.push(image);
+ secs -= gap;
+ images[count] = image;
}
- this.setCurrentTime(0);
- this.thumbnails = images;
+ if (start === 0 && end === this.video.duration)
+ this.thumbnails = images;
resolve(images);
})();
} catch (err) {
@@ -103,8 +97,9 @@ class Video {
};
revoke = () => {
- if (this.getSrc()) {
- URL.revokeObjectURL(this.getSrc());
+ const src = this.get('src');
+ if (src) {
+ URL.revokeObjectURL(src);
this.video.removeAttribute('src');
this.load();
}
diff --git a/client/src/webgl/webglController.ts b/client/src/webgl/webglController.ts
index 78661eb..00ccd41 100644
--- a/client/src/webgl/webglController.ts
+++ b/client/src/webgl/webglController.ts
@@ -23,104 +23,63 @@ interface ProgramInfo {
};
}
-class WebglController {
- copyVideo: Boolean;
+const RATIO = 1.25;
+const INVERSE = 1 / RATIO;
- positions: Array;
+class WebglController {
+ positions: number[][];
buffers: Buffers;
gl: WebGLRenderingContext;
+ init = {
+ positions: [
+ [-1.0, -1.0],
+ [1.0, -1.0],
+ [1.0, 1.0],
+ [-1.0, 1.0],
+ ],
+ };
+
constructor() {
- this.copyVideo = false;
- this.positions = [-1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0];
+ this.positions = this.init.positions.map(pair => [...pair]);
}
rotateLeft90Degree = () => {
- this.positions.push(this.positions.shift());
+ // 0123 => 1230
this.positions.push(this.positions.shift());
this.buffers = this.initBuffers();
};
rotateRight90Degree = () => {
- this.positions.unshift(this.positions.pop());
+ // 0123 => 3012
this.positions.unshift(this.positions.pop());
this.buffers = this.initBuffers();
};
reverseUpsideDown = () => {
- const x1 = this.positions.shift();
- const y1 = this.positions.shift();
-
- const x2 = this.positions.shift();
- const y2 = this.positions.shift();
-
- const x3 = this.positions.shift();
- const y3 = this.positions.shift();
- this.positions.push(x3);
- this.positions.push(y3);
-
- this.positions.push(x2);
- this.positions.push(y2);
-
- this.positions.push(x1);
- this.positions.push(y1);
-
+ // 0123 => 3210
+ this.positions.reverse();
this.buffers = this.initBuffers();
};
reverseSideToSide = () => {
- const x1 = this.positions.shift();
- const y1 = this.positions.shift();
-
- const x2 = this.positions.shift();
- const y2 = this.positions.shift();
-
- const x3 = this.positions.shift();
- const y3 = this.positions.shift();
-
- this.positions.push(x3);
- this.positions.push(y3);
-
- this.positions.unshift(y1);
- this.positions.unshift(x1);
-
- this.positions.unshift(y2);
- this.positions.unshift(x2);
-
+ // 0123 => 1032
+ this.positions = [
+ ...this.positions.slice(0, 2).reverse(),
+ ...this.positions.slice(-2).reverse(),
+ ];
this.buffers = this.initBuffers();
};
enlarge = () => {
- const temp = [];
-
- this.positions.forEach(element => {
- if (element < 0) {
- temp.push(element - 1);
- } else {
- temp.push(element + 1);
- }
- });
-
- this.positions = temp;
-
+ this.positions = this.positions.map(pair => pair.map(val => val * RATIO));
this.buffers = this.initBuffers();
};
reduce = () => {
- const temp = [];
-
- this.positions.forEach(element => {
- if (element < 0) {
- temp.push(element + 1);
- } else {
- temp.push(element - 1);
- }
- });
-
- this.positions = temp;
-
+ this.positions = this.positions.map(pair => pair.map(val => val * INVERSE));
this.buffers = this.initBuffers();
};
@@ -139,7 +98,7 @@ class WebglController {
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
- new Float32Array(this.positions),
+ new Float32Array(this.positions.flat()),
this.gl.STATIC_DRAW
);
@@ -353,8 +312,8 @@ class WebglController {
glInit = () => {
this.gl = this.initCanvas(
- video.getVideoWidth().toString(),
- video.getVideoHeight().toString()
+ video.get('videoWidth').toString(),
+ video.get('videoHeight').toString()
);
this.buffers = this.initBuffers();
const shaderProgram = this.initShaderProgram();
@@ -384,7 +343,7 @@ class WebglController {
const texture = this.initTexture();
const render = () => {
- if (!video.getSrc()) return;
+ if (!video.get('src')) return;
this.updateTexture(texture);
this.drawScene(programInfo, texture);
@@ -397,5 +356,11 @@ class WebglController {
main = () => {
this.glInit();
};
+
+ reset = () => {
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
+ this.positions = this.init.positions.map(pair => [...pair]);
+ this.initBuffers();
+ };
}
export default new WebglController();
diff --git a/tsconfig.json b/tsconfig.json
index 199fa1d..242701f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,7 @@
"lib": [
"es5",
"es6",
- "es2015",
+ "es2019",
"DOM"
],
"jsx": "react",