Canvas, SVG 및 멀티 터치를 사용하여 타일식 퍼즐 게임을 만드는 방법

기본 사항에서 시작하고 Canvas 및 SVG를 모두 사용하는 멀티 터치 이미지 퍼즐 게임으로 완료되는 이 자습서에서는 마우스, 펜 또는 하나 이상의 손가락(멀티 터치)으로 생성된 이벤트 등 포인터 이벤트를 처리하는 방법을 단계별로 설명합니다.

참고  포인터 이벤트에는 Windows 8 이상이 필요합니다.

참고  Internet Explorer 10에 대해 이 문서가 작성된 이후 Internet Explorer 포인터 이벤트 구현이 약간 변경되었습니다. 코드를 업데이트하고 미래에도 경쟁력을 갖추는 방법에 대한 자세한 내용은 포인터 이벤트 업데이트를 참조하세요.

소개

Internet Explorer 10 및 JavaScript로 작성한 Windows 스토어 앱에서 개발자는 포인터라는 입력 유형을 사용할 수 있습니다. 이 컨텍스트에서 포인터는 마우스, 펜, 손가락 또는 여러 손가락에 의한 화면 접촉 지점일 수 있습니다. 이 자습서는 먼저 포인터 시작 방법을 설명한 다음 Canvas와 SVG를 모두 사용하는 다중 포인터 이미지 퍼즐 게임의 구현 과정을 안내합니다.

Canvas와 SVG를 사용하는 멀티 터치 사용 퍼즐 이미지

마우스 이벤트 사용 방법을 이미 알고 있는 경우 MSPointerDown, MSPointerMove, MSPointerUp, MSPointerOver, MSPointerOut 등의 포인터 이벤트가 생소하지 않을 것입니다. 마우스 및 포인터 이벤트는 다음 예제 1과 같이 간단하게 처리됩니다. 또한 멀티 터치 사용 장치의 경우 장치가 처리할 수 있는 것만큼 많은 동시 접촉(손가락) 지점에서 예제 1이 있는 그대로 작동합니다. 각 화면 접촉 지점에 대해 포인터 이벤트가 발생하기 때문입니다. 따라서 다음 기본 드로잉 예제와 같은 앱은 특별한 코딩 없이 멀티 터치를 지원합니다.

예제 1 - 기본 드로잉 앱


<!DOCTYPE html>
<html>

<head>
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
  <title>Example 1</title>
  <style>
    html {
      -ms-touch-action: double-tap-zoom;  
    }    
  </style>
</head>

<body>
  <canvas id="drawSurface" width="500" height="500" style="border: 1px black dashed;"></canvas>
  <script>
    var _canvas = document.getElementById("drawSurface");
    var _context = _canvas.getContext("2d");
    
    _context.fillStyle = "rgba(255, 0, 0, 0.5)";
    
    if (navigator.msPointerEnabled) {
      _canvas.addEventListener('MSPointerMove', paintCanvas, false);
    }
    else {
      _canvas.addEventListener('mousemove', paintCanvas, false);
    }
        
    function paintCanvas(evt) {
      _context.fillRect(evt.clientX, evt.clientY, 5, 5);
    }
  </script>
</body>

</html>

일반적으로 브라우저는 터치 이벤트를 고유한 용도로 사용합니다. 예를 들어 웹 페이지를 위로 스크롤하려면 사용자가 화면(링크 아님)을 터치하고 풀다운할 수 있습니다. 또는 페이지를 확대하려면 두 손가락을 모아서 확대하는 동작을 사용할 수 있습니다. 예제 1에서는 이러한 기본 동작이 발생하지 않도록 하지만 하나 이상의 손가락으로 드로잉을 만들면 대신 페이지가 이동(또는 확대/축소)됩니다. 이러한 이벤트가 JavaScript 코드로 진행될 수 있도록 다음 CSS를 사용합니다.


html {
  -ms-touch-action: double-tap-zoom;  
}

이 경우 페이지를 확대하는 두 번 탭을 제외하고 브라우저가 모든 터치 동작을 "무시"합니다. 즉, 이제 두 번 탭 이벤트를 캡처하는 기능을 제외하고 JavaScript 코드에서 모든 터치 이벤트를 사용할 수 있습니다. -ms-touch-action의 다른 가능한 값은 터치가 용이한 사이트 빌드 지침에 설명된 대로 auto, none, manipulationinherit입니다.

addEventListener 관점에서 Windows Internet Explorer 포인터 이벤트와 기존 마우스 이벤트는 함께 사용할 수 없으므로 포인터 이벤트가 사용 가능한 경우 마우스 이벤트도 포함됩니다. 즉, paintCanvas 이벤트 수신기를 mousemoveMSPointerMove에 동시에 등록할 수 없습니다.



// DO NOT DO THE FOLLOWING:
_canvas.addEventListener('mousemove', paintCanvas, false);
_canvas.addEventListener('MSPointerMove', paintCanvas, false);

