From a17d51701fc767e4e2c37ba3591bdc5da33c5d6b Mon Sep 17 00:00:00 2001 From: mjseok Date: Sun, 13 Dec 2020 02:24:41 +0900 Subject: [PATCH 01/40] =?UTF-8?q?[FE]=20:=20[FIX]=20#33=2010=EC=B4=88?= =?UTF-8?q?=EB=AF=B8=EB=A7=8C=EC=9D=BC=EB=95=8C=20=EB=92=A4=EB=A1=9C?= =?UTF-8?q?=EA=B0=80=EA=B8=B0=ED=95=98=EB=A9=B4=20currentTime=20update?= =?UTF-8?q?=EB=90=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/molecules/CurrentTime/CurrentTime.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/src/components/molecules/CurrentTime/CurrentTime.tsx b/client/src/components/molecules/CurrentTime/CurrentTime.tsx index cee1d9c..69fb5bf 100644 --- a/client/src/components/molecules/CurrentTime/CurrentTime.tsx +++ b/client/src/components/molecules/CurrentTime/CurrentTime.tsx @@ -22,9 +22,7 @@ const getVideoCurrentTime = () => video.get('currentTime'); const CurrentTime: React.FC = () => { const { start, end } = useSelector(getStartEnd, shallowEqual); const isCrop = useSelector(getIsCrop); - const [time, setTime] = useState( - Math.floor(getVideoCurrentTime() - (!isCrop && start)) - ); + const [time, setTime] = useState(0); const visible = useSelector(getVisible); const dispatch = useDispatch(); @@ -46,8 +44,7 @@ const CurrentTime: React.FC = () => { if (time !== newTime) setTime(newTime); }, 50); return () => clearInterval(timer); - }, [isCrop, visible, start, end]); - + }, [isCrop, time, visible, start, end]); return ( From dcef5fe1d7cc26d9917632acc6aba633ffe82e5d Mon Sep 17 00:00:00 2001 From: mjseok Date: Sun, 13 Dec 2020 16:03:06 +0900 Subject: [PATCH 02/40] =?UTF-8?q?[FE]=20:=20[FIX]=20=EB=A1=9C=EB=94=A9?= =?UTF-8?q?=EC=A4=91=EC=9D=B4=EB=82=98=20=ED=8C=8C=EC=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EC=9E=85=EB=A0=A5=ED=95=A0=20=EB=95=8C=20?= =?UTF-8?q?=ED=82=A4=EB=B3=B4=EB=93=9C=EC=9D=B4=EB=B2=A4=ED=8A=B8=EA=B0=80?= =?UTF-8?q?=20=EC=9E=91=EB=8F=99=ED=95=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/organisms/Tools/Tools.tsx | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/client/src/components/organisms/Tools/Tools.tsx b/client/src/components/organisms/Tools/Tools.tsx index 6f16b75..10d04f8 100644 --- a/client/src/components/organisms/Tools/Tools.tsx +++ b/client/src/components/organisms/Tools/Tools.tsx @@ -8,7 +8,12 @@ import ButtonGroup from '@/components/molecules/ButtonGroup'; import UploadArea from '@/components/molecules/UploadArea'; import video from '@/video'; import { play, pause, moveTo } from '@/store/currentVideo/actions'; -import { getStartEnd, getPlaying, getVisible } from '@/store/selectors'; +import { + getStartEnd, + getPlaying, + getVisible, + getMessage, +} from '@/store/selectors'; import { cropStart, cropCancel, cropConfirm } from '@/store/crop/actions'; import webglController from '@/webgl/webglController'; import reducer, { initialData, ButtonTypes } from './reducer'; @@ -86,7 +91,7 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { const [buttonData, dispatchButtonData] = useReducer(reducer, initialData); const hasEmptyVideo = !useSelector(getVisible); const [isSign, setIsSign] = useState(false); - + const message = useSelector(getMessage); const { start, end } = useSelector(getStartEnd, shallowEqual); const glCanvas = document.getElementById('glcanvas'); @@ -130,20 +135,22 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { document.onkeydown = (event: KeyboardEvent) => { const element = document.activeElement as HTMLButtonElement; - if (element.tagName !== 'INPUT') element.blur(); - - switch (event.code) { - case 'ArrowLeft': - backwardVideo(); - break; - case 'Space': - playPauseVideo(); - break; - case 'ArrowRight': - forwardVideo(); - break; - default: - break; + + if (element.tagName !== 'INPUT' && message === '') { + element.blur(); + switch (event.code) { + case 'ArrowLeft': + backwardVideo(); + break; + case 'Space': + playPauseVideo(); + break; + case 'ArrowRight': + forwardVideo(); + break; + default: + break; + } } }; From 1ea12d3da10fa9c3d0045d8a8ef41b7d89ae0b85 Mon Sep 17 00:00:00 2001 From: mjseok Date: Sun, 13 Dec 2020 21:56:47 +0900 Subject: [PATCH 03/40] =?UTF-8?q?[FE]=20:=20[FIX]=20#10=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=EB=B2=84=ED=8A=BC=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit subTool이 나와있는 상태에서 취소버튼을 누르면 subTool이 들어가지않고 그대로 노출되어있었는데 이 오류를 해결함 --- .../src/components/organisms/Tools/Tools.tsx | 18 ++++++++++++++---- client/src/store/crop/actions.ts | 10 ++++++++-- client/src/store/crop/reducer.ts | 10 +++++++++- client/src/store/currentVideo/reducer.ts | 8 +++++++- client/src/store/selectors.ts | 3 +++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/client/src/components/organisms/Tools/Tools.tsx b/client/src/components/organisms/Tools/Tools.tsx index 10d04f8..db1e1e2 100644 --- a/client/src/components/organisms/Tools/Tools.tsx +++ b/client/src/components/organisms/Tools/Tools.tsx @@ -1,4 +1,10 @@ -import React, { useState, useReducer, useCallback, useMemo } from 'react'; +import React, { + useState, + useReducer, + useCallback, + useMemo, + useEffect, +} from 'react'; import styled, { keyframes } from 'styled-components'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; @@ -13,6 +19,7 @@ import { getPlaying, getVisible, getMessage, + getIsCancel, } from '@/store/selectors'; import { cropStart, cropCancel, cropConfirm } from '@/store/crop/actions'; import webglController from '@/webgl/webglController'; @@ -93,7 +100,7 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { const [isSign, setIsSign] = useState(false); const message = useSelector(getMessage); const { start, end } = useSelector(getStartEnd, shallowEqual); - + const isCancel = useSelector(getIsCancel); const glCanvas = document.getElementById('glcanvas'); const input = document.createElement('input'); @@ -196,12 +203,10 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { const closeSubtool = () => { removeSignEvent(); - setEdit(DOWN); setToolType(null); dispatchButtonData({ type: null }); }; - const openSubtool = (type: ButtonTypes, payload: (() => void)[]) => { removeSignEvent(); webglController.setSignEdit(false); @@ -288,6 +293,11 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { if (webglController.sign) webglController.setSignEdit(true); } else closeSubtool(); }; + useEffect(() => { + if (toolType !== null) { + closeSubtool(); + } + }, [isCancel]); return ( diff --git a/client/src/store/crop/actions.ts b/client/src/store/crop/actions.ts index be44e79..32cc7c8 100644 --- a/client/src/store/crop/actions.ts +++ b/client/src/store/crop/actions.ts @@ -1,4 +1,9 @@ -import { CROP_START, CROP_CANCEL, CROP_CONFIRM } from '../actionTypes'; +import { + CROP_START, + CROP_CANCEL, + CROP_CONFIRM, + ResetAction, +} from '../actionTypes'; import { CropAction } from '../currentVideo/actions'; export const cropStart = () => ({ @@ -29,4 +34,5 @@ export type CropStoreAction = | CropStartAction | CropCancelAction | CropConfirmAction - | CropAction; + | CropAction + | ResetAction; diff --git a/client/src/store/crop/reducer.ts b/client/src/store/crop/reducer.ts index 70270d3..3e4bd57 100644 --- a/client/src/store/crop/reducer.ts +++ b/client/src/store/crop/reducer.ts @@ -1,4 +1,10 @@ -import { CROP_START, CROP_CANCEL, CROP_CONFIRM, CROP } from '../actionTypes'; +import { + CROP_START, + CROP_CANCEL, + CROP_CONFIRM, + CROP, + RESET, +} from '../actionTypes'; import { CropStoreAction } from './actions'; export interface CropState { @@ -36,6 +42,8 @@ export default ( isCrop: false, isCropConfirm: false, }; + case RESET: + return initialState; default: return state; } diff --git a/client/src/store/currentVideo/reducer.ts b/client/src/store/currentVideo/reducer.ts index 8240000..7164514 100644 --- a/client/src/store/currentVideo/reducer.ts +++ b/client/src/store/currentVideo/reducer.ts @@ -17,6 +17,7 @@ export interface CurrentVideoState { end: number; playing: boolean; thumbnails: string[]; + isCancel: boolean; } const initialState: CurrentVideoState = { @@ -25,6 +26,7 @@ const initialState: CurrentVideoState = { end: 0, playing: false, thumbnails: [], + isCancel: false, }; export default ( @@ -57,6 +59,7 @@ export default ( return { ...state, ...action.payload, + isCancel: false, }; case CROP: return { @@ -65,7 +68,10 @@ export default ( end: action.payload.current.end, }; case RESET: - return initialState; + return { + ...initialState, + isCancel: true, + }; case ERROR: default: return state; diff --git a/client/src/store/selectors.ts b/client/src/store/selectors.ts index ec0c9f4..43ac1ee 100644 --- a/client/src/store/selectors.ts +++ b/client/src/store/selectors.ts @@ -25,6 +25,9 @@ export const getStartEnd = (state: RootState) => { }; export const getThumbnails = (state: RootState) => state.currentVideo.thumbnails; +export const getIsCancel = (state: RootState) => { + return state.currentVideo.isCancel; +}; // crop export const getIsCrop = (state: RootState) => state.crop.isCrop; From 9e966f74ec93b3a817dcc36215ffed70b8b56f08 Mon Sep 17 00:00:00 2001 From: mjseok Date: Mon, 14 Dec 2020 10:14:25 +0900 Subject: [PATCH 04/40] =?UTF-8?q?[FE]=20:=20[FIX]=20=EC=84=9C=EB=AA=85=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B6=88?= =?UTF-8?q?=EB=9F=AC=EC=99=94=EC=9D=84=20=EB=95=8C=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/organisms/Tools/Tools.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/src/components/organisms/Tools/Tools.tsx b/client/src/components/organisms/Tools/Tools.tsx index db1e1e2..78c5580 100644 --- a/client/src/components/organisms/Tools/Tools.tsx +++ b/client/src/components/organisms/Tools/Tools.tsx @@ -187,18 +187,19 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { const handleInputChange = useCallback(({ target }) => { const file = (target as HTMLInputElement).files[0]; const img = document.createElement('img'); - setIsSign(!!file); img.src = URL.createObjectURL(file); - webglController.setSign(img); - webglController.setSignEdit(true); + img.addEventListener('load', () => { + webglController.setSign(img); + webglController.setSignEdit(true); + }); }, []); - const removeSignEvent = () => { const canvas = document.getElementById('glcanvas'); canvas.removeEventListener('mousedown', handleCanvasMouseDown); canvas.removeEventListener('mousedown', handleCanvasMouseUp); input.removeEventListener('change', handleInputChange); + input.value = null; }; const closeSubtool = () => { From f22da57d1c8742705d2df84a62dd096806496238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Mon, 14 Dec 2020 16:53:29 +0900 Subject: [PATCH 05/40] =?UTF-8?q?[FE]:=20[CHORE]=20ffmpeg.wasm=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package-lock.json | 29 +++++++++++++++++++++++++++-- client/package.json | 2 ++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index e975981..2dd3ed7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1289,6 +1289,22 @@ } } }, + "@ffmpeg/core": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@ffmpeg/core/-/core-0.8.5.tgz", + "integrity": "sha512-hemVFmhVLbD/VZaCG2BvCzFglKytMIMJ5aJfc12eXN4O4cG0wXnGTMVzlK1KKW/6viHhJMPkc9h4UDnJW8Uivg==" + }, + "@ffmpeg/ffmpeg": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.9.6.tgz", + "integrity": "sha512-ov5FAV3dHRJ/+ZxQH9V5GvY/iczwq5vrKWeu4tpytxZewTSAhZ1aKD/sFBzd79nQNF+CTktxUp3LWuGECXBNeA==", + "requires": { + "is-url": "^1.2.4", + "node-fetch": "^2.6.1", + "regenerator-runtime": "^0.13.7", + "resolve-url": "^0.2.1" + } + }, "@nicolo-ribaudo/chokidar-2": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8.tgz", @@ -5647,6 +5663,11 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -6268,6 +6289,11 @@ "tslib": "^1.10.0" } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", @@ -7821,8 +7847,7 @@ "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, "ret": { "version": "0.1.15", diff --git a/client/package.json b/client/package.json index 61083d5..1e0f374 100644 --- a/client/package.json +++ b/client/package.json @@ -50,6 +50,8 @@ "webpack-dev-server": "^3.11.0" }, "dependencies": { + "@ffmpeg/core": "^0.8.5", + "@ffmpeg/ffmpeg": "^0.9.6", "@open-wc/webpack-import-meta-loader": "^0.4.7", "@types/react-redux": "^7.1.11", "axios": "^0.21.0", From d78eba2c361b190f3aef2fc3a8e22beba337909e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Mon, 14 Dec 2020 16:54:26 +0900 Subject: [PATCH 06/40] =?UTF-8?q?[FE]:=20[FIX]=20frame=20=EB=B3=B5?= =?UTF-8?q?=EC=82=AC=EB=A5=BC=20=ED=86=B5=ED=95=9C=20fps=20=EB=B3=B4?= =?UTF-8?q?=EC=A1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/video/encoding.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/client/src/video/encoding.ts b/client/src/video/encoding.ts index 9ef5b8e..34e0285 100644 --- a/client/src/video/encoding.ts +++ b/client/src/video/encoding.ts @@ -14,6 +14,7 @@ interface WebCodecs { } const framerate = 30; +const interval = 1e6 / framerate; interface VideoElement extends HTMLVideoElement { captureStream(): MediaStream; @@ -48,26 +49,31 @@ export default async (start, end) => { fps: 30, }); - const applyEffect = async frame => { + let processedTimestamp = 0; + + const applyEffect = async (frame, timestamp) => { const image = await frame.createImageBitmap(); const pixels = webglController.getPixelsFromImage(image); - encoder.encodeRGB(pixels); frame.destroy(); + do { + encoder.encodeRGB(pixels); + processedTimestamp += interval; + } while (timestamp >= processedTimestamp + interval); // maintain fps }; const duration = (end - start) * 1e6; // seconds -> microseconds - const finished = frame => frame.timestamp > duration; + const finished = frame => frame.timestamp >= duration; return new Promise(resolve => { const handleFrame = frame => { if (finished(frame)) { videoTrackReader.stop(); video.pause(); - const mp4 = encoder.end(); - resolve(new Blob([mp4], { type: 'video/mp4' })); - } else { - applyEffect(frame); - } + applyEffect(frame, duration).then(() => { + const mp4 = encoder.end(); + resolve(new Blob([mp4], { type: 'video/mp4' })); + }); + } else applyEffect(frame, frame.timestamp); }; video.setCurrentTime(start); From dae78c82d4810836b013409bc78e32f529da84d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Mon, 14 Dec 2020 17:31:31 +0900 Subject: [PATCH 07/40] =?UTF-8?q?[FE]:=20[FEAT]=20ffmpeg.wasm=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=9C=20mux=20=EC=9E=91=EC=97=85=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - audio 특정 구간 추출 - encoding 된 영상과 mux --- client/src/store/originalVideo/sagas.ts | 31 +++++++---- client/src/video/mux.ts | 71 +++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 client/src/video/mux.ts diff --git a/client/src/store/originalVideo/sagas.ts b/client/src/store/originalVideo/sagas.ts index 394ff0d..1d542fd 100644 --- a/client/src/store/originalVideo/sagas.ts +++ b/client/src/store/originalVideo/sagas.ts @@ -4,7 +4,7 @@ import video from '@/video/video'; import encodeVideo from '@/video/encoding'; import webglController from '@/webgl/webglController'; import videoAPI from '@/api/video'; - +import muxVideoAndAudio from '@/video/mux'; import { setVideo, loadMetadata, @@ -21,7 +21,7 @@ import { error, reset, } from '../actionTypes'; -import { getStartEnd } from '../selectors'; +import { getFile, getStartEnd } from '../selectors'; import { clear } from '../history/actions'; const TIMEOUT = 5_000; @@ -93,10 +93,11 @@ export function* watchSetVideo() { yield takeLatest(SET_VIDEO, load); } -const downloadFile = (url, filename) => { +const downloadFile = (file: File): void => { const a = document.createElement('a'); + const url = URL.createObjectURL(file); a.href = url; - a.download = filename; + a.download = file.name; a.click(); a.remove(); URL.revokeObjectURL(url); @@ -105,17 +106,29 @@ const downloadFile = (url, filename) => { function* encode(action: EncodeStartAction) { try { const { start, end } = yield select(getStartEnd); - const blob: Blob = yield call(encodeVideo, start, end); - const url: string = yield call(URL.createObjectURL, blob); + const encodeVideoBlob: Blob = yield call(encodeVideo, start, end); + + const originalVideoFile: File = yield select(getFile); + + const muxedVideoFile: File = yield call( + muxVideoAndAudio, + encodeVideoBlob, + originalVideoFile, + action.payload.name, + { + start, + end, + } + ); + const isAgree = yield call( window.confirm, '인코딩된 영상을 다운받으시겠습니까?' ); - if (isAgree) downloadFile(url, action.payload.name); - const file = new File([blob], action.payload.name, { type: 'video/mp4' }); + if (isAgree) downloadFile(muxedVideoFile); - yield put(uploadStart(file)); + yield put(uploadStart(muxedVideoFile)); } catch (err) { console.log(err); yield put(error()); diff --git a/client/src/video/mux.ts b/client/src/video/mux.ts new file mode 100644 index 0000000..63d10e6 --- /dev/null +++ b/client/src/video/mux.ts @@ -0,0 +1,71 @@ +import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg'; + +const CROPPED_AUDIO = 'croppedAudio.mp3'; +const ENCODED_VIDEO = 'encodedVideo.mp4'; +const MUXED_VIDEO = 'muxedVideo.mp4'; + +const initFFmpeg = async () => { + const ffmpeg = createFFmpeg({ + corePath: './node_modules/@ffmpeg/core/dist/ffmpeg-core.js', + log: false, + }); + + await ffmpeg.load(); + + return ffmpeg; +}; + +const getAudioFromOriginalVideo = async ( + ffmpeg, + originalVideoFile: File, + interval: { start: number; end: number } +): Promise => { + const originalVideo: Uint8Array = await fetchFile(originalVideoFile); + + ffmpeg.FS('writeFile', originalVideoFile.name, originalVideo); + + await ffmpeg.run( + '-i', + originalVideoFile.name, + '-vn', + '-ss', + interval.start.toString(), + '-to', + interval.end.toString(), + CROPPED_AUDIO + ); +}; + +const muxVideoAndAudio = async ( + encodedVideoBlob: Blob, + originalVideoFile: File, + fileName: string, + interval: { start: number; end: number } +): Promise => { + const ffmpeg = await initFFmpeg(); + + await getAudioFromOriginalVideo(ffmpeg, originalVideoFile, interval); + + const encodedVideo: Uint8Array = await fetchFile(encodedVideoBlob); + ffmpeg.FS('writeFile', ENCODED_VIDEO, encodedVideo); + + await ffmpeg.run( + '-i', + CROPPED_AUDIO, + '-i', + ENCODED_VIDEO, + '-c', + 'copy', + MUXED_VIDEO + ); + + const muxedVideo: Uint8Array = ffmpeg.FS('readFile', MUXED_VIDEO); + + const muxedVideoFile = new File([muxedVideo], fileName, { + type: 'video/mp4', + }); + + return muxedVideoFile; +}; + +export default muxVideoAndAudio; From daabd774b370b56bf43ef6a3e727dcadf01dccb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Mon, 14 Dec 2020 17:32:18 +0900 Subject: [PATCH 08/40] =?UTF-8?q?[FE]:=20[FIX]=20video=20->=20encoding=20?= =?UTF-8?q?=EC=88=9C=ED=99=98=EC=B0=B8=EC=A1=B0=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/store/crop/sagas.ts | 2 +- client/src/store/originalVideo/sagas.ts | 11 +++++++---- client/src/video/encoding.ts | 5 ++--- client/src/video/index.tsx | 2 ++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/client/src/store/crop/sagas.ts b/client/src/store/crop/sagas.ts index 3341f88..7e570a6 100644 --- a/client/src/store/crop/sagas.ts +++ b/client/src/store/crop/sagas.ts @@ -1,6 +1,6 @@ import { put, call, takeLatest, select } from 'redux-saga/effects'; -import video from '@/video/video'; +import video from '@/video'; import webglController from '@/webgl/webglController'; import { setThumbnails, moveTo, pause } from '../currentVideo/actions'; import { CROP, error } from '../actionTypes'; diff --git a/client/src/store/originalVideo/sagas.ts b/client/src/store/originalVideo/sagas.ts index 1d542fd..00710a0 100644 --- a/client/src/store/originalVideo/sagas.ts +++ b/client/src/store/originalVideo/sagas.ts @@ -1,10 +1,8 @@ import { put, call, takeLatest, select } from 'redux-saga/effects'; -import video from '@/video/video'; -import encodeVideo from '@/video/encoding'; +import video, { encodeVideo, muxVideoAndAudio } from '@/video'; import webglController from '@/webgl/webglController'; import videoAPI from '@/api/video'; -import muxVideoAndAudio from '@/video/mux'; import { setVideo, loadMetadata, @@ -107,7 +105,12 @@ function* encode(action: EncodeStartAction) { try { const { start, end } = yield select(getStartEnd); - const encodeVideoBlob: Blob = yield call(encodeVideo, start, end); + const encodeVideoBlob: Blob = yield call( + encodeVideo, + start, + end, + webglController + ); const originalVideoFile: File = yield select(getFile); diff --git a/client/src/video/encoding.ts b/client/src/video/encoding.ts index 34e0285..8c62ab7 100644 --- a/client/src/video/encoding.ts +++ b/client/src/video/encoding.ts @@ -1,6 +1,5 @@ import loadEncoder from 'mp4-h264'; -import webglController from '@/webgl/webglController'; -import video from '.'; +import video from './video'; interface TrackReader { new (track: MediaStreamTrack): { @@ -33,7 +32,7 @@ const init = () => { return videoTrackReader; }; -export default async (start, end) => { +export default async (start, end, webglController) => { const videoTrackReader = init(); const Encoder = await loadEncoder(); diff --git a/client/src/video/index.tsx b/client/src/video/index.tsx index f4b6c44..b7b5426 100644 --- a/client/src/video/index.tsx +++ b/client/src/video/index.tsx @@ -1 +1,3 @@ export { default } from './video'; +export { default as encodeVideo } from './encoding'; +export { default as muxVideoAndAudio } from './mux'; From ed74765c6a2de6469189c199d786dadf35b6e918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Mon, 14 Dec 2020 17:32:32 +0900 Subject: [PATCH 09/40] =?UTF-8?q?[FE]:=20[FIX]=20Button=20=EC=88=9C?= =?UTF-8?q?=ED=99=98=EC=B0=B8=EC=A1=B0=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/atoms/Button/Button.tsx | 3 +-- client/src/components/atoms/Button/index.ts | 2 -- client/src/components/atoms/Button/style.ts | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/client/src/components/atoms/Button/Button.tsx b/client/src/components/atoms/Button/Button.tsx index 1f9e1bf..9158c03 100644 --- a/client/src/components/atoms/Button/Button.tsx +++ b/client/src/components/atoms/Button/Button.tsx @@ -2,8 +2,7 @@ import React from 'react'; import styled from 'styled-components'; import color from '@/theme/colors'; -import style from './style'; -import { ButtonType } from '.'; +import style, { ButtonType } from './style'; interface StyledProps { buttonType: ButtonType; diff --git a/client/src/components/atoms/Button/index.ts b/client/src/components/atoms/Button/index.ts index 612f586..efe8c80 100644 --- a/client/src/components/atoms/Button/index.ts +++ b/client/src/components/atoms/Button/index.ts @@ -1,3 +1 @@ export { default } from './Button'; - -export type ButtonType = 'default' | 'transparent' | 'selected'; diff --git a/client/src/components/atoms/Button/style.ts b/client/src/components/atoms/Button/style.ts index 3b52c96..ec0fd5e 100644 --- a/client/src/components/atoms/Button/style.ts +++ b/client/src/components/atoms/Button/style.ts @@ -1,6 +1,6 @@ import color from '@/theme/colors'; -import { ButtonType } from '.'; +export type ButtonType = 'default' | 'transparent' | 'selected'; export default (type: ButtonType) => { switch (type) { From 67d831c9c3a79d2c387ec3caa737edcacf0261fc Mon Sep 17 00:00:00 2001 From: SSH1997 Date: Mon, 14 Dec 2020 19:02:12 +0900 Subject: [PATCH 10/40] =?UTF-8?q?[FE]=20:=20[FEAT]=20=EC=98=81=EC=83=81=20?= =?UTF-8?q?blur=20=EB=B0=8F=20=EC=83=89=EA=B0=90=EC=A1=B0=EC=A0=88=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/webgl/fragmentShaderSource.ts | 26 ++++- client/src/webgl/webglController.ts | 142 +++++++++++++++++++++-- 2 files changed, 157 insertions(+), 11 deletions(-) diff --git a/client/src/webgl/fragmentShaderSource.ts b/client/src/webgl/fragmentShaderSource.ts index 4af20d3..d36b310 100644 --- a/client/src/webgl/fragmentShaderSource.ts +++ b/client/src/webgl/fragmentShaderSource.ts @@ -1,10 +1,34 @@ const fragmentShaderSource = ` + precision mediump float; varying highp vec2 vTextureCoord; uniform sampler2D uSampler; + uniform float chroma[3]; + + uniform vec2 u_textureSize; + uniform float u_kernel[9]; + uniform float u_kernelWeight; void main(void) { - gl_FragColor = texture2D(uSampler, vTextureCoord); + vec2 onePixel = vec2(1.0, 1.0) / u_textureSize; + + vec4 colorSum = + texture2D(uSampler, vTextureCoord + onePixel * vec2(-1, -1)) * u_kernel[0] + + texture2D(uSampler, vTextureCoord + onePixel * vec2( 0, -1)) * u_kernel[1] + + texture2D(uSampler, vTextureCoord + onePixel * vec2( 1, -1)) * u_kernel[2] + + texture2D(uSampler, vTextureCoord + onePixel * vec2(-1, 0)) * u_kernel[3] + + texture2D(uSampler, vTextureCoord + onePixel * vec2( 0, 0)) * u_kernel[4] + + texture2D(uSampler, vTextureCoord + onePixel * vec2( 1, 0)) * u_kernel[5] + + texture2D(uSampler, vTextureCoord + onePixel * vec2(-1, 1)) * u_kernel[6] + + texture2D(uSampler, vTextureCoord + onePixel * vec2( 0, 1)) * u_kernel[7] + + texture2D(uSampler, vTextureCoord + onePixel * vec2( 1, 1)) * u_kernel[8] ; + + vec4 weightColor = colorSum / u_kernelWeight; + + gl_FragColor.r = weightColor.r * chroma[0]; + gl_FragColor.g = weightColor.g * chroma[1]; + gl_FragColor.b = weightColor.b * chroma[2]; + gl_FragColor.a = weightColor.a; } `; diff --git a/client/src/webgl/webglController.ts b/client/src/webgl/webglController.ts index 426751d..16e38e9 100644 --- a/client/src/webgl/webglController.ts +++ b/client/src/webgl/webglController.ts @@ -92,6 +92,11 @@ class WebglController { signGrid: HTMLImageElement = new Image(); + // filter params + chroma: number[] = [1.0, 1.0, 1.0]; + + blurRatio: number = 0; + constructor() { this.positions = this.init.positions.map(pair => [...pair]); } @@ -171,16 +176,20 @@ class WebglController { return pixels; }; - updateTexture = (texture: WebGLTexture) => { - this.gl.bindTexture(this.gl.TEXTURE_2D, texture); - this.gl.texImage2D( - this.gl.TEXTURE_2D, - level, - this.internalFormat, - this.srcFormat, - this.srcType, - video.getVideo() - ); + setChromaRed = chroma => { + this.chroma[0] = chroma; + }; + + setChromaGreen = chroma => { + this.chroma[1] = chroma; + }; + + setChromaBlue = chroma => { + this.chroma[2] = chroma; + }; + + setBlur = blurRatio => { + this.blurRatio = blurRatio; }; moveSign = (diffX: number, diffY: number) => { @@ -200,6 +209,18 @@ class WebglController { this.sign = sign; }; + updateTexture = (texture: WebGLTexture) => { + this.gl.bindTexture(this.gl.TEXTURE_2D, texture); + this.gl.texImage2D( + this.gl.TEXTURE_2D, + level, + this.internalFormat, + this.srcFormat, + this.srcType, + video.getVideo() + ); + }; + drawGrid = (modelViewMatrix, projectionMatrix, programInfo) => { this.gl.useProgram(programInfo.program); @@ -337,6 +358,42 @@ class WebglController { this.gl.LINEAR ); + const chromaRedLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'chroma[0]' + ); + const chromaBlueLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'chroma[1]' + ); + + const chromaGreenLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'chroma[2]' + ); + + this.gl.uniform1f(chromaRedLocation, 1.0); + + this.gl.uniform1f(chromaBlueLocation, 1.0); + + this.gl.uniform1f(chromaGreenLocation, 1.0); + + const edgeDetectKernel = [0, 0, 0, 0, 1, 0, 0, 0, 0]; + + const kernelLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'u_kernel[0]' + ); + + this.gl.uniform1fv(kernelLocation, edgeDetectKernel); + + const kernelWeightLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'u_kernelWeight' + ); + + this.gl.uniform1f(kernelWeightLocation, 1); + this.gl.drawElements( this.gl.TRIANGLES, vertexCount, @@ -423,6 +480,71 @@ class WebglController { this.gl.uniform1i(programInfo.uniformLocations.uSampler, 0); + const textureSizeLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'u_textureSize' + ); + + const canvas = this.gl.canvas as HTMLCanvasElement; + + this.gl.uniform2f( + textureSizeLocation, + canvas.clientWidth, + canvas.clientHeight + ); + + const chromaRedLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'chroma[0]' + ); + const chromaBlueLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'chroma[1]' + ); + + const chromaGreenLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'chroma[2]' + ); + + this.gl.uniform1f(chromaRedLocation, this.chroma[0]); + + this.gl.uniform1f(chromaBlueLocation, this.chroma[1]); + + this.gl.uniform1f(chromaGreenLocation, this.chroma[2]); + + const edgeDetectKernel = [ + this.blurRatio, + this.blurRatio, + this.blurRatio, + this.blurRatio, + 1, + this.blurRatio, + this.blurRatio, + this.blurRatio, + this.blurRatio, + ]; + + const kernelLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'u_kernel[0]' + ); + + this.gl.uniform1fv(kernelLocation, edgeDetectKernel); + + const kernelWeightLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'u_kernelWeight' + ); + + const tempWeight = edgeDetectKernel.reduce((prev, curr) => { + return prev + curr; + }); + + const weight = tempWeight <= 0 ? 1 : tempWeight; + + this.gl.uniform1f(kernelWeightLocation, weight); + this.gl.drawElements( this.gl.TRIANGLES, vertexCount, From 43d9fb727b9f6de30b520069b9835ad731b1361e Mon Sep 17 00:00:00 2001 From: SSH1997 Date: Mon, 14 Dec 2020 19:02:51 +0900 Subject: [PATCH 11/40] =?UTF-8?q?[FE]=20:=20[FEAT]=20Effect=EB=A5=BC=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=88=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?slider=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../molecules/EffectSliders/EffectSliders.tsx | 49 +++++++++++++++++ .../molecules/EffectSliders/Range.tsx | 54 +++++++++++++++++++ .../molecules/EffectSliders/index.ts | 1 + 3 files changed, 104 insertions(+) create mode 100644 client/src/components/molecules/EffectSliders/EffectSliders.tsx create mode 100644 client/src/components/molecules/EffectSliders/Range.tsx create mode 100644 client/src/components/molecules/EffectSliders/index.ts diff --git a/client/src/components/molecules/EffectSliders/EffectSliders.tsx b/client/src/components/molecules/EffectSliders/EffectSliders.tsx new file mode 100644 index 0000000..82c11c7 --- /dev/null +++ b/client/src/components/molecules/EffectSliders/EffectSliders.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import styled from 'styled-components'; + +import Range from './Range'; + +const StyledColorOuterDiv = styled.div` + display: flex; + justify-content: center; + width: 100%; +`; + +const StyledColorNestedDiv = styled.div` + display: flex; + flex-direction: column; + margin-right: 5%; +`; + +const StyledP = styled.p` + margin: 0; +`; + +const EffectSlider: React.FC = () => { + return ( + + + R + G + B + + + + + + + + Blur + Effect2 + Effect3 + + + + + + + + ); +}; + +export default EffectSlider; diff --git a/client/src/components/molecules/EffectSliders/Range.tsx b/client/src/components/molecules/EffectSliders/Range.tsx new file mode 100644 index 0000000..a58b163 --- /dev/null +++ b/client/src/components/molecules/EffectSliders/Range.tsx @@ -0,0 +1,54 @@ +import React, { useCallback, useState } from 'react'; +import styled from 'styled-components'; + +import webglController from '@/webgl/webglController'; + +interface Props { + id: string; +} + +const StyledInput = styled.input` + width: 4rem; + background-color: ${({ color }) => color} !important; + box-shadow: 0 0 10px 2px rgba(255, 255, 255, 0.3); + margin: 10% 0%; +`; + +const Range: React.FC = ({ id }) => { + const [value, setValue] = useState(100); + + const handleChange = useCallback(e => { + const currentValue = e.target.value; + switch (e.target.id) { + case 'red': + webglController.setChromaRed(currentValue / 100); + break; + case 'green': + webglController.setChromaGreen(currentValue / 100); + break; + case 'blue': + webglController.setChromaBlue(currentValue / 100); + break; + case 'blur': + webglController.setBlur(currentValue / 100); + break; + default: + break; + } + setValue(currentValue); + }, []); + + return ( + + ); +}; + +export default Range; diff --git a/client/src/components/molecules/EffectSliders/index.ts b/client/src/components/molecules/EffectSliders/index.ts new file mode 100644 index 0000000..ed0f465 --- /dev/null +++ b/client/src/components/molecules/EffectSliders/index.ts @@ -0,0 +1 @@ +export { default } from './EffectSliders'; From b204660e5160c678ca6c2c7b4d153b7c09634bee Mon Sep 17 00:00:00 2001 From: SSH1997 Date: Mon, 14 Dec 2020 19:03:43 +0900 Subject: [PATCH 12/40] =?UTF-8?q?[FE]=20:=20[FEAT]=20Filter=20handle=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=B0=8F=20=EA=B7=B8=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20subtool=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/organisms/Tools/Tools.tsx | 25 +++++++++++++++++++ .../components/organisms/Tools/buttonData.tsx | 14 +++++++++++ .../components/organisms/Tools/reducer.tsx | 3 +++ 3 files changed, 42 insertions(+) diff --git a/client/src/components/organisms/Tools/Tools.tsx b/client/src/components/organisms/Tools/Tools.tsx index 78c5580..f327dec 100644 --- a/client/src/components/organisms/Tools/Tools.tsx +++ b/client/src/components/organisms/Tools/Tools.tsx @@ -12,6 +12,7 @@ import { Effect, applyEffect } from '@/store/history/actions'; import Range from '@/components/atoms/Range'; import ButtonGroup from '@/components/molecules/ButtonGroup'; import UploadArea from '@/components/molecules/UploadArea'; +import EffectSlider from '@/components/molecules/EffectSliders'; import video from '@/video'; import { play, pause, moveTo } from '@/store/currentVideo/actions'; import { @@ -23,6 +24,7 @@ import { } from '@/store/selectors'; import { cropStart, cropCancel, cropConfirm } from '@/store/crop/actions'; import webglController from '@/webgl/webglController'; + import reducer, { initialData, ButtonTypes } from './reducer'; import { getEditToolData, @@ -85,6 +87,17 @@ const SubEditTool = styled(ButtonGroup)` const VideoTool = styled(ButtonGroup)` display: flex; `; +const modalLayout = ` + top: 0vh; + left: 0vw; + width: 30vw; + height: 20vh; +`; +const layoutStyle = ` + width: 30vw; + height: 20vh; + backgound-color: red; +`; interface props { setEdit: Function; @@ -103,6 +116,7 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { const isCancel = useSelector(getIsCancel); const glCanvas = document.getElementById('glcanvas'); const input = document.createElement('input'); + const [modalVisible, setModalVisible] = useState(false); const backwardVideo = () => { let dstTime = video.get('currentTime') - 10; @@ -260,6 +274,7 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { ], crop: [/* handleCropManually , */ handleCropConfirm, handleCropCancel], sign: [handleSignUpload, handleSignConfirm, handleSignCancel], + filter: [], }), [] ); @@ -294,11 +309,19 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { if (webglController.sign) webglController.setSignEdit(true); } else closeSubtool(); }; + const handleFilter = () => { + if (toolType !== ButtonTypes.filter) { + if (isEdit === UP) setEdit(DOWN); + openSubtool(ButtonTypes.filter, methods.filter); + } else closeSubtool(); + }; + useEffect(() => { if (toolType !== null) { closeSubtool(); } }, [isCancel]); + const handleModalCancel = () => setModalVisible(false); return ( @@ -316,6 +339,7 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { {toolType === ButtonTypes.sign && isSign && } + {toolType === ButtonTypes.filter && } )} = ({ setEdit, isEdit }) => { handleRatio, handleCrop, handleSign, + handleFilter, hasEmptyVideo, toolType )} diff --git a/client/src/components/organisms/Tools/buttonData.tsx b/client/src/components/organisms/Tools/buttonData.tsx index b459c13..1d12968 100644 --- a/client/src/components/organisms/Tools/buttonData.tsx +++ b/client/src/components/organisms/Tools/buttonData.tsx @@ -9,6 +9,7 @@ import { } from 'react-icons/bs'; import { RiScissorsLine, RiCopyrightLine } from 'react-icons/ri'; import { MdScreenRotation } from 'react-icons/md'; +import { VscSymbolColor } from 'react-icons/vsc'; import size from '@/theme/sizes'; @@ -62,6 +63,7 @@ export const getEditToolData = ( ratio: () => void, crop: () => void, sign: () => void, + filter: () => void, hasEmptyVideo: boolean, toolType: ButtonTypes ): button[] => [ @@ -115,6 +117,18 @@ export const getEditToolData = ( ), disabled: hasEmptyVideo, }, + { + onClick: filter, + message: '필터', + type: toolType === ButtonTypes.sign ? 'selected' : 'transparent', + children: ( + + ), + disabled: hasEmptyVideo, + }, ]; export const getSubEditToolsData = (buttonData: ButtonData): button[] => diff --git a/client/src/components/organisms/Tools/reducer.tsx b/client/src/components/organisms/Tools/reducer.tsx index 6e5ac9d..a614388 100644 --- a/client/src/components/organisms/Tools/reducer.tsx +++ b/client/src/components/organisms/Tools/reducer.tsx @@ -6,6 +6,8 @@ import { MdRotateRight, MdZoomIn, MdZoomOut, + MdBlurOn, + MdInvertColors, } from 'react-icons/md'; import { CgMergeHorizontal, CgMergeVertical } from 'react-icons/cg'; @@ -16,6 +18,7 @@ export enum ButtonTypes { videoEffect = 'videoEffect', ratio = 'ratio', sign = 'sign', + filter = 'filter', } export interface ButtonData { From 34c5984fef4cfcd9be4ce0a59c2b9010bdb879ce Mon Sep 17 00:00:00 2001 From: SSH1997 Date: Tue, 15 Dec 2020 13:46:34 +0900 Subject: [PATCH 13/40] =?UTF-8?q?[FE]=20:=20[FEAT]=20Webgl=20blur=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B0=9D=EA=B8=B0=EC=A1=B0=EC=A0=88=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/webgl/fragmentShaderSource.ts | 19 +++++--- client/src/webgl/webglController.ts | 56 +++++++++++++++++++----- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/client/src/webgl/fragmentShaderSource.ts b/client/src/webgl/fragmentShaderSource.ts index d36b310..da6e883 100644 --- a/client/src/webgl/fragmentShaderSource.ts +++ b/client/src/webgl/fragmentShaderSource.ts @@ -8,9 +8,11 @@ const fragmentShaderSource = ` uniform vec2 u_textureSize; uniform float u_kernel[9]; uniform float u_kernelWeight; + uniform bool grayScaleFlag; + uniform float luminance; void main(void) { - vec2 onePixel = vec2(1.0, 1.0) / u_textureSize; + vec2 onePixel = vec2(7.0, 7.0) / u_textureSize; vec4 colorSum = texture2D(uSampler, vTextureCoord + onePixel * vec2(-1, -1)) * u_kernel[0] + @@ -22,12 +24,19 @@ const fragmentShaderSource = ` texture2D(uSampler, vTextureCoord + onePixel * vec2(-1, 1)) * u_kernel[6] + texture2D(uSampler, vTextureCoord + onePixel * vec2( 0, 1)) * u_kernel[7] + texture2D(uSampler, vTextureCoord + onePixel * vec2( 1, 1)) * u_kernel[8] ; - + vec4 weightColor = colorSum / u_kernelWeight; - gl_FragColor.r = weightColor.r * chroma[0]; - gl_FragColor.g = weightColor.g * chroma[1]; - gl_FragColor.b = weightColor.b * chroma[2]; + if (grayScaleFlag) { + gl_FragColor.r = (weightColor.r + weightColor.g + weightColor.b) / 3.0 + luminance; + gl_FragColor.g = (weightColor.r + weightColor.g + weightColor.b) / 3.0 + luminance; + gl_FragColor.b = (weightColor.r + weightColor.g + weightColor.b) / 3.0 + luminance; + } else { + gl_FragColor.r = weightColor.r * chroma[0] + luminance; + gl_FragColor.g = weightColor.g * chroma[1] + luminance; + gl_FragColor.b = weightColor.b * chroma[2] + luminance; + } + gl_FragColor.a = weightColor.a; } `; diff --git a/client/src/webgl/webglController.ts b/client/src/webgl/webglController.ts index 16e38e9..f0f256d 100644 --- a/client/src/webgl/webglController.ts +++ b/client/src/webgl/webglController.ts @@ -31,6 +31,8 @@ const offset = 0; const numComponents = 2; const vertexCount = 6; const normalize = false; +const TURE = 1; +const FALSE = 0; class WebglController { // gl config params @@ -97,6 +99,10 @@ class WebglController { blurRatio: number = 0; + graySacle: number = FALSE; + + luminance: number = 0.0; + constructor() { this.positions = this.init.positions.map(pair => [...pair]); } @@ -209,6 +215,14 @@ class WebglController { this.sign = sign; }; + setGrayScale = grayScale => { + this.graySacle = grayScale; + }; + + setLuminance = luminance => { + this.luminance = luminance; + }; + updateTexture = (texture: WebGLTexture) => { this.gl.bindTexture(this.gl.TEXTURE_2D, texture); this.gl.texImage2D( @@ -358,6 +372,13 @@ class WebglController { this.gl.LINEAR ); + const grayScaleFlag = this.gl.getUniformLocation( + this.programInfo.program, + 'grayScaleFlag' + ); + + this.gl.uniform1i(grayScaleFlag, FALSE); + const chromaRedLocation = this.gl.getUniformLocation( this.programInfo.program, 'chroma[0]' @@ -378,6 +399,13 @@ class WebglController { this.gl.uniform1f(chromaGreenLocation, 1.0); + const luminanceLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'luminance' + ); + + this.gl.uniform1f(luminanceLocation, 0.0); + const edgeDetectKernel = [0, 0, 0, 0, 1, 0, 0, 0, 0]; const kernelLocation = this.gl.getUniformLocation( @@ -493,6 +521,13 @@ class WebglController { canvas.clientHeight ); + const grayScaleFlag = this.gl.getUniformLocation( + this.programInfo.program, + 'grayScaleFlag' + ); + + this.gl.uniform1i(grayScaleFlag, this.graySacle); + const chromaRedLocation = this.gl.getUniformLocation( this.programInfo.program, 'chroma[0]' @@ -513,17 +548,16 @@ class WebglController { this.gl.uniform1f(chromaGreenLocation, this.chroma[2]); - const edgeDetectKernel = [ - this.blurRatio, - this.blurRatio, - this.blurRatio, - this.blurRatio, - 1, - this.blurRatio, - this.blurRatio, - this.blurRatio, - this.blurRatio, - ]; + const luminanceLocation = this.gl.getUniformLocation( + this.programInfo.program, + 'luminance' + ); + + this.gl.uniform1f(luminanceLocation, this.luminance); + + const edgeDetectKernel = Array.from({ length: 8 }, () => this.blurRatio); + + edgeDetectKernel.splice(4, 0, 1); const kernelLocation = this.gl.getUniformLocation( this.programInfo.program, From b54d17a5a272783df12a7ceeee46c6cb29c79bfb Mon Sep 17 00:00:00 2001 From: SSH1997 Date: Tue, 15 Dec 2020 13:47:25 +0900 Subject: [PATCH 14/40] =?UTF-8?q?[FE]=20:=20[FEAT]=20#79=20EffectSlider=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RGB / Blur / Luminance 조절 가능 --- .../molecules/EffectSliders/EffectSliders.tsx | 8 ++-- .../molecules/EffectSliders/Range.tsx | 44 +++++++++++++++++-- client/src/theme/colors.tsx | 9 ++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/client/src/components/molecules/EffectSliders/EffectSliders.tsx b/client/src/components/molecules/EffectSliders/EffectSliders.tsx index 82c11c7..88309f4 100644 --- a/client/src/components/molecules/EffectSliders/EffectSliders.tsx +++ b/client/src/components/molecules/EffectSliders/EffectSliders.tsx @@ -34,13 +34,13 @@ const EffectSlider: React.FC = () => { Blur - Effect2 - Effect3 + Luminance + GrayScale - - + + ); diff --git a/client/src/components/molecules/EffectSliders/Range.tsx b/client/src/components/molecules/EffectSliders/Range.tsx index a58b163..93cc391 100644 --- a/client/src/components/molecules/EffectSliders/Range.tsx +++ b/client/src/components/molecules/EffectSliders/Range.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; - +import Color from '@/theme/colors'; import webglController from '@/webgl/webglController'; interface Props { @@ -9,13 +9,43 @@ interface Props { const StyledInput = styled.input` width: 4rem; - background-color: ${({ color }) => color} !important; + background: ${({ color, value }) => { + switch (color) { + case 'red': + case 'green': + case 'blue': + return `linear-gradient(0.25turn, ${Color.BLACK}, ${color})`; + case 'luminance': + return `linear-gradient(0.25turn, ${Color.BLACK}, ${Color.WHITE})`; + case 'blur': + return 'transparent'; + case 'grayScale': + return value === '1' + ? `linear-gradient(0.25turn, ${Color.ORIGINAL_RED}, ${Color.ORIGINAL_ORANGE}, ${Color.ORIGINAL_YELLOW}, + ${Color.ORIGINAL_GREEN}, ${Color.ORIGINAL_BLUE}, ${Color.ORIGINAL_DARK_BLUE},${Color.ORIGINAL_VIOLET})` + : Color.GRAY; + default: + return null; + } + }} !important; box-shadow: 0 0 10px 2px rgba(255, 255, 255, 0.3); margin: 10% 0%; `; const Range: React.FC = ({ id }) => { - const [value, setValue] = useState(100); + let res = 100; + let max = 100; + + if (id === 'blur') { + res = 0; + } else if (id === 'luminance') { + res = 50; + } else if (id === 'grayScale') { + res = 0; + max = 1; + } + + const [value, setValue] = useState(res); const handleChange = useCallback(e => { const currentValue = e.target.value; @@ -32,6 +62,12 @@ const Range: React.FC = ({ id }) => { case 'blur': webglController.setBlur(currentValue / 100); break; + case 'luminance': + webglController.setLuminance((currentValue - 50) / 100); + break; + case 'grayScale': + webglController.setGrayScale(currentValue); + break; default: break; } @@ -43,7 +79,7 @@ const Range: React.FC = ({ id }) => { id={id} type="range" min="0" - max="100" + max={max} value={value} onChange={handleChange} color={id} diff --git a/client/src/theme/colors.tsx b/client/src/theme/colors.tsx index 53272d3..9efa019 100644 --- a/client/src/theme/colors.tsx +++ b/client/src/theme/colors.tsx @@ -16,6 +16,15 @@ const colors = { DARK_GREEN: '#2CA25F', MODAL: '#0A0A0A', VIDEO: '#181818', + + // Rainbow + ORIGINAL_RED: '#FF0000', + ORIGINAL_ORANGE: '#FFA500', + ORIGINAL_YELLOW: '#FFFF00', + ORIGINAL_GREEN: '#00FF00', + ORIGINAL_BLUE: '#0000FF', + ORIGINAL_DARK_BLUE: '#00008B', + ORIGINAL_VIOLET: '#EE82EE', }; export default colors; From 41d71282a031e41489d93a23c54be20e9ed2ab51 Mon Sep 17 00:00:00 2001 From: SSH1997 Date: Tue, 15 Dec 2020 22:01:38 +0900 Subject: [PATCH 15/40] =?UTF-8?q?[FE]=20:=20[FEAT]=20#81=20VolumeRange=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../atoms/VolumeRange/VolumeRange.tsx | 89 +++++++++++++++++++ .../src/components/atoms/VolumeRange/index.ts | 1 + 2 files changed, 90 insertions(+) create mode 100644 client/src/components/atoms/VolumeRange/VolumeRange.tsx create mode 100644 client/src/components/atoms/VolumeRange/index.ts diff --git a/client/src/components/atoms/VolumeRange/VolumeRange.tsx b/client/src/components/atoms/VolumeRange/VolumeRange.tsx new file mode 100644 index 0000000..69ec38b --- /dev/null +++ b/client/src/components/atoms/VolumeRange/VolumeRange.tsx @@ -0,0 +1,89 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; + +import color from '@/theme/colors'; +import { setAudio } from '@/store/currentVideo/actions'; +import video from '@/video'; + +interface Props { + volume: number; + setVolumeVisible; +} + +const StyledOuterDiv = styled.div` + position: absolute; + left: 250px; + top: -120%; + width: 40px; + height: 130px; + background-color: ${color.DARK_GRAY}; // darkgray + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 10px 2px rgba(255, 255, 255, 0.3); +`; + +const StyledInnerDiv = styled.div` + width: 25px; + height: 110px; + background-color: ${color.BLACK}; + display: flex; + align-items: center; + justify-content: center; +`; + +const StyledInput = styled.input` + width: 112px !important; + height: 26px !important; + border-radius: 5px !important; + transform: rotate(-90deg) !important; + background: linear-gradient( + -0.25turn, + ${color.BLACK} 0%, + ${color.BLACK} ${({ value }) => 100 - value}%, + ${color.PURPLE} ${({ value }) => 100 - value + 1}%, + ${color.PALE_PURPLE} + ) !important; + + ::-webkit-slider-thumb { + border-radius: 5px !important; + height: 31px !important; + } +`; + +const VolumeRange: React.FC = ({ volume, setVolumeVisible }) => { + const dispatch = useDispatch(); + const [value, setValue] = useState(volume * 100); + + const handleChange = useCallback(e => { + const currentValue = e.target.value; // todo + setValue(currentValue); + dispatch(setAudio(Number(currentValue / 100))); + }, []); + + useEffect(() => { + setValue(volume * 100); + video.setVolume(volume); + }, [volume]); + + const handleMouseLeave = () => { + setVolumeVisible(false); + }; + + return ( + + + + + + ); +}; + +export default VolumeRange; diff --git a/client/src/components/atoms/VolumeRange/index.ts b/client/src/components/atoms/VolumeRange/index.ts new file mode 100644 index 0000000..ebbb836 --- /dev/null +++ b/client/src/components/atoms/VolumeRange/index.ts @@ -0,0 +1 @@ +export { default } from './VolumeRange'; From 75dbc42be1669d0dc505acf5ed720397bd9129df Mon Sep 17 00:00:00 2001 From: SSH1997 Date: Tue, 15 Dec 2020 22:06:53 +0900 Subject: [PATCH 16/40] =?UTF-8?q?[FE]=20:=20[FEAT]=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EC=8D=B8=EB=84=A4=EC=9D=BC?= =?UTF-8?q?=EC=97=90=EB=8F=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../molecules/EffectSliders/Range.tsx | 19 +++++++-- .../molecules/Thumbnail/Thumbnail.tsx | 39 ++++++++++++------- .../components/organisms/Tools/buttonData.tsx | 2 +- client/src/store/actionTypes.ts | 1 + client/src/store/history/actions.ts | 22 +++++++++++ client/src/store/history/reducer.ts | 25 +++++++++++- client/src/store/selectors.ts | 2 + 7 files changed, 90 insertions(+), 20 deletions(-) diff --git a/client/src/components/molecules/EffectSliders/Range.tsx b/client/src/components/molecules/EffectSliders/Range.tsx index 93cc391..6148c16 100644 --- a/client/src/components/molecules/EffectSliders/Range.tsx +++ b/client/src/components/molecules/EffectSliders/Range.tsx @@ -1,7 +1,10 @@ import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; + import Color from '@/theme/colors'; import webglController from '@/webgl/webglController'; +import { applyFilter } from '@/store/history/actions'; interface Props { id: string; @@ -21,9 +24,10 @@ const StyledInput = styled.input` return 'transparent'; case 'grayScale': return value === '1' - ? `linear-gradient(0.25turn, ${Color.ORIGINAL_RED}, ${Color.ORIGINAL_ORANGE}, ${Color.ORIGINAL_YELLOW}, - ${Color.ORIGINAL_GREEN}, ${Color.ORIGINAL_BLUE}, ${Color.ORIGINAL_DARK_BLUE},${Color.ORIGINAL_VIOLET})` - : Color.GRAY; + ? Color.GRAY + : `linear-gradient(0.25turn, ${Color.ORIGINAL_RED}, ${Color.ORIGINAL_RED}, ${Color.ORIGINAL_ORANGE}, + ${Color.ORIGINAL_YELLOW}, ${Color.ORIGINAL_GREEN}, ${Color.ORIGINAL_BLUE}, + ${Color.ORIGINAL_DARK_BLUE}, ${Color.ORIGINAL_VIOLET})`; default: return null; } @@ -45,6 +49,8 @@ const Range: React.FC = ({ id }) => { max = 1; } + const dispatch = useDispatch(); + const [value, setValue] = useState(res); const handleChange = useCallback(e => { @@ -60,12 +66,19 @@ const Range: React.FC = ({ id }) => { webglController.setChromaBlue(currentValue / 100); break; case 'blur': + dispatch(applyFilter({ blur: currentValue / 50 })); webglController.setBlur(currentValue / 100); break; case 'luminance': + dispatch(applyFilter({ brightness: currentValue / 50 })); webglController.setLuminance((currentValue - 50) / 100); break; case 'grayScale': + if (currentValue === '1') { + dispatch(applyFilter({ grayScale: 100 })); + } else { + dispatch(applyFilter({ grayScale: 0 })); + } webglController.setGrayScale(currentValue); break; default: diff --git a/client/src/components/molecules/Thumbnail/Thumbnail.tsx b/client/src/components/molecules/Thumbnail/Thumbnail.tsx index e7e232c..8a32e36 100644 --- a/client/src/components/molecules/Thumbnail/Thumbnail.tsx +++ b/client/src/components/molecules/Thumbnail/Thumbnail.tsx @@ -13,16 +13,12 @@ import { getIsCropAndDuration, getStartEnd, getStatus, + getFilterStatus, } from '@/store/selectors'; +import { Status, FilterStatus } from '@/store/history/actions'; import CropLayer from '@/components/molecules/CropLayer'; import color from '@/theme/colors'; -interface Status { - scale: number; - rotation: number; - flipped: boolean; -} - const StyledDiv = styled.div` position: relative; display: flex; @@ -37,6 +33,9 @@ const StyledImg = styled.img` transform: scale(${props => props.status.scale}) scaleY(${props => (props.status.flipped ? -1 : 1)}) rotate(${props => props.status.rotation}deg); + filter: grayScale(${props => props.filterStatus.grayScale}%) + brightness(${props => props.filterStatus.brightness}) + blur(${props => props.filterStatus.blur}px); `; const ImageDiv = styled.div` overflow: hidden; @@ -45,15 +44,25 @@ const ImageDiv = styled.div` justify-content: center; width: 3.3333%; height: 50px; - background-color: transparent; background-color: ${color.BLACK}; min-width: 3.3333%; min-height: 50px; `; -const renderThumbnails = (thumbnails: string[], status: Status) => + +const renderThumbnails = ( + thumbnails: string[], + status: Status, + filterStatus: FilterStatus +) => thumbnails.map(image => ( - + )); @@ -63,6 +72,7 @@ const Thumbnail: React.FC = () => { const { isCrop, duration } = useSelector(getIsCropAndDuration, shallowEqual); const { start, end } = useSelector(getStartEnd, shallowEqual); const status = useSelector(getStatus); + const filterStatus = useSelector(getFilterStatus); const [time, setTime] = useState(0); const dispatch = useDispatch(); @@ -74,7 +84,6 @@ const Thumbnail: React.FC = () => { video.setCurrentTime(start + time); dispatch(moveTo(start + time)); }; - const handleMouseMove = (event: MouseEvent) => { const slider = hoverSliderRef.current; @@ -101,13 +110,13 @@ const Thumbnail: React.FC = () => { }; const OriginalThumbnails = useMemo( - () => renderThumbnails(video.getThumbnails(), status), + () => renderThumbnails(video.getThumbnails(), status, filterStatus), [message] // URL is not enough to check whether thumbnail is ready ); - const Thumbnails = useMemo(() => renderThumbnails(thumbnails, status), [ - thumbnails, - status, - ]); + const Thumbnails = useMemo( + () => renderThumbnails(thumbnails, status, filterStatus), + [thumbnails, status, filterStatus] + ); return ( ({ }, }); +export const applyFilter = (filterStatus: FilterStatus) => ({ + type: APPLY_FILTER, + payload: { + filterStatus, + }, +}); + export type HistoryUndoSuccessAction = { type: typeof UNDO_SUCCESS; payload: { @@ -122,11 +136,19 @@ export type HistoryApplyCropAction = { }; }; +export type HistoryApplyFilterAction = { + type: typeof APPLY_FILTER; + payload: { + filterStatus: FilterStatus; + }; +}; + export type HistoryAction = | HistoryUndoSuccessAction | HistoryRedoSuccessAction | HistoryClearAction | HistoryApplyEffectAction | HistoryApplyCropAction + | HistoryApplyFilterAction | CropAction | ResetAction; diff --git a/client/src/store/history/reducer.ts b/client/src/store/history/reducer.ts index 1412613..cec9696 100644 --- a/client/src/store/history/reducer.ts +++ b/client/src/store/history/reducer.ts @@ -6,8 +6,9 @@ import { RESET, CLEAR, APPLY_CROP, + APPLY_FILTER, } from '../actionTypes'; -import { HistoryAction, Log, Effect, Status } from './actions'; +import { HistoryAction, Log, Effect, Status, FilterStatus } from './actions'; export const MAX_HISTORY = 20; @@ -15,6 +16,7 @@ export interface HistoryState { logs: Log[]; index: number; status: Status; + filterStatus: FilterStatus; } const initialState: HistoryState = { @@ -25,6 +27,11 @@ const initialState: HistoryState = { rotation: 0, flipped: false, }, + filterStatus: { + blur: 0, + grayScale: 0, + brightness: 1, + }, }; const getStatusFromEffect = (status: Status, effect: Effect) => { @@ -106,6 +113,22 @@ export default ( ], index: state.index === MAX_HISTORY ? state.index : state.index + 1, }; + case APPLY_FILTER: + return { + ...state, + filterStatus: { + blur: action.payload.filterStatus.blur + ? action.payload.filterStatus.blur + : state.filterStatus.blur, + grayScale: + action.payload.filterStatus.grayScale !== undefined + ? action.payload.filterStatus.grayScale + : state.filterStatus.grayScale, + brightness: action.payload.filterStatus.brightness + ? action.payload.filterStatus.brightness + : state.filterStatus.brightness, + }, + }; case CLEAR: case RESET: return initialState; diff --git a/client/src/store/selectors.ts b/client/src/store/selectors.ts index 43ac1ee..5fc3fce 100644 --- a/client/src/store/selectors.ts +++ b/client/src/store/selectors.ts @@ -76,3 +76,5 @@ export const getIsNextDisabled = (state: RootState) => { }; export const getStatus = (state: RootState) => state.history.status; + +export const getFilterStatus = (state: RootState) => state.history.filterStatus; From 6d7f3dc278f02e2e8b176e0254c19c111e0dd919 Mon Sep 17 00:00:00 2001 From: SSH1997 Date: Tue, 15 Dec 2020 22:07:27 +0900 Subject: [PATCH 17/40] =?UTF-8?q?[FE]=20:=20[FEAT]=20Video=20Volume=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=88=20=EB=A7=88=EC=9A=B0=EC=8A=A4=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=B0=8F=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/atoms/Button/Button.tsx | 20 +++++++++-- .../molecules/ButtonGroup/ButtonGroup.tsx | 4 +++ .../src/components/organisms/Tools/Tools.tsx | 34 ++++++++++++++++++- .../components/organisms/Tools/buttonData.tsx | 28 +++++++++++++++ client/src/video/video.tsx | 9 +++++ 5 files changed, 92 insertions(+), 3 deletions(-) diff --git a/client/src/components/atoms/Button/Button.tsx b/client/src/components/atoms/Button/Button.tsx index 9158c03..346b47b 100644 --- a/client/src/components/atoms/Button/Button.tsx +++ b/client/src/components/atoms/Button/Button.tsx @@ -34,13 +34,29 @@ interface Props { children?: React.ReactChild; message: string; onClick?: () => void; + onMouseEnter?: () => void; + onMouseLeave?: () => void; type: ButtonType; disabled: boolean; } const Button: React.FC = React.memo( - ({ children, message, onClick, type, disabled }) => ( - + ({ + children, + message, + onClick, + onMouseEnter, + onMouseLeave, + type, + disabled, + }) => ( + {children} {children &&
} {message} diff --git a/client/src/components/molecules/ButtonGroup/ButtonGroup.tsx b/client/src/components/molecules/ButtonGroup/ButtonGroup.tsx index e03cd6f..c806380 100644 --- a/client/src/components/molecules/ButtonGroup/ButtonGroup.tsx +++ b/client/src/components/molecules/ButtonGroup/ButtonGroup.tsx @@ -13,6 +13,8 @@ const StyledDiv = styled.div` interface button { onClick: () => void; + onMouseEnter?: () => void; + onMouseLeave?: () => void; message: string; type: 'default' | 'transparent' | 'selected'; children: React.ReactChild; @@ -30,6 +32,8 @@ const ButtonGroup: React.FC = ({ buttonData, StyledProps }) => ( ); }; diff --git a/client/src/components/molecules/EffectSliders/Range.tsx b/client/src/components/molecules/EffectSliders/Range.tsx index 9477576..26bdc16 100644 --- a/client/src/components/molecules/EffectSliders/Range.tsx +++ b/client/src/components/molecules/EffectSliders/Range.tsx @@ -1,101 +1,108 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; -import Color from '@/theme/colors'; +import color from '@/theme/colors'; import webglController from '@/webgl/webglController'; -import { applyFilter } from '@/store/history/actions'; +import { applyFilter, Filter } from '@/store/history/actions'; +import { + getRed, + getGreen, + getBlue, + getBrightness, + getBlur, + getGrayscale, +} from '@/store/selectors'; interface Props { - id: string; + filter: Filter; } +const filterMapper = { + [Filter.RED]: { + selector: getRed, + max: 100, + action: value => applyFilter({ [Filter.RED]: value }), + sideEffect: value => webglController.setChromaRed(value / 100), // 0 ~ 1 + background: `linear-gradient(0.25turn, ${color.BLACK}, ${color.ORIGINAL_RED})`, + }, + [Filter.GREEN]: { + selector: getGreen, + max: 100, + action: value => applyFilter({ [Filter.GREEN]: value }), + sideEffect: value => webglController.setChromaGreen(value / 100), // 0 ~ 1 + background: `linear-gradient(0.25turn, ${color.BLACK}, ${color.ORIGINAL_GREEN})`, + }, + [Filter.BLUE]: { + selector: getBlue, + max: 100, + action: value => applyFilter({ [Filter.BLUE]: value }), + sideEffect: value => webglController.setChromaBlue(value / 100), // 0 ~ 1 + background: `linear-gradient(0.25turn, ${color.BLACK}, ${color.ORIGINAL_BLUE})`, + }, + [Filter.LUMINANCE]: { + selector: getBrightness, + max: 100, + action: value => applyFilter({ [Filter.LUMINANCE]: value }), + sideEffect: value => webglController.setLuminance((value - 50) / 100), // -0.5 ~ 0.5 + background: `linear-gradient(0.25turn, ${color.BLACK}, ${color.WHITE})`, + }, + [Filter.BLUR]: { + selector: getBlur, + max: 100, + action: value => applyFilter({ [Filter.BLUR]: value }), + sideEffect: value => webglController.setBlur(value / 100), // 0 ~ 1 + background: `transparent`, + }, + [Filter.GRAYSCALE]: { + selector: getGrayscale, + max: 1, + action: value => applyFilter({ [Filter.GRAYSCALE]: value }), + sideEffect: value => webglController.setGrayScale(value), + background: value => + value + ? `${color.GRAY}` + : `linear-gradient(0.25turn, ${color.ORIGINAL_RED}, ${color.ORIGINAL_RED}, ${color.ORIGINAL_ORANGE}, + ${color.ORIGINAL_YELLOW}, ${color.ORIGINAL_GREEN}, ${color.ORIGINAL_BLUE}, + ${color.ORIGINAL_DARK_BLUE}, ${color.ORIGINAL_VIOLET})`, + }, +}; + const StyledInput = styled.input` width: 4rem; - background: ${({ color, value }) => { - switch (color) { - case 'red': - case 'green': - case 'blue': - return `linear-gradient(0.25turn, ${Color.BLACK}, ${color})`; - case 'luminance': - return `linear-gradient(0.25turn, ${Color.BLACK}, ${Color.WHITE})`; - case 'blur': - return 'transparent'; - case 'grayScale': - return value === '1' - ? Color.GRAY - : `linear-gradient(0.25turn, ${Color.ORIGINAL_RED}, ${Color.ORIGINAL_RED}, ${Color.ORIGINAL_ORANGE}, - ${Color.ORIGINAL_YELLOW}, ${Color.ORIGINAL_GREEN}, ${Color.ORIGINAL_BLUE}, - ${Color.ORIGINAL_DARK_BLUE}, ${Color.ORIGINAL_VIOLET})`; - default: - return null; - } - }} !important; + background: ${({ id, background, value }) => + id === Filter.GRAYSCALE ? background(value) : background} !important; + box-shadow: 0 0 10px 2px rgba(255, 255, 255, 0.3); margin: 10% 0%; `; -const Range: React.FC = ({ id }) => { - let initialValue = 100; - let max = 100; - - if (id === 'blur') { - initialValue = 0; - } else if (id === 'luminance') { - initialValue = 50; - } else if (id === 'grayScale') { - initialValue = 0; - max = 1; - } +const Range: React.FC = ({ filter }) => { + const { selector, max, action, sideEffect, background } = filterMapper[ + filter + ]; const dispatch = useDispatch(); + const value = useSelector(selector); - const [value, setValue] = useState(initialValue); - - const handleChange = useCallback(e => { - const currentValue = e.target.value; - switch (e.target.id) { - case 'red': - webglController.setChromaRed(currentValue / 100); - break; - case 'green': - webglController.setChromaGreen(currentValue / 100); - break; - case 'blue': - webglController.setChromaBlue(currentValue / 100); - break; - case 'blur': - dispatch(applyFilter({ blur: currentValue / 50 })); - webglController.setBlur(currentValue / 100); - break; - case 'luminance': - dispatch(applyFilter({ brightness: currentValue / 50 })); - webglController.setLuminance((currentValue - 50) / 100); - break; - case 'grayScale': - if (currentValue === '1') { - dispatch(applyFilter({ grayScale: 100 })); - } else { - dispatch(applyFilter({ grayScale: 0 })); - } - webglController.setGrayScale(currentValue); - break; - default: - break; - } - setValue(currentValue); - }, []); + const handleChange = useCallback( + ({ target }) => { + const currentValue = Number(target.value); + dispatch(action(currentValue)); + sideEffect(currentValue); + }, + [filter] + ); return ( ); }; diff --git a/client/src/store/actionTypes.ts b/client/src/store/actionTypes.ts index 5091771..af2e02e 100644 --- a/client/src/store/actionTypes.ts +++ b/client/src/store/actionTypes.ts @@ -39,7 +39,7 @@ export const CLEAR = 'history/CLEAR'; export const APPLY_EFFECT = 'history/APPLY_EFFECT'; export const APPLY_CROP = 'history/APPLY_CROP'; export const APPLY_FILTER = 'history/APPLY_FILTER'; - +export const RESET_FILTER = 'history/RESET_FILTER'; // global export const RESET = 'RESET'; export const reset = () => ({ type: RESET }); diff --git a/client/src/store/history/actions.ts b/client/src/store/history/actions.ts index f84f839..ea47fd5 100644 --- a/client/src/store/history/actions.ts +++ b/client/src/store/history/actions.ts @@ -7,6 +7,7 @@ import { APPLY_EFFECT, APPLY_CROP, APPLY_FILTER, + RESET_FILTER, ResetAction, } from '../actionTypes'; import { CropAction } from '../currentVideo/actions'; @@ -38,12 +39,29 @@ export interface Status { flipped: boolean; } -export interface FilterStatus { - blur?: number; - grayScale?: number; - brightness?: number; +export enum Filter { + RED = 'r', + GREEN = 'g', + BLUE = 'b', + LUMINANCE = 'brightness', + BLUR = 'blur', + GRAYSCALE = 'grayscale', } +type RedFilter = { [Filter.RED]: number }; +type GreenFilter = { [Filter.GREEN]: number }; +type BlueFilter = { [Filter.BLUE]: number }; +type LuminanceFilter = { [Filter.LUMINANCE]: number }; +type BlurFilter = { [Filter.BLUR]: number }; +type GrayscaleFilter = { [Filter.GRAYSCALE]: number }; + +export type FilterStatus = RedFilter & + GreenFilter & + BlueFilter & + LuminanceFilter & + BlurFilter & + GrayscaleFilter; + export interface Thumbnails { prev: string[]; current: string[]; @@ -96,13 +114,25 @@ export const applyCrop = (thumbnails: Thumbnails, interval: Interval) => ({ }, }); -export const applyFilter = (filterStatus: FilterStatus) => ({ +export const applyFilter = ( + filterStatus: + | RedFilter + | GreenFilter + | BlueFilter + | LuminanceFilter + | BlurFilter + | GrayscaleFilter +) => ({ type: APPLY_FILTER, payload: { filterStatus, }, }); +export const resetFilter = () => ({ + type: RESET_FILTER, +}); + export type HistoryUndoSuccessAction = { type: typeof UNDO_SUCCESS; payload: { @@ -143,6 +173,10 @@ export type HistoryApplyFilterAction = { }; }; +export type HistoryResetFilterAction = { + type: typeof RESET_FILTER; +}; + export type HistoryAction = | HistoryUndoSuccessAction | HistoryRedoSuccessAction @@ -150,5 +184,6 @@ export type HistoryAction = | HistoryApplyEffectAction | HistoryApplyCropAction | HistoryApplyFilterAction + | HistoryResetFilterAction | CropAction | ResetAction; diff --git a/client/src/store/history/reducer.ts b/client/src/store/history/reducer.ts index cec9696..2e1688b 100644 --- a/client/src/store/history/reducer.ts +++ b/client/src/store/history/reducer.ts @@ -7,8 +7,17 @@ import { CLEAR, APPLY_CROP, APPLY_FILTER, + RESET_FILTER, } from '../actionTypes'; -import { HistoryAction, Log, Effect, Status, FilterStatus } from './actions'; + +import { + HistoryAction, + Log, + Effect, + Status, + Filter, + FilterStatus, +} from './actions'; export const MAX_HISTORY = 20; @@ -28,9 +37,12 @@ const initialState: HistoryState = { flipped: false, }, filterStatus: { - blur: 0, - grayScale: 0, - brightness: 1, + [Filter.RED]: 100, + [Filter.GREEN]: 100, + [Filter.BLUE]: 100, + [Filter.LUMINANCE]: 50, + [Filter.BLUR]: 0, + [Filter.GRAYSCALE]: 0, }, }; @@ -117,18 +129,15 @@ export default ( return { ...state, filterStatus: { - blur: action.payload.filterStatus.blur - ? action.payload.filterStatus.blur - : state.filterStatus.blur, - grayScale: - action.payload.filterStatus.grayScale !== undefined - ? action.payload.filterStatus.grayScale - : state.filterStatus.grayScale, - brightness: action.payload.filterStatus.brightness - ? action.payload.filterStatus.brightness - : state.filterStatus.brightness, + ...state.filterStatus, + ...action.payload.filterStatus, }, }; + case RESET_FILTER: + return { + ...state, + filterStatus: initialState.filterStatus, + }; case CLEAR: case RESET: return initialState; diff --git a/client/src/store/history/sagas.ts b/client/src/store/history/sagas.ts index 22c81ff..586cbda 100644 --- a/client/src/store/history/sagas.ts +++ b/client/src/store/history/sagas.ts @@ -3,7 +3,14 @@ import webglController from '@/webgl/webglController'; import video from '@/video'; import { MAX_HISTORY } from '@/store/history/reducer'; -import { APPLY_EFFECT, UNDO, REDO, CLEAR, error } from '../actionTypes'; +import { + APPLY_EFFECT, + UNDO, + REDO, + CLEAR, + error, + RESET_FILTER, +} from '../actionTypes'; import { Effect, Log, undoSuccess, redoSuccess } from './actions'; import { getIndexAndLogs } from '../selectors'; import { updateStartEnd, setThumbnails, moveTo } from '../currentVideo/actions'; @@ -125,6 +132,10 @@ function* clearEffect(action) { }); } +function* resetFilter(action) { + yield call(webglController.initEffectProps); +} + export function* watchApplyEffect() { yield takeLeading(APPLY_EFFECT, controlWebgl); yield takeLeading(APPLY_EFFECT, checkApplyEffect); @@ -134,4 +145,5 @@ export function* watchHistory() { yield takeLeading(UNDO, undoEffect); yield takeLeading(REDO, redoEffect); yield takeLeading(CLEAR, clearEffect); + yield takeLeading(RESET_FILTER, resetFilter); } diff --git a/client/src/store/selectors.ts b/client/src/store/selectors.ts index 44c3ff8..c30b387 100644 --- a/client/src/store/selectors.ts +++ b/client/src/store/selectors.ts @@ -80,4 +80,15 @@ export const getIsNextDisabled = (state: RootState) => { export const getStatus = (state: RootState) => state.history.status; -export const getFilterStatus = (state: RootState) => state.history.filterStatus; +export const getFilterStatus = (state: RootState) => { + const { brightness, blur, grayscale } = state.history.filterStatus; + return { brightness, blur, grayscale }; +}; +export const getRed = (state: RootState) => state.history.filterStatus.r; +export const getGreen = (state: RootState) => state.history.filterStatus.g; +export const getBlue = (state: RootState) => state.history.filterStatus.b; +export const getBrightness = (state: RootState) => + state.history.filterStatus.brightness; +export const getBlur = (state: RootState) => state.history.filterStatus.blur; +export const getGrayscale = (state: RootState) => + state.history.filterStatus.grayscale; diff --git a/client/src/webgl/webglController.ts b/client/src/webgl/webglController.ts index 969c39d..5dc1b74 100644 --- a/client/src/webgl/webglController.ts +++ b/client/src/webgl/webglController.ts @@ -627,10 +627,18 @@ class WebglController { this.ratio = 1; }; + initEffectProps = () => { + this.luminance = 0.0; + this.chroma = [1.0, 1.0, 1.0]; + this.blurRatio = 0; + this.graySacle = FALSE; + }; + clear = () => { this.positions = this.init.positions.map(pair => [...pair]); this.buffers = initBuffers(this.gl, this.positions); this.initProps(); + this.initEffectProps(); }; reset = () => { From b14082e840e5f724f8acb9bc43c156a91763ddd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Wed, 16 Dec 2020 17:45:52 +0900 Subject: [PATCH 27/40] =?UTF-8?q?[FE]:=20[FEAT]=20ENCODE=5FSUCCESS=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/store/originalVideo/actions.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/src/store/originalVideo/actions.ts b/client/src/store/originalVideo/actions.ts index 3032c58..30b73af 100644 --- a/client/src/store/originalVideo/actions.ts +++ b/client/src/store/originalVideo/actions.ts @@ -9,6 +9,7 @@ import { SET_VIDEO, LOAD_METADATA, ENCODE_START, + ENCODE_SUCCESS, UPLOAD_START, ResetAction, ErrorAction, @@ -35,6 +36,10 @@ export const encodeStart = name => ({ payload: { name }, }); +export const encodeSuccess = () => ({ + type: ENCODE_SUCCESS, +}); + export const uploadStart = file => ({ type: UPLOAD_START, payload: { file }, @@ -61,6 +66,10 @@ export type EncodeStartAction = { }; }; +type EncodeSuccessAction = { + type: typeof ENCODE_SUCCESS; +}; + type UploadStartAction = { type: typeof UPLOAD_START; payload: { @@ -75,6 +84,7 @@ export type OriginalVideoAction = | SetThumbnailsAction | CropConfirmAction | EncodeStartAction + | EncodeSuccessAction | UploadStartAction | UploadSuccessAction | ErrorAction From 9dbe89f9e4eb6747c755c1406503cc251397fb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Wed, 16 Dec 2020 17:47:21 +0900 Subject: [PATCH 28/40] =?UTF-8?q?[FE]:=20[STYLE]=20#19=20brightness=20?= =?UTF-8?q?=ED=9A=A8=EA=B3=BC=20=EA=B3=B5=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../molecules/Thumbnail/Thumbnail.tsx | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/client/src/components/molecules/Thumbnail/Thumbnail.tsx b/client/src/components/molecules/Thumbnail/Thumbnail.tsx index 8a32e36..e846894 100644 --- a/client/src/components/molecules/Thumbnail/Thumbnail.tsx +++ b/client/src/components/molecules/Thumbnail/Thumbnail.tsx @@ -30,13 +30,21 @@ const StyledDiv = styled.div` const StyledImg = styled.img` width: 100%; height: 50px; - transform: scale(${props => props.status.scale}) - scaleY(${props => (props.status.flipped ? -1 : 1)}) - rotate(${props => props.status.rotation}deg); - filter: grayScale(${props => props.filterStatus.grayScale}%) - brightness(${props => props.filterStatus.brightness}) - blur(${props => props.filterStatus.blur}px); + ${({ + status: { scale, flipped, rotation }, + filterStatus: { grayscale, brightness, blur }, + }) => ` + transform: + scale(${scale}) + scaleY(${flipped ? -1 : 1}) + rotate(${rotation}deg); + filter: + grayScale(${grayscale * 100}%) + brightness(${20 + brightness * 0.6 + brightness ** 2 * 0.02}%) + blur(${blur / 50}px); + `} `; + const ImageDiv = styled.div` overflow: hidden; display: flex; @@ -49,11 +57,7 @@ const ImageDiv = styled.div` min-height: 50px; `; -const renderThumbnails = ( - thumbnails: string[], - status: Status, - filterStatus: FilterStatus -) => +const renderThumbnails = (thumbnails: string[], status: Status, filterStatus) => thumbnails.map(image => ( { const thumbnails = useSelector(getThumbnails); const { isCrop, duration } = useSelector(getIsCropAndDuration, shallowEqual); const { start, end } = useSelector(getStartEnd, shallowEqual); - const status = useSelector(getStatus); - const filterStatus = useSelector(getFilterStatus); + const status = useSelector(getStatus, shallowEqual); + const filterStatus = useSelector(getFilterStatus, shallowEqual); const [time, setTime] = useState(0); const dispatch = useDispatch(); @@ -84,6 +88,7 @@ const Thumbnail: React.FC = () => { video.setCurrentTime(start + time); dispatch(moveTo(start + time)); }; + const handleMouseMove = (event: MouseEvent) => { const slider = hoverSliderRef.current; @@ -111,7 +116,7 @@ const Thumbnail: React.FC = () => { const OriginalThumbnails = useMemo( () => renderThumbnails(video.getThumbnails(), status, filterStatus), - [message] // URL is not enough to check whether thumbnail is ready + [message, status, filterStatus] // URL is not enough to check whether thumbnail is ready ); const Thumbnails = useMemo( () => renderThumbnails(thumbnails, status, filterStatus), From 6c27da624756b9ed4f5dd6ac88f47a6ffdd9caae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Wed, 16 Dec 2020 17:48:00 +0900 Subject: [PATCH 29/40] =?UTF-8?q?[FE]:=20[FIX]=20update=EB=90=9C=20video?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=EC=9D=B4=20=EC=8B=9C=EA=B0=84=EC=88=9C?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=B6=9C=EB=A0=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/store/video/reducer.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/store/video/reducer.ts b/client/src/store/video/reducer.ts index 805dae3..629fff7 100644 --- a/client/src/store/video/reducer.ts +++ b/client/src/store/video/reducer.ts @@ -25,7 +25,12 @@ export default ( case UPLOAD_SUCCESS: return { videos: state.videos - ? [...state.videos, action.payload.video] + ? [ + action.payload.video, + ...state.videos.filter( + video => video.id !== action.payload.video.id + ), + ] : [action.payload.video], }; case FETCH_LIST_START: From e7975d59c84d6504712ea8d6841ce449df8afb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Wed, 16 Dec 2020 17:48:24 +0900 Subject: [PATCH 30/40] =?UTF-8?q?[FE]:=20[FIX]=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/atoms/VolumeRange/VolumeRange.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/atoms/VolumeRange/VolumeRange.tsx b/client/src/components/atoms/VolumeRange/VolumeRange.tsx index 47fcdc6..80e4f82 100644 --- a/client/src/components/atoms/VolumeRange/VolumeRange.tsx +++ b/client/src/components/atoms/VolumeRange/VolumeRange.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; import { useDispatch, useSelector } from 'react-redux'; From 5d1388acc9d8bf38a74bedc526d2d1f1caea60ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Thu, 17 Dec 2020 15:50:05 +0900 Subject: [PATCH 31/40] =?UTF-8?q?[FE]:=20[CHORE]=20mediainfo=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=84=A4=EC=B9=98=20=EB=B0=8F=20webpack=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package-lock.json | 171 +++++++++++++++++++++++++++++++++++---- client/package.json | 1 + client/webpack.common.ts | 4 + 3 files changed, 159 insertions(+), 17 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 2dd3ed7..040c6fe 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3270,8 +3270,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decode-uri-component": { "version": "0.2.0", @@ -4785,8 +4784,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { "version": "1.0.1", @@ -5985,6 +5983,152 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, + "mediainfo.js": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/mediainfo.js/-/mediainfo.js-0.1.4.tgz", + "integrity": "sha512-M7j4JJ17WrvrmjtFls5KBOJtw2kX1aXT6ki6e96Y10RglNpNzYwdQZhKOrfIT6w//OVMGjfMwQaIH9aEzJMacA==", + "requires": { + "yargs": "^15.4.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "memory-fs": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", @@ -6817,7 +6961,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "requires": { "p-try": "^2.0.0" } @@ -6848,8 +6991,7 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "pako": { "version": "1.0.11", @@ -7773,14 +7915,12 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "requires-port": { "version": "1.0.0", @@ -8128,8 +8268,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { "version": "2.0.1", @@ -10085,8 +10224,7 @@ "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wide-align": { "version": "1.1.3", @@ -10204,8 +10342,7 @@ "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yallist": { "version": "2.1.2", diff --git a/client/package.json b/client/package.json index 1e0f374..b3edb8d 100644 --- a/client/package.json +++ b/client/package.json @@ -58,6 +58,7 @@ "clean-webpack-plugin": "^3.0.0", "gl-matrix": "^3.3.0", "html-webpack-plugin": "^4.5.0", + "mediainfo.js": "^0.1.4", "mp4-h264": "^1.0.4", "react": "^17.0.1", "react-dom": "^17.0.1", diff --git a/client/webpack.common.ts b/client/webpack.common.ts index f58b364..483e18c 100644 --- a/client/webpack.common.ts +++ b/client/webpack.common.ts @@ -6,6 +6,10 @@ module.exports = { entry: { main: './src/index.tsx', }, + node: { + fs: 'empty', + net: 'empty', + }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js', From dc11181575d89daf05d19cad040ce2741f658993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Thu, 17 Dec 2020 15:51:19 +0900 Subject: [PATCH 32/40] =?UTF-8?q?[FE]:=20[FIX]=20=EC=84=B8=EB=A1=9C=20?= =?UTF-8?q?=EC=98=81=EC=83=81=EC=9D=84=20=EC=98=AC=EB=A6=AC=EB=A9=B4=20?= =?UTF-8?q?=EC=98=81=EC=83=81=EC=9D=B4=20=EB=8F=8C=EC=95=84=EA=B0=80?= =?UTF-8?q?=EC=84=9C=20=EC=B6=9C=EB=A0=A5=EB=90=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mediainfo 모듈을 사용하여 원본 영상의 metadata의 Rotation 속성을 읽어 처리 --- client/src/store/originalVideo/sagas.ts | 11 ++++++++++ client/src/video/metadata.ts | 28 +++++++++++++++++++++++++ client/src/webgl/webglConfig.ts | 26 ++++++++++++++++------- 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 client/src/video/metadata.ts diff --git a/client/src/store/originalVideo/sagas.ts b/client/src/store/originalVideo/sagas.ts index db7b6ef..b9ce698 100644 --- a/client/src/store/originalVideo/sagas.ts +++ b/client/src/store/originalVideo/sagas.ts @@ -3,6 +3,8 @@ import { put, call, takeLatest, select } from 'redux-saga/effects'; import video, { encodeVideo, muxVideoAndAudio } from '@/video'; import webglController from '@/webgl/webglController'; import videoAPI from '@/api/video'; +import readMetaData from '@/video/metadata'; + import { setVideo, loadMetadata, @@ -77,6 +79,15 @@ function* load(action) { yield put(loadMetadata(duration)); const thumbnails: string[] = yield call(video.makeThumbnails, 0, duration); + const file = yield select(getFile); + + const { + media: { track }, + } = yield call(readMetaData, file); + + const rotation = Math.round(track[1].Rotation); + + yield call(webglController.setVideoRotation, rotation); yield call(webglController.main); yield call(webglController.clear); diff --git a/client/src/video/metadata.ts b/client/src/video/metadata.ts new file mode 100644 index 0000000..ed2994e --- /dev/null +++ b/client/src/video/metadata.ts @@ -0,0 +1,28 @@ +import MediaInfo from 'mediainfo.js'; + +const readMetaData = async (videoFile: File) => { + const scriptDirectory = `${window.location.href}node_modules/mediainfo.js/dist/MediaInfoModule.wasm`; + + const mediainfo = await MediaInfo({ + locateFile: () => scriptDirectory, + }); + + const getSize = () => videoFile.size; + + const readChunk = (chunkSize, offset) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = ({ target: { error, result } }) => { + if (error) reject(error); + resolve(new Uint8Array(result as ArrayBuffer)); + }; + + reader.readAsArrayBuffer(videoFile.slice(offset, offset + chunkSize)); + }); + + const metadata = await mediainfo.analyzeData(getSize, readChunk); + return metadata; +}; + +export default readMetaData; diff --git a/client/src/webgl/webglConfig.ts b/client/src/webgl/webglConfig.ts index d27cacc..e9e2979 100644 --- a/client/src/webgl/webglConfig.ts +++ b/client/src/webgl/webglConfig.ts @@ -76,7 +76,8 @@ const initShaderProgram = (gl: WebGLRenderingContext) => { export const initBuffers = ( gl: WebGLRenderingContext, - positions: number[][] + positions: number[][], + videoRotation: number ) => { const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); @@ -86,12 +87,20 @@ export const initBuffers = ( gl.STATIC_DRAW ); - const textureCoordinates = [0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0]; + const textureCoordinates = { + 0: [0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0], + 90: [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0], + 180: [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + 270: [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0], + }; + const textureCoordBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer); + gl.bufferData( gl.ARRAY_BUFFER, - new Float32Array(textureCoordinates), + new Float32Array(textureCoordinates[videoRotation]), gl.STATIC_DRAW ); @@ -113,8 +122,11 @@ export const initBuffers = ( const initCanvas = (videoWidth: string, videoHeight: string) => { const canvas = document.getElementById('glcanvas') as HTMLCanvasElement; - canvas.setAttribute('width', videoWidth); - canvas.setAttribute('height', videoHeight); + const factor = 1; + + canvas.setAttribute('width', (Number(videoWidth) / factor).toString()); + canvas.setAttribute('height', (Number(videoHeight) / factor).toString()); + const gl = (canvas.getContext('webgl', { alpha: false }) || canvas.getContext('experimental-webgl', { alpha: false, @@ -123,13 +135,13 @@ const initCanvas = (videoWidth: string, videoHeight: string) => { return gl; }; -export const initConfig = (positions: number[][]) => { +export const initConfig = (positions: number[][], videoRotation: number) => { const gl = initCanvas( video.get('videoWidth').toString(), video.get('videoHeight').toString() ); - const buffers = initBuffers(gl, positions); + const buffers = initBuffers(gl, positions, videoRotation); const shaderProgram = initShaderProgram(gl); From 1c9a6778da702b3ef9a07ea26f0231f50e077ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Thu, 17 Dec 2020 15:52:24 +0900 Subject: [PATCH 33/40] =?UTF-8?q?[FE]:=20[FEAT]=20#20=20=EC=98=81=EC=83=81?= =?UTF-8?q?=20=EB=B9=84=EC=9C=A8=20=EC=A1=B0=EC=A0=95=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 4:3, 16:9 비율 조정 기능 추가 --- .../src/components/organisms/Tools/Tools.tsx | 7 +- .../components/organisms/Tools/reducer.tsx | 14 ++- client/src/webgl/webglController.ts | 117 +++++++++--------- 3 files changed, 72 insertions(+), 66 deletions(-) diff --git a/client/src/components/organisms/Tools/Tools.tsx b/client/src/components/organisms/Tools/Tools.tsx index c47308a..109a301 100644 --- a/client/src/components/organisms/Tools/Tools.tsx +++ b/client/src/components/organisms/Tools/Tools.tsx @@ -254,7 +254,6 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { dispatchButtonData({ type, payload }); }; - // const handleCropManually = useCallback(() => {}, []); // TODO: 정말정말진짜로 시간이 남는다면 해보자!! const handleCropConfirm = useCallback(() => { dispatch(cropConfirm()); closeSubtool(); @@ -293,8 +292,11 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { ratio: [ () => dispatch(applyEffect(Effect.Enlarge)), () => dispatch(applyEffect(Effect.Reduce)), + () => webglController.updateRatio(3 / 4), // FIXME: store 변경 필요 + () => webglController.updateRatio(9 / 16), + () => webglController.restoreRatio(), ], - crop: [/* handleCropManually , */ handleCropConfirm, handleCropCancel], + crop: [handleCropConfirm, handleCropCancel], sign: [handleSignUpload, handleSignConfirm, handleSignCancel], filter: [], }), @@ -344,7 +346,6 @@ const Tools: React.FC = ({ setEdit, isEdit }) => { closeSubtool(); } }, [isCancel]); - const handleModalCancel = () => setModalVisible(false); return ( diff --git a/client/src/components/organisms/Tools/reducer.tsx b/client/src/components/organisms/Tools/reducer.tsx index a614388..cb2961e 100644 --- a/client/src/components/organisms/Tools/reducer.tsx +++ b/client/src/components/organisms/Tools/reducer.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import { BsTerminal, BsCheck, BsX, BsUpload } from 'react-icons/bs'; +import { BsCheck, BsX, BsUpload } from 'react-icons/bs'; import { GoTrashcan } from 'react-icons/go'; import { MdRotateLeft, MdRotateRight, MdZoomIn, MdZoomOut, - MdBlurOn, - MdInvertColors, + MdCropOriginal, } from 'react-icons/md'; +import { RiRuler2Line, RiRuler2Fill } from 'react-icons/ri'; import { CgMergeHorizontal, CgMergeVertical } from 'react-icons/cg'; import size from '@/theme/sizes'; @@ -34,9 +34,8 @@ interface ButtonDataAction { } // crop -const cropMessages = [/* '직접입력', */ '확인', '취소']; +const cropMessages = ['확인', '취소']; const cropChildrens = [ - // , , , ]; @@ -51,10 +50,13 @@ const rotateReverseChildrens = [ ]; // ratio -const ratioMessages = ['확대', '축소']; +const ratioMessages = ['확대', '축소', '4:3', '16:9', '원본 비율']; const ratioChildrens = [ , , + , + , + , ]; // sign diff --git a/client/src/webgl/webglController.ts b/client/src/webgl/webglController.ts index 5dc1b74..2c6f000 100644 --- a/client/src/webgl/webglController.ts +++ b/client/src/webgl/webglController.ts @@ -31,7 +31,6 @@ const offset = 0; const numComponents = 2; const vertexCount = 6; const normalize = false; -const TURE = 1; const FALSE = 0; class WebglController { @@ -69,8 +68,6 @@ class WebglController { typeShort: number; // edit params - rate: number = 1; - rotate: boolean = false; phase: number = 0; @@ -99,31 +96,37 @@ class WebglController { blurRatio: number = 0; - graySacle: number = FALSE; + grayScale: number = FALSE; luminance: number = 0.0; + // video params + videoRotation: number = 0; + + originalRatio: number; + + originalWidth: number; + + originalHeight: number; + constructor() { this.positions = this.init.positions.map(pair => [...pair]); } + swapWidthHeight = () => { + const { width } = this.gl.canvas; + this.gl.canvas.width = this.gl.canvas.height; + this.gl.canvas.height = width; + }; + rotateLeft90Degree = () => { this.phase += this.flip ? -1 : 1; - - this.rate *= this.rotate - ? this.gl.canvas.width / this.gl.canvas.height - : this.gl.canvas.height / this.gl.canvas.width; - - this.rotate = !this.rotate; + this.swapWidthHeight(); }; rotateRight90Degree = () => { this.phase += this.flip ? 1 : -1; - - this.rate *= this.rotate - ? this.gl.canvas.width / this.gl.canvas.height - : this.gl.canvas.height / this.gl.canvas.width; - this.rotate = !this.rotate; + this.swapWidthHeight(); }; reverseUpsideDown = () => { @@ -132,7 +135,6 @@ class WebglController { reverseSideToSide = () => { this.flip = !this.flip; - this.phase += 2; }; @@ -204,13 +206,17 @@ class WebglController { }; setGrayScale = grayScale => { - this.graySacle = grayScale; + this.grayScale = grayScale; }; setLuminance = luminance => { this.luminance = luminance; }; + setVideoRotation = videoRotation => { + this.videoRotation = videoRotation; + }; + updateTexture = (texture: WebGLTexture) => { this.gl.bindTexture(this.gl.TEXTURE_2D, texture); this.gl.texImage2D( @@ -284,37 +290,25 @@ class WebglController { [0.0, 0.0, 1.0] ); - if (this.flip) { - mat4.scale(modelViewMatrix, modelViewMatrix, [1.0, -1.0, 1.0]); - } - - mat4.scale(modelViewMatrix, modelViewMatrix, [ - 1 / (this.rate * this.rate), - 1.0, - 1.0, - ]); - - mat4.scale(modelViewMatrix, modelViewMatrix, [ - 1 / this.ratio, - 1 / this.ratio, - 1.0, - ]); + let scaleX = 1 / this.ratio; + let scaleY = this.flip ? -scaleX : scaleX; + mat4.scale(modelViewMatrix, modelViewMatrix, [scaleX, scaleY, 1]); - const canvas = this.gl.canvas as HTMLElement; + const { clientWidth, clientHeight } = this.gl.canvas as HTMLElement; mat4.translate(modelViewMatrix, modelViewMatrix, [ - this.signX / (canvas.clientWidth / 2), - this.signY / (canvas.clientHeight / 2), + this.signX / (clientWidth / 2), + this.signY / (clientHeight / 2), 0.0, ]); - mat4.scale(modelViewMatrix, modelViewMatrix, [ - this.signRatio, + scaleX = this.signRatio; + scaleY = this.signRatio * - (this.gl.canvas.width / this.gl.canvas.height) * - (this.sign.height / this.sign.width), - 1.0, - ]); + (this.gl.canvas.width / this.gl.canvas.height) * + (this.sign.height / this.sign.width); + + mat4.scale(modelViewMatrix, modelViewMatrix, [scaleX, scaleY, 1.0]); this.gl.useProgram(programInfo.program); @@ -430,17 +424,10 @@ class WebglController { const modelViewMatrix = mat4.create(); mat4.translate(modelViewMatrix, modelViewMatrix, [0.0, 0.0, -0.5]); - mat4.scale(modelViewMatrix, modelViewMatrix, [ - this.rate * this.rate, - 1.0, - 1.0, - ]); - mat4.scale(modelViewMatrix, modelViewMatrix, [this.ratio, this.ratio, 1.0]); - - if (this.flip) { - mat4.scale(modelViewMatrix, modelViewMatrix, [1.0, -1.0, 1.0]); - } + const scaleX = this.ratio; + const scaleY = this.flip ? -this.ratio : this.ratio; + mat4.scale(modelViewMatrix, modelViewMatrix, [scaleX, scaleY, 1]); mat4.rotate( modelViewMatrix, @@ -514,7 +501,7 @@ class WebglController { 'grayScaleFlag' ); - this.gl.uniform1i(grayScaleFlag, this.graySacle); + this.gl.uniform1i(grayScaleFlag, this.grayScale); const chromaRedLocation = this.gl.getUniformLocation( this.programInfo.program, @@ -584,7 +571,7 @@ class WebglController { }; glInit = () => { - const config = initConfig(this.positions); + const config = initConfig(this.positions, this.videoRotation); this.gl = config.gl; this.buffers = config.buffers; @@ -597,6 +584,9 @@ class WebglController { this.srcType = this.gl.UNSIGNED_BYTE; this.type = this.gl.FLOAT; this.typeShort = this.gl.UNSIGNED_SHORT; + this.originalRatio = video.get('videoWidth') / video.get('videoHeight'); + this.originalWidth = video.get('videoWidth'); + this.originalHeight = video.get('videoHeight'); const render = () => { if (!video.get('src')) return; @@ -618,25 +608,24 @@ class WebglController { }; initProps = () => { - this.rotate = false; this.flip = false; this.encode = false; this.sign = null; - this.rate = 1; this.phase = 0; this.ratio = 1; + this.restoreRatio(); }; initEffectProps = () => { this.luminance = 0.0; this.chroma = [1.0, 1.0, 1.0]; this.blurRatio = 0; - this.graySacle = FALSE; + this.grayScale = FALSE; }; clear = () => { this.positions = this.init.positions.map(pair => [...pair]); - this.buffers = initBuffers(this.gl, this.positions); + this.buffers = initBuffers(this.gl, this.positions, this.videoRotation); this.initProps(); this.initEffectProps(); }; @@ -645,5 +634,19 @@ class WebglController { this.gl.clear(this.gl.COLOR_BUFFER_BIT); this.clear(); }; + + updateRatio = (ratio: number) => { + const currentResolution = this.gl.canvas.width * this.gl.canvas.height; + const y = Math.sqrt(ratio * currentResolution); + const x = currentResolution / y; + + this.gl.canvas.width = Math.round(x >> 1) << 1; + this.gl.canvas.height = Math.round(y >> 1) << 1; + }; + + restoreRatio = () => { + this.gl.canvas.width = this.originalWidth; + this.gl.canvas.height = this.originalHeight; + }; } export default new WebglController(); From ba33f26c0abfb7b84a08b4be0890ff67516f0192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Thu, 17 Dec 2020 15:52:45 +0900 Subject: [PATCH 34/40] =?UTF-8?q?[FE]:=20[STYLE]=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/organisms/VideoContainer/VideoContainer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/organisms/VideoContainer/VideoContainer.tsx b/client/src/components/organisms/VideoContainer/VideoContainer.tsx index 51b0eec..c92bc60 100644 --- a/client/src/components/organisms/VideoContainer/VideoContainer.tsx +++ b/client/src/components/organisms/VideoContainer/VideoContainer.tsx @@ -3,6 +3,7 @@ import styled, { css, keyframes } from 'styled-components'; import color from '@/theme/colors'; import { Message } from '@/store/originalVideo/reducer'; +import video from '@/video'; const UP = 'up'; const DOWN = 'down'; From 8208cdbd95c99071b595e8c7af472c4020e8e8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=AF=E1=86=AB=E1=84=8B=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=8B=E1=85=A5=E1=86=AB?= Date: Thu, 17 Dec 2020 16:41:00 +0900 Subject: [PATCH 35/40] =?UTF-8?q?[FE]:=20[FIX]=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/webgl/webglConfig.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/src/webgl/webglConfig.ts b/client/src/webgl/webglConfig.ts index e9e2979..bbbe728 100644 --- a/client/src/webgl/webglConfig.ts +++ b/client/src/webgl/webglConfig.ts @@ -74,6 +74,13 @@ const initShaderProgram = (gl: WebGLRenderingContext) => { return shaderProgram; }; +const textureCoordinatesByRotation = { + 0: [0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0], + 90: [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0], + 180: [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + 270: [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0], +}; + export const initBuffers = ( gl: WebGLRenderingContext, positions: number[][], @@ -87,20 +94,13 @@ export const initBuffers = ( gl.STATIC_DRAW ); - const textureCoordinates = { - 0: [0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0], - 90: [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0], - 180: [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], - 270: [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0], - }; - const textureCoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer); gl.bufferData( gl.ARRAY_BUFFER, - new Float32Array(textureCoordinates[videoRotation]), + new Float32Array(textureCoordinatesByRotation[videoRotation]), gl.STATIC_DRAW ); From 9e39456affb52b2b733a3f997bfb903897305ebf Mon Sep 17 00:00:00 2001 From: mjseok Date: Fri, 18 Dec 2020 02:39:41 +0900 Subject: [PATCH 36/40] =?UTF-8?q?[FE]=20:=20[FEAT]=20#10=EB=8F=84=EC=9B=80?= =?UTF-8?q?=EB=A7=90=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WAVE 설명서 작성 --- .../components/atoms/ModalComponent/Help.tsx | 137 ++++++++++++++++++ .../components/organisms/Header/Header.tsx | 35 ++++- 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 client/src/components/atoms/ModalComponent/Help.tsx diff --git a/client/src/components/atoms/ModalComponent/Help.tsx b/client/src/components/atoms/ModalComponent/Help.tsx new file mode 100644 index 0000000..ca876f4 --- /dev/null +++ b/client/src/components/atoms/ModalComponent/Help.tsx @@ -0,0 +1,137 @@ +/* eslint-disable react/jsx-one-expression-per-line */ +import React from 'react'; +import styled from 'styled-components'; +import color from '@/theme/colors'; + +const StyledDiv = styled.div` + display: flex; + flex-direction: column; + align-items: center; + overflow-y: scroll; + width: 100%; + height: 89%; + + p { + align-items: baseline; + } +`; + +const StyledTitle = styled.h3` + margin-top: 5%; +`; + +const StyledP = styled.p` + margin-top: 5%; +`; + +const Help: React.FC = () => { + return ( + + 1. 비디오 불러오기 + 1 + + ① 내 컴퓨터에 있는 비디오를 불러올 수 있습니다. +
② 서버에 올라가있는 비디오를 불러올 수 있습니다. +
+ 2. 비디오 재생 / 정지하기 + 2 + + ① 버튼을 클릭하거나 space bar를 눌러서 비디오를 재생 / 정지할 수 + 있습니다. +
② 버튼을 클릭하거나 왼쪽 방향키를 눌러서 비디오를 10초 전으로 + 이동할 수 있습니다. +
③ 버튼을 클릭하거나 오른쪽 방향키를 눌러서 비디오를 10초 후로 + 이동할 수 있습니다. +
④ 썸네일을 클릭하여 해당하는 위치로 영상의 재생시간을 이동시킬 수 + 있습니다. +
+ + 3. 비디오 소리조절하기 + 3 + + ① 버튼을 클릭하여 음소거를 할 수 있습니다. +
② 마우스를 올려 볼륨 조절 창을 나타나게할 수 있습니다.
③ + 마우스로 볼륨 조절 슬라이더를 조절하여 볼륨을 조절할 수 있습니다. +
+ + 4. 비디오 편집하기 + 4 + + ① 회전 / 반전 하기 ② 비율 조절하기
③ 비디오 길이 자르기
④ + 서명 추가하기
⑤ 비디오 필터 입히기 +
+ + 5. 비디오 편집하기 - 회전 / 반전 하기 + 5 + + ① 버튼을 클릭하여 비디오를 왼쪽으로 90도 회전할 수 있습니다
② + 버튼을 클릭하여 비디오를 오른쪽으로 90도 회전할 수 있습니다
③ + 버튼을 클릭하여 비디오를 상하로 반전시킬 수 있습니다.
④ 버튼을 + 클릭하여 비디오를 좌우로 반전시킬 수 있습니다. +
+ + 6. 비디오 편집하기 - 비율 조절하기 + 6 + + ① 버튼을 클릭하여 비디오를 확대할 수 있습니다. +
② 버튼을 클릭하여 비디오를 축소할 수 있습니다. +
③ 버튼을 클릭하여 비디오의 비율을 4:3으로 조정할 수 있습니다. +
④ 버튼을 클릭하여 비디오의 비율을 16:9로 조정할 수 있습니다. +
⑤ 버튼을 클릭하여 비디오의 비율을 원래대로 되돌릴 수 있습니다. +
+ + 7. 비디오 편집하기 - 길이 자르기 + 7 + + ① 마우스로 썸네일 위의 보라색 막대기를 드래그하여 원하는 길이만큼만 + 비디오를 자를 수 있습니다.
② 버튼을 클릭하여 자르기 효과를 + 비디오에 적용시킬 수 있습니다.
③ 버튼을 클릭하여 자르기 효과를 + 취소할 수 있습니다. +
+ + 8. 비디오 편집하기 - 서명 추가하기 + 8 + + ① 슬라이더를 조절하여 서명의 크기를 조절할 수 있습니다.
② 버튼을 + 클릭하여 원하는 서명 파일을 업로드 할 수 있습니다.
③ 버튼을 + 클릭하여 서명을 비디오에 적용시킬 수 있습니다.
④ 버튼을 클릭하여 + 업로드 된 서명을 삭제할 수 있습니다. +
+ + 9. 비디오 편집하기 - 비디오 필터 입히기 + 9 + + ① 슬라이더를 조절하여 비디오의 RGB 색감을 조절할 수 있습니다.
② + 슬라이더를 조절하여 비디오의 블러 효과를 조절할 수 있습니다.
③ + 슬라이더를 조절하여 비디오의 밝기를 조절할 수 있습니다.
④ + 슬라이더를 조절하여 비디오에 흑백필터를 씌울 수 있습니다.
⑤ + 초기화 버튼을 클릭하여 비디오에 적용된 필터를 초기화 시킬 수 있습니다. +
+ + 10. 히스토리 + 10 + + ① 버튼을 클릭하여 직전에 적용된 편집효과를 취소할 수 있습니다. +
② 버튼을 클릭하여 취소된 편집효과를 재 적용시킬 수 있습니다. +
③ 버튼을 클릭하여 비디오에 적용된 편집효과를 초기화 시킬 수 + 있습니다. +
+ + 11. 취소하기 + 11 + + ① 취소버튼을 누르면 비디오를 불러오기 이 전의 상태로 돌아갑니다. + + + 12. 비디오 업로드 / 다운로드 + 12 + + ① 비디오의 이름을 바꿀 수 있습니다. +
② 확인버튼을 누르면 비디오가 서버에 업로드되거나 다운을 받을 수 + 있습니다. +
+
+ ); +}; + +export default Help; diff --git a/client/src/components/organisms/Header/Header.tsx b/client/src/components/organisms/Header/Header.tsx index 00ee6b2..9687a8e 100644 --- a/client/src/components/organisms/Header/Header.tsx +++ b/client/src/components/organisms/Header/Header.tsx @@ -4,6 +4,7 @@ import { BsArrowClockwise, BsArrowCounterclockwise, BsArrowRepeat, + BsQuestionCircle, } from 'react-icons/bs'; import { useSelector, useDispatch } from 'react-redux'; import { @@ -16,6 +17,7 @@ import { import size from '@/theme/sizes'; import Logo from '@/components/atoms/Logo'; import { TextInput } from '@/components/atoms/ModalComponent'; +import { TextInput, Help } from '@/components/atoms/ModalComponent'; import ButtonGroup from '@/components/molecules/ButtonGroup'; import Modal from '@/components/molecules/Modal'; import color from '@/theme/colors'; @@ -73,10 +75,18 @@ const getHistoryToolData = ( ]; const getCancelConfirmData = ( + handleHelp: () => void, handleCancel: () => void, handleConfirm: () => void, hasEmptyVideo: boolean ): button[] => [ + { + onClick: handleHelp, + message: '', + type: 'transparent', + children: , + disabled: false, + }, { onClick: handleCancel, message: '취소', @@ -115,11 +125,17 @@ top: 35vh; left: 40vw; width: 20vw; height: 12vh; +const modalHelpLayout = ` +top: 15vh; +left: 22vw; +width: 56vw; +height: 70vh; `; const Header: React.FC = () => { const dispatch = useDispatch(); const [modalVisible, setModalVisible] = useState(false); + const [helpVisible, sethelpVisible] = useState(false); const name = useSelector(getName); const hasEmptyVideo = !useSelector(getVisible); const isPrevDisabled = useSelector(getIsPrevDisabled); @@ -141,9 +157,15 @@ const Header: React.FC = () => { setModalVisible(false); }; + const handleHelpModalConfirm = () => { + sethelpVisible(false); + }; + const handleHelp = () => { + sethelpVisible(true); + }; const handleModalCancel = () => setModalVisible(false); const handleComplete = () => setModalVisible(true); - + const handleHelpModalCancel = () => sethelpVisible(false); return ( @@ -162,11 +184,22 @@ const Header: React.FC = () => { + {helpVisible && ( + + )} {modalVisible && ( Date: Fri, 18 Dec 2020 02:45:01 +0900 Subject: [PATCH 37/40] =?UTF-8?q?[FE]=20:=20[FEAT]=20=20=ED=95=B4=EC=83=81?= =?UTF-8?q?=EB=8F=84=20=EA=B3=A0=EB=A5=BC=20=EC=88=98=20=EC=9E=88=EB=8A=94?= =?UTF-8?q?=20radio=20=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../atoms/ModalComponent/TextInput.tsx | 171 ++++++++++++++++-- .../components/atoms/ModalComponent/index.ts | 1 + .../components/organisms/Header/Header.tsx | 14 +- client/src/theme/globalStyle.tsx | 69 +++++++ 4 files changed, 233 insertions(+), 22 deletions(-) diff --git a/client/src/components/atoms/ModalComponent/TextInput.tsx b/client/src/components/atoms/ModalComponent/TextInput.tsx index f4eb797..27d3c15 100644 --- a/client/src/components/atoms/ModalComponent/TextInput.tsx +++ b/client/src/components/atoms/ModalComponent/TextInput.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useMemo, useEffect } from 'react'; import styled from 'styled-components'; -import color from '@/theme/colors'; +import color from '@/theme/colors'; +import webglController from '@/webgl/webglController'; import Props from './props'; const StyledModalRow = styled.div` @@ -9,15 +10,26 @@ const StyledModalRow = styled.div` justify-content: space-between; align-items: center; padding: 1rem; + overflow-y: auto; +`; + +const StyledModalCol = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + padding: 0 1rem 1rem 1rem; + width: 100%; `; const StyledInput = styled.input` - width: 75%; + width: ${({ type }) => (type === 'radio' ? null : '75%')}; padding: 0.5rem; border-radius: 5px; border: none; - box-shadow: 0 0 1px 2px rgba(255, 255, 255, 0.1); - background-color: ${color.MODAL}; + box-shadow: ${({ type }) => + type === 'radio' ? null : '0 0 1px 2px rgba(255, 255, 255, 0.1)'}; + background-color: ${({ disabled }) => (disabled ? color.GRAY : color.MODAL)}; color: ${color.WHITE}; outline: none; `; @@ -29,19 +41,148 @@ const StyledP = styled.p` text-align: center; `; -const TextInput: React.FC> = ({ - state: value, - setState: setValue, -}) => { - const handleChange = ({ target }) => { - setValue(target.value); +const StyledListP = styled(StyledP)` + margin: 0; + font-size: 12px; + text-align: center; + white-space: nowrap; + opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; +`; + +const StyledLabel = styled.label` + margin: 3% 0%; + font-size: 14px; + width: 100%; + text-shadow: 0 0 5px 5px ${color.WHITE}; + opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; +`; + +const RadioDiv = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +`; + +const MAX_RESOLUTION = 1920 * 1080; +const LEVELS = [1080, 720, 480, 360, 240]; + +const calculateResolution = (level, width, height) => ({ + other: + Math.floor( + (level * (width > height ? width / height : height / width)) / 2 + ) * 2, + level, +}); + +const calculateResolutions = (width, height) => { + const choices = { + original: calculateResolution( + width < height ? width : height, + width, + height + ), + }; + const sizes = { + original: width * height, + }; + + LEVELS.forEach(level => { + const choice = calculateResolution(level, width, height); + choices[level] = choice; + sizes[level] = choice.other * level; + }); + + return { choices, sizes }; +}; + +interface State { + name: string; + radio: string | number; +} + +const TextInput: React.FC> = ({ state, setState }) => { + const resolutionWidth = webglController.originalWidth; + const resolutionHeight = webglController.originalHeight; + const { choices, sizes } = useMemo( + () => calculateResolutions(resolutionWidth, resolutionHeight), + [resolutionWidth, resolutionHeight] + ); + + useEffect(() => { + webglController.setResolution(choices[state.radio]); + }, [state.radio]); + + const handleTextInputChange = ({ target }) => { + setState({ + radio: state.radio, + name: target.value, + }); + }; + + const handleRadioInputChange = ({ target }) => { + setState({ + radio: target.value, + name: state.name, + }); }; return ( - - 파일 이름 : - - + <> + + 파일 이름 : + + + + + MAX_RESOLUTION} + /> + MAX_RESOLUTION} + htmlFor="resOriginal" + > + 원본 + + MAX_RESOLUTION}> + {`(${choices.original.other} x ${choices.original.level})`} + + + {LEVELS.map(level => { + const disabled = + sizes[level] > sizes.original || sizes[level] > MAX_RESOLUTION; + return ( + + + + {`${level}p`} + + + {`(${choices[level].other} x ${level})`} + + + ); + })} + + ); }; diff --git a/client/src/components/atoms/ModalComponent/index.ts b/client/src/components/atoms/ModalComponent/index.ts index bee93b3..f5ca641 100644 --- a/client/src/components/atoms/ModalComponent/index.ts +++ b/client/src/components/atoms/ModalComponent/index.ts @@ -1,3 +1,4 @@ export { default as TextInput } from './TextInput'; export { default as VideoList } from './VideoList'; +export { default as Help } from './Help'; export { default as ComponentProps } from './props'; diff --git a/client/src/components/organisms/Header/Header.tsx b/client/src/components/organisms/Header/Header.tsx index 9687a8e..5c0f9fd 100644 --- a/client/src/components/organisms/Header/Header.tsx +++ b/client/src/components/organisms/Header/Header.tsx @@ -16,7 +16,6 @@ import { import size from '@/theme/sizes'; import Logo from '@/components/atoms/Logo'; -import { TextInput } from '@/components/atoms/ModalComponent'; import { TextInput, Help } from '@/components/atoms/ModalComponent'; import ButtonGroup from '@/components/molecules/ButtonGroup'; import Modal from '@/components/molecules/Modal'; @@ -121,10 +120,12 @@ const CancelConfirmStyle = ` `; const modalLayout = ` -top: 35vh; +top: 33vh; left: 40vw; width: 20vw; -height: 12vh; +height: 40vh; +`; + const modalHelpLayout = ` top: 15vh; left: 22vw; @@ -152,11 +153,10 @@ const Header: React.FC = () => { }; const handleCancel = () => dispatch(reset()); - const handleModalConfirm = fileName => { - dispatch(encodeStart(fileName)); + const handleModalConfirm = state => { + dispatch(encodeStart(state.name)); setModalVisible(false); }; - const handleHelpModalConfirm = () => { sethelpVisible(false); }; @@ -207,7 +207,7 @@ const Header: React.FC = () => { handleCancel={handleModalCancel} handleConfirm={handleModalConfirm} component={TextInput} - initialState={name} + initialState={{ name, radio: '240' }} /> )} diff --git a/client/src/theme/globalStyle.tsx b/client/src/theme/globalStyle.tsx index c149fea..1374d40 100644 --- a/client/src/theme/globalStyle.tsx +++ b/client/src/theme/globalStyle.tsx @@ -45,6 +45,75 @@ input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.2, 1.3); box-shadow: 0 0 5px 2px rgba(255, 255, 255, 0.5); } + +input[type="radio"] { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +input[type="radio"]:hover + label:before { + border-color: #999; +} + +input[type="radio"]:active + label:before { + transition-duration: 0; + filter: brightness(0.2); +} + +input[type="radio"] + label { + position: relative; + padding-left: 26px; + font-weight: normal; +} + +input[type="radio"] + label:before, +input[type="radio"] + label:after { + box-sizing: content-box; + position: absolute; + content: ''; + display: block; + left: 0; +} + +input[type="radio"] + label:before { + top: 50%; + width: 16px; + height: 16px; + margin-top: -10px; + border: 2px solid #d9d9d9; + text-align: center; +} + +input[type="radio"] + label:after { + background-color: ${color.LIGHT_PURPLE}; + top: 50%; + left: 6px; + width: 8px; + height: 8px; + margin-top: -4px; + transform: scale(0); + transform-origin: 50%; + transition: transform 200ms ease-out; +} + +input[type="radio"]:checked + label:before { + -moz-animation: borderscale 300ms ease-in; + -webkit-animation: borderscale 300ms ease-in; + animation: borderscale 300ms ease-in; + background-color: ${color.LIGHT_PURPLE}; +} +input[type="radio"]:checked + label:after { + transform: scale(1); +} +input[type="radio"] + label:before, input[type="radio"] + label:after { + border-radius: 50%; +} `; export default GlobalStyle; From 931f238664967bc215e4284bff29ca6336f6d194 Mon Sep 17 00:00:00 2001 From: mjseok Date: Fri, 18 Dec 2020 02:45:38 +0900 Subject: [PATCH 38/40] =?UTF-8?q?[FE]=20:=20[FIX]=20progress=20bar=20101%?= =?UTF-8?q?=20=EB=90=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/atoms/Encoding/Encoding.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/atoms/Encoding/Encoding.tsx b/client/src/components/atoms/Encoding/Encoding.tsx index 669362f..b62f2d7 100644 --- a/client/src/components/atoms/Encoding/Encoding.tsx +++ b/client/src/components/atoms/Encoding/Encoding.tsx @@ -96,7 +96,7 @@ const Encoding: React.FC = ({ message }) => { useEffect(() => { const timer = setInterval(() => { - const progressPercent = Math.round( + const progressPercent = Math.floor( (video.get('currentTime') - start) / divisor ); From 754bdeedd13ce129af52c3a0b16e0e563402f612 Mon Sep 17 00:00:00 2001 From: mjseok Date: Fri, 18 Dec 2020 04:01:47 +0900 Subject: [PATCH 39/40] =?UTF-8?q?[FE]=20:=20[FIX]=20=EA=B0=81=EC=A2=85=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비율 조정 ,회전했을 때 서명 사라지는 오류, 해상도 기본값 설정 --- client/src/store/crop/sagas.ts | 1 - client/src/video/encoding.ts | 2 ++ client/src/video/video.tsx | 7 +++++- client/src/webgl/webglController.ts | 33 +++++++++++++++++++++++++++-- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/client/src/store/crop/sagas.ts b/client/src/store/crop/sagas.ts index 7e570a6..802621f 100644 --- a/client/src/store/crop/sagas.ts +++ b/client/src/store/crop/sagas.ts @@ -16,7 +16,6 @@ function* updateThumbnails(action) { yield call(video.pause); yield put(pause()); const thumbnails: string[] = yield call(video.makeThumbnails, start, end); - yield call(webglController.main); yield put(moveTo(start)); yield put(setThumbnails(thumbnails)); diff --git a/client/src/video/encoding.ts b/client/src/video/encoding.ts index 1c5f589..1bb4ed9 100644 --- a/client/src/video/encoding.ts +++ b/client/src/video/encoding.ts @@ -5,10 +5,12 @@ const framerate = 30; const interval = 1 / framerate; export default async (start, end, webglController) => { + webglController.setCanvasResolution(); const { drawingBufferWidth: width, drawingBufferHeight: height, } = webglController.getDrawingBufferWidthHeight(); + const Encoder = await loadEncoder(); const encoder = Encoder.create({ width, diff --git a/client/src/video/video.tsx b/client/src/video/video.tsx index da4720a..be237bf 100644 --- a/client/src/video/video.tsx +++ b/client/src/video/video.tsx @@ -48,6 +48,7 @@ class Video { // setter setSrc = (src: string) => { this.video.src = src; + this.thumbnails = []; }; setCurrentTime = (time: number) => { @@ -74,7 +75,11 @@ class Video { secs -= gap; images[count] = image; } - if (start === 0 && end === this.video.duration) + if ( + start === 0 && + end === this.video.duration && + !this.thumbnails.length + ) this.thumbnails = images; resolve(images); })(); diff --git a/client/src/webgl/webglController.ts b/client/src/webgl/webglController.ts index 2c6f000..0d6b33a 100644 --- a/client/src/webgl/webglController.ts +++ b/client/src/webgl/webglController.ts @@ -22,6 +22,11 @@ interface ProgramInfo { }; } +interface Resolution { + other: number; + level: number; +} + export const RATIO = 1.25; export const INVERSE = 1 / RATIO; @@ -109,14 +114,22 @@ class WebglController { originalHeight: number; + resolution: Resolution; + constructor() { this.positions = this.init.positions.map(pair => [...pair]); } swapWidthHeight = () => { + const relX = this.signX / (this.gl.canvas as HTMLElement).clientWidth; + const relY = this.signY / (this.gl.canvas as HTMLElement).clientHeight; + const { width } = this.gl.canvas; this.gl.canvas.width = this.gl.canvas.height; this.gl.canvas.height = width; + + this.signX = (this.gl.canvas as HTMLElement).clientWidth * relX; + this.signY = (this.gl.canvas as HTMLElement).clientHeight * relY; }; rotateLeft90Degree = () => { @@ -217,6 +230,20 @@ class WebglController { this.videoRotation = videoRotation; }; + setResolution = resolution => { + this.resolution = resolution; + }; + + setCanvasResolution = () => { + if (this.gl.canvas.width > this.gl.canvas.height) { + this.gl.canvas.width = this.resolution.other; + this.gl.canvas.height = this.resolution.level; + } else { + this.gl.canvas.height = this.resolution.other; + this.gl.canvas.width = this.resolution.level; + } + }; + updateTexture = (texture: WebGLTexture) => { this.gl.bindTexture(this.gl.TEXTURE_2D, texture); this.gl.texImage2D( @@ -436,8 +463,10 @@ class WebglController { [0.0, 0.0, 1.0] ); - if (this.encode) { + if (this.phase % 2 === 0 && this.encode) { mat4.scale(modelViewMatrix, modelViewMatrix, [1.0, -1.0, 1.0]); + } else if (this.encode) { + mat4.scale(modelViewMatrix, modelViewMatrix, [-1.0, 1.0, 1.0]); } this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffers.position); @@ -589,7 +618,7 @@ class WebglController { this.originalHeight = video.get('videoHeight'); const render = () => { - if (!video.get('src')) return; + if (!video.get('src') || this.encode) return; this.updateTexture(this.texture); this.drawScene(this.programInfo, this.texture); From 743c5674e2ec4e829c0d1ca4bd386e2aafca974e Mon Sep 17 00:00:00 2001 From: mjseok Date: Fri, 18 Dec 2020 10:16:00 +0900 Subject: [PATCH 40/40] =?UTF-8?q?[FE]:=20[FIX]=20typo=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/organisms/Header/Header.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/components/organisms/Header/Header.tsx b/client/src/components/organisms/Header/Header.tsx index 5c0f9fd..d6e5016 100644 --- a/client/src/components/organisms/Header/Header.tsx +++ b/client/src/components/organisms/Header/Header.tsx @@ -136,7 +136,7 @@ height: 70vh; const Header: React.FC = () => { const dispatch = useDispatch(); const [modalVisible, setModalVisible] = useState(false); - const [helpVisible, sethelpVisible] = useState(false); + const [helpVisible, setHelpVisible] = useState(false); const name = useSelector(getName); const hasEmptyVideo = !useSelector(getVisible); const isPrevDisabled = useSelector(getIsPrevDisabled); @@ -158,14 +158,14 @@ const Header: React.FC = () => { setModalVisible(false); }; const handleHelpModalConfirm = () => { - sethelpVisible(false); + setHelpVisible(false); }; const handleHelp = () => { - sethelpVisible(true); + setHelpVisible(true); }; const handleModalCancel = () => setModalVisible(false); const handleComplete = () => setModalVisible(true); - const handleHelpModalCancel = () => sethelpVisible(false); + const handleHelpModalCancel = () => setHelpVisible(false); return (