initiail commit

This commit is contained in:
yoosangwook 2024-06-17 14:29:49 +09:00
commit be0520ef42
24 changed files with 4290 additions and 0 deletions

39
.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.idea
.vscode

10
.prettierrc Normal file
View File

@ -0,0 +1,10 @@
{
"singleQuote": true,
"semi": false,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80,
"arrowParens": "always",
"endOfLine": "auto"
}

36
README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

7
jsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}

15
next.config.mjs Normal file
View File

@ -0,0 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// reactStrictMode: false,
webpack: (config) => {
config.externals.push({
"utf-8-validate": "commonjs utf-8-validate",
bufferutil: "commonjs bufferutil",
canvas: "commonjs canvas",
});
// config.infrastructureLogging = { debug: /PackFileCache/ };
return config;
},
};
export default nextConfig;

1737
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "q.cast.prototype-nontype",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"fabric": "^5.3.0",
"next": "14.2.3",
"react": "^18",
"react-dom": "^18",
"uuid": "^9.0.1"
},
"devDependencies": {
"postcss": "^8",
"tailwindcss": "^3.4.1"
}
}

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

33
src/app/globals.css Normal file
View File

@ -0,0 +1,33 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* :root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
} */

15
src/app/intro/page.jsx Normal file
View File

@ -0,0 +1,15 @@
'use client'
import Hero from '@/components/Hero'
import Intro from '@/components/Intro'
export default function IntroPage() {
return (
<>
<Hero title="Drawing on canvas 2D Intro" />
<div className="container flex flex-wrap items-center justify-between mx-auto p-4 m-4 border">
<Intro />
</div>
</>
)
}

21
src/app/layout.js Normal file
View File

@ -0,0 +1,21 @@
import { Inter } from 'next/font/google'
import './globals.css'
import Headers from '@/components/Headers'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
<Headers />
{children}
</body>
</html>
)
}

5
src/app/page.js Normal file
View File

@ -0,0 +1,5 @@
import Hero from '@/components/Hero'
export default function Home() {
return <Hero title="Q.CAST III - Prototype" />
}

15
src/app/roof/page.jsx Normal file
View File

@ -0,0 +1,15 @@
'use client'
import Hero from '@/components/Hero'
import Roof from '@/components/Roof'
export default function RoofPage() {
return (
<>
<Hero title="Drawing on canvas 2D Roof" />
<div className="flex flex-col justify-center my-8">
<Roof />
</div>
</>
)
}

View File

@ -0,0 +1,75 @@
/**
* Collection of utils to use on canvas
*/
// define a function that can locate the controls
export function polygonPositionHandler(dim, finalMatrix, fabricObject) {
// @ts-ignore
let x = fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x
// @ts-ignore
let y = fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y
return fabric.util.transformPoint(
{ x, y },
fabric.util.multiplyTransformMatrices(
fabricObject.canvas.viewportTransform,
fabricObject.calcTransformMatrix(),
),
)
}
function getObjectSizeWithStroke(object) {
let stroke = new fabric.Point(
object.strokeUniform ? 1 / object.scaleX : 1,
object.strokeUniform ? 1 / object.scaleY : 1,
).multiply(object.strokeWidth)
return new fabric.Point(object.width + stroke.x, object.height + stroke.y)
}
// define a function that will define what the control does
export function actionHandler(eventData, transform, x, y) {
let polygon = transform.target,
currentControl = polygon.controls[polygon.__corner],
mouseLocalPosition = polygon.toLocalPoint(
new fabric.Point(x, y),
'center',
'center',
),
polygonBaseSize = getObjectSizeWithStroke(polygon),
size = polygon._getTransformedDimensions(0, 0)
polygon.points[currentControl.pointIndex] = {
x:
(mouseLocalPosition.x * polygonBaseSize.x) / size.x +
polygon.pathOffset.x,
y:
(mouseLocalPosition.y * polygonBaseSize.y) / size.y +
polygon.pathOffset.y,
}
return true
}
// define a function that can keep the polygon in the same position when we change its width/height/top/left
export function anchorWrapper(anchorIndex, fn) {
return function (eventData, transform, x, y) {
let fabricObject = transform.target
let originX =
fabricObject?.points[anchorIndex].x - fabricObject.pathOffset.x
let originY = fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y
let absolutePoint = fabric.util.transformPoint(
{
x: originX,
y: originY,
},
fabricObject.calcTransformMatrix(),
)
let actionPerformed = fn(eventData, transform, x, y)
let newDim = fabricObject._setPositionDimensions({})
let polygonBaseSize = getObjectSizeWithStroke(fabricObject)
let newX =
(fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) /
polygonBaseSize.x
let newY =
(fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) /
polygonBaseSize.y
fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5)
return actionPerformed
}
}

View File