마우스 이벤트도 보고하는 MS 포인터 이벤트를 사용할 수 있는 경우 대신 사용됩니다. 그렇지 않으면 기존 마우스 이벤트를 사용합니다.


if (window.navigator.msPointerEnabled) {
  _canvas.addEventListener('MSPointerMove', paintCanvas, false);
}
else {
  _canvas.addEventListener('mousemove', paintCanvas, false);
}

따라서 이전 코드 조각에서는 드로잉 앱이 터치 사용 장치나 기존의 비터치 장치에서 작동할 수 있습니다.

포인터 이벤트의 기본 사항이 이해되었으므로 보다 현실적인 사용인 이미지 퍼즐 게임 구현을 살펴보겠습니다.

Canvas와 SVG를 사용하는 멀티 터치 사용 퍼즐을 표시하는 이미지

이미지 슬라이싱

이러한 게임을 만드는 첫 번째 단계는 이미지 조각이나 타일을 만드는 것입니다. Canvas API의 drawImage 메서드를 사용하면 복사하거나 표시할 이미지 부분을 설정하여 원본 이미지를 Canvas로 쉽게 슬라이싱할 수 있습니다. 구문은 drawImage(image, i_x, i_y, i_width, i_eight, c_x, c_y, c_width, c_height)입니다. 다음 두 그림은 canvas에서 <img> 요소 부분을 선택 및 표시하는 방법을 보여 줍니다.

이미지를 조각 또는 타일로 나누거나 슬라이싱할 수 있음을 표시하는 이미지

원본 이미지

이미지를 조각 또는 타일로 나누거나 슬라이싱할 수 있음을 표시하는 이미지

Canvas

원본 퍼즐 이미지를 일련의 행과 열(테이블)로 나누면 각 테이블 셀에 drawImage 메서드를 적용하여 필요한 개별 이미지 타일을 만들 수 있습니다.

개별 타일이 결합되어 전체를 만드는 방법을 보여 주는 이미지

이 타일 생성 프로세스는 예제 2에서 보여 줍니다. 예제 2의 소스 코드를 보려면 예제 2 페이지를 마우스 오른쪽 단추로 클릭하고 소스 보기를 선택합니다. 예제 2에 대한 논의는 다음 두 개의 섹션으로 나뉩니다.

X-UA-Compatible 메타 태그

예제 2는 Internet Explorer를 사용하여 로컬 인트라넷에서 개발되었으므로 <meta http-equiv="X-UA-Compatible" content="IE=10"> 태그를 사용하여 Internet Explorer가 올바른 브라우저와 문서 모드로 설정되도록 했습니다. 자세한 내용은 문서 호환성 정의를 참조하세요. 일반적으로 이 태그는 페이지를 프로덕션으로 이동하기 직전에 제거해야 합니다.

이미지 슬라이싱

위의 400 x 400 퍼즐 이미지를 유용한 퍼즐 조각(또는 타일)으로 변환하려면 먼저 슬라이싱할 이미지(400x400.png)의 메모리 내 이미지 개체를 만든 다음 이미지가 완전히 로드되면 익명 함수를 호출합니다.


var image = new Image();
image.src = "images/400x400.png";
image.onload = function () { ... }

원본 이미지(image.widthimage.height)의 크기와 이미지를 나누려는 행과 열 수(NUM_COLSNUM_ROWS)에 따라 익명 함수 내에서 필요한 타일 크기를 계산합니다.


var dx = _canvas.width = image.width / NUM_COLS;
var dy = _canvas.height = image.height / NUM_ROWS;

이 Canvas에서 이러한 타일을 모두 만들기 때문에 Canvas 너비와 높이를 타일과 동일한 크기로 설정합니다. 개별 타일 이미지는 다음과 같이 만들어집니다.


for (var row = 0; row < NUM_ROWS; row++) {
  for (var col = 0; col < NUM_COLS; col++) {
    ctx.drawImage(image, dx * col, dy * row, dx, dy, 0, 0, dx, dy);
    ctx.strokeRect(0, 0, dx, dy); // Place a border around each tile.
    slicedImageTable.push( _canvas.toDataURL() );
  }
}

이 이중 중첩된 for 루프를 이해하기 쉽도록 5개 행 x 5개 열 퍼즐을 만들려고 하며 현재 rowcol 변수가 각각 2와 3이라고 가정합니다. 즉, 현재 원본 이미지의 테이블 셀(2, 3)에 있습니다.

테이블 셀 좌표(2, 3)가 선택된 5개 행 x 5개 열 퍼즐을 보여 주는 이미지

원본 이미지의 크기가 400 x 400 픽셀인 경우


dx = canvas.width = 400 / 5 = 80
dy = canvas.height = 400 / 5 = 80

결과:


ctx.drawImage(image, 80*3, 80*2, 80, 80, 0, 0, 80, 80)

또는


ctx.drawImage(image, 240, 160, 80, 80, 0, 0, 80, 80)

