고정 레이아웃과 canvas에 이미지 불러오는 부분은 따로 정리해두었으니 참고!!

 

이 예제에는 드래그모드와 클릭모드 총 2가지 모드와 지우개가 구현되어있다.

드래그모드는 마우스 누름 → 드래그 → 마우스 놓음 순서로 이벤트를 잡아서 누른순간부터 놓는 순간의 영역만큼 모자이크를 하는 것이고, 클릭 모드는 미리 정해진 영역에 클릭을 하면 그 영역만 모자이크가 되는 원리이다.

 

예제 설명은 드래그모드 위주로!! (드래그 모드가 더 어려웠음.. ㅠㅠ)

 

공통사항

1. 캔버스는 모두 3개가 필요하다.

  • 원본이 그려질 캔버스(originCanvas)
  • 모자이크가 되어질 캔버스(mozaicCanvas)
  • 마우스의 움직임(드래그, 호버)에 따라 사각형을 그려줄 캔버스(hoverCanvas)

2. 지우개와 모자이크가 되는 방식은 모두 동일하다. (모자이크 되는 영역이 다를뿐이다.)

3. 이미지 불러오기와 고정 레이아웃은 다른 게시글에 설명이 있다.

 

드래그 모드의 핵심

드래그모드의 이벤트 순서

  1. 마우스 누름 (mousedown) → 시작좌표 획득
  2. 드래그 (mousemove)   → hover영역에 영역을 알려주는 빨간색 네모상자를 그려주는 역할
  3. 마우스 놓음 (mouseup) → 끝좌표 획득 및 width와 height를 구하여 모자이크 실행

좌표 확득
width, heigth 구하기 (초록색, 파란색)

래스터 정보와 getImageData

이미지는 수많은 픽셀로 구성되어 있다. 이미지를 구성하는 픽셀 정보를 래스터(Raster)라고 하며 이미지 표면에 어떤 그림이 그려져 있는지를 저장한다. 래스터 데이터를 직접 조작하면 그리기 메서드로는 불가능한 효과를 낼 수 있다.

 

좌표와 width,height를 구하는 이유는 래스터 정보를 얻을 수 있는 getImageData(x, y, w, h) 메소드의 파라미터이기 때문이다.

전체코드

[HTML 코드 + CSS코드]

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>테스트</title>
    <link rel="stylesheet" href="reset.css">
    <style>
        html,body{
            height: 100%;
        }
        .wrap{
            min-width: 800px; /*전체 사이트의 길이가 800px보다 작아지지 않도록 설정*/
            height: 100%;
        }
        .header{
            height: 100px;
            position: fixed;
            top: 0; left: 0; right: 0;
            background-color: aqua;
            z-index: 100;
        }
        .container{
            /* height를 %로 지정하기 위해서는 부모요소로 부터 높이값을 상속받아야한다. */
            min-height: 100%;
            margin: 0 auto;
            margin-top: -100px;
            padding-top: 200px;  /* padding으로 해야 border-box를 사용하여 원하는 레이아웃을 만들 수 있다. */
            box-sizing: border-box;
        }
        .content{
            margin: 0 auto;
            max-width: 1200px;
            background-color: aliceblue;
        }
        .footer{
            width: 100%;
            min-height: 100px;
            background-color: blue;
        }
        #canvas_area{
            position: relative;
            display: inline-block;
            width: 100%;
            height: 600px;
            overflow: auto;
        }
        #canvas_area canvas{
            position: absolute;
            left: 0;
        }
    </style>
</head>
<body>
    <div class="wrap">
        <div class="header">
            <div>
                <button id="mode_change">모드변경</button>
                <button id="eraser">지우개</button>
            </div>
            <div>
                <input type="file" id="img_loading"></button>
            </div>
            
        </div>
        <div class="container">
            <div class="content">
                <div id="canvas_area">
                    <canvas id="origin"></canvas> <!--원본-->
                    <canvas id="mosaic"></canvas> <!--모자이크 영역-->
                    <canvas id="hover"></canvas> <!--마우스영역-->
                </div>
            </div>
        </div>
        <div class="footer">footer</div>
    </div>
</body>
<script>
...
</script>
</html>

[JS코드]

