최근에는 웹 페이지 이미지 처리와 관련된 프로젝트를 진행하고 있는데, 이는 캔버스에 대한 첫 경험이라고 할 수 있습니다. 프로젝트 요구 사항에는 이미지에 워터마크를 추가하는 기능이 포함되어 있습니다. 브라우저 측에서 이미지에 워터마크를 추가하는 일반적인 방법은 canvas
의 drawImage
메서드를 사용하는 것임을 알고 있습니다. 일반적인 합성(기본 이미지와 PNG 워터마크 이미지의 합성 등)의 경우 일반적인 구현 원리는 다음과 같습니다.
var canvas = document.getElementById(canvas);var ctx = canvas.getContext('2d');// img: 기본 이미지 // watermarkImg: 워터마크 이미지 // x, y는 캔버스 ctx에 img를 배치하는 좌표입니다. drawImage(img, x, y);ctx.drawImage(watermarkImg, x, y);
해당 이미지를 canvas
에 그리려면 drawImage()
직접적이고 지속적으로 사용하면 됩니다.
이상이 배경 소개이다. 하지만 조금 귀찮은 점은 워터마크를 추가해야 할 때 구현해야 하는 또 다른 기능이 있다는 것입니다. 즉, 사용자가 워터마크의 위치를 전환할 수 있다는 것입니다. 사용자 canvas
워터마크 위치를 undo
하면 먼저 이전 drawImage
작업을 취소한 다음 워터마크 이미지 위치를 다시 그립니다.
restore
/ save
?
가장 효율적이고 편리한 방법은 canvas 2D
네이티브 API에 이 기능이 있는지 확인하는 것입니다. 몇 번 검색한 후 API restore
/ save
쌍이 표시되었습니다. 먼저 이 두 API에 대한 설명을 살펴보겠습니다.
CanvasRenderingContext2D.restore()는 그리기 상태 스택의 최상위 상태를 팝하여 캔버스를 가장 최근에 저장된 상태로 복원하는 Canvas 2D API 메서드입니다. 저장된 상태가 없으면 이 메서드는 변경되지 않습니다.
CanvasRenderingContext2D.save()는 현재 상태를 스택에 넣어 캔버스의 전체 상태를 저장하는 Canvas 2D API의 메소드입니다.
언뜻 보면 요구 사항을 충족하는 것 같습니다. 공식 샘플 코드를 살펴보겠습니다.
var canvas = document.getElementById(canvas);var ctx = canvas.getContext(2d);ctx.save(); // 기본 상태 저장 ctx.fillStyle = green;ctx.fillRect(10, 10, 100, 100) ;ctx.restore(); //마지막으로 저장된 기본 상태로 복원 ctx.fillRect(150, 75, 100, 100);
결과는 아래와 같습니다:
이상하게도 우리가 기대했던 결과와 일치하지 않는 것 같습니다. 우리가 원하는 결과는 save
메소드를 호출한 후 현재 캔버스의 스냅샷을 저장할 수 있고, resolve
메소드를 호출한 후 마지막으로 저장된 스냅샷의 상태로 완전히 돌아갈 수 있는 것입니다.
API를 자세히 살펴보겠습니다. 우리는 그리기 drawing state
상태라는 중요한 개념을 놓친 것으로 나타났습니다. 스택에 저장된 그리기 상태에는 다음 부분이 포함됩니다.
다음 속성의 현재 값은 다음과 같습니다.
음, drawImage
작업 후 캔버스에 대한 변경 사항은 그리기 상태에 전혀 존재하지 않습니다. 따라서 resolve
/ save
사용하면 필요한 실행 취소 기능을 얻을 수 없습니다.
도면 상태를 저장하기 위한 기본 API 스택은 요구 사항을 충족할 수 없으므로 당연히 작업 저장을 위한 스택을 직접 시뮬레이션하는 것을 생각해 보겠습니다. 다음 질문은 다음과 같습니다. 각 그리기 작업 후에 스택에 어떤 데이터를 저장해야 합니까? 앞서 언급했듯이 우리가 원하는 것은 각 그리기 작업 후에 현재 캔버스의 스냅샷을 저장할 수 있는 것입니다. 스냅샷 데이터를 얻고 스냅샷 데이터를 사용하여 캔버스를 복원할 수 있다면 문제는 해결될 것입니다.
다행스럽게도 canvas 2D
기본적으로 스냅샷을 얻고 스냅샷을 통해 캔버스를 복원하기 위한 API( getImageData
/ putImageData
를 제공합니다. 다음은 API 설명입니다.
/* * @param { Number } sx 추출할 이미지 데이터의 직사각형 영역 왼쪽 상단의 x 좌표 * @param { Number } sy 직사각형 영역의 왼쪽 상단 y 좌표 추출할 이미지 데이터 * @param { Number } sw 추출할 이미지 데이터의 직사각형 영역의 너비 * @param { Number } sh 추출할 이미지 데이터의 직사각형 영역의 높이 * @return { Object } ImageData에는 캔버스가 포함되어 있습니다. 주어진 직사각형 이미지 데이터 */ ImageData ctx.getImageData(sx, sy, sw, sh) /* * @param { Object } 픽셀 값을 포함하는 imagedata 객체 * @param { Number } 대상 캔버스의 dx 소스 이미지 데이터 position offset (x축 방향 오프셋) * @param { Number } dy 대상 캔버스 내 원본 이미지 데이터의 위치 오프셋 (y축 방향 오프셋) */ void ctx.putImageData(imagedata, dx, dy);
간단한 애플리케이션을 살펴보겠습니다.
class WrappedCanvas { 생성자(캔버스) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; ]; } drawImage (...params) { const imgData = this.ctx.getImageData(0, 0, this.width, this.height); this.imgStack.push(imgData);this.ctx.drawImage(...params); } undo () { if (this.imgStack.length > 0) { const imgData = this.imgStack.pop (); this.ctx.putImageData(imgData, 0, 0) } }}
canvas
의 drawImage
메서드를 캡슐화하고 이 메서드를 호출할 때마다 이전 상태의 스냅샷을 시뮬레이션된 스택에 저장합니다. undo
작업을 수행할 때 가장 최근에 저장된 스냅샷을 스택에서 제거한 다음 캔버스를 다시 그려 실행 취소 작업을 구현합니다. 실제 테스트도 기대에 부응했습니다.
이전 섹션에서는 canvas
의 실행 취소 기능을 매우 대략적으로 구현했습니다. 왜 거칠다고 말해요? 한 가지 분명한 이유는 이 솔루션의 성능이 좋지 않다는 것입니다. 우리의 솔루션은 매번 전체 캔버스를 다시 그리는 것과 같습니다. 작업 단계가 많다고 가정하면 메모리인 시뮬레이션 스택에 미리 저장된 이미지 데이터를 많이 저장하게 됩니다. 또한, 그림을 그리는 것이 너무 복잡할 경우 getImageData
와 putImageData
두 가지 메소드를 사용하면 심각한 성능 문제가 발생할 수 있습니다. stackoverflow에 대한 자세한 토론이 있습니다. 왜 putImageData가 그렇게 느립니까? jsperf의 테스트 사례 데이터에서도 이를 확인할 수 있습니다. Taobao FED는 또한 애니메이션에서 putImageData
메소드를 사용하지 않으려는 Canvas 모범 사례에서도 언급했습니다. 또한 기사에서는 렌더링 오버헤드가 낮은 API를 최대한 많이 호출해야 한다고 언급했습니다. 여기서부터 최적화 방법에 대해 생각해 볼 수 있습니다.
앞에서 언급했듯이 캔버스 전체의 스냅샷을 저장하여 각 작업을 기록합니다. 다른 각도에서 생각하면 각 그리기 작업을 배열에 저장하면 실행 취소 작업이 수행될 때마다 캔버스가 먼저 지워지고 다음이 됩니다. 이 그리기 작업 배열을 다시 그리면 작업을 취소하는 기능도 구현할 수 있습니다. 타당성 측면에서는 첫째, 메모리에 저장되는 데이터의 양을 줄일 수 있고, 둘째, 렌더링 오버헤드가 높은 putImageData
의 사용을 피할 수 있습니다. drawImage
비교 개체로 사용하고 jsperf에 대한 이 테스트 사례를 살펴보면 둘 사이의 성능 차이가 10배 정도 높습니다.
따라서 우리는 이 최적화 솔루션이 실현 가능하다고 믿습니다.
개선된 적용 방법은 대략 다음과 같습니다.
class WrappedCanvas { 생성자(캔버스) { this.ctx = canvas.getContext('2d'); this.width = this.ctx.canvas.width; this.height = this.ctx.canvas.height; ]; } drawImage (...params) { this.executionArray.push({ 메서드: 'drawImage', params: params });this.ctx.drawImage(...params); } clearCanvas () { this.ctx.clearRect(0, 0, this.width, this.height) } undo () { if (this.executionArray. length > 0) { // 캔버스를 지웁니다. this.clearCanvas(); // 현재 작업을 삭제합니다. this.executionArray.pop() // 다시 그리기 위한 그리기 작업을 하나씩 실행합니다. (this.executionArray의 exe를 실행하세요) { this[exe.method](...exe.params) } } }}
캔버스를 처음 접하시는 분들은 오류나 부족한 점을 지적해주세요. 위의 내용은 이 기사의 전체 내용입니다. 모든 분들의 학습에 도움이 되기를 바랍니다. 또한 모든 분들이 VeVb Wulin Network를 지지해 주시길 바랍니다.