이번 포스팅에는 기존에 canvas를 resize하는 방법이 아닌 canvas의 일부를 잘라서 적용하는 crop 기능을 만들어보도록하자
이번에도 동일하게 클래스로 생성하지만 전역변수와 함수로도 충분히 구현할 수 있다
base64로 인코딩된 문자열을 불러올 수도 있지만 이번에는 svg태그에서 문자열을 추출하여 canvas에 예제이미지를 넣어보도록 하자
사용된 이미지는 다음과같다
<svg id="svgImage" width="1707" height="1067" viewBox="0 0 1707 1067">
<rect width="1706.67" height="1066.67" fill="#6996C0"/>
<path d="M684.933 261.867C677.333 264 669.867 269.6 666.4 276C664.533 279.467 664 282.533 664.4 288.4C665.2 301.867 672.933 308.4 695.333 314.667C705.867 317.733 710.8 319.733 713.067 322.133C716.667 326.267 716.8 328 713.2 332.533C710.8 335.467 709.467 336 703.2 336C697.2 336 695.333 335.467 692.533 332.667C690.8 330.933 688.933 327.467 688.4 325.2L687.6 320.933L678.8 321.867C674 322.267 667.733 322.667 664.933 322.667C660.133 322.667 660 322.8 660.8 326.267C664.8 344.933 675.6 353.067 697.333 354.4C725.6 356 742.8 344.267 742.8 323.467C742.8 307.467 733.867 299.467 708.933 293.333C692.267 289.2 690.667 288.267 690.667 283.867C690.667 276.4 702.8 274.667 709.467 281.2C711.6 283.333 713.333 286.133 713.333 287.333C713.333 289.333 714.667 289.467 726.4 288.667C733.467 288.133 739.467 287.467 739.733 287.333C741.067 286.133 737.867 276.533 734.667 272.133C728.8 264.133 720.8 260.933 704.8 260.4C696.267 260.133 688.933 260.667 684.933 261.867Z" fill="black"/>
<path d="M474.667 272V282.667H489.333H504V318V353.333H518H532V318V282.667H546.667H561.333V272V261.333H518H474.667V272Z" fill="black"/>
<path d="M573.333 307.333V353.333H612H650.667V342.667V332H626H601.333V323.333V314.667H623.333H645.333V305.333V296H623.333H601.333V289.333V282.667H625.333H649.333V272V261.333H611.333H573.333V307.333Z" fill="black"/>
<path d="M750.667 272V282.667H765.333H780V318V353.333H794H808V318V282.667H822.667H837.333V272V261.333H794H750.667V272Z" fill="black"/>
<path d="M1196 640.8C1169.73 645.333 1155.87 653.867 1148.27 670.4C1145.47 676.533 1144.8 680 1144.93 688C1145.07 713.2 1161.33 727.2 1202.93 737.867C1212.93 740.4 1223.07 743.333 1225.6 744.4C1235.6 748.667 1239.73 758.4 1234.8 766.267C1231.07 772.267 1226.67 774.667 1218.13 775.6C1203.47 777.067 1192.27 769.467 1188.27 755.467L1186.13 747.733L1177.47 748.533C1172.53 748.933 1161.87 749.6 1153.73 750.133C1139.6 750.933 1138.67 751.2 1138.67 753.867C1138.67 758.667 1142.13 769.733 1145.87 777.067C1156.27 797.6 1173.07 806 1206.67 807.6C1244.27 809.333 1269.33 798.533 1280.4 776C1292.13 752 1285.73 727.333 1264.27 713.6C1255.47 707.867 1243.73 703.6 1220.67 698C1209.87 695.333 1199.2 692.267 1197.07 691.2C1186.93 686 1191.33 672.667 1203.73 671.067C1218 669.2 1228.27 674.8 1231.87 686.533L1233.6 692.267L1243.73 691.467C1249.33 691.067 1260.13 690.4 1267.6 689.867L1281.33 688.933L1280.53 685.467C1279.07 678.667 1275.6 668.667 1273.6 665.333C1267.2 654.933 1255.87 646.933 1242 643.067C1235.6 641.2 1202.8 639.6 1196 640.8Z" fill="black"/>
<path d="M808 662.667V682.667H833.333H858.667V744V805.333H884H909.333V744V682.667H935.333H961.333V662.667V642.667H884.667H808V662.667Z" fill="black"/>
<path d="M982.667 724V805.333H1051.33H1120V786.667V768H1076.67H1033.33V752V736H1072.67H1112V719.333V702.667H1072.67H1033.33V690V677.333H1075.33H1117.33V660V642.667H1050H982.667V724Z" fill="black"/>
<path d="M1300 662.667V682.667H1325.33H1350.67V744V805.333H1376H1401.33V744V682.667H1427.33H1453.33V662.667V642.667H1376.67H1300V662.667Z" fill="black"/>
</svg>
#svgImage{display:none; }
이미지는 canvas에 입력하는 용도로만 사용될 것이기 때문에 display:none으로 숨겨둔다
캔버스를 선언한 뒤 svg를 불러오도록 하자
const canvas: fabric.Canvas = new fabric.Canvas('canvas')
const svgEl = document.getElementById('svgImage')
if (svgEl) {
// XML 문자열 변환하기
const serializer = new XMLSerializer().serializeToString(svgEl)
fabric.loadSVGFromString(serializer, (objects, options) => {
// svg 배열 반환받기
const obj = fabric.util.groupSVGElements(objects, options)
if (obj.width && obj.height) {
// 캔버스를 obj 크기에 맞춰준다
canvas.setDimensions({width: obj.width, height: obj.height})
}
obj.set({ // 컨트롤 불가능하게 설정하기
hasBorders: false,
hasControls: false,
selectable: false,
hoverCursor: 'default',
type: 'svg'
})
canvas.add(obj).requestRenderAll()
})
}
우선 svg element를 가져온뒤XMLSerializer객체를 생성하고 serializeToString()에 dom을 전달하여 사용하여 XML DOM을 구성하는 문자열을 반환받는다
https://developer.mozilla.org/en-US/docs/Web/API/XMLSerializer
XMLSerializer - Web APIs | MDN
The XMLSerializer interface provides the serializeToString() method to construct an XML string representing a DOM tree.
developer.mozilla.org
그 다음에 fabric.loadSVGFromString에 string을 전달하면 구성된 svg와 내부 요소를 배열로 반환하는objects<fabric.Object[]>과 svg element의 width, height등을 담은 options값을 callback함수의 agument로 전달받을 수 있다
canvas의 크기를 svg에 맞추고, 컨트롤할 수 없게 만들기 위해 fabric.util.groupSVGElements에 objects와 options를 전달하여 그룹화를 시켜준다
그런뒤 canvas의 setDimensions함수를 활용하여 캔버스의 크기를 재설정해준다
이후 컨트롤이 불가능하게 hasBorders: false, hasControls: false, selectable: false, hoverCursor: 'default', type: 'svg'를 설정한뒤 캔버스에 추가하고 랜더링해주자
class 필드선언 및 초기화를 해준다
class Crop {
canvas: fabric.Canvas // 캔버스
cropMode = false // crop모드 설정
strokeWidth = 1
stroke = 'rgb(255, 255, 255)'
fill = 'rgb(255, 255, 255, 0.5)'
drawing = false // 그리기중인지
startX = 0
startY = 0
target: fabric.Object|null = null // crop대상
visibleOption = { // crop select 옵션
ml: false, // 왼쪽
mt: false, // 위
mr: false, // 오른쪽
mb: false, // 아래
mtr: false, // 상단 회전
}
constructor(canvas: fabric.Canvas) {
this.canvas = canvas
}
별도의 interface가 필요없기 때문에 canvas는 fabric.Canvas로 지정한다
mouse:down상태를 저장할 cropMode필드를, crop을 그릴때 사용될 속성인 strokeWidth, stroke, fill을 선언해주고 드래그를 판단할 drawing, 마우스의 시작점을 저장할 startX, startY도 추가해준다
crop 대상인 target은 fabric.Object로 타입을 지정하고 visibleOption옵션을 선언하는데 추후 Object의 setControlsVisibility함수에 사용된다
http://fabricjs.com/docs/fabric.Object.html#setControlsVisibility
JSDoc: Class: Object
animation context (or an array if passed multiple properties) As object — multiple properties object.animate({ left: ..., top: ... }); object.animate({ left: ..., top: ... }, { duration: ... }); As string — one property object.animate('left', ...); obj
fabricjs.com
crop class 메소드를 작성해준다
reset () {
if (this.target) this.canvas.remove(this.target)
this.cropMode = false
}
reset 메소드로 crop object가 있다면 삭제해준다
drawCrop (options: fabric.IEvent<MouseEvent>) {
// crop 그리기
this.cropMode = true // crop모드 실행
if (options.target?.type != 'crop') {
// 클릭 대상이 crop이 아닌경우만 active객체 모두 선택해제후 다시그리기
this.canvas.discardActiveObject()
const pointer = this.canvas.getPointer(options.e)
if (this.target) {
// 이미 존재한다면 삭제하기
this.canvas.remove(this.target)
}
// 박스 새로생성
this.target = new fabric.Rect({
type: 'crop',
fill: this.fill,
transparentCorners: false,
hoverCursor: 'move'
})
this.target.setControlsVisibility(this.visibleOption)
this.canvas.add(this.target)
if (this.target) {
// 시작점 좌표 설정
this.target.left = this.startX = pointer.x || 0
this.target.top = this.startY = pointer.y || 0
}
// 드로잉 켜주기
this.canvas.requestRenderAll()
this.drawing = true
}
}
mouse:down 이벤트시 crop 박스를 그리기위한 세팅을 하는 용도로 사용된다
이벤트의 상태를 저장할 cropMode = true로 바꿔주고, 이벤트 대상의 type이 crop이라면 선택이 되도록, 아니라면 crop객체를 신규 생성 할 수 있도록 if문으로 options.target?.type != 'crop'을 체크해준다
type이 crop이 아닌경우 discardActiveObject()함수로 모든 선택을 해제하고 시작점 좌표를 설정할 수 있게 canvas의 getPointer(options.e)함수로 포인터의 위치를 가져온다
crop박스는 항상 calss의 target필드에 담을 것 이기때문에 박스가 이미 존재한다면 remove(this.target)으로 삭제해주자
그 후에 fabric.Rect으로 박스를 생성해주고 type은 'crop'으로 할당한 뒤 class의 필드로 선언된 fill값을 할당해준다
이때 fill은 반투명으로 설정했기 때문에 transparentCorners속성을 false로 선언하여 가시성을 높여주도록 하자
hoverCursor은 move로 할당하여 마우스 hover시 이동이 가능하다는걸 표현해준다
그리고 crop박스는 항상 같은 비율로 늘어나고 줄어들기를 원하기 때문에 setControlsVisibility함수에 기존 class필드에 선언한 visibleOption을 전달하여 컨트롤을 제한해준다
이후 canvas에 crop박스를 추가해주고 박스의 left와 top을 startX와 startY의 값으로 할당해준다 이때 left와 top은 undefined가 될 수 있으므로 없으면 0으로 할당한다
그리고 캔버스를 재 랜더링하고 drawing = true로 할당하여 드로잉중으로 세팅한다
updateCrop (options: fabric.IEvent<MouseEvent>) {
// 크롭 업데이트
if (this.cropMode && this.target && this.canvas.width && this.canvas.height) {
if (this.drawing) {
// 직접그리기
const pointer = this.canvas.getPointer(options.e)
// 마우스 포인터가 시작점보다 작으면 object 위치보정
const left = Math.max(0, pointer.x)
const top = Math.max(0, pointer.y)
if(this.startX > pointer.x) {
this.target.set({ left: left })
} else {
this.target.set({ left: this.startX })
}
if(this.startY > pointer.y) {
this.target.set({ top: top })
} else {
this.target.set({ top: this.startY })
}
const width = Math.abs(this.startX - left)
const height = Math.abs(this.startY - top)
this.target.set({
width: width,
height: height,
})
} else {
// 그리기가 아닌경우
if (this.target.width && this.target.height && this.target.left && this.target.top && this.target.scaleX && this.target.scaleY) {
// 포인터가 있는경우 최대값 설정
this.target.set({
left: Math.min(Math.max(this.target.left, 0), this.canvas.width - this.target.width * this.target.scaleX),
top: Math.min(Math.max(this.target.top, 0), this.canvas.height - this.target.height * this.target.scaleY),
})
this.target.setCoords()
}
}
}
this.canvas.requestRenderAll()
}
mouse:down 이후 mouse:move시 크롭 박스를 업데이트하여 드래그를 구현하는 용도로 사용된다
불필요한 랜더가 일어나는 것을 방지하기위해 cropMode를 체크하고, drawCrop메소드에서 target이 생성되었는지, canvas의 width와 height는 undefined일 수 있기 때문에 값이있는지 체크하고 드로잉 중일때만 메소드를 실행하기 위해서 drawing값도 체크해준다
crop 드래그를 구현할때 넓이는 (시작점 - left or top)값이 되는데 left값이 음수이면 화면을 벗어났을 때 음수를 빼게되므로 박스가 커지는 문제가 발생한다
그래서 left 또는 top의 값은 pointer의 x 또는 y로 할당하되 Math.max함수를 활용하여 0보다 작아지지 않게 한다
이때 위에서 음수값을 보정해버렸기 영역 내에서 시작점 보다 적은위치에 드래그했을때 박스가 그려지지 않는 문제가 발생한다
this.target.set({ left: left })와 this.target.set({ top: top })을 추가하여 박스의 left또는 top의 값을 변동하여 박스가 그려지게하자
이후 else문에서 양수값일땐 기존 좌표로 돌리는 this.target.set({ left: this.startX })와 this.target.set({ top: this.startY })도 추가하자
넓이는 left와 top을 보정한 것 처럼 박스의 width와 height도 음수가 될 수 있어서 반대로 그려지는데 Math.abs을 추가하여 정수화시켜 올바르게 그려지게하자
drawing이 아닌경우 crop박스의 이동 및 크기를 바꾸는 경우인데 crop박스의 width, height, left, top, scaleX, scaleY 모두 undefined일 수 있기 때문에 if체크를 해주고 crop박스의 left를 보정해준다
이후 setCoords로 좌표를 재설정하고 랜더링해준다
endDrawing () {
if (this.target) {
this.canvas.setActiveObject(this.target)
this.target.set({
fill: this.fill,
})
this.canvas.requestRenderAll()
}
if (this.drawing) {
this.drawing = false
}
this.cropMode = false
}
mouse:up시 드래그를 종료하고 객체 드로잉을 초기화할때 사용한다
target이 있을경우만 실행하고 박스가 만들어지면 객체를 선택하기 위해서 setActiveObject()함수를 사용하여 target을 선택해준다
그리고 fill 을 세팅해준뒤 재 랜더링 한다
drawing필드가 true라면 drawing도 false로 바꿔준다
그리고 cropMode를 false로 바꿔 그리기를 종료한다
updateCropScale () {
if (this.target) {
if (this.canvas.width && this.canvas.height &&
this.target.width && this.target.height &&
this.target.lef && this.target.top &&
this.target.scaleX && this.target.scaleY)
{
// 크기는 음수이동이 안되게 제한
this.target.set({
left: Math.max(this.target.left, 0),
top: Math.max(this.target.top, 0),
})
// 최대값은 sacle 배율로 정한다
const max = {
x: (this.canvas.width - this.target.left) / this.target.width,
y: (this.canvas.height - this.target.top) / this.target.height,
}
// 가로세로 비율이 동일하게 지정
const ratio = Math.min(Math.min(this.target.scaleX, max.x), Math.min(this.target.scaleY, max.y))
this.target.set({
scaleX: ratio,
scaleY: ratio,
})
}
this.canvas.requestRenderAll()
}
}
object:scaling시 박스의 크기가 영역을 벗어나는 것을 제한하는데 사용한다
canvase와 target의 width, height값이 있을때, target의 left와 top, scaleX, scaleY값이 있을때를 체크하고, target의 left와 top은 Math.max함수를 사용하여 0보다 작아지지 않게 값을 설정하고, scale의 최대값은 캔버스의 크기에서 target의 left 또는 top 값을 뺀뒤 현재 target의 크기로 나눈값으로한다
비율을 항상 작은 값으로 고정하지 않으면 crop을 했을때 영역을 벗어나고 벗어난 부분은 공백으로 처리된다
그렇기 때문에 Math.min함수를 활용하여 scale배율과 x또는 y값 중에 가장 작은값으로 고정한다
그 뒤 랜더링 하도록 하자
setCrop (x:number, y:number) {
// 비율로 직접그리기
this.reset()
if (this.canvas.width && this.canvas.height) {
// 적은값을 기준으로 잡는다 나눌때는 높은값을 기준으로 잡는다
const reference = (this.canvas.width > this.canvas.height ? this.canvas.height : this.canvas.width) / (x > y ? x : y)
const ratio = {
x: (reference * x) / 2,
y: (reference * y) / 2,
}
const rect = new fabric.Rect({
width: ratio.x,
height: ratio.y,
fill: this.fill,
})
const option = {strokeWidth: this.strokeWidth, stroke: this.stroke}
// 배율만큼 나눠준다
const w = ratio.x / x
const h = ratio.y / y
const column: Array<fabric.Line> = []
const row: Array<fabric.Line> = []
for (let i=1; i<x; i++) column.push(new fabric.Line([w*i, 0, w*i, h*y], option))
for (let i=1; i<y; i++) row.push(new fabric.Line([0, h*i, w*x, h*i], option))
const group = new fabric.Group([rect, ...column, ...row], {
type: 'crop',
transparentCorners: false,
})
this.target = group
group.setControlsVisibility(this.visibleOption)
this.canvas.centerObject(group)
this.canvas.add(group)
// 생성된 객체 선택
this.canvas.setActiveObject(group)
}
this.canvas.requestRenderAll()
}
드래그로 자유롭게 그리는게 아닌 비율로 직접그리게 하는 메서드이다
메서드 실행시 기존값은 모두 필요없기 때문에 reset으로 초기화를 해주고 캔버스의 크기를 체크한다
canvas의 크기를 체크하고 배율을 맞춰줄 reference값을 잡는데 이때도 canvas의 좁은곳을 벗어나지 않게 하기위해 width, height중 작은값 / x, y중 큰값을 기준으로잡는다
ratio는 기준값 * x또는 y로 설정하고 박스가 너무 크게 나오는 것을 방지하기위해 2로 나눠준뒤 crop박스의 width와 height로 설정하고 fill값을 할당한다
그 뒤 x와 y의 비율을 나타내줄 가이드라인을 그려줄건데 우선 fabirc.Line의 옵션으로 사용될 option을 선언하고 strokeWidth와 stroke에 class의 필드값을 할당해준 다음 w와 h를 각각 ratio의 x, y값으로 나눠준뒤 각각 column과 row배열을 선언한다
x와 y값의 -1 만큼 라인이 그려져야 배율이 완성되니 for loop문을 1부터 시작하여 배열에 fabric.Line을 push해준다
fabric.Line은 좌표값으로 그려지기 때문에 Array<point.x, point.y, point.x, point.y>형태가 되어야 하므로 넓이 i, 0, 넓이 i, 높이 * y형태로 넣어준뒤 옵션을 추가해준다.
높이도 같은 좌표방식으로 그려준다
http://fabricjs.com/docs/fabric.Line.html#initialize
JSDoc: Class: Line
keeps the value of the last hovered corner during mouse move. 0 is no corner, or 'mt', 'ml', 'mtr' etc.. It should be private, but there is no harm in using it as a read-only property. Type: Inherited From: Default Value: Source: Meaningful ONLY when the o
fabricjs.com
박스와 선이 같이 이동되고 크기도 같이 늘어나야 하므로 fabric.Group으로 만들어준다
http://fabricjs.com/docs/fabric.Group.html#initialize
JSDoc: Class: Group
keeps the value of the last hovered corner during mouse move. 0 is no corner, or 'mt', 'ml', 'mtr' etc.. It should be private, but there is no harm in using it as a read-only property. Type: Inherited From: Default Value: Source: Meaningful ONLY when the o
fabricjs.com
group은 parameter를 objects라는 배열로 받기때문에 배열에 rect를 추가하고 column, row를 Destructuring assignment로 추가해준다
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
구조 분해 할당 - JavaScript | MDN
구조 분해 할당 구문은 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 JavaScript 표현식입니다.
developer.mozilla.org
그리고 옵션을 추가하는데 기존과 마찬가지로 type: 'crop'을 transparentCorners: false를 추가하여 투명하게한다
target에 group을 할당하고 setControlsVisibility함수에 visibleOption을 전달하여 컨트롤을 제한해준다
canvas.centerObject(group)함수를 활용하여 객체를 중앙정렬하고 canvas.add(group)로 추가해준뒤 setActiveObject(group) 함수로 객체를 선택한다
이후 재 랜더링하자
cropImage () {
// 실행
let image: fabric.Object | undefined;
image = this.canvas.getObjects().find(x => x.type == 'svg')
if (image) {
if (this.target && this.target.width && this.target.height) {
this.canvas.clear()
const cropped = new Image()
cropped.src = image.toDataURL({
top: this.target.top,
left: this.target.left,
width: this.target.width * (this.target.scaleX || 1),
height: this.target.height * (this.target.scaleY || 1),
})
cropped.onload = () => {
const newimage = new fabric.Image(cropped)
this.canvas.add(newimage)
newimage.set({
// 이미지 잠그기
hasBorders: false,
hasControls: false,
selectable: false,
hoverCursor: 'default',
type: 'svg'
})
newimage.setCoords()
this.target = null
if (newimage.width && newimage.height) this.canvas.setDimensions({width: newimage.width, height: newimage.height})
this.canvas.requestRenderAll()
}
}
}
}
마지막 메서드인 cropImage는 생성된 crop박스에 맞춰서 이미지를 자르고 재구성하는 용도로 사용된다
image: fabric.Object | undefined로 선언해주고 canvas.getObjects()함수에서 find함수로 type == svg인 값을 찾는다
해당하는 값이 있다면 target과 width, height값이 있을때 canvas.clear()함수를 활용하여 캔버스의 모든 객체를 삭제하고 cropped = new Image()변수를 선언한다
cropped의 src속성은 image.toDataURL함수의 값을 전달하는데 옵션의 top과 left는 target과 같게, width와 height는 target의 scaleX와 scaleY을 할당해주는데 값이 없다면 논리연산자로 1을 할당한다
image의 toDataURL을 가져올때 기준이 되는 이미지에서 top, left, width, height는 이미지와 같은 형식으로 좌표 내부의 이미지만 가져오고, scale은 옵션으로 전달해줄 수 없기 때문에 각각 width와 height에 값을 곱하여 이미지를 가져오도록 한다
이후 cropped가 로드되면 fabric.Image의 값으로 초기화를 시키고 캔버스에 추가해준뒤 최초 svg를 추가할때 처럼 이미지를 컨트롤 할 수 없게 옵션을 전달한뒤 type은 동일하게 svg로 할당해준다
그리고 좌표를 재설정하고 target을 null로 할당하여 삭제해준뒤 canvas.setDimensions함수를 활용하여 캔버스의 width와 height를 재설정한다
이후 랜더링해주면 이미지가 crop된다
crop에 사용되는 이벤트 리스너
const crop = new Crop(canvas)
canvas.on('mouse:down', (options) => {
crop.drawCrop(options) // crop모드 및 좌표지정
})
canvas.on('mouse:up', () => {
crop.endDrawing() // crop 종료
})
canvas.on('mouse:move', (options) => {
crop.updateCrop(options) // crop 업데이트
})
canvas.on('object:scaling', () => {
crop.updateCropScale() // crop 크기변경
})
fabric.js Image Editor - (4) filter (0) | 2023.03.31 |
---|---|
fabric.js Image Editor - (2) Shape (0) | 2023.03.19 |
fabric.js Image Editor - (1) import image (1) | 2023.03.19 |
댓글 영역