관련 지식
javascript, canvas, html2canvas, dimmed

이전에 만들었던 ‘[javascript] html2canvas를 이용한 웹 화면 캡쳐’ 를 개선해서 이번엔 부분 캡쳐가 가능한 기능을 만들어보겠습니다. 여기서 사용할 샘플은 이전 게시물의 최종 샘플을 참고하시면 됩니다.

구현에 대한 방식은 시놀로지 웹 클리퍼의 기능을 참고하였습니다.

먼저 최종 실행 결과부터 보겠습니다.

동작 방식

캡쳐 버튼을 클릭했을때 동작하는 방식을 크게 보면 아래와 같습니다.

  1. 화면 전체에 dimmed(반투명 음영) 추가
  2. 마우스 이동에 따라 캡쳐 영역 생성
  3. mouseup 이벤트 시 화면 전체 캡쳐
  4. 캡쳐된 이미지를 캡쳐 영역의 정보대로 crop
  5. crop한 이미지 저장

여기서 3번과 5번 절차는 이전에 만들었던 소스를 재활용하면 되기 때문에 어렵지 않습니다.
4번 항목도 canvas 에서 제공하는 api를 이용할 것이기 때문에 크게 어렵지 않지만 1,2의 경우 HTML 퍼블리싱 지식도 조금 필요합니다. 차례대로 살펴보겠습니다.

딤/딤드 추가

‘캡쳐’ 버튼을 클릭했을때 컨텐츠 전체를 감싸는 반투명 음영, 즉 dimmed가 추가 됩니다. 딤 처리를 할때 흔히 사용하는 방법이 있으실텐데 그것과는 조금 다를수 있습니다. 아래와 같이 background-color 속성이 아닌 border 속성으로 처리를 해야 합니다.

  1. #screenshot_mask {
  2. width: 100%;
  3. height: 100%;
  4. position: fixed;
  5. top: 0px;
  6. left: 0px;
  7. display: block;
  8. opacity: 0.3;
  9. text-align: center;
  10. box-sizing: border-box;
  11. z-index: 2147483647;
  12. border-color: black;
  13. border-style: solid;
  14. }

border 속성을 이용했기 때문에 위의 css 만으로는 딤 처리가 되지 않습니다. div를 추가할때 border-width 를 설정해줘야만 합니다.

  1. var height = window.innerHeight;
  2. var $mask = $('<div id="screenshot_mask"></div>').css("border-width", "0 0 " + height + "px 0");
  3. $("body").append($mask); //dimmed 추가

background-color 속성을 사용했을 때와의 차이는 이전에 올렸던 ‘[css] dimmed 처리 방법 두가지’ 에 설명되어있으니 참고하시기 바랍니다.

마우스 포커스 효과

마우스 커서의 움직임에 따라 화면을 4분할 한것처럼 보이는 효과는 <div> 객체에 css의 가상 요소를 이용합니다.

  1. #screenshot_focus:before,#screenshot_focus:after{
  2. border:none !important;
  3. content:"" !important;
  4. height:100% !important;
  5. position:absolute !important;
  6. width:100% !important
  7. }
  8. #screenshot_focus:before{
  9. border-right:1px solid white !important;
  10. border-bottom:1px solid white !important;
  11. left:-100% !important;
  12. top:-100% !important
  13. }
  14. #screenshot_focus:after{
  15. border-top:1px solid white !important;
  16. border-left:1px solid white !important;
  17. left:0 !important;
  18. top:0 !important
  19. }
  20. #screenshot_focus{
  21. height:100% !important;
  22. position:fixed !important;
  23. width:100% !important;
  24. z-index:2147483648 !important
  25. }

이것 또한 단순히 <div> 를 추가한다고 적용되지 않고 커서의 움직임에 따라 해당 엘리먼트의 lefttop 값을 변경해주어야 합니다.

  1. var $focus = $('<div id="screenshot_focus"></div>');
  2. $("body").append($focus); //마우스 커서에 따라 캡쳐 영역을 만들 div
  3. function mousemove(e) {
  4. var x = e.clientX;
  5. var y = e.clientY;
  6. $focus.css("left", x); //마우스 커서 따라 좌표 포커스 이동
  7. $focus.css("top", y);
  8. }

캡쳐 영역 선택

이젠 마우스 클릭->이동에 따라 캡쳐가 될 영역을 만들어주어야 합니다. 정확히 말하면 캡쳐가 되는 영역인것 처럼 공간을 열어줘야 합니다.

현재 상태)

마우스 영역 선택에 따른 border 값 변화)