즉, 위치 (240, 160)에 원본 이미지의 80 x 80 스냅숏을 만듭니다.

테이블 셀 좌표(240, 160)가 선택된 퍼즐의 스냅숏 이미지

80px x 80px 캔버스의 왼쪽 맨 위에 스냅숏을 배치합니다.

독점적으로 테이블 셀 좌표(240, 160)의 스냅숏을 표시하는 이미지

이 Canvas는 다음과 같이 데이터 URL 이미지로 변환되고 슬라이싱된 이미지 배열에 저장됩니다.


slicedImageTable.push( _canvas.toDataURL() );

예제 2의 나머지 부분은 개별 타일이 취득(슬라이싱)된 것과 동일한 순서로 타일을 표시하여 퍼즐 이미지가 성공적으로 슬라이싱되도록 합니다.

이미지 슬라이스를 SVG로 변환

이제 이미지 슬라이스(즉, 타일)를 생성할 수 있으므로 다음에는 이러한 데이터 URL 이미지 타일을 SVG 이미지 개체로 변환하는 방법을 살펴보겠습니다. 이 프로세스는 예제 3에서 보여 줍니다. 예제 3의 출력을 보려면 브라우저의 디버거 콘솔 창이 열려야 합니다. 자세한 내용은 F12 개발자 도구를 사용하여 웹 페이지를 디버그하는 방법을 참조하세요. 예제 2와 3의 주요 차이점은 다음과 같이 SVG 이미지 요소를 만들고 설정하는 방법입니다.


for (var row = 0; row < NUM_ROWS; row++) {
  for (var col = 0; col < NUM_COLS; col++) {
    ctx.drawImage(image, dx*col, dy*row, dx, dy, 0, 0, dx, dy);
    ctx.strokeRect(0, 0, dx, dy); // Place a border around each tile.

    var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image');
    svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", _canvas.toDataURL());
    svgImage.setAttribute('width', dx);
    svgImage.setAttribute('height', dy);
    svgImage.setAttribute('x', dx * col);
    svgImage.setAttribute('y', dy * row);
    svgImage.correctX = dx * col;
    svgImage.correctY = dy * row;
    slicedImageTable.push(svgImage);
  }
}

SVG는 XML의 한 형태이므로 SVG 개체 모델 외부에서 SVG 요소를 만들 때 네임스페이스를 지정해야 합니다.


var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image')

SVG 이미지 요소는 href 특성을 사용합니다. 이 특성은 비-SVG <img> 요소에 사용되는 src 특성과 반대입니다. 또한 Internet Explorer의 경우 svgImage.setAttribute('href', _canvas.toDataURL())을 사용하여 SVG 이미지 요소의 href 특성을 설정할 수 있습니다. 그러나 다른 브라우저에서는 XLink 구문이 필요할 수 있으므로 이 경우 다음 사항이 대신 사용됩니다.


svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", _canvas.toDataURL());

SVG 이미지의 기본 widthheight0이므로 다음 값을 명시적으로 설정해야 합니다.


svgImage.setAttribute('width', dx);
svgImage.setAttribute('height', dy);

마지막으로, SVG 뷰포트에 연결된 좌표계(SVG 좌표 변형 참조)와 관련하여 각 타일이 올바른(즉, 해결한) 퍼즐에 있어야 하는 위치를 기록하기 위해 두 개의 사용자 지정 속성인 correctXcorrectY를 만들고 설정합니다.

SVG 이미지 표시

이제 타일 배열에 저장된 SVG 이미지를 배치했으므로 다음 작업은 화면에 SVG 이미지를 표시하는 것입니다. 이 작업을 쉽게 수행하려면 다음 세 개의 섹션에 설명되고 예제 4에 표시된 대로 몇 가지 다른 증대와 함께 SVG 요소를 <svg>에 추가합니다(소스를 보려면 마우스 오른쪽 단추 클릭).

유동적 SVG

단순히 SVG 요소를 추가하는 것 외에도 몇 가지 CSS 속성을 사용하여 SVG 뷰포트를 완전히 유동적이거나 유연하게 설정합니다. 고려할 첫 번째 항목은 SVG 요소 자체입니다.


<svg width="75%" height="75%" viewbox="0 0 400 400"></svg>

여기서 사각형 SVG 뷰포트는 가장 작은 브라우저 뷰포트 크기의 75%이며 400 x 400 단위 좌표계가 적용됩니다. 유동적 SVG가 원하는 대로 작동하려면 다음 CSS 규칙을 적용해야 합니다.

  • htmlbody 요소에는 100%height가 필요합니다.
    
    html, body {
      margin: 0;
      padding: 0;
      height: 100%
    }
    
    

    이제 브라우저의 뷰포트 크기가 줄었으므로 SVG 뷰포트의 크기도 줄어듭니다. 기본 400 x 400 단위 SVG 뷰포트 좌표계는 그대로 유지되고 좌표 단위의 크기만 변경됩니다.

  • <img> 요소처럼 SVG 요소는 인라인 요소입니다. 따라서 브라우저 뷰포트 내의 중심에 배치하기 위해 display 속성을 block으로 설정하고 leftright 여백을 auto로 설정합니다.
    
    svg {
      display: block; 
      margin: 0 auto;
    }
    
    