@ -0,0 +1,17 @@
import Link from 'next/link'
export default function Headers() {
return (
<div className="w-full absolute z-10">
<nav className="container relative flex flex-wrap items-center justify-between mx-auto p-8">
<Link href="/" className="font-bold text-3xl">
Home
</Link>
<div className="space-x-4 text-xl">
<Link href="/intro">Intro</Link>
<Link href="/roof">Roof</Link>
</div>
</nav>
</div>
)
}

7
src/components/Hero.jsx Normal file
View File

@ -0,0 +1,7 @@
export default function Hero(props) {
return (
<div className="pt-48 flex justify-center">
<h1 className="text-6xl">{props.title}</h1>
</div>
)
}

8
src/components/Intro.jsx Normal file
View File

@ -0,0 +1,8 @@
export default function Intro() {
return (
<>
<h1>Intro</h1>
<p>Drawing on canvas 2D is a simple way to create graphics on the web.</p>
</>
)
}

198
src/components/Roof.jsx Normal file
View File

@ -0,0 +1,198 @@
import { useCanvas } from '@/hooks/useCanvas'
import { fabric } from 'fabric'
import { v4 as uuidv4 } from 'uuid'
export default function Roof() {
const {
canvas,
addShape,
handleUndo,
handleRedo,
handleClear,
handleCopy,
handleDelete,
handleSave,
handlePaste,
handleRotate,
attachCustomControlOnPolygon,
} = useCanvas('canvas')
const addRect = () => {
const rect = new fabric.Rect({
height: 200,
width: 200,
top: 10,
left: 10,
opacity: 0.4,
fill: randomColor(),
stroke: 'red',
name: uuidv4(),
})
addShape(rect)
}
const addHorizontalLine = () => {
/**
* 시작X,시작Y,도착X,도착Y 좌표
*/
const horizontalLine = new fabric.Line([20, 20, 100, 20], {
name: uuidv4(),
stroke: 'red',
strokeWidth: 3,
selectable: true,
})
addShape(horizontalLine)
}
const addVerticalLine = () => {
const verticalLine = new fabric.Line([10, 10, 10, 100], {
name: uuidv4(),
stroke: 'red',
strokeWidth: 3,
selectable: true,
})
addShape(verticalLine)
}
const addTriangle = () => {
const triangle = new fabric.Triangle({
name: uuidv4(),
top: 50,
left: 50,
width: 100,
stroke: randomColor(),
strokeWidth: 3,
})
addShape(triangle)
}
const addTrapezoid = () => {
const trapezoid = new fabric.Polygon(
[
{ x: 100, y: 100 }, //
{ x: 300, y: 100 }, //
{ x: 250, y: 200 }, //
{ x: 150, y: 200 }, //
],
{
name: uuidv4(),
stroke: 'red',
opacity: 0.4,
strokeWidth: 3,
selectable: true,
},
)
attachCustomControlOnPolygon(trapezoid)
addShape(trapezoid)
}
const randomColor = () => {
return '#' + Math.round(Math.random() * 0xffffff).toString(16)
}
return (
<>
<div className="flex justify-center my-8">
<div className="flex justify-center my-8">
<p>
ctrl을 누른 채로 클릭하면 점이 생성되고 점을 하나 만들면 선이
생성됩니다.
</p>
</div>
<button
className="w-30 mx-2 p-2 rounded bg-blue-500 text-white"
onClick={addRect}
>
ADD RECTANGLE
</button>
<button
className="w-30 mx-2 p-2 rounded bg-blue-500 text-white"
onClick={addHorizontalLine}
>
ADD HORIZONTAL LINE
</button>
<button
className="w-30 mx-2 p-2 rounded bg-blue-500 text-white"
onClick={addVerticalLine}
>
ADD VERTICALITY LINE
</button>
<button
className="w-30 mx-2 p-2 rounded bg-blue-500 text-white"
onClick={addTriangle}
>
ADD TRIANGLE
</button>
<button
className="w-30 mx-2 p-2 rounded bg-blue-500 text-white"
onClick={addTrapezoid}
>
ADD TRAPEZOID
</button>
<button
className="w-30 mx-2 p-2 rounded bg-black text-white"
onClick={handleCopy}
>
COPY shape
</button>
<button
className="w-30 mx-2 p-2 rounded bg-red-500 text-white"
onClick={handleDelete}
>
DELETE
</button>
<button
className="w-30 mx-2 p-2 rounded bg-red-500 text-white"
onClick={handleClear}
>
CLEAR
</button>
<button
className="w-30 mx-2 p-2 rounded bg-green-500 text-white"
onClick={handleUndo}
>
UNDO
</button>
<button
className="w-30 mx-2 p-2 rounded bg-green-300 text-white"
onClick={handleRedo}
>
REDO
</button>
<button
className="w-30 mx-2 p-2 rounded bg-black text-white"
onClick={handleSave}
>
저장
</button>
<button
className="w-30 mx-2 p-2 rounded bg-black text-white"
onClick={handlePaste}
>
붙여넣기
</button>
<button
className="w-30 mx-2 p-2 rounded bg-black text-white"
onClick={() => handleRotate()}
>
45 회전
</button>
</div>
<div
className="flex justify-center"
style={{
border: '1px solid',
width: 1000,
height: 1000,
margin: 'auto',
}}
>
<canvas id="canvas" />
</div>
</>
)
}

