UI를 멋지게 만든 방법
소개
Chrome으로 음악 만들기는 Google에서 만든 웹 기반 음악 프로젝트입니다. Chrome의 JAM을 사용하면 전 세계 사용자가 밴드를 결성하고 브라우저 내에서 실시간으로 연주할 수 있습니다. DinahMoe는 Chrome의 Web Audio API로 가능한 한계를 넓혔으며, Tool of North America팀은 컴퓨터를 마치 악기처럼 연주하고, 연주하고, 연주할 수 있는 인터페이스를 제작했습니다.
일러스트레이터 롭 베일리는 Google Creative Lab의 크리에이티브 디렉션에 따라 JAM으로 사용할 수 있는 19가지 악기 각각에 대해 정교한 일러스트를 제작했습니다. 이를 바탕으로 인터랙티브 디렉터 벤 트리클배נק과 YouTube의 디자인팀은 각 악기에 맞는 쉽고 전문적인 인터페이스를 만들었습니다.
각 악기는 시각적으로 고유하므로 Tool의 기술 디렉터인 Bartek Drozdz와 저는 PNG 이미지, CSS, SVG, 캔버스 요소를 조합하여 악기를 이어 붙였습니다.
대부분의 악기는 DinahMoe의 사운드 엔진과의 인터페이스를 동일하게 유지하면서 다양한 상호작용 방법 (예: 클릭, 드래그, 연주 - 악기로 할 수 있는 모든 작업)을 처리해야 했습니다. 멋진 재생 환경을 제공하려면 JavaScript의 mouseup 및 mousedown 외의 것이 필요하다는 사실을 발견했습니다.
이러한 모든 변형을 처리하기 위해 플레이 가능한 영역을 덮고 모든 악기에서 클릭, 드래그, 스트럼을 처리하는 '스테이지' 요소를 만들었습니다.
The Stage
스테이지는 계기 전체에서 기능을 설정하는 데 사용하는 컨트롤러입니다. 사용자가 상호작용할 수 있는 계기의 다양한 부분을 추가하는 것과 같은 작업을 할 수 있습니다. '히트'와 같은 상호작용을 더 추가하면 스테이지의 프로토타입에 추가할 수 있습니다.
function Stage(el) {
// Grab the elements from the dom
this.el = document.getElementById(el);
this.elOutput = document.getElementById("output-1");
// Find the position of the stage element
this.position();
// Listen for events
this.listeners();
return this;
}
Stage.prototype.position = function() {
// Get the position
};
Stage.prototype.offset = function() {
// Get the offset of the element in the window
};
Stage.prototype.listeners = function() {
// Listen for Resizes or Scrolling
// Listen for Mouse events
};
요소 및 마우스 위치 가져오기
첫 번째 작업은 브라우저 창의 마우스 좌표를 스테이지 요소를 기준으로 변환하는 것입니다. 이를 위해 페이지에서 스테이지가 있는 위치를 고려해야 했습니다.
요소가 상위 요소뿐만 아니라 전체 창을 기준으로 어디에 있는지 찾아야 하므로 요소의 offsetTop 및 offsetLeft를 보는 것보다 약간 더 복잡합니다. 가장 쉬운 방법은 마우스 이벤트와 마찬가지로 창을 기준으로 한 위치를 제공하고 최신 브라우저에서 잘 지원되는 getBoundingClientRect를 사용하는 것입니다.
Stage.prototype.offset = function() {
var _x, _y,
el = this.el;
// Check to see if bouding is available
if (typeof el.getBoundingClientRect !== "undefined") {
return el.getBoundingClientRect();
} else {
_x = 0;
_y = 0;
// Go up the chain of parents of the element
// and add their offsets to the offset of our Stage element
while (el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {
_x += el.offsetLeft;
_y += el.offsetTop;
el = el.offsetParent;
}
// Subtract any scrolling movment
return {top: _y - window.scrollY, left: _x - window.scrollX};
}
};
getBoundingClientRect가 없는 경우 본문에 도달할 때까지 요소 상위 요소의 체인을 위로 이동하면서 오프셋을 더하는 간단한 함수가 있습니다. 그런 다음 창이 스크롤된 거리를 빼서 창을 기준으로 한 위치를 가져옵니다. jQuery를 사용하는 경우 offset() 함수는 플랫폼 전반에서 위치를 파악하는 복잡성을 잘 처리하지만 스크롤된 양은 빼야 합니다.
페이지를 스크롤하거나 크기를 조절할 때마다 요소의 위치가 변경될 수 있습니다. 이러한 이벤트를 수신 대기하고 위치를 다시 확인할 수 있습니다. 이러한 이벤트는 일반적인 스크롤이나 크기 조절 시 여러 번 실행되므로 실제 애플리케이션에서는 위치를 재확인하는 빈도를 제한하는 것이 가장 좋습니다. 이를 수행하는 방법에는 여러 가지가 있지만 HTML5 Rocks에는 requestAnimationFrame을 사용하여 스크롤 이벤트를 데브운스하는 방법에 관한 도움말이 있으며 이 방법이 여기에서 잘 작동합니다.
이 첫 번째 예에서는 충돌 감지를 처리하기 전에 마우스가 스테이지 영역에서 움직일 때마다 상대 x 및 y를 출력합니다.
Stage.prototype.listeners = function() {
var output = document.getElementById("output");
this.el.addEventListener('mousemove', function(e) {
// Subtract the elements position from the mouse event's x and y
var x = e.clientX - _self.positionLeft,
y = e.clientY - _self.positionTop;
// Print out the coordinates
output.innerHTML = (x + "," + y);
}, false);
};
마우스 움직임을 관찰하려면 새 Stage 객체를 만들고 Stage로 사용하려는 div의 ID를 전달합니다.
//-- Create a new Stage object, for a div with id of "stage"
var stage = new Stage("stage");
간단한 조회 감지
Chrome을 사용하는 JAM에서는 모든 악기 인터페이스가 복잡하지는 않습니다. 드럼 머신 패드는 단순한 직사각형이므로 클릭이 경계 내에 있는지 쉽게 감지할 수 있습니다.
직사각형으로 시작하여 몇 가지 기본 도형 유형을 설정합니다. 각 도형 객체는 경계를 알고 있어야 하며 점이 경계 내에 있는지 확인할 수 있어야 합니다.
function Rect(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
return this;
}
Rect.prototype.inside = function(x, y) {
return x >= this.x && y >= this.y
&& x <= this.x + this.width
&& y <= this.y + this.height;
};
추가하는 각 새로운 도형 유형에는 이를 타격 지역으로 등록하는 Stage 객체 내 함수가 필요합니다.
Stage.prototype.addRect = function(id) {
var el = document.getElementById(id),
rect = new Rect(
el.offsetLeft,
el.offsetTop,
el.offsetWidth,
el.offsetHeight
);
rect.el = el;
this.hitZones.push(rect);
return rect;
};
마우스 이벤트에서 각 도형 인스턴스는 전달된 마우스 x 및 y가 도형에 대한 히트인지 확인하고 true 또는 false를 반환합니다.
정사각형 위로 스크롤할 때 마우스 커서가 포인터로 변경되는 'active' 클래스를 스테이지 요소에 추가할 수도 있습니다.
this.el.addEventListener ('mousemove', function(e) {
var x = e.clientX - _self.positionLeft,
y = e.clientY - _self.positionTop;
_self.hitZones.forEach (function(zone){
if (zone.inside(x, y)) {
// Add class to change colors
zone.el.classList.add('hit');
// change cursor to pointer
this.el.classList.add('active');
} else {
zone.el.classList.remove('hit');
this.el.classList.remove('active');
}
});
}, false);
도형 더보기
도형이 더 복잡해질수록 점이 도형 내에 있는지 확인하는 수학이 더 복잡해집니다. 하지만 이러한 방정식은 잘 확립되어 있으며 온라인의 여러 위치에 매우 자세히 설명되어 있습니다. 제가 본 최고의 JavaScript 예시 중 하나는 케빈 린드시의 기하학 라이브러리입니다.
다행히 Chrome으로 JAM을 빌드할 때는 원과 직사각형을 넘어서지 않아도 되었으며, 도형과 레이어링의 조합을 사용하여 추가 복잡성을 처리했습니다.
원
점이 원형 드럼 내에 있는지 확인하려면 원형 기본 도형을 만들어야 합니다. 직사각형과 매우 유사하지만 경계를 결정하고 점이 원 안에 있는지 확인하는 자체 메서드가 있습니다.
function Circle(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
return this;
}
Circle.prototype.inside = function(x, y) {
var dx = x - this.x,
dy = y - this.y,
r = this.radius;
return dx * dx + dy * dy <= r * r;
};
색상을 변경하는 대신 히트 클래스를 추가하면 CSS3 애니메이션이 트리거됩니다. 배경 크기를 사용하면 드럼의 위치에 영향을 주지 않고 드럼 이미지를 빠르게 조정할 수 있습니다. 이를 사용하려면 다른 브라우저의 접두사 (-moz, -o, -ms)를 추가해야 하며 접두사가 없는 버전도 추가하는 것이 좋습니다.
#snare.hit{
{ % mixin animation: drumHit .15s linear infinite; % }
}
@{ % mixin keyframes drumHit % } {
0% { background-size: 100%;}
10% { background-size: 95%; }
30% { background-size: 97%; }
50% { background-size: 100%;}
60% { background-size: 98%; }
70% { background-size: 100%;}
80% { background-size: 99%; }
100% { background-size: 100%;}
}
문자열
GuitarString 함수는 캔버스 ID와 Rect 객체를 사용하여 직사각형의 중앙을 가로질러 선을 그립니다.
function GuitarString(rect) {
this.x = rect.x;
this.y = rect.y + rect.height / 2;
this.width = rect.width;
this._strumForce = 0;
this.a = 0;
}
진동시키려면 strum 함수를 호출하여 현을 움직이게 설정합니다. 렌더링하는 모든 프레임은 연주된 힘을 약간 줄이고 줄이 앞뒤로 진동하도록 하는 카운터를 증가시킵니다.
GuitarString.prototype.strum = function() {
this._strumForce = 5;
};
GuitarString.prototype.render = function(ctx, canvas) {
ctx.strokeStyle = "#000000";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.bezierCurveTo(
this.x, this.y + Math.sin(this.a) * this._strumForce,
this.x + this.width, this.y + Math.sin(this.a) * this._strumForce,
this.x + this.width, this.y);
ctx.stroke();
this._strumForce *= 0.99;
this.a += 0.5;
};
교차 및 스트러밍
문자열의 히트 영역은 다시 상자가 됩니다. 이 상자 내부를 클릭하면 문자열 애니메이션이 트리거됩니다. 하지만 기타를 클릭하고 싶은 사람은 없을 겁니다.
스트러밍을 추가하려면 현 상자와 사용자가 마우스를 이동하는 선의 교차점을 확인해야 합니다.
마우스의 이전 위치와 현재 위치 간에 충분한 거리를 확보하려면 마우스 이동 이벤트를 가져오는 속도를 느려야 합니다. 이 예에서는 50밀리초 동안 mousemove 이벤트를 무시하도록 플래그를 설정하기만 합니다.
document.addEventListener('mousemove', function(e) {
var x, y;
if (!this.dragging || this.limit) return;
this.limit = true;
this.hitZones.forEach(function(zone) {
this.checkIntercept(
this.prev[0],
this.prev[1],
x,
y,
zone
);
});
this.prev = [x, y];
setInterval(function() {
this.limit = false;
}, 50);
};
다음으로 마우스 움직임 선이 직사각형의 중앙과 교차하는지 확인하기 위해 케빈 린드시가 작성한 교차 코드를 사용해야 합니다.
Rect.prototype.intersectLine = function(a1, a2, b1, b2) {
//-- http://www.kevlindev.com/gui/math/intersection/Intersection.js
var result,
ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x),
ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x),
u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
if (u_b != 0) {
var ua = ua_t / u_b;
var ub = ub_t / u_b;
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
result = true;
} else {
result = false; //-- No Intersection
}
} else {
if (ua_t == 0 || ub_t == 0) {
result = false; //-- Coincident
} else {
result = false; //-- Parallel
}
}
return result;
};
마지막으로 문자열 악기를 만드는 새 함수를 추가합니다. 새 스테이지를 만들고 여러 문자열을 설정하고 그려질 캔버스의 컨텍스트를 가져옵니다.
function StringInstrument(stageID, canvasID, stringNum){
this.strings = [];
this.canvas = document.getElementById(canvasID);
this.stage = new Stage(stageID);
this.ctx = this.canvas.getContext('2d');
this.stringNum = stringNum;
this.create();
this.render();
return this;
}
다음으로 문자열의 히트 영역을 배치한 다음 스테이지 요소에 추가합니다.
StringInstrument.prototype.create = function() {
for (var i = 0; i < this.stringNum; i++) {
var srect = new Rect(10, 90 + i * 15, 380, 5);
var s = new GuitarString(srect);
this.stage.addString(srect, s);
this.strings.push(s);
}
};
마지막으로 StringInstrument의 렌더링 함수가 모든 문자열을 반복하고 렌더링 메서드를 호출합니다. requestAnimationFrame이 적절하다고 판단하는 대로 항상 빠르게 실행됩니다. requestAnimationFrame에 대한 자세한 내용은 폴 아이리시의 도움말 스마트 애니메이션을 위한 requestAnimationFrame을 참고하세요.
실제 애플리케이션에서는 애니메이션이 발생하지 않을 때 플래그를 설정하여 새 캔버스 프레임 그리기를 중지하는 것이 좋습니다.
StringInstrument.prototype.render = function() {
var _self = this;
requestAnimFrame(function(){
_self.render();
});
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (var i = 0; i < this.stringNum; i++) {
this.strings[i].render(this.ctx);
}
};
마무리
모든 상호작용을 처리하는 공통 Stage 요소를 사용하는 데는 단점이 있습니다. 계산상 더 복잡하며 커서 포인터 이벤트는 이를 변경하는 추가 코드를 추가하지 않으면 제한적입니다. 하지만 Chrome을 사용하는 JAM의 경우 개별 요소에서 마우스 이벤트를 추상화할 수 있다는 이점이 매우 효과적이었습니다. 이를 통해 인터페이스 디자인을 더 많이 실험하고, 요소 애니메이션 방법 간에 전환하고, SVG를 사용하여 기본 도형의 이미지를 대체하고, 히트 영역을 쉽게 사용 중지할 수 있습니다.
드럼과 스팅을 사용해 보려면 나만의 JAM을 시작하고 표준 드럼 또는 클래식 클린 일렉트릭 기타를 선택합니다.