qcast-front/src/hooks/useCanvas.js

599 lines
15 KiB
JavaScript

import { useEffect, useRef, useState } from 'react'
import { fabric } from 'fabric'
import { actionHandler, anchorWrapper, polygonPositionHandler } from '@/util/canvas-util'
import { useRecoilState } from 'recoil'
import { canvasSizeState, fontSizeState } from '@/store/canvasAtom'
import { QLine } from '@/components/fabric/QLine'
import QRect from '@/components/fabric/QRect'
import { QPolygon } from '@/components/fabric/QPolygon'
import { defineQLine } from '@/util/qline-utils'
import { defineQPloygon } from '@/util/qpolygon-utils'
export function useCanvas(id) {
const [canvas, setCanvas] = useState()
const [isLocked, setIsLocked] = useState(false)
const [history, setHistory] = useState([])
const [canvasSize] = useRecoilState(canvasSizeState)
const [fontSize] = useRecoilState(fontSizeState)
const points = useRef([])
/**
* 처음 셋팅
*/
useEffect(() => {
const c = new fabric.Canvas(id, {
height: canvasSize.vertical,
width: canvasSize.horizontal,
backgroundColor: 'white',
selection: false,
})
setCanvas(c)
return () => {
c.dispose()
}
}, [])
useEffect(() => {
// canvas 사이즈가 변경되면 다시
removeEventOnCanvas()
addEventOnCanvas()
}, [canvasSize])
useEffect(() => {
canvas
?.getObjects()
?.filter((obj) => obj.type === 'textbox' || obj.type === 'text' || obj.type === 'i-text')
.forEach((obj) => {
obj.set({ fontSize: fontSize })
})
canvas
?.getObjects()
?.filter((obj) => obj.type === 'QLine' || obj.type === 'QPolygon' || obj.type === 'QRect')
.forEach((obj) => {
obj.setFontSize(fontSize)
})
canvas?.getObjects().length > 0 && canvas?.renderAll()
}, [fontSize])
/**
* 캔버스 초기화
*/
useEffect(() => {
if (canvas) {
initialize()
canvas?.on('object:added', onChange)
canvas?.on('object:modified', onChange)
canvas?.on('object:removed', onChange)
canvas?.on('mouse:move', drawMouseLines)
canvas?.on('mouse:out', removeMouseLines)
}
}, [canvas])
const addEventOnCanvas = () => {
canvas?.on('object:added', onChange)
canvas?.on('object:modified', onChange)
canvas?.on('object:removed', onChange)
canvas?.on('object:added', () => {
document.addEventListener('keydown', handleKeyDown)
})
canvas?.on('mouse:move', drawMouseLines)
canvas?.on('mouse:down', handleMouseDown)
canvas?.on('mouse:out', removeMouseLines)
}
const removeEventOnCanvas = () => {
canvas?.off('object:added')
canvas?.off('object:modified')
canvas?.off('object:removed')
canvas?.off('object:added')
canvas?.off('mouse:move', drawMouseLines)
canvas?.off('mouse:down', handleMouseDown)
}
/**
* 마우스 포인터의 가이드라인을 제거합니다.
*/
const removeMouseLines = () => {
if (canvas?._objects.length > 0) {
const mouseLines = canvas?._objects.filter((obj) => obj.name === 'mouseLine')
mouseLines.forEach((item) => canvas?.remove(item))
}
canvas?.renderAll()
}
const initialize = () => {
canvas.getObjects().length > 0 && canvas?.clear()
// 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
fabric.QLine = QLine
fabric.QPolygon = QPolygon
QPolygon.prototype.canvas = canvas
QLine.prototype.canvas = canvas
QRect.prototype.canvas = canvas
defineQLine()
defineQPloygon()
}
/**
* 캔버스에 도형을 추가한다. 도형은 사용하는 페이지에서 만들어서 파라미터로 넘겨주어야 한다.
*/
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 drawMouseLines = (e) => {
// 현재 마우스 포인터의 위치를 가져옵니다.
const pointer = canvas?.getPointer(e.e)
// 기존에 그려진 가이드라인을 제거합니다.
removeMouseLines()
if (canvas?.getActiveObject()) {
return
}
// 가로선을 그립니다.
const horizontalLine = new fabric.Line([0, pointer.y, canvasSize.horizontal, pointer.y], {
stroke: 'black',
strokeWidth: 1,
selectable: false,
name: 'mouseLine',
})
// 세로선을 그립니다.
const verticalLine = new fabric.Line([pointer.x, 0, pointer.x, canvasSize.vertical], {
stroke: 'black',
strokeWidth: 1,
selectable: false,
name: 'mouseLine',
})
// 선들을 캔버스에 추가합니다.
canvas?.add(horizontalLine, verticalLine)
// 캔버스를 다시 그립니다.
canvas?.renderAll()
}
const handleMouseDown = (e) => {
// 현재 마우스 포인터의 위치를 가져옵니다.
if (canvas?.getActiveObject()) {
points.current = []
return
}
const pointer = canvas?.getPointer(e.e)
// 클릭한 위치를 배열에 추가합니다.
points.current.push(pointer)
// 두 점을 모두 찍었을 때 사각형을 그립니다.
if (points.current.length === 2) {
const rect = new fabric.Rect({
left: points.current[0].x,
top: points.current[0].y,
width: points.current[1].x - points.current[0].x,
height: points.current[1].y - points.current[0].y,
fill: 'transparent',
stroke: 'black',
strokeWidth: 1,
})
// 사각형을 캔버스에 추가합니다.
canvas?.add(rect)
// 배열을 초기화합니다.
points.current = []
}
}
/**
* 눈금 모양에 맞게 움직이도록 한다.
*/
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 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)
}
/**
* 페이지 내 캔버스에 저장한 내용 불러오기
* 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.objectCaching = false
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()
}
/**
* 이미지로 저장하는 함수
* @param {string} title - 저장할 이미지 이름
*/
const saveImage = (title = 'canvas') => {
const dataURL = canvas?.toDataURL('png')
// 이미지 다운로드 링크 생성
const link = document.createElement('a')
link.download = `${title}.png`
link.href = dataURL
// 링크 클릭하여 이미지 다운로드
link.click()
}
const handleFlip = () => {
const target = canvas?.getActiveObject()
if (!target) {
return
}
// 현재 scaleX 및 scaleY 값을 가져옵니다.
const scaleX = target.scaleX
// const scaleY = target.scaleY;
// 도형을 반전시킵니다.
target.set({
scaleX: scaleX * -1,
// scaleY: scaleY * -1
})
// 캔버스를 다시 그립니다.
canvas?.renderAll()
}
function fillCanvasWithDots(canvas, gap) {
const width = canvas.getWidth()
const height = canvas.getHeight()
for (let x = 0; x < width; x += gap) {
for (let y = 0; y < height; y += gap) {
const circle = new fabric.Circle({
radius: 1,
fill: 'black',
left: x,
top: y,
selectable: false,
})
canvas.add(circle)
}
}
canvas?.renderAll()
}
const setCanvasBackgroundWithDots = (canvas, gap) => {
// Create a new canvas and fill it with dots
const tempCanvas = new fabric.StaticCanvas()
tempCanvas.setDimensions({
width: canvas.getWidth(),
height: canvas.getHeight(),
})
fillCanvasWithDots(tempCanvas, gap)
// Convert the dotted canvas to an image
const dataUrl = tempCanvas.toDataURL({ format: 'png' })
// Set the image as the background of the original canvas
fabric.Image.fromURL(dataUrl, function (img) {
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
scaleX: canvas.width / img.width,
scaleY: canvas.height / img.height,
})
})
}
const addCanvas = () => {
// const canvasState = canvas
const objs = canvas?.toJSON([
'selectable',
'name',
'parentId',
'id',
'length',
'idx',
'direction',
'lines',
'points',
'lockMovementX',
'lockMovementY',
'lockRotation',
'lockScalingX',
'lockScalingY',
'opacity',
])
const str = JSON.stringify(objs)
canvas?.clear()
setTimeout(() => {
// 역직렬화하여 캔버스에 객체를 다시 추가합니다.
canvas?.loadFromJSON(JSON.parse(str), function () {
// 모든 객체가 로드되고 캔버스에 추가된 후 호출됩니다.
canvas?.renderAll() // 캔버스를 다시 그립니다.
})
}, 1000)
}
const changeCanvas = (idx) => {
canvas?.clear()
const canvasState = JSON.parse(canvasList[idx])
}
return {
canvas,
addShape,
handleUndo,
handleRedo,
handleCopy,
handleDelete,
handleSave,
handlePaste,
handleRotate,
attachCustomControlOnPolygon,
saveImage,
handleFlip,
setCanvasBackgroundWithDots,
addCanvas,
changeCanvas,
}
}