Connected Line Generator

얼마 전 참여했던 프로젝트에서 디자인 요소 중 하나인 라인과 관련해 이슈가 있었다. 디자인에서는 제품의 특정 부분과 그 부분에 대한 상세 이미지를 선으로 이어 연관성을 표시하고 있었는데, 해당 프로젝트는 반응형이었고, 브라우저의 크기에 따라 제품 이미지와 상세 이미지의 크기 및 위치가 달라져야 했다. 사실 이 두가지 이미지는 별도의 이미지로 구성되어 각각의 스타일 속성만 제어해주면 되는 일인데, 문제는 이 두 이미지를 잇는 선에 있었다. 바뀐 이미지의 위치에 따라 이 선의 길이와 각도 역시 변해야 했기 때문이다.

하지만, 이 페이지는 내 담당이 아니었기에 나는 그런 이슈가 있다는 얘기만 들었을 뿐 따로 고민하지는 않았다. 며칠이 지나고 이 페이지의 결과물을 볼 수 있었는데, svg로 라인을 그리고 리사이즈가 될 때마다 x1, y1, x2, y2와 같은 좌표 값을 변경하는 방식으로 구현되어 있었다. 어떻게 보면 당연한 해결책이지만, ‘svg가 아닌 다른 방법으로는 안될까?’, ‘좀 더 다양한 상황에서도 간단하게 선을 표시할 수 있는 방법은 없을까?’ 하는 호기심이 생겼다(svg 방식이 나쁘다는 것은 아니다).

내가 생각한 방법은 다음과 같다. 두 오브젝트에 연결할 기준점, 즉 좌표 값을 구한 후 이 값을 바탕으로 사각형 박스를 생성하고, 여기에 오브젝트를 연결할 선을 담은 뒤 회전.

즉, 첫번째 그림처럼 우선 각각의 오브젝트에 좌표(+ 모양)를 설정하고, 이 좌표 값을 기준으로 두번째 그림처럼 사각형 박스를 생성한다. 그리고 마찬가지로 좌표 값을 기준으로 길이(거리)를 구한다. 이렇게 구한 길이와 같은 높이를 가진 선(사실은 박스)을 생성하고, 세번째 그림처럼 박스의 정중앙에 위치시킨다. 마지막으로 네번째 그림처럼 선이 담긴 박스를 적절하게 회전시켜주면 완성.

이를 코드로 옮겨보면 다음과 같다.

HTML

<div class="box obj1"></div>
<div class="box obj2"></div>

CSS

body {
	background: #1e2534;
	* {
		pointer-events: none;
	}
}
.box {
	position: absolute;
	width: 80px;
	height: 80px;
	z-index: 10;
	pointer-events: all;
}
.box.obj1 {
	top: 20%;
	left: 20%;
	background: #089acb;
}
.box.obj2 {
	right: 20%;
	bottom: 20%;
	background: #be1e31;
}
.line-generator-dot {
	position: absolute;
	width: 0;
	height: 0;
	z-index: 9999;
}
.line-generater-container {
	position: absolute;
	background: rgba(122,255,81,0.3);
	z-index: 9999;
}
.line-generater-container .line {
	position: absolute;
	top: 50%;
	left: 50%;
	background: yellow;
	transform: translate(-50%, -50%);
}

Javascript