위의 그림은 시작점으로 부터 큰쪽으로 드래그 했을때의 border 값 변화를 보여주고 있습니다. 물론 실제로 만들때는 드래그 방향에 따른 좌표 처리를 해주어야 합니다.
우리가 흔히 border:1px solid black 처럼 단순히 써오던 것을 테두리의 두께를 조절하고 opacity를 적용함으로써 화면의 일부 영역을 강조하는 것처럼 만들수 있습니다.

  1. if(selectArea) { //캡쳐 영역 선택 그림
  2. var top = Math.min(y, startY);
  3. var right = width - Math.max(x, startX);
  4. var bottom = height - Math.max(y, startY);
  5. var left = Math.min(x, startX);
  6. $mask.css("border-width", [top + 'px', right + 'px', bottom + 'px', left + 'px'].join(' '));
  7. }

부분 화면 캡쳐

화면의 일부를 캡쳐하는 것은 이전 글 ‘[javascript] html2canvas를 이용한 웹 화면 캡쳐’ 에서도 다뤘지만 그것과는 방식이 다릅니다.

  • 이전 방식 : document의 특정 DOM을 기준으로 캡쳐
  • 개선 방식 : document 전체를 캡쳐하고 선택 영역만큼을 crop

따라서 먼저 document를 캡쳐해야 합니다.

  1. html2canvas(document.body).then(function(canvas) { //전체 화면 캡쳐
  2. });

콜백함수로 전달된 canvas에는 화면 전체에 대한 이미지 정보가 있습니다. 이번엔 그 값에서 원하는 영역만 추출하면 됩니다. getImageData() 로 원하는 영역에 대한 이미지를 추출후 새로운 캔버스에 넣어줍니다.

  1. var img = canvas.getContext('2d').getImageData(left, top, width, height); //선택 영역만큼 crop
  2. var c = document.createElement("canvas");
  3. c.width = width;
  4. c.height = height;
  5. c.getContext('2d').putImageData(img, 0, 0);
  6. save(c); //crop한 이미지 저장

이제 남은 것은 파일로 저장하는 일 뿐입니다. 이전 글에서 썼던 로직을 함수화 시켜서 호출하면 끝입니다.

정리

이 글에서는 부분 캡쳐에 대한 원리를 쉽게 보이기 위해 세세한 처리가 빠져있습니다. 따라서 실제 서비스에 적용할때는 스크롤에 대한 처리를 추가하고 디테일한 좌표 계산을 해야합니다.

이미지 부분 캡쳐를 위해 특정 브라우저 요소만을 위한것은 아무것도 사용하지 않았습니다. css 속성을 복제해서 이미지화 하는 html2canvas의 기능상 브라우저와 조금 달라보일수 있는 부분은 아쉽지만 IE10 이상이면 동작한다는 것이 장점일듯 합니다.

만약 화면에 보이는것과 완전히 동일한 이미지 캡쳐를 하고 싶다면 확장 플러그인에서 사용 가능한 captureVisibleTab() 함수를 사용하시기 바랍니다.

tabs.captureVisibleTab() : https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/captureVisibleTab