기능 검색

addEventListener, Canvas 및 SVG를 사용하기 때문에 퍼즐을 표시하기 전에 이러한 기능을 검색하겠습니다. 예제 4에서는 다음과 같이 이 기능을 수행합니다.


if (window.addEventListener) {
  window.addEventListener('load', main, false);
} else {
  document.getElementsByTagName('body')[0].innerHTML = "<h1>The addEventListener method is not supported - please upgrade your browser and try again.</h1>";
} // if-else

function main() {
  var game = {};
  game.canvas = document.createElement('canvas');
  game.hasCanvas = !!game.canvas.getContext;
  game.svg = document.getElementsByTagName('svg')[0];
  game.hasSVG = game.svg.namespaceURI == "http://www.w3.org/2000/svg";
  
  .
  .
  .

  if (!game.hasCanvas) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>Canvas is not supported - please upgrade your browser and try again.</h1>";
    return;
  }

  if (!game.hasSVG) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>SVG is not supported - please upgrade your browser and try again.</h1>";
    return;
  } 
        
  .
  .
  .
  
} // main

addEventListener 메서드가 지원되는 경우에만 main 함수를 입력합니다. 그런데 addEventListener가 지원되지 않는 경우 Canvas나 SVG가 지원될 가능성이 거의 없습니다.

main 함수에서 모든 게임 관련 "전역" 변수와 상태를 포함할 game 변수를 만듭니다. 사용자 브라우저에서 필요한 모든 기능을 지원하지 않는 경우 문제를 식별하는 메시지가 표시됩니다.

필요한 모든 기능이 지원되는 경우 NUM_ROWSgame.numRows로 변경되고 slicedImageTablegame.tiles로 변경되는 등 일부 변수 이름이 변경된다는 점을 제외하고 예제 3에서 사용한 것과 동일한 방법을 계속합니다. 그런 다음 이중 중첩된 for 루프에 모든 SVG 이미지 요소(타일)를 만든 후 다음에 설명된 대로 표시합니다.

타일 표시

각 타일의 위치가 이미 계산 및 설정되었으므로 SVG 요소에 추가하기만 하면 SVG 타일 이미지를 표시할 수 있습니다.


function displaySvgImages() {
  for (var i = 0; i < game.tiles.length; i++) {
    game.svg.appendChild(game.tiles[i]);
  }
 }

SVG 타일의 유동성(또는 유연성)을 확인하려면 브라우저 창의 크기를 변경합니다. SVG 뷰포트는 항상 가장 작은 브라우저 뷰포트 크기(너비 또는 높이)의 75%입니다.

타일 이동

이제 퍼즐 타일이 표시되어 있으므로 다음 작업은 사용자가 포인터 장치로 타일을 이동할 수 있게 하는 것입니다. 먼저 예제 5에 표시된 대로 세 개의 SVG 원을 이동하는 방법을 검사하여 이 문제를 살펴보겠습니다(소스를 보려면 마우스 오른쪽 단추 클릭).

먼저 현재 활성 상태인 원이 포함될 전역 배열을 만듭니다. 즉, mousedown 또는 MSPointerDown 이벤트에서 표시된 대로 "클릭"한 원입니다.


var _activeCircles = [];

다음 코드 조각을 설명하기 쉽도록 mousemove 이벤트 처리기가 SVG 원에 직접 연결된 경우(적절해 보임) 사용자가 마우스를 빠르게 이동하여 원이 "손실"될 수 있습니다. 즉, SVG 원의 이동이 사용자의 빠른 마우스 이동을 따라갈 수 없습니다. mousemove 이벤트 처리기가 화면에서 원을 이동하는 경우 마우스와 원이 실제로 분리되는 즉시 mousemove 이벤트 처리기의 실행이 중지되며 원의 이동이 중지됩니다. 이런 이유로 SVG 원 대신 window 개체에 mousemove 이벤트 처리기를 연결합니다. 사용자가 마우스를 이동하는 속도에 관계없이 보편적인 window 개체가 손실되지 않습니다.


if (navigator.msPointerEnabled) {
  window.addEventListener('MSPointerMove', handlePointerEvents, false);
} else {
  window.addEventListener('mousemove', handlePointerEvents, false);
}

이전 코드 조각처럼, 사용 가능한 경우 기존의 마우스 이벤트도 처리하는 MSPointerMove 포인터 이벤트 처리기를 등록하고 사용할 수 없는 경우 mousemove 이벤트 처리기를 등록합니다. 예제 1에 설명된 대로 동일한 개체에 두 이벤트 유형(MSPointerMovemousemove)을 모두 등록할 수 없습니다.

다음에는 각 SVG circle 요소에 포인터 down 및 포인터 up 이벤트 처리기를 등록합니다.


