이정민

Merge branch 'develop'

......@@ -27,3 +27,6 @@ out
node_modules.nosync/
*.env.*
package-lock.json
yarn.lock
\ No newline at end of file
......
This diff could not be displayed because it is too large.
import GIF from "gifencoder";
class GifGenerator {
constructor(canvas) {
this.canvas = canvas;
this.width = canvas.getWidth();
this.height = canvas.getHeight();
this.gif = new GIF(this.width, this.height);
this.gif.start();
this.gif.setTransparent(null);
this.gif.setRepeat(0);
this.gif.setQuality(10);
}
addFrame(delay = 0) {
this.gif.setDelay(delay);
this.gif.addFrame(this.canvas.getContext());
}
render() {
this.gif.finish();
const byte = new Uint8Array(this.gif.out.data);
return new Blob([byte], { type: "image/gif" });
}
}
window.GifGenerator = GifGenerator;
const aws = require('aws-sdk');
const s3 = new aws.S3();
exports.handler = async (event) => {
if(!event['queryStringParameters'] || !event['queryStringParameters']['id']) {
return {
statusCode: 400
}
}
const id = event['queryStringParameters']['id'];
const data = await download(id);
return {
statusCode: 200,
headers:{
"Content-Type":"image/gif"
},
isBase64Encoded:true,
body: data.Body.toString("base64")
}
};
const download = (id) => {
const bucket = 'gif-generator';
const path = `/gif/${id}.gif`;
const params = {
Bucket: bucket,
Key: path
}
return new Promise((resolve, reject) => {
s3.getObject(params, (err, data) => {
if(err){
console.log("download err");
console.log(err);
reject(err);
}else{
console.log("download success");
console.log(data);
resolve(data);
}
});
});
}
\ No newline at end of file
node_modules
dist
\ No newline at end of file
{
"name": "gif-upload",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"aws-sdk": "^2.905.0",
"busboy": "^0.3.1",
"uuid-random": "^1.3.2"
},
"devDependencies": {
"uglifyjs-webpack-plugin": "^2.2.0"
}
}
AWS Lambda 업로드를 위해 사용하는 코드
## bundle
webpack
## usage
aws lambda에서는 npm을 사용할 수 없기 때문에 사용할 npm 모듈을 미리 로드하여 bundle.
사용할 npm 모듈을 import에 넣어 webpack을 이용해 bundle한 후 aws lambda에 import.js 파일 업로드
이후 index.js에서 import.js를 불러와 사용
\ No newline at end of file
module.exports = {
Busboy:require('busboy'),
UUID:require('uuid-random')
}
\ No newline at end of file
const imports = require('./import')['import'];
const Busboy = imports.Busboy;
const UUID = imports.UUID;
const aws = require('aws-sdk');
const s3 = new aws.S3();
exports.handler = async (event, context) => {
if(!event.body || !/^multipart\/form\-data/.test(event.headers['content-type'])) {
return {
statusCode: 400
}
}
const formData = await parse(event);
if(!formData['gif']) {
return {
statusCode: 400
}
}
const id = await upload(formData['gif']);
return {
statusCode: 200,
headers:{
"Content-Type":"json"
},
body: JSON.stringify({
id
}),
}
}
const parse = (event) => new Promise((resolve, reject) => {
const bodyBuffer = new Buffer(event.body.toString(), "base64");
const busboy = new Busboy({
headers: {
'content-type': event.headers['content-type']
}
});
const formData = {};
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
console.log('File [%s]: filename=%j; encoding=%j; mimetype=%j', fieldname, filename, encoding, mimetype);
const chunks = [];
file.on('data', data => {
chunks.push(data);
}).on('end', () => {
formData[fieldname] = {
name:filename,
data:Buffer.concat(chunks),
mimetype:mimetype
};
console.log("File [%s] finished.", filename);
});
});
busboy.on('field', (fieldname, value) => {
console.log("[" + fieldname + "] >> " + value);
formData[fieldname] = value;
});
busboy.on('error', error => {
reject(error);
});
busboy.on('finish', () => {
resolve(formData);
});
busboy.write(bodyBuffer, event.isBase64Encoded ? 'base64' : 'binary');
busboy.end();
});
const upload = ({data, mimetype}) => new Promise((resolve, reject) => {
const bucket = 'gif-generator';
const path = '/gif';
const id = UUID().replace(/\-/g, '');
const fileFullName = path + '/' + id + '.gif';
const params = {
Bucket: bucket,
Key: fileFullName,
Body: data,
ContentType: mimetype
};
s3.upload(params, (err, data) => {
if(err){
console.log("upload err");
console.log(err);
reject(err);
}else{
console.log("upload success");
console.log(data);
resolve(id);
}
});
});
\ No newline at end of file
const path = require('path');
module.exports = {
entry: './src/import.js',
output: {
path: __dirname + '/dist',
filename: 'import.js',
library: 'import',
libraryTarget: 'commonjs2'
},
module: {
rules: [
{
test: /\.js$/,
include: [
path.resolve(__dirname, 'src/js')
],
exclude: /node_modules/
}
]
},
mode: 'development',
devtool:false,
resolve: {
fallback: {
"fs":false,
"stream": require.resolve("stream-browserify")
}
}
};
\ No newline at end of file
# AWS Lambda API
[https://9davbjzey4.execute-api.ap-northeast-2.amazonaws.com](https://9davbjzey4.execute-api.ap-northeast-2.amazonaws.com)
## gif upload
### request
- endpoint : /
- Method : POST
- Content-Type : multipart/form-data
- Body : gif=`<file>`
### response
- 200 : { id: `<id>` }
- 400 : no gif
- 500 : server error
## gif download
### request
- endpoint : /
- Method : GET
- QueryString : id=`<id>`
### response
- 200 : Content-type:image/gif
- 400 : no id
- 404 : not found
- 500 : server error
\ No newline at end of file
......@@ -7,13 +7,16 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@toast-ui/react-image-editor": "3.14.2",
"@types/fabric": "^4.2.5",
"axios": "^0.21.1",
"fabric": "^4.4.0",
"next": "^10.0.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"styled-components": "^5.2.3",
"styled-reset": "^4.3.4",
"tui-image-editor": "3.14.2",
"@toast-ui/react-image-editor": "3.14.2",
"web-vitals": "^1.0.1"
},
"devDependencies": {
......
import axios from "axios";
const baseURL = "https://9davbjzey4.execute-api.ap-northeast-2.amazonaws.com";
export const postGif = async (formData) => {
console.log("file", formData);
const { data } = await axios.post(baseURL, formData);
return data;
};
export const getGif = async (id) => {
const { data } = await axios.get(baseURL, {
params: {
id: id,
},
});
return data;
};
import { useEffect, useRef, useState } from "react";
import styled from "styled-components";
import TuiImageEditor from "tui-image-editor";
import "gif-generator/dist/gif-generator";
import { postGif } from "api";
declare global {
interface Window {
GifGenerator: any;
}
}
const GifEditor = ({ previewURL }) => {
const [imageEditor, setImageEditor] = useState(null);
const rootEl = useRef();
const [download, setDownload] = useState(null);
const [blob, setBlob] = useState(null);
const [isMakeStarted, setIsMakeStarted] = useState(false);
const [isUploadLoading, setIsUploadLoading] = useState(false);
const [viewLink, setViewLink] = useState(null);
useEffect(() => {
if (window) {
setImageEditor(
new TuiImageEditor(rootEl.current, {
includeUI: {
loadImage: {
path: previewURL,
name: "SampleImage",
},
uiSize: {
width: "100%",
height: "600px",
},
menu: ["draw", "text"],
menuBarPosition: "bottom",
},
cssMaxWidth: 500,
cssMaxHeight: 700,
usageStatistics: false,
})
);
}
}, []);
useEffect(() => {
if (imageEditor) {
console.log(imageEditor._graphics.getCanvas().getObjects());
}
}, [imageEditor]);
const makeGif = () => {
setIsMakeStarted(true);
const gifGenerator = new window.GifGenerator(
imageEditor._graphics.getCanvas()
);
gifGenerator.make().then(
(blob) => {
setBlob(blob);
setDownload(window.URL.createObjectURL(blob));
},
(error) => {
alert(error);
}
);
};
const handleUpload = async () => {
setIsUploadLoading(true);
const file = new File([blob], "new_gif.gif");
const formData = new FormData();
formData.append("gif", file);
const res = await postGif(formData);
console.log(res);
setIsUploadLoading(false);
setViewLink(
`https://gif-generator.s3.ap-northeast-2.amazonaws.com//gif/${res.id}.gif`
);
};
return (
<>
<Wrapper>
{((isMakeStarted && !download) || isUploadLoading) && (
<>
<div className="background" />
<div className="download">loading...</div>
</>
)}
{!isUploadLoading && viewLink && (
<div className="download" style={{ zIndex: 200 }}>
<a href={viewLink}>{viewLink}</a>
</div>
)}
{download && !isUploadLoading && (
<>
<div className="background" />
<div className="download">
<div className="download__btn">
<a href={download} download="new_gif.gif">
Download a File
</a>
</div>
<div className="download__btn">
<div onClick={handleUpload}>Upload to Server</div>
</div>
</div>
</>
)}
<div onClick={makeGif} className="make">
Make a Gif
</div>
<div ref={rootEl} />
</Wrapper>
</>
);
};
const Wrapper = styled.div`
position: fixed;
width: 90%;
top: 10rem;
border-radius: 1.5rem;
box-shadow: ${({ theme }) => theme.boxShadow.normal};
display: flex;
flex-direction: column;
align-items: center;
a {
color: black;
text-decoration: none;
}
.make {
font: 800 11.5px Arial;
position: absolute;
right: 0;
top: 0;
width: 120px;
height: 40px;
background: red;
z-index: 10;
border-radius: 20px;
margin: 8px;
background-color: #fdba3b;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: black;
opacity: 0.7;
z-index: 100;
}
.download {
position: absolute;
top: 15rem;
z-index: 100;
display: flex;
background-color: white;
padding: 1.5rem 2rem;
border-radius: 2rem;
&__btn {
cursor: pointer;
:last-child {
margin-left: 1rem;
}
}
}
.tui-image-editor-container {
border-radius: 1.5rem;
}
.tui-image-editor-container .tui-image-editor-help-menu.top {
left: 19rem;
top: 1rem;
}
.tui-image-editor-header-logo {
display: none;
}
.tui-image-editor-header-buttons {
display: none;
}
`;
export default GifEditor;
import dynamic from "next/dynamic";
import { useState } from "react";
import styled from "styled-components";
const ToastEditor = dynamic(() => import("components/ToastEditor"), {
ssr: false,
});
const Image = ({ previewURL, setPreviewURL }) => {
const [file, setFile] = useState(undefined);
console.log("previewURL", previewURL);
// const uploadImage = (file) => {
// if (!file) {
// return;
// }
// };
// const selectImg = (e) => {
// const reader = new FileReader();
// const targetFile = e.target.files[0];
// setFile(targetFile);
// // uploadImage(targetFile);
// reader.onloadend = () => {
// setPreviewURL(reader.result);
// };
// reader.readAsDataURL(targetFile);
// };
// const [isEditorOpened, setIsEditorOpened] = useState(false);
// const handleEditor = () => {
// setIsEditorOpened(true);
// };
return (
<>
<Container>
<ImgBox>
{/* <div onClick={handleEditor}>asdf</div> */}
{/* {file === undefined ? ( */}
<>
{/* <div className="sub-flex">
<BlankBox />
<div>Click to add a photo</div>
<input
type="file"
style={{
position: "absolute",
top: 0,
paddingLeft: 0,
zIndex: 0,
width: "90%",
height: "100%",
border: "none",
cursor: "pointer",
outline: "none",
}}
onChange={selectImg}
/>
</div>
<div className="sub-flex">Open Image Editor</div> */}
</>
{/* ) : ( */}
<img
id="image"
alt={""}
style={{
objectFit: "cover",
display: "flex",
maxHeight: "90%",
maxWidth: "90%",
}}
src={previewURL as string}
/>
{/* )} */}
</ImgBox>
{/* <Menu /> */}
</Container>
{/* {isEditorOpened && <ToastEditor {...{ setPreviewURL, setIsImgAdded }} />} */}
</>
);
};
const Menu = () => {
return (
<div style={{ width: "15rem", marginLeft: "2rem" }}>
<Box />
<Box />
<Box />
<Box />
</div>
);
};
const Container = styled.div`
width: 100%;
display: flex;
justify-content: center;
margin-top: 10rem;
`;
const ImgBox = styled.div`
position: relative;
width: 90%;
/* height: 30rem; */
background-color: white;
box-shadow: ${({ theme }) => theme.boxShadow.normal};
border-radius: 2rem;
margin-top: 2rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
display: flex;
/* flex: 0.6; */
padding: 1rem 0;
/* .sub-flex {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
:first-child {
border-right: 1px solid ${({ theme }) => theme.color.gray};
}
} */
`;
const Box = styled.div`
width: 100%;
height: 10rem;
margin-top: 2rem;
border-radius: 1rem;
background-color: white;
box-shadow: ${({ theme }) => theme.boxShadow.normal};
`;
const BlankBox = styled.div`
z-index: 1;
position: absolute;
top: 0;
width: 90%;
height: 50px;
background-color: white;
`;
export default Image;
/// <reference path="react-image-editor.d.ts" />
import ImageEditor from "@toast-ui/react-image-editor";
import { useEffect, useState } from "react";
import { useState } from "react";
import styled from "styled-components";
import "tui-image-editor/dist/tui-image-editor.css";
const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => {
// const [lowerCanvas, setLowerCanvas] = useState<HTMLCanvasElement>();
// const [upperCanvas, setUpperCanvas] = useState<HTMLCanvasElement>();
// // console.log(
// // document.getElementsByClassName("lower-canvas")[0]?.toDataURL("image/png")
// // );
// console.log("s");
// // const [upperCanvas, setUpperCanvas] = useState(
// // document.getElementsByClassName("upper-canvas ")[0]
// // );
// useEffect(() => {
// window?.addEventListener("click", () => {
// setLowerCanvas(
// document.getElementsByClassName("lower-canvas")[0] as HTMLCanvasElement
// );
// setUpperCanvas(
// document.getElementsByClassName("upper-canvas")[0] as HTMLCanvasElement
// );
// });
// }, []);
// useEffect(() => {
// const img = lowerCanvas?.toDataURL("image/png");
// const uploaded = document.getElementById("image");
// console.log(uploaded);
// // let w = window.open();
// // if (w?.window) w.document.body.innerHTML = "<img src='" + img + "'>";
// const image = new Image();
// // image.onload = function () {
// // lowerCanvas.width = uploaded.clientWidth;
// // lowerCanvas.height = uploaded.clientHeight;
// // lowerCanvas?.getContext("2d").drawImage(image, 0, 0);
// // };
// image.src = previewURL;
// console.log("b");
// if (lowerCanvas?.getContext&&upperCanvas?.getContext) {
// image.onload = function () {
// lowerCanvas.width = 1000;
// lowerCanvas.height = 572;
// upperCanvas.width = 1000;
// upperCanvas.height = 572;
// lowerCanvas?.getContext("2d").drawImage(image, 0, 0);
// };
// console.log(lowerCanvas.getContext("2d"));
// }
// }, [lowerCanvas?.toDataURL("image/png")]);
const [alertIsShown, setAlertIsShown] = useState(false);
const handleEnd = () => {
const lowerCanvas = document.getElementsByClassName(
"lower-canvas"
)[0] as HTMLCanvasElement;
if (
lowerCanvas.toDataURL("image/png") ===
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAEYklEQVR4Xu3UAQkAAAwCwdm/9HI83BLIOdw5AgQIRAQWySkmAQIEzmB5AgIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlAABg+UHCBDICBisTFWCEiBgsPwAAQIZAYOVqUpQAgQMlh8gQCAjYLAyVQlKgIDB8gMECGQEDFamKkEJEDBYfoAAgYyAwcpUJSgBAgbLDxAgkBEwWJmqBCVAwGD5AQIEMgIGK1OVoAQIGCw/QIBARsBgZaoSlACBB1YxAJfjJb2jAAAAAElFTkSuQmCC"
) {
setAlertIsShown(true);
setTimeout(() => {
setAlertIsShown(false);
}, 1000);
} else {
setPreviewURL(lowerCanvas.toDataURL("image/png"));
console.log("asdf");
setIsImgAdded(true);
setIsEditorOpened(false);
}
};
return (
<Container>
<div onClick={handleEnd} className="upload">
Upload
<div onClick={handleEnd} className="move">
Move to Gif
</div>
<ImageEditor
includeUI={{
loadImage: {
// path: 'img/sampleImage.jpg',
name: "SampleImage",
},
// theme: myTheme,
menu: ["shape", "filter"],
initMenu: "filter",
uiSize: {
width: "100%",
height: "700px",
height: "600px",
},
menuBarPosition: "bottom",
}}
......@@ -92,6 +50,9 @@ const ToastEditor = ({ setPreviewURL, setIsImgAdded, setIsEditorOpened }) => {
}}
usageStatistics={true}
/>
<div className="alert" style={{ opacity: alertIsShown ? 1 : 0 }}>
Please select a photo.
</div>
</Container>
);
};
......@@ -105,7 +66,7 @@ const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
.upload {
.move {
font: 800 11.5px Arial;
position: absolute;
right: 0;
......@@ -122,11 +83,21 @@ const Container = styled.div`
justify-content: center;
cursor: pointer;
}
.alert {
position: fixed;
border-radius: 0.5rem;
transition: 1s;
top: 7rem;
}
.tui-image-editor-container {
border-radius: 1.5rem;
}
.tui-image-editor-container .tui-image-editor-help-menu.top {
top: 2rem;
left: 19rem;
top: 1rem;
}
.tui-image-editor-header-logo {
display: none;
}
`;
......
node_modules
dist/*
package-lock.json
yarn.lock
\ No newline at end of file
......
......@@ -59,10 +59,37 @@
imageEditor.ui.resizeEditor();
};
console.log("imageeiasdfasdf", imageEditor);
let gifGenerator;
setTimeout(function () {
gifGenerator = new GifGenerator(imageEditor._graphics.getCanvas());
gifGenerator.on("progress", (p) => console.log(p));
}, 1000);
function render() {
gifGenerator.make().then(
(blob) => {
window.open(window.URL.createObjectURL(blob));
},
(error) => {
alert(error);
}
);
}
</script>
<button
style="
position: absolute;
top: 70px;
right: 70px;
border: 1px solid #fff;
background: rgba(0, 0, 0, 0);
color: #fff;
padding: 10px 20px;
"
onClick="render();"
>
GIF 생성
</button>
</body>
</html>
......
......@@ -20,7 +20,9 @@
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@dhdbstjr98/gif.js": "^1.0.0",
"gifencoder": "^2.0.1",
"hangul-js": "^0.2.6",
"stream": "0.0.2"
}
}
......
import ComponentInterface from "./ComponentInterface";
import Color from "./Color";
import { fabric } from "fabric";
class Brush extends ComponentInterface {
constructor(fabricObj) {
super(fabricObj.path.length);
this.color = new Color(fabricObj.stroke);
this.paths = fabricObj.path;
this.size = fabricObj.strokeWidth;
}
getCurrentFabricObject() {
const paths = this.paths.filter((_, i) => i < this.state.current);
if (paths.length > 0) {
const popCount = paths[paths.length - 1].length - 3;
for (let i = 0; i < popCount; i++) {
paths[paths.length - 1].pop();
}
paths[paths.length - 1][0] = "L";
}
return new fabric.Path(paths, {
stroke: this.color.getRgba(),
strokeWidth: this.size,
fill: null,
strokeLineCap: "round",
strokeLineJoin: "round",
});
}
next() {
return this.addState(30);
}
}
export default Brush;
class Color {
constructor(colorData) {
if (colorData[0] == "#") {
this.r = parseInt(colorData.substring(1, 3), 16);
this.g = parseInt(colorData.substring(3, 5), 16);
this.b = parseInt(colorData.substring(5, 7), 16);
this.a = 1;
} else {
const rgba = colorData.substring(5, colorData.length - 1).split(",");
this.r = parseInt(rgba[0]);
this.g = parseInt(rgba[1]);
this.b = parseInt(rgba[2]);
this.a = parseFloat(rgba[3]);
}
}
getColorCode() {
return `#${this.r.toString(16)}${this.g.toString(16)}${this.b.toString(
16
)}`;
}
getRgba() {
return `rgba(${this.r},${this.g},${this.b},${this.a})`;
}
}
export default Color;
class ComponentInterface {
constructor(maxState) {
this.state = {
current: 0,
max: maxState,
};
}
getCurrentFabricObject() {}
addState(count = 1) {
if (this.state.current == this.state.max) {
this.state.current++;
} else {
this.state.current = Math.min(this.state.current + count, this.state.max);
}
}
end() {
return this.state.current == this.state.max + 1;
}
}
export default ComponentInterface;
import ComponentInterface from "./ComponentInterface";
import Color from "./Color";
import Hangul from "hangul-js";
import { fabric } from "fabric";
class Text extends ComponentInterface {
constructor(fabricObj) {
const textSplited = Hangul.disassemble(fabricObj.text);
super(textSplited.length);
this.color = new Color(fabricObj.fill);
this.text = {
plain: fabricObj.text,
splited: textSplited,
};
this.position = {
top:
fabricObj.originY == "center"
? fabricObj.top - fabricObj.height / 2
: fabricObj.top,
left:
fabricObj.originX == "center"
? fabricObj.left - fabricObj.width / 2
: fabricObj.left,
};
this.font = {
size: fabricObj.fontSize,
style: fabricObj.fontStyle,
weight: fabricObj.fontWeight,
family: fabricObj.fontFamily,
};
}
getCurrentFabricObject() {
return new fabric.Text(
Hangul.assemble(
this.text.splited.filter((_, i) => i < this.state.current)
),
{
top: this.position.top,
left: this.position.left,
originX: "left",
originY: "top",
fontFamily: this.font.family,
fontSize: this.font.size,
fontWeight: this.font.weight,
fontStyle: this.font.style,
fill: this.color.getColorCode(),
}
);
}
next() {
return this.addState();
}
}
export default Text;
import Brush from "./Brush";
import Text from "./Text";
const Component = {
Brush,
Text,
};
export default Component;
import GIF from "@dhdbstjr98/gif.js";
import { fabric } from "fabric";
import Component from "./components";
export class GifGenerator {
constructor(canvas) {
this.canvas = canvas;
this.width = canvas.getWidth();
this.height = canvas.getHeight();
this.events = {};
this._initializeGif();
}
_initializeGif() {
this.gif = new GIF({
width: this.width,
height: this.height,
transparent: null,
repeat: 0,
setQuality: 10,
});
Object.keys(this.events).map((event) => {
this.events[event].map((callback) => {
this.gif.on(event, callback);
});
});
}
_addFrame(delay = 0) {
this.gif.addFrame(this.canvas.getContext(), { delay, copy: true });
}
_render(callback) {
this.gif.on("finished", (blob) => {
callback(blob);
});
this.gif.render();
}
on(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}
make() {
this._initializeGif();
const fabricObjs = this.canvas.getObjects();
const objs = [];
fabricObjs.map((fabricObj) => {
if (fabricObj.path !== undefined) {
objs.push(new Component.Brush(fabricObj));
this.canvas.remove(fabricObj);
} else if (fabricObj.text !== undefined) {
objs.push(new Component.Text(fabricObj));
this.canvas.remove(fabricObj);
}
});
return new Promise((resolve, reject) => {
if (objs.length > 0) {
let objIdx = 0;
let isAddMode = true;
const draw = () => {
const obj = objs[objIdx];
if (isAddMode) {
const fabricObj = obj.getCurrentFabricObject();
obj.next();
isAddMode = false;
this.canvas.add(fabricObj);
} else {
this._addFrame(1);
isAddMode = true;
if (obj.end()) {
if (objIdx < objs.length - 1) {
objIdx++;
draw();
} else {
this.canvas.off("after:render", draw);
this._render(resolve);
}
} else {
this.canvas.remove(
this.canvas._objects[this.canvas._objects.length - 1]
);
}
}
};
this.canvas.on("after:render", draw);
draw();
} else {
reject(new Error("no objects"));
}
});
}
}
window.GifGenerator = GifGenerator;
import { useRouter } from "next/dist/client/router";
const Detail = () => {
const id = useRouter().query.id;
return (
<div>
<img
src={`https://9davbjzey4.execute-api.ap-northeast-2.amazonaws.com/?id=${id}`}
/>
</div>
);
};
export default Detail;
import Header from "components/Header";
import Image from "components/Image";
import styled from "styled-components";
import dynamic from "next/dynamic";
import { useState } from "react";
......@@ -7,6 +6,9 @@ import { useState } from "react";
const ToastEditor = dynamic(() => import("components/ToastEditor"), {
ssr: false,
});
const GifEditor = dynamic(() => import("components/GifEditor"), {
ssr: false,
});
const Index = () => {
const [isEditorOpened, setIsEditorOpened] = useState(false);
......@@ -35,19 +37,7 @@ const Index = () => {
</button>
</div>
) : (
!isEditorOpened && (
<>
<div style={{ position: "fixed", top: "5rem" }}>
<button
className="open-button"
onClick={() => setIsEditorOpened(true)}
>
Change Image
</button>
</div>
<Image {...{ previewURL, setPreviewURL }} />
</>
)
!isEditorOpened && <GifEditor {...{ previewURL }} />
)}
{isEditorOpened && (
<ToastEditor {...{ setPreviewURL, setIsImgAdded, setIsEditorOpened }} />
......
......@@ -1367,6 +1367,11 @@
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b"
integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==
"@types/fabric@^4.2.5":
version "4.2.5"
resolved "https://registry.yarnpkg.com/@types/fabric/-/fabric-4.2.5.tgz#095e412b49c896e2fa0d441bd2fceb6690074c4d"
integrity sha512-nifrrxvgsYRoxNJB+xZUBe6pLWoqGbZdfwJ0y9zzdt3uU89SzP+8L9Whwrxbvu7eIXjPArSZOyuQmbD4zewdZA==
"@types/hoist-non-react-statics@*":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
......@@ -1851,6 +1856,13 @@ axe-core@^4.0.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.2.tgz#7cf783331320098bfbef620df3b3c770147bc224"
integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg==
axios@^0.21.1:
version "0.21.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
dependencies:
follow-redirects "^1.10.0"
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
......@@ -3086,7 +3098,7 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
fabric@^4.2.0:
fabric@^4.2.0, fabric@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/fabric/-/fabric-4.4.0.tgz#2b73454008b8082f2d234c4637bdf645876c490c"
integrity sha512-mX6BZqssJjrT6LN1B4Wcmgm93NIlmKfPN5qTqon9wdDJgRAxPfrhfz2iT+QmDso9P8+s0qyLXFhuVpxOBBMHEw==
......@@ -3179,6 +3191,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
follow-redirects@^1.10.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
foreach@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
......