최종 샘플

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>CSS Template</title>
  5. <meta charset="utf-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1">
  7. <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
  8. <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
  9. <script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.min.js"></script>
  10. <script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js"></script>
  11. <style>
  12. * {
  13. box-sizing: border-box;
  14. }
  15. body {
  16. font-family: Arial, Helvetica, sans-serif;
  17. }
  18. /* Style the header */
  19. header {
  20. background-color: #666;
  21. padding: 30px;
  22. text-align: center;
  23. font-size: 35px;
  24. color: white;
  25. }
  26. /* Create two columns/boxes that floats next to each other */
  27. nav {
  28. float: left;
  29. width: 30%;
  30. height: 300px; /* only for demonstration, should be removed */
  31. background: #ccc;
  32. padding: 20px;
  33. }
  34. /* Style the list inside the menu */
  35. nav ul {
  36. list-style-type: none;
  37. padding: 0;
  38. }
  39. article {
  40. float: left;
  41. padding: 20px;
  42. width: 70%;
  43. background-color: #f1f1f1;
  44. height: 300px; /* only for demonstration, should be removed */
  45. }
  46. /* Clear floats after the columns */
  47. section:after {
  48. content: "";
  49. display: table;
  50. clear: both;
  51. }
  52. /* Style the footer */
  53. footer {
  54. background-color: #777;
  55. padding: 10px;
  56. text-align: center;
  57. color: white;
  58. }
  59. /* Responsive layout - makes the two columns/boxes stack on top of each other instead of next to each other, on small screens */
  60. @media (max-width: 600px) {
  61. nav, article {
  62. width: 100%;
  63. height: auto;
  64. }
  65. }
  66. </style>
  67. </head>
  68. <body>
  69. <input type="button" value="캡쳐" />
  70. <h2>CSS Layout Float</h2>
  71. <p>In this example, we have created a header, two columns/boxes and a footer. On smaller screens, the columns will stack on top of each other.</p>
  72. <p>Resize the browser window to see the responsive effect (you will learn more about this in our next chapter - HTML Responsive.)</p>
  73. <header>
  74. <h2>Cities</h2>
  75. </header>
  76. <section>
  77. <nav>
  78. <ul>
  79. <li><a href="#">London</a></li>
  80. <li><a href="#">Paris</a></li>
  81. <li><a href="#">Tokyo</a></li>
  82. </ul>
  83. </nav>
  84. <article>
  85. <h1>London</h1>
  86. <p>London is the capital city of England. It is the most populous city in the United Kingdom, with a metropolitan area of over 13 million inhabitants.</p>
  87. <p>Standing on the River Thames, London has been a major settlement for two millennia, its history going back to its founding by the Romans, who named it Londinium.</p>
  88. </article>
  89. </section>
  90. <footer>
  91. <p>Footer</p>
  92. </footer>
  93. <a id="target" style="display: none"></a>
  94. <style>
  95. #screenshot_mask {
  96. width: 100%;
  97. height: 100%;
  98. position: fixed;
  99. top: 0px;
  100. left: 0px;
  101. display: block;
  102. opacity: 0.3;
  103. text-align: center;
  104. box-sizing: border-box;
  105. z-index: 2147483647;
  106. border-color: black;
  107. border-style: solid;
  108. }
  109. #screenshot_focus:before,#screenshot_focus:after{
  110. border:none !important;
  111. content:"" !important;
  112. height:100% !important;
  113. position:absolute !important;
  114. width:100% !important
  115. }
  116. #screenshot_focus:before{
  117. border-right:1px solid white !important;
  118. border-bottom:1px solid white !important;
  119. left:-100% !important;
  120. top:-100% !important
  121. }
  122. #screenshot_focus:after{
  123. border-top:1px solid white !important;
  124. border-left:1px solid white !important;
  125. left:0 !important;
  126. top:0 !important
  127. }
  128. #screenshot_focus{
  129. height:100% !important;
  130. position:fixed !important;
  131. width:100% !important;
  132. z-index:2147483648 !important
  133. }
  134. </style>
  135. <script>
  136. $(":button").on('click', function(e) { //캡쳐 기능 활성화
  137. var height = window.innerHeight;
  138. var width = $(document).width();
  139. var $mask = $('<div id="screenshot_mask"></div>').css("border-width", "0 0 " + height + "px 0");
  140. var $focus = $('<div id="screenshot_focus"></div>');
  141. $("body").append($mask); //dimmed 추가
  142. $("body").append($focus); //마우스 커서에 따라 캡쳐 영역을 만들 div
  143. var selectArea = false;
  144. $("body").one("mousedown", function(e) { //캡쳐 영역 선택 시작
  145. e.preventDefault();
  146. selectArea = true;
  147. startX = e.clientX;
  148. startY = e.clientY;
  149. }).one('mouseup', function(e) { //캡쳐 시작
  150. selectArea = false;
  151. $("body").off('mousemove', mousemove); //이벤트 삭제
  152. $("#screenshot_focus").remove(); //마우스 포커스 삭제
  153. $("#screenshot_mask").remove(); //딤 삭제
  154. var x = e.clientX;
  155. var y = e.clientY;
  156. var top = Math.min(y, startY);
  157. var left = Math.min(x, startX);
  158. var width = Math.max(x, startX) - left;
  159. var height = Math.max(y, startY) - top;
  160. html2canvas(document.body).then(function(canvas) { //전체 화면 캡쳐
  161. var img = canvas.getContext('2d').getImageData(left, top, width, height); //선택 영역만큼 crop
  162. var c = document.createElement("canvas");
  163. c.width = width;
  164. c.height = height;
  165. c.getContext('2d').putImageData(img, 0, 0);
  166. save(c); //crop한 이미지 저장
  167. });
  168. }).on("mousemove", mousemove); //캡쳐 영역 크기 변경
  169. function mousemove(e) {
  170. var x = e.clientX;
  171. var y = e.clientY;
  172. $focus.css("left", x); //마우스 커서 따라 좌표 포커스 이동
  173. $focus.css("top", y);
  174. if(selectArea) { //캡쳐 영역 선택 그림
  175. var top = Math.min(y, startY);
  176. var right = width - Math.max(x, startX);
  177. var bottom = height - Math.max(y, startY);
  178. var left = Math.min(x, startX);
  179. $mask.css("border-width", [top + 'px', right + 'px', bottom + 'px', left + 'px'].join(' '));
  180. }
  181. }
  182. function save(canvas) { //파일로 저장
  183. if (navigator.msSaveBlob) {
  184. var blob = canvas.msToBlob();
  185. return navigator.msSaveBlob(blob, '파일명.jpg');
  186. } else {
  187. var el = document.getElementById("target");
  188. el.href = canvas.toDataURL("image/jpeg");
  189. el.download = '파일명.jpg';
  190. el.click();
  191. }
  192. }
  193. });
  194. </script>
  195. </body>
  196. </html>