var svgCircles = document.getElementsByTagName('circle');
for (var i = 0; i < svgCircles.length; i++) {
  if (navigator.msPointerEnabled) {
    svgCircles[i].addEventListener('MSPointerDown', handlePointerEvents, false);
    svgCircles[i].addEventListener('MSPointerUp', handlePointerEvents, false);
  } else {
    svgCircles[i].addEventListener('mousedown', handlePointerEvents, false);
    svgCircles[i].addEventListener('mouseup', handlePointerEvents, false);
  }
} // for

handlePointerEvents 함수에 주의하세요. 이 함수는 다음에 설명하겠습니다.


function handlePointerEvents(evt) {
  var activeCircle; 
  var activeCircleIndex = evt.pointerId || 0;

  switch (evt.type) {
    case "mousedown":
    case "MSPointerDown":
      _svgElement.removeChild(evt.target);
      _svgElement.appendChild(evt.target);
      if (evt.pointerId) {
        evt.target.msSetPointerCapture(evt.pointerId);
      }
      _activeCircles[activeCircleIndex] = evt.target;
      break;
    case "mousemove":
    case "MSPointerMove":
      activeCircle = _activeCircles[activeCircleIndex];
      if (activeCircle) {
        var svgPoint = _svgElement.createSVGPoint();
        svgPoint.x = evt.clientX;
        svgPoint.y = evt.clientY;

        var ctm = activeCircle.getScreenCTM();
        svgPoint = svgPoint.matrixTransform(ctm.inverse());
        activeCircle.setAttribute('cx', svgPoint.x);
        activeCircle.setAttribute('cy', svgPoint.y);
      } // if
      break;
    case "mouseup":
    case "MSPointerUp":
      if (evt.pointerId) {
        _activeCircles[activeCircleIndex].msReleasePointerCapture(evt.pointerId);
      }              
      delete _activeCircles[activeCircleIndex];
      break;
    default:
      alert("Error in handlePointerEvents on: " + evt.type);
  } // switch
} // handlePointerEvents

handlePointerEvents를 설명하기 쉽도록 단일 원을 이동한 다음 두 원을 동시에 이동하는 두 가지 시나리오를 안내합니다.

단일 원 이동

처리할 세 가지 이벤트(down, move 및 up)가 있습니다.

down 이벤트

사용자가 마우스, 펜 또는 손가락으로 단일 원을 터치하면 down 이벤트가 발생하고 handlePointerEvents가 호출됩니다. 포인터 이벤트가 지원되는 경우 evt.pointerId는 null이 아니며 activeCircleIndexevt.pointerId와 같습니다. 그렇지 않으면 evt.pointerId || 0 때문에 activeCircleIndex0입니다. evt.pointerIdnull인 경우 한 번에 하나의 원만 활성 상태일 수 있습니다. 즉, 마우스가 허용되는 유일한 포인터 장치인 경우 _activeCircles[0]만 가능합니다.

다음에 switch 문은 evt.type을 확인하고 제어 흐름을 mousedown/MSPointerDown 절로 이동합니다. 활성 원이 항상 다른 모든 개체 위에 있도록 하려면 원을 제거한 다음 DOM에 추가합니다(마지막으로 추가한 요소가 항상 렌더링되는 마지막/최상위 요소임).

evt.pointerId가 정의된 경우 원이 등록된 모든 이벤트를 계속 수신할 수 있도록 evt.target(즉, 활성 원)에서 msSetPointerCapture를 호출합니다. 이렇게 하면 실제로 원을 제거한 다음 브라우저의 뷰포트에 다시 배치할 수 있습니다.

마지막으로, 터치한 원을 활성 원 목록에 기록합니다.


_activeCircles[activeCircleIndex] = evt.target;

move 이벤트

window 개체에서 포인팅 장치를 이동하면 mousemove/MSPointerMove 이벤트 처리기(handlePointerEvents)가 실행됩니다. 이 경우 제어 흐름이 switch 문의 mousemove/MSPointerMove 절로 이동됩니다. 이전에 원을 터치하지 않은 경우 _activeCircles 배열이 비어 있으며 activeCircle = _activeCircles[activeCircleIndex]null입니다. 이 경우 break 문으로 바로 이동하고 switch 문을 종료합니다.

반면 activeCirclenull이 아닌 경우(즉, 이동해야 하는 활성 원이 있음) 포인팅 장치의 좌표(evt.clientX, evt.clientY)를 변환합니다. 이 좌표는 브라우저의 뷰포트인 400 x 400 SVG 좌표계를 기준으로 합니다. 이 작업은 CTM(좌표 변형 매트릭스)을 통해 수행됩니다. 자세한 내용은 SVG 좌표 변형을 참조하세요.

마지막으로, 원의 중심(cx, cy)을 변형된 좌표 값으로 이동합니다.


activeCircle.setAttribute('cx', svgPoint.x);
activeCircle.setAttribute('cy', svgPoint.y);

up 이벤트

원에서 up 이벤트가 발생하면 제어 흐름이 switch 문의 mouseup/MSPointerUp 절로 이동합니다. activeCircleIndex로 표시된 터치/클릭한 원이 활성 원 목록에서 제거되며, 해당하는 경우 캡처 이벤트 요청이 msRelasePointerCapture를 통해 해제됩니다.

이중 원 이동

단일 원의 경우처럼 처리할 세 가지 이벤트(down, move 및 up)가 있습니다.

down 이벤트

이 경우 거의 동시에 발생하는 원 down 이벤트 개체 두 개가 switchmousedown/MSPointerDown 절로 진행됩니다. 이제 _activeCircles 배열에 두 개의 원 개체가 포함되어 있습니다. 액세스하는 인덱스에 연결된 evt.pointerId 값이 있습니다.

move 이벤트

거의 동시에 발생하는 창 이동 개체 두 개가 switchmousemove/MSPointerMove 절로 진행하면 각 원이 단일 원의 경우처럼 차례로 이동됩니다.

up 이벤트

거의 동시에 발생하는 원 up 이벤트 개체가 각각 mouseup/MSPointerUp 절로 진행하면 각 원이 단일 원의 경우처럼 차례로 정리됩니다.

이미지 퍼즐

이제 완전한 기능을 갖춘(모양이 반드시 멋질 필요는 없음) 터치가 용이한 이미지 퍼즐 게임을 만드는 데 필요한 모든 필수 부분이 있습니다. 다소 긴 논의를 이해하기 쉬운 많은 구성 요소로 나누겠습니다. 예제 6, 게임의 뼈대 프레임워크에서 시작합니다. 완성된 게임은 예제 7에서 제공되며 나중에 설명합니다.

뼈대 프레임워크

고급 SVG 애니메이션에 제공된 게임과 반대로, 퍼즐의 뼈대 프레임워크는 예제 6에 표시된 대로 비교적 간단합니다(소스를 보려면 마우스 오른쪽 단추 클릭). 예제 6의 핵심 태그는 다음과 같습니다.


<table>
  <tr>
    <td colspan="3"><h1>A Multi-Touch Enabled Image Puzzle Using Canvas &amp; SVG</h1></td>
  </tr>
  <tr>
    <td id="playButtonWrapper"><button id="playButton">Play</button></td>
    <td id="messageBox">Always carefully study the image before clicking the play button!</td>
    <td id="scoreBox"><strong>Score:</strong> 0</td>
  </tr>
</table>
<svg width="75%" height="75%" viewBox="0 0 400 400">
  <rect width="100%" height="100%" style="fill: black; stroke: black;" />
</svg>

<rect> 요소는 현재 크기에 관계없이 전체 SVG 뷰포트 색을 검정으로 지정하는 데 사용됩니다. 그러면 퍼즐의 타일 조각에 대한 "경기장"이 제공됩니다. 이 효과는 다음 예제 6 스크린샷에서 확인할 수 있습니다.

퍼즐의 타일 조각에 대한 뼈대 "경기장"을 보여 주는 스크린샷

예제 6이 나머지 뼈대 프레임은 다음 JavaScript로 구성됩니다.


if (window.addEventListener) {
  window.addEventListener('load', main, false);
} else {
  document.getElementsByTagName('body')[0].innerHTML = "<h1>The addEventListener method is not supported - please upgrade your browser and try again.</h1>";
} // if-else

function main() {
  var imageList = ["images/puzzle0.png", "images/puzzle1.png", "images/puzzle2.png", "images/puzzle3.png", "images/puzzle4.png", "images/puzzle5.png"]; // Contains the paths to the puzzle images (and are cycled through as necessary).
  var game = new Game(2, imageList);

  function Game(size_in, imageList_in) {
    var gameData = {};
    gameData.elements = {};
    gameData.elements.canvas = document.createElement('canvas');
    gameData.elements.svg = document.getElementsByTagName('svg')[0];
    
    this.hasCanvas = !!gameData.elements.canvas.getContext;
    this.hasSVG = gameData.elements.svg.namespaceURI == "http://www.w3.org/2000/svg";

    this.init = function() { alert("The init() method fired.") };
  } // Game

  if (!game.hasCanvas) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>Canvas is not supported - please upgrade your browser and try again.</h1>";
    return;
  } 
  
  if (!game.hasSVG) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>SVG is not supported - please upgrade your browser and try again.</h1>";
    return; 
  } 

  // Assert: The user's browser supports all required features.

  game.init(); // Start the game.
} // main

이전 기능 검색 섹션에서 설명된 대로 addEventListener를 사용할 수 있는 경우 main 함수를 발생시킵니다. 그런 다음 게임에 사용할 이미지(즉, 타일로 구분하고 불규칙화하고 해결할 이미지)의 경로가 포함된 imageList 배열을 초기화합니다. 사용자가 일곱 번째 수준으로 설정하면 첫 번째 이미지(puzzle0.png)가 무한히 재활용됩니다.

다음에는 Game 생성자 함수를 호출합니다. 첫 번째 매개 변수인 2는 열과 행이 두 개씩 있는 game 개체를 생성하도록 생성자에 지정합니다. 물론, 마지막 매개 변수는 필요한 경우 순환할 퍼즐 이미지 목록입니다.

생성자 내에서 모든 "전역" 변수를 유용한 gameData 변수에 넣습니다. this 키워드에 익숙하지 않은 경우 this.hasCanvas = !!game.elements.canvas.getContext 문은 생성자가 생성한 개체에서 hasCanvas라는 속성을 만들고 설정합니다(game 변수). 이중 부정(!!)은 game.elements.canvas.getContext 식을 부울 값(Canvas가 지원되는 경우 true)으로 강제 설정합니다.

마찬가지로, this.init = function() { … }은 생성자로 만든 모든 개체에 대해 init라는 메서드를 정의합니다(하나의 해당 개체 game만 있음). 무엇보다도 game.init()는 게임을 시작합니다.

멀티 터치 이미지 퍼즐 게임

이제 예제 7에 표시된 대로 위의 모든 정보를 완전한 기능을 갖춘 멀티 터치 이미지 퍼즐 게임으로 결합할 수 있습니다. 예제 7과 연결된 소스 코드는 익숙하고 주석이 적절하게 지정되어 있지만 추가 설명을 사용하여 다음 두 구성 요소를 더 명확하게 지정할 수 있습니다.

타일 불규칙화

createAndAppendTiles 함수에서는 예제 4와 같이 SVG 타일 이미지 개체를 생성합니다. 그러나 개체를 SVG 요소에 추가하기 전에 불규칙화하고, 불규칙화된 결과 패턴이 원본(전체) 퍼즐 이미지와 정확하게 일치하지 않도록 합니다(첫 번째 4-타일 수준의 유일한 문제임).


var randomizationOK = false; 
while (!randomizationOK) {
  coordinatePairs.sort(function() { return 0.5 - Math.random(); });
  for (var i = 0; i < gameData.tiles.length; i++) {
    if (gameData.tiles[i].correctX != coordinatePairs[i].x || 
        gameData.tiles[i].correctY != coordinatePairs[i].y) {
      randomizationOK = true;
      break;
    } // if
  } // for
} // while

타일을 쉽게 불규칙화하려면 관련 좌표 쌍을 배열(coordinatePairs)에 넣고 JavaScript 배열 sort 메서드를 사용합니다.



coordinatePairs.sort(function() { return 0.5 - Math.random(); });

sort 메서드(JavaScript)에 설명된 대로 Math.random()에서 01 사이의 값을 반환하는 경우 이 익명 정렬 함수가 배열 요소를 불규칙적으로 정렬합니다.

확장 포인터 up 절

switch 문 내의 down 및 move 절은 이전 예제와 거의 동일합니다. 그러나 up 절은 훨씬 확장되었습니다.



case "mouseup":
case "MSPointerUp":
activeTile = gameData.activeTiles[activeTileIndex];
var currentX = activeTile.getAttribute('x');
var currentY = activeTile.getAttribute('y');

for (var i = 0; i < gameData.tiles.length; i++) {
  var correctX = gameData.tiles[i].correctX;
  var correctY = gameData.tiles[i].correctY;

  if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) && 
      currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
    activeTile.setAttribute('x', correctX);
    activeTile.setAttribute('y', correctY);
    break;
  } // if
} // for

if (evt.pointerId) {
  gameData.activeTiles[activeTileIndex].msReleasePointerCapture(evt.pointerId);
} 

delete gameData.activeTiles[activeTileIndex];

if (gameData.inProgress) {
  for (var i = 0; i < gameData.tiles.length; i++) {
    currentX = Math.round(gameData.tiles[i].getAttribute('x'));
    currentY = Math.round(gameData.tiles[i].getAttribute('y'));

    correctX = Math.round(gameData.tiles[i].correctX);
    correctY = Math.round(gameData.tiles[i].correctY);

    if (currentX != correctX || currentY != correctY) {
      return;
    } // if
  } // for

  // Assert: The user has solved the puzzle.

  gameData.inProgress = false;

  gameData.score += gameData.size * gameData.size;
  gameData.elements.scoreBox.innerHTML = "<strong>Score:</strong> " + gameData.score;

  ++(gameData.size);

  var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
  document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];

  for (var i = 0; i < gameData.tiles.length; i++) {
    gameData.elements.svg.removeChild(gameData.tiles[i])
  }
  
  createAndAppendTiles();

  gameData.elements.playButton.innerHTML = "Play";
} // if
break;

논의할 up 절의 첫 번째 구성 요소는 타일 끌기입니다. 이 게임을 비롯한 많은 퍼즐 게임에서는 진행 중인 퍼즐 조각(타일)이 올바른 위치에 가깝게 이동될 경우 제자리에 맞춰져야 합니다. 이전 예제에서 복사할 때 이 작업에 사용되는 코드는 다음과 같습니다.


activeTile = gameData.activeTiles[activeTileIndex];
var currentX = activeTile.getAttribute('x');
var currentY = activeTile.getAttribute('y');

for (var i = 0; i < gameData.tiles.length; i++) {
  var correctX = gameData.tiles[i].correctX;
  var correctY = gameData.tiles[i].correctY;

  if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) && 
      currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
    activeTile.setAttribute('x', correctX);
    activeTile.setAttribute('y', correctY);
    break; // We've positioned the active tile correctly, so exit the FOR loop now.
  } // if
} // for

이 코드 조각에서는 up 이벤트가 발생할 때 활성 타일의 위치를 가져오고 모든 타일을 반복하여 임의 타일의 올바른 위치를 확인합니다. 활성 타일의 위치가 이러한 올바른 위치 중 하나에 "충분히 가까우면" 다른 올바른 위치를 확인할 필요가 없으므로 제자리로 끌고 즉시 for 루프를 중단합니다. if 절은 은유적으로 올바른 위치를 중심으로 작은 충돌 검색 상자를 그리고, 활성 타일의 위치가 이 상자 안에 있으면 절이 true가 되고 다음 두 개의 setAttribute 메서드 호출을 통해 타일을 제자리로 끕니다.


if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) && 
    currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
  activeTile.setAttribute('x', correctX);
  activeTile.setAttribute('y', correctY);
  break;
} // if

gameData.snapDelta가 증가하면 충돌 검색 상자의 크기도 증가하여 타일 끌기를 덜 민감하게 만듭니다.

게임이 현재 진행 중인 경우 모든 타일을 반복하고 무차별 암호 대입을 검사하여 활성 타일의 배치가 올바른 최종 배치인지 확인합니다.


if (gameData.inProgress) {
  for (var i = 0; i < gameData.tiles.length; i++) {
    currentX = Math.round(gameData.tiles[i].getAttribute('x'));
    currentY = Math.round(gameData.tiles[i].getAttribute('y'));

    correctX = Math.round(gameData.tiles[i].correctX);
    correctY = Math.round(gameData.tiles[i].correctY);

    if (currentX != correctX || currentY != correctY) {
      return;
    } // if
  } // for

타일 중 일부가 올바른 위치에 없는 경우 즉시 handlePointerEvents를 종료하고 다음 포인터 이벤트가 handlePointerEvents를 트리거할 때까지 기다립니다. 그렇지 않으면 사용자가 퍼즐을 해결했으며 다음 코드가 실행됩니다.


gameData.inProgress = false;

gameData.score += gameData.size * gameData.size;
gameData.elements.scoreBox.innerHTML = "<strong>Score:</strong> " + gameData.score;

++(gameData.size);

var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];

for (var i = 0; i < gameData.tiles.length; i++) {
  gameData.elements.svg.removeChild(gameData.tiles[i])
}

createAndAppendTiles();

gameData.elements.playButton.innerHTML = "Play";

이 게임 수준이 해결되었으므로 gameData.inProgressfalse로 설정하고 새 점수를 표시합니다. 게임의 현재 크기(행과 열 수)도 현재 게임 수준(즉, 사용자가 지금까지 해결한 퍼즐 수)을 나타내는 데 사용되는 경우 게임의 난이도가 행 수(또는 동일하기 때문에 열 수)의 제곱에 비례하므로 점수가 게임의 현재 크기 제곱만큼 증가합니다.


gameData.score += gameData.size * gameData.size;

그런 다음 게임 수준 프록시인 gameData.size를 늘리고 가능한 "이겼습니다" 문장 배열에서 임의 재담을 표시합니다.


++(gameData.size);

var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];

마지막으로, createAndAppendTiles를 통해 만들고 SVG 요소에 추가할 다른 SVG 이미지 요소의 다음 라운드를 준비하기 위해 SVG 요소에 첨부된 기존 SVG 이미지 요소(타일)를 제거합니다.


for (var i = 0; i < gameData.tiles.length; i++) {
  gameData.elements.svg.removeChild(gameData.tiles[i])
}

createAndAppendTiles();
gameData.elements.playButton.innerHTML = "Play";

 

여기서는 이 자습서를 통해 현실적인 시나리오, 즉 터치가 용이한 퍼즐 게임 구현에서 멀티 터치 이벤트를 처리하는 방법을 보여 주려고 했습니다. 이 지침은 관련 Canvas와 SVG를 포함하여 많은 상황에서 터치 이벤트를 처리하는 데 충분한 정보를 제공해야 합니다.

관련 항목

Contoso 이미지 사진 갤러리
제스처 이벤트
터치가 용이한 사이트 빌드 지침
터치 사용 장치에서 가리키기를 시뮬레이션하는 방법
HTML5 그래픽
Internet Explorer 샘플 및 자습서
포인터 이벤트
포인터 이벤트 업데이트

 

 

표시:
© 2014 Microsoft