<script>
    class Mozaic{

        constructor(){
            window.mode = 0; //드래그 모드

            this.$originCanvas = document.querySelector("#origin");
            this.$mosaiCanvasc = document.querySelector("#mosaic");
            this.$hoverCanvas = document.querySelector("#hover");

            this.$imgLoading = document.querySelector("#img_loading");
            this.$eraser = document.querySelector("#eraser");
            this.$modeChange = document.querySelector("#mode_change");
            
            this.originCtx = this.$originCanvas.getContext('2d');
            this.mosaicCtx = this.$mosaiCanvasc.getContext('2d');
            this.hoverCtx = this.$hoverCanvas.getContext('2d');
            
            this.eventBinding();
            this.init();
        }

        //초기화
        init(mode=0){
            window.isErase = false;
            window.isDrag = false;
            window.sx=0, window.ex=0;
            window.sy=0, window.ey=0;
            this.hoverCtx.clearRect(0,0,this.$hoverCanvas.width, this.$hoverCanvas.height); //그려진 호버 영역 지우기
            if(mode===1){ //클릭모드 초기화 
                //모자이크 영역 초기화
                window.mozXSize = 15;
                window.mozYSize = 15;
            }
        }

        eventBinding(){
            this.$imgLoading.addEventListener('change',this.loadImg.bind(this));
            //드래그 ↔ 클릭 모드 변경
            this.$modeChange.addEventListener('click',()=>{
                window.mode = Number(!window.mode); //모드 체인지 (0:드래그[기본], 1:클릭)
                this.isErase = false; //지우개 선택되었을 시 지우개 해제
                this.init(window.mode);
                console.log('현재 모드 : ', window.mode);
            });
            //지우개 클릭 : 지우개 모드 활성화
            this.$eraser.addEventListener('click',()=>{
                window.isErase = true;
            });
            
            this.$hoverCanvas.addEventListener('mousedown',(e)=>this.hoverCanvasMouseDown.call(this,e));
            this.$hoverCanvas.addEventListener('mousemove',(e)=>this.hoverCanvasMouseMove.call(this,e));
            this.$hoverCanvas.addEventListener('mouseup',(e)=>this.hoverCanvasMouseUp.call(this,e));
            this.$hoverCanvas.addEventListener('mouseout',(e)=>this.hoverCanvasMouseOut.call(this,e));
            this.$hoverCanvas.addEventListener('click',(e)=>this.hoverCanvasMouseClick.call(this,e));
        }
        
        //호버영역 마우스 벗어났을 때 핸들링
        hoverCanvasMouseOut(e){

        }

        //호버영역 마우스 드래그 시작 (드래그 모드)
        hoverCanvasMouseDown(e){
            if(window.mode===0){ //드래그 모드인지 확인
                window.isDrag = true;
                window.sx = e.layerX;
                window.sy = e.layerY;
                this.hoverCtx.strokeRect(sx,sy,0,0);
            }
        }
        
        //호버영역 마우스 클릭(클릭모드)
        hoverCanvasMouseClick(e){
            if(window.mode === 1){
                //좌표가져오기
                window.ex = Math.max(0, e.layerX);
                window.ey = Math.max(0, e.layerY);
                const imgData = this.originCtx.getImageData(window.ex,window.ey,window.mozXSize,window.mozYSize);
                this.doMosaic(imgData,ex,ey);
            }
        }

        //호버영역 마우스 드래그 끝 (드래그 모드)
        hoverCanvasMouseUp(e){
            if(window.mode === 0){ //드래그 모드 
                isDrag = false; //드래그끝을 알린다.
                this.hoverCtx.clearRect(0,0,this.$hoverCanvas.width, this.$hoverCanvas.height); //표시된 드래그 영역 삭제

                window.ex = e.layerX;
                window.ey = e.layerY;

                const startX = Math.min(window.sx, window.ex);
                const startY = Math.min(window.sy, window.ey);
                const endX = Math.max(window.sx,window.ex);
                const endY = Math.max(window.sy,window.ey);

                const imgData = this.originCtx.getImageData(startX,startY, endX - startX, endY - startY);
                this.doMosaic(imgData,startX,startY);
            }
        }

        //호버영역 마우스 이동 (드래그모드, 클릭 모드)
        hoverCanvasMouseMove(e){
            if(window.mode === 0){ //드래그 모드 확인
                if(isDrag){ //드래그 중일때만 동작
                    this.hoverCtx.clearRect(0,0,e.target.width, e.target.height);
                    this.hoverCtx.strokeStyle = "#FF0000"; //빨간색으로 드래그 영역 잡히게 설정
                    this.hoverCtx.strokeRect(sx,sy, e.layerX - sx, e.layerY - sy);
                }
            }else if(window.mode === 1){ //클릭 모드
                //좌표 가져오기
                const x = Math.max(e.layerX,0);
                const y = Math.max(e.layerY,0);
                //기존 호버영역 그려진 것들 삭제
                this.hoverCtx.clearRect(0,0,e.target.width, e.target.height);
                this.hoverCtx.strokeStyle = 'black';
                this.hoverCtx.strokeWidth = 30;
                this.hoverCtx.fillStyle = 'rgba(0,255,255,0.5)';
                this.hoverCtx.fillRect(x,y,window.mozXSize, window.mozYSize);
            }
        }

        //이미지 불러오기
        loadImg(){
            const file = this.$imgLoading.files[0];
            console.log(file);
            if(!file.type.match(/image.*/)){
                alert('이미지 파일이 아닙니다.');
                return;
            }
            
            const reader = new FileReader();
            reader.onload = (e)=>{
                const img = new Image();
                img.onload = ()=>{
                    this.$originCanvas.width = img.width;
                    this.$originCanvas.height = img.height;
                    
                    this.$mosaiCanvasc.width = img.width;
                    this.$mosaiCanvasc.height = img.height;

                    this.$hoverCanvas.width = img.width;
                    this.$hoverCanvas.height = img.height;

                    this.originCtx.drawImage(img,1,1);
                    this.mosaicCtx.drawImage(img,1,1);
                }
                img.src = e.target.result;
            }
            reader.readAsDataURL(file);
        }

    }
    new Mozaic();