397
src/hooks/useCanvas.js Normal file
View File

@ -0,0 +1,397 @@
import { useEffect, useRef, useState } from 'react'
import { fabric } from 'fabric'
import {
actionHandler,
anchorWrapper,
polygonPositionHandler,
} from '@/app/util/canvas-util'
const CANVAS = {
WIDTH: 1000,
HEIGHT: 1000,
}
export function useCanvas(id) {
const [canvas, setCanvas] = useState()
const [isLocked, setIsLocked] = useState(false)
const [history, setHistory] = useState([])
const connectMode = useRef(false)
const points = useRef([])
/**
* 처음 셋팅
*/
useEffect(() => {
const c = new fabric.Canvas(id, {
height: CANVAS.HEIGHT,
width: CANVAS.WIDTH,
})
// settings for all canvas in the app
fabric.Object.prototype.transparentCorners = false
fabric.Object.prototype.cornerColor = '#2BEBC8'
fabric.Object.prototype.cornerStyle = 'rect'
fabric.Object.prototype.cornerStrokeColor = '#2BEBC8'
fabric.Object.prototype.cornerSize = 6
setCanvas(c)
return () => {
c.dispose()
}
}, [])
/**
* 캔버스 초기화
*/
useEffect(() => {
if (canvas) {
initialize()
canvas.on('object:added', onChange)
canvas.on('object:added', () => {
document.addEventListener('keydown', handleKeyDown)
})
canvas.on('object:modified', onChange)
canvas.on('object:removed', onChange)
}
}, [canvas])
/**
* 눈금 그리기
*/
const initialize = () => {
canvas?.clear()
const width = canvas?.getWidth()
const height = canvas?.getHeight()
let startX = 0
let startY = 0
while (startX <= width) {
startX += 10
const verticalLine = new fabric.Line([startX, 0, startX, height], {
name: 'defaultLine',
stroke: 'black',
strokeWidth: 1,
selectable: false,
opacity: 0.5,
})
addShape(verticalLine)
}
while (startY <= height) {
startY += 10
const verticalLine = new fabric.Line([0, startY, width, startY], {
name: 'defaultLine',
stroke: 'black',
strokeWidth: 1,
selectable: false,
opacity: 0.5,
})
addShape(verticalLine)
}
}
/**
* 캔버스에 도형을 추가한다. 도형은 사용하는 페이지에서 만들어서 파라미터로 넘겨주어야 한다.
*/
const addShape = (shape) => {
canvas?.add(shape)
canvas?.setActiveObject(shape)
canvas?.requestRenderAll()
}
const onChange = (e) => {
const target = e.target
if (target) {
settleDown(target)
}
if (!isLocked) {
setHistory([])
}
setIsLocked(false)
}
/**
* 눈금 모양에 맞게 움직이도록 한다.
*/
const settleDown = (shape) => {
const left = Math.round(shape?.left / 10) * 10
const top = Math.round(shape?.top / 10) * 10
shape?.set({ left: left, top: top })
}
/**
* redo, undo가 필요한 곳에서 사용한다.
*/
const handleUndo = () => {
if (canvas) {
if (canvas._objects.length > 0) {
const poppedObject = canvas._objects.pop()
setHistory((prev) => {
if (prev === undefined) {
return poppedObject ? [poppedObject] : []
}
return poppedObject ? [...prev, poppedObject] : prev
})
canvas.renderAll()
}
}
}
const handleRedo = () => {
if (canvas && history) {
if (history.length > 0) {
setIsLocked(true)
canvas.add(history[history.length - 1])
const newHistory = history.slice(0, -1)
setHistory(newHistory)
}
}
}
/**
* 해당 캔버스를 비운다.
*/
const handleClear = () => {
canvas?.clear()
initialize()
}
/**
* 선택한 도형을 복사한다.
*/
const handleCopy = () => {
const activeObjects = canvas?.getActiveObjects()
if (activeObjects?.length === 0) {
return
}
activeObjects?.forEach((obj) => {
obj.clone((cloned) => {
cloned.set({ left: obj.left + 10, top: obj.top + 10 })
addShape(cloned)
})
})
}
/**
* 선택한 도형을 삭제한다.
*/
const handleDelete = () => {
const targets = canvas?.getActiveObjects()
if (targets?.length === 0) {
alert('삭제할 대상을 선택해주세요.')
return
}
if (!confirm('정말로 삭제하시겠습니까?')) {
return
}
targets?.forEach((target) => {
canvas?.remove(target)
})
}
/**
* 페이지 캔버스 저장
* todo : 현재는 localStorage에 그냥 저장하지만 나중에 변경해야함
*/
const handleSave = () => {
const objects = canvas?.getObjects()
if (objects?.length === 0) {
alert('저장할 대상이 없습니다.')
return
}
const jsonStr = JSON.stringify(canvas)
localStorage.setItem('canvas', jsonStr)
handleClear()
}
/**
* 페이지 캔버스에 저장한 내용 불러오기
* todo : 현재는 localStorage에 그냥 저장하지만 나중에 변경해야함
*/
const handlePaste = () => {
const jsonStr = localStorage.getItem('canvas')
if (!jsonStr) {
alert('붙여넣기 할 대상이 없습니다.')
return
}
canvas?.loadFromJSON(JSON.parse(jsonStr), () => {
localStorage.removeItem('canvas')
console.log('paste done')
})
}
const moveDown = () => {
const targetObj = canvas?.getActiveObject()
if (!targetObj) {
return
}
let top = targetObj.top + 10
if (top > CANVAS.HEIGHT) {
top = CANVAS.HEIGHT
}
targetObj.set({ top: top })
canvas?.renderAll()
}
const moveUp = () => {
const targetObj = canvas?.getActiveObject()
if (!targetObj) {
return
}
let top = targetObj.top - 10
if (top < 0) {
top = 0
}
targetObj.set({ top: top })
canvas?.renderAll()
}
const moveLeft = () => {
const targetObj = canvas?.getActiveObject()
if (!targetObj) {
return
}
let left = targetObj.left - 10
if (left < 0) {
left = 0
}
targetObj.set({ left: left })
canvas?.renderAll()
}
const moveRight = () => {
const targetObj = canvas?.getActiveObject()
if (!targetObj) {
return
}
let left = targetObj.left + 10
if (left > CANVAS.WIDTH) {
left = CANVAS.WIDTH
}
targetObj.set({ left: left })
canvas?.renderAll()
}
/**
* 각종 키보드 이벤트
* https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
*/
const handleKeyDown = (e) => {
const key = e.key
switch (key) {
case 'Delete':
case 'Backspace':
handleDelete()
break
case 'Down': // IE/Edge에서 사용되는 값
case 'ArrowDown':
// "아래 화살표" 키가 눌렸을 때의 동작입니다.
moveDown()
break
case 'Up': // IE/Edge에서 사용되는 값
case 'ArrowUp':
// "위 화살표" 키가 눌렸을 때의 동작입니다.
moveUp()
break
case 'Left': // IE/Edge에서 사용되는 값
case 'ArrowLeft':
// "왼쪽 화살표" 키가 눌렸을 때의 동작입니다.
moveLeft()
break
case 'Right': // IE/Edge에서 사용되는 값
case 'ArrowRight':
// "오른쪽 화살표" 키가 눌렸을 때의 동작입니다.
moveRight()
break
case 'Enter':
// "enter" 또는 "return" 키가 눌렸을 때의 동작입니다.
break
case 'Esc': // IE/Edge에서 사용되는 값
case 'Escape':
break
default:
return // 키 이벤트를 처리하지 않는다면 종료합니다.
}
e.preventDefault()
}
const handleRotate = (degree = 45) => {
const target = canvas?.getActiveObject()
if (!target) {
return
}
const currentAngle = target.angle
target.set({ angle: currentAngle + degree })
canvas?.renderAll()
}
/**
* Polygon 타입만 가능
* 생성한 polygon을 넘기면 해당 polygon은 꼭지점으로 컨트롤 가능한 polygon이
*/
const attachCustomControlOnPolygon = (poly) => {
const lastControl = poly.points?.length - 1
poly.cornerStyle = 'rect'
poly.cornerColor = 'rgba(0,0,255,0.5)'
poly.controls = poly.points.reduce(function (acc, point, index) {
acc['p' + index] = new fabric.Control({
positionHandler: polygonPositionHandler,
actionHandler: anchorWrapper(
index > 0 ? index - 1 : lastControl,
actionHandler,
),
actionName: 'modifyPolygon',
pointIndex: index,
})
return acc
}, {})
poly.hasBorders = !poly.edit
canvas?.requestRenderAll()
}
return {
canvas,
addShape,
handleUndo,
handleRedo,
handleClear,
handleCopy,
handleDelete,
handleSave,
handlePaste,
handleRotate,
attachCustomControlOnPolygon,
}
}

18
tailwind.config.js Normal file
View File

@ -0,0 +1,18 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};

1605
yarn.lock Normal file

File diff suppressed because it is too large Load Diff