$(function() {
	function lineGenerator(obj1Attr, obj2Attr, options) {		
		var t1Horizonal = obj1Attr.origin.split(" ")[0];
		var t1Vertical  = obj1Attr.origin.split(" ")[1];

		if(t1Horizonal == "top") {
			t1Horizonal = 0;
		} else if(t1Horizonal == "center") {
			t1Horizonal = '50%';
		} else if(t1Horizonal == "bottom") {
			t1Horizonal = '100%';
		}

		if(t1Vertical == "left") {
			t1Vertical = 0;
		} else if(t1Vertical == "center") {
			t1Vertical = '50%';
		} else if(t1Vertical == "right") {
			t1Vertical = '100%';
		}

		var t2Horizonal = obj2Attr.origin.split(" ")[0];
		var t2Vertical  = obj2Attr.origin.split(" ")[1];

		if(t2Horizonal == "top") {
			t2Horizonal = 0;
		} else if(t2Horizonal == "center") {
			t2Horizonal = '50%';
		} else if(t2Horizonal == "bottom") {
			t2Horizonal = '100%';
		}

		if(t2Vertical == "left") {
			t2Vertical = 0;
		} else if(t2Vertical == "center") {
			t2Vertical = '50%';
		} else if(t2Vertical == "right") {
			t2Vertical = '100%';
		}

		var $dot1 = $('<div class="line-generator-dot dot1"></div>');
		var $dot2 = $('<div class="line-generator-dot dot2"></div>');

		$dot1.css({top: t1Horizonal, left: t1Vertical});
		$dot2.css({top: t2Horizonal, left: t2Vertical});

		obj1Attr.target.prepend($dot1);
		obj2Attr.target.prepend($dot2);

		var $lineContainer = $('<div class="line-generater-container"></div>');
		var $line = $('<div class="line"></div>');

		$lineContainer.append($line);
		$('body').append($lineContainer);

		window.lineGeneraterOptions = options;
		$(window).on('resize', dotChecker).trigger('resize');
	};
	
	function dotChecker() {
		var dot1Position = $('.line-generator-dot.dot1').offset();
		var dot2Position = $('.line-generator-dot.dot2').offset();
		
		var lgcTop, lgcLeft, lgcWidth, lgcHeight;
		if(dot1Position.top < dot2Position.top) {
			lgcTop = dot1Position.top;
			lgcHeight = dot2Position.top - dot1Position.top;
		} else {
			lgcTop = dot2Position.top;
			lgcHeight = dot1Position.top - dot2Position.top;
		}
		if(dot1Position.left < dot2Position.left) {
			lgcLeft = dot1Position.left;
			lgcWidth = dot2Position.left - dot1Position.left;
		} else {
			lgcLeft = dot2Position.left;
			lgcWidth = dot1Position.left - dot2Position.left;
		}
		$('.line-generater-container').css({top: lgcTop, left: lgcLeft, width: lgcWidth, height: lgcHeight});
		getLength(lgcWidth, lgcHeight);
		getAngle(dot1Position.left, dot1Position.top, dot2Position.left, dot2Position.top);
	}
	
	function getLength(l1, l2) { // 대각선 길이 구하기
		l1 = Math.pow(parseFloat(l1), 2);
		l2 = Math.pow(parseFloat(l2), 2);
		var dl = Math.sqrt(l1 + l2);
		
		$('.line-generater-container .line').css({width: window.lineGeneraterOptions.width, height: parseFloat(dl.toFixed(2)), background: window.lineGeneraterOptions.color});
	}
	
	function getAngle(a1x, a1y, a2x, a2y) { // 두 좌표간 각도 구하기
		var dx = a2x - a1x;
		var dy = a2y - a1y;
		var rad = Math.atan2(dx, dy);
		
		$('.line-generater-container').css('transform', 'rotate(' + ((rad * -180) / Math.PI).toFixed(2) + 'deg)');
	}
	
	lineGenerator({
		target: $('.box.obj1'),
		origin: '50% center', // 상하(top, center, bottom, %, px) 좌우(left, center, right, %, px)
	}, {
		target: $('.box.obj2'),
		origin: 'center 50%', // 상하(top, center, bottom, %, px) 좌우(left, center, right, %, px)
	}, {
		width: 4,
		color: '#ffc619',
	});
});

그리고… 결과물

“자세한 설명은 생략한다.”
위 코드의 결과물은 다음과 같다. 어떤 형태로 작동하는지를 보여주기 위한 예제지만, 지금 상태로도 화면의 크기에 따라 도형과 선이 조정되는 것을 볼 수 있다.

사용법은 다음과 같다.

Javascript

lineGenerator(
    // <!-- 첫번째 오브젝트 설정
    {
        target: $('.box.obj1'),
        origin: '50% center'
    },
    // 첫번째 오브젝트 설정 -->
    
    // <!-- 두번째 오브젝트 설정
    {
        target: $('.box.obj2'),
        origin: 'center 50%'
    },
    // 두번째 오브젝트 설정 -->
    
    // <!-- 기타 옵션
    {
        width: 4,
        color: '#ffc619'
    }
    // 기타 옵션 -->
);

우선 첫번째와 두번째 오브젝트에 대한 정보를 입력한다. 각각 target으로는 해당 오브젝트를 jQuery 셀렉터 방식으로 입력하고, origin에는 좌표의 위치를 입력한다. origin은 transform-origin과 동일하다고 보면 된다. 순서대로 상하 정렬 값과 좌우 정렬 값을 스페이스로 구분하여 입력하면 되고, top, bottom, left, right, center 또는 % 단위의 값과 px 단위의 값을 입력할 수 있다. 예제에서는 오브젝트의 정중앙을 좌표의 위치로 설정했다.

다음으로 width의 값으로 선의 두께를 px 단위로 (단위는 생략하고 숫자만)입력하고, color의 값으로 색상을 입력해준다.

그리고… 기능 추가

플러그인 까지는 아니지만, 드래그 기능과 지그재그 형태로 선을 잇는 타입을 하나 더 추가하여 조금 더 쓸만하게 만든 업그레이드 버전이다. 또, 처음과는 달리 단 2개가 아닌 여러개의 오브젝트를 연결할 수 있다.

적용 예시

이런 식으로 응용하여 사용할 수 있다.

마무리

이것으로 “만들어 두면 쓸데가 있을지도 몰라”의 첫번째 글을 마친다. 이 프로젝트는 프로젝트 이름처럼 누군가, 언젠가, 어디선가는 쓸데가 있을 법한 기능들을 구현해보는 프로젝트다. 아무쪼록 단 몇명이라도 누군가에게 ‘쓸모 있는’ 자료가 되었으면 한다. 그리고 쓸모가 있었다면, 부디 그냥 “퍼가요~” 하고 지나치진 않았으면 좋겠다. 그건 도리가 아니다. 고맙다는 인사도 최선은 아니다. 돈, 돈으로 달라. 얼마가 됐든 돈이 최고다. 내가 없어서 그렇지, 돈 엄청 좋아한다.

카카오뱅크 3333-01-7680446 김철중

나 지금 되게 진지하다.