</script>

reset.css
0.00MB

reset.css를 다운로드 받아서 같은 경로에 놔두면 된다.

 

[결과물]

drag 모드

 

클릭모드

 

참고사이트

http://www.soen.kr/html5/html3/3-2-2.htm

 

HTML5 매뉴얼

3-2-2.이미지 데이터 이미지는 수많은 픽셀로 구성되어 있다. 이미지를 구성하는 픽셀 정보를 래스터(Raster)라고 하며 이미지 표면에 어떤 그림이 그려져 있는지를 저장한다. 래스터 데이터를 직

www.soen.kr

 

1. FileReader 객체

FileReader 객체는 웹 애플리케이션이 비동기적으로 데이터를 읽기 위하여 읽을 파일을 가리키는File 혹은 Blob 객체를 이용해 파일의 내용을(혹은 raw data버퍼로) 읽고 사용자의 컴퓨터에 저장하는 것을 가능하게 해줍니다.

File 객체는 <input> 태그를 이용하여 유저가 선택한 파일들의 결과로 반환된 FileList 객체, 드래그 앤 드랍으로 반환된 DataTransfer객체 혹은 HTMLCanvasElement의 mozGetAsFile() API로 부터 얻습니다.

 

1-1. FileReader 메서드

MDN 사이트에서 찾아보니 FileReader에 대한 설명이 자세히 나와있었다. 그 중에 우리가 볼 것은 딱 세가지!!!

FileReader.readAsDataURL  Blob의 내용을 읽어서 URL로 표현 (여기에서 URL은 base64)
FileReader.onload  파일 읽기가 완료되면 호출되는 콜백함수
FileReader.readyState  FileReader의 상태를 나타내는 숫자
  • EMPTY : 0 : 아직 데이터가 로드 되지 않았음.
  • LOADING : 1 : 데이터가 로딩 중.
  • DONE : 2 : 모든 읽기 요청이 완료됨.

 

2. 코드

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CANVAS_1</title>
</head>
<body>
    <input type="file">
    <canvas id="canvas"></canvas>
</body>
</html>

<HTML 코드>

 

window.addEventListener('DOMContentLoaded', (e) => {
        window.$canvas = document.querySelector("#canvas");
        window.ctx = $canvas?.getContext('2d');
        document.querySelector('input')?.addEventListener('change',(e)=>loadFile(e)); //파일 업로드 이벤트는 input의 change로 잡을 수 있다.
    });

    function loadFile(e){
        const file = e.target.files[0];
        console.log(file);
        if(!file.type.match(/image.*/)){ //이미지 파일이 아니라면 필터링
            alert('이미지 파일이 아닙니다.');
            return;
        }
        const reader = new FileReader();
        reader.onload = (e)=>{
            console.log(e);
            const img = new Image();
            img.onload = ()=>{
                $canvas.width = img.width;
                $canvas.height = img.height;
                ctx.drawImage(img,1,1);
            }
            img.src = e.target.result;
        }
        reader.readAsDataURL(file);
    }

<javascript 코드>

메서드 실행순서

  1. input의 change메서드에 이벤트 핸들러를 지정한다. (input의 파일 업로드는 change 이벤트가 발생!)
  2. 사용자가 파일을 업로드
  3. change 이벤트 핸들러인 loadFile() 함수 호출
    1. 이벤트 객체로부터 가져온 file 정보를 기반으로 FileReader의 readAsDataURL메서드 호출 
    2. readAsDataURL메서드가 파일을 모두 읽으면 onload 콜백함수 호출
    3. onload함수 안에서는 Image객체를 생성하고 src를 readeAsDataURL 메서드의 결과물인 Base64로 읽힌 파일(e.target.result) 을 넣어준다.
    4. Image파일의 로드가 모두 끝나면 img.onload 콜백함수가 또 실행되는데 이 함수에서는 canvas의 크기와 canvas에 설정될 이미지를 설정해준다.

 

결과물

 

 

이벤트 객체

위의 코드에서 console.log의 결과물이다.

ProgressEvent객체의 target은 FileReader인데 FileReader의 result를 보면 base64로 인코딩된 file을 볼 수 있고, readerState도 DONE(2) 이 된것을 볼 수 있다.

 

 

 

 

참고사이트 : https://developer.mozilla.org/ko/docs/Web/API/FileReader

 

FileReader - Web API | MDN

FileReader 객체는 웹 애플리케이션이 비동기적으로 데이터를 읽기 위하여 읽을 파일을 가리키는File 혹은 Blob 객체를 이용해 파일의 내용을(혹은 raw data버퍼로) 읽고 사용자의 컴퓨터에 저장하는

developer.mozilla.org

 

'코딩이야기 > JS' 카테고리의 다른 글

[canvas] 캔버스로 모자이크 하기  (0) 2022.11.01
객체 DEEP COPY  (0) 2022.10.17
JavaScript 배열 랜덤 섞기  (0) 2022.10.17

배열, 객체의 call by value, call by reference에 대해 공부하다가 깊은 복사를 단순하게라도 구현해보고 싶은 마음이 생겼다.

 

1. 객체 깊은 복사 코드

function objectDeep(origin){
        const target = {};
        Object.keys(origin).forEach((k)=>{
            if(Array.isArray(origin[k])){
                target[k] = arrayDeep(origin[k]);
            }else if(typeof origin[k] === 'object'){
                target[k] = objectDeep(origin[k]);
            }else{
                target[k] = origin[k];
            }
        });
        return target;
    }

2. 배열 깊은 복사 코드

    function arrayDeep(origin){
        const target = [];
        origin.forEach((v)=>{
            if(Array.isArray(v)){
                target.push(arrayDeep(v));
            }else if(typeof v === 'object'){
                target.push(objectDeep(v));
            }else{
                target.push(v);
            }
        });
        return target;
    }

 

두 함수는 각각 객체 깊은 복사함수(1번), 배열 깊은 복사함수(2번)이다.

두 함수의 생김새나 하는 역할은 매우 유사한 것을 확인할 수 있다.

 

objectDeep을 위주로 설명해보자면 

1. 재귀함수 개념을 사용하였다.

2. 해당 키 또는 인덱스에 있는 값이 배열이면 arrayDeep을 호출한다.

3. 해당 키 또는 인덱스에 있는 값이 객체이면 objectDeep을 호출한다.

4. 2번, 3번에 해당하지 않으면 원시값으로 판단하고 그대로 대입하여 복사한다.

 

↓ 3번은 객체안의 객체값이 해당한다고 생각하면 된다.

const obj = {
 inner: {
  text: '객체안의 객체',
 },
}

 

원시값이면 복사할 배열(target)에 바로 값을 대입하지만 객체이거나 배열이라면 그대로 값을 대입해버리면 참조값만 대입되어 얉은 복사가 되므로 주의해야 한다.

 

3. 테스트

더미데이터

const array = [1,2,3,4,5,[6,7,8,[9,10,11]],12,13,14, {a:1,b:2}];
const target = arrayDeep(array); //array의 데이터를 깊은 복사한 배열

테스트 케이스

array.push(15);
target[9].a=55;
target[5][0] = 77777;
target[5][3][0] = 99999;

결과

테스트1. 원본 데이터 변경

array.push(15)

원본인 array의 끝에 15라는 값을 추가했지만 복사본인 target에는 추가되지 않은 것을 확인할 수 있다.

 

테스트2. 복사본 데이터 변경

target[9].a=55;

복사본(target)의 9번 인덱스에 위치한 객체의 a를 55로 바꾸었지만 원본(array)의 9번 인덱스에 위치한 객체는 변경되지 않은 것을 확인할 수 있다.

 

테스트3. 복사본 데이터 변경2

target[5][0] = 77777;

설명이 필요없다. 테스트2번의 경우와 똑같다.

 

아래도 마찬가지

target[5][3][0] = 99999;

이렇게 원본 또는 복사본에 있는 원시값이 아닌 객체들을 변경해도 서로 메모리가 공유되어 있지 않으니 각각 적용이 되는 것을 볼 수 있었다.

개발을 하다보면 배열에 있는 값을 랜덤으로 섞어서 다른 배열을 만들어야할 때가 있다.

 

 

그때그때 짜기 보다는 그냥 하나 만들어두고 복붙하는게 좋을거같아서 끄적여본다 ㅎㅎ.

    const origin = [1,2,3,4,5,6,7,8,9];
    const shuffle = shuffleFunc(origin);

    function shuffleFunc(origin){
        const shuffle = [];    
        const dummy = origin.slice();
        
        while(dummy.length > 0){
            const index = Math.floor(Math.random() * dummy.length); // 0 <= x < length
            shuffle.push(dummy[index]);
            dummy.splice(index,1); //버려준다.
        }
        return shuffle;
    }

 

[설명]

1. 변수 설명

const shuffle 배열은 무작위하게 섞인 항목들이 들어갈 배열이 된다.
const dummy 배열은 해당 함수가 splice를 사용하여 구현되기 때문에 원본배열을 손상시키지 않기 위해서 
원본배열의 참조값을 가지고 있는 배열일 뿐이다.

 

2. 주요 로직

[ 무작위 값 선택 ] 

주의할 점은 배열의 범위를 넘어서는 index를 선택하면 안된다.

Math.random() 은 무작위로 0 <= x < 1의 값을 반환한다.

우리는 dummy 배열의 인덱스를 무작위로 추출해야하므로 dummy배열의 length를 활용한다.

 

Math.random() * dummy.length 의 반환값은 아래와 같을 것이다.

0 <= x < dummy.length 즉, 위의 예시에서는 0 <= x < 9 (0~8까지)

해당 식의 반환값은 현재 dummy 길이를 초과하지 않는 index값이 반환될 것이고, shuffle에 해당 index의 값을 넣어주기만 하면된다.

 

[ 후처리 ]

index하나를 무작위로 선택하여 결과 배열(shuffle)에 넣었으니 선택된 index는 다시 선택되지 않도록 처리해줘야한다.

 

그것을 splice로 처리하고 있다.

 

splice(i, N)  하면 i번째 부터 N개까지 추출하여 배열로 반환하는 메소드이며, 해당 메소드는 대상이 되는 원본배열을 수정해버리기 때문에 주의해야한다. (slice와 다름 주의!!) 

 

결과적으로 splice(index, 1)을 하게되면 해당 index의 항목이 빠지게 되는 효과를 볼 수 있다.

 

[ 정리 ]

const index = Math.floor(Math.random() * dummy.length); ===> 4가 나왔다고 가정

 

 

shuffle.push(dummy[index]);

 


           

dummy.splice(index,1);

 

 

 

 

 

+ Recent posts