กรณีศึกษา - JAM with Chrome

วิธีที่เราทำให้ UI สั่นสะเทือน

เกริ่นนำ

JAM with Chrome เป็นโครงการเพลงบนเว็บที่ Google สร้างขึ้น JAM ใน Chrome ช่วยให้ผู้คนจากทั่วทุกมุมโลกตั้งวงดนตรีและ JAM ได้แบบเรียลไทม์ในเบราว์เซอร์ DinahMoe ก้าวข้ามขอบเขตของสิ่งที่เป็นไปได้ด้วย Web Audio API ของ Chrome ทีมของเราจาก Tool of North America ได้ออกแบบอินเทอร์เฟซสำหรับการตีกลอง ตีกลอง และเล่นคอมพิวเตอร์ให้ราวกับว่าเป็นเครื่องดนตรี

ด้วยแนวทางการสร้างสรรค์ของ Google Creative Lab นักวาดภาพประกอบ Rob Bailey ได้สร้างภาพประกอบอันสลับซับซ้อนสำหรับเครื่องดนตรี 19 ชนิดที่ JAM มีให้บริการ เพื่อแก้ไขปัญหานี้ ผู้อำนวยการฝ่ายอินเทอร์แอกทีฟอย่าง Ben Tricklebank และทีมออกแบบของเราที่ Tool ได้สร้างอินเทอร์เฟซที่ใช้งานง่ายและเป็นมืออาชีพสำหรับเครื่องดนตรีแต่ละชิ้น

ภาพตัดต่อเต็มเรื่อง

Bartek Drozdz ซึ่งเป็นผู้อำนวยการฝ่ายเทคนิคที่มีภาพเครื่องดนตรีแต่ละชิ้นมีเอกลักษณ์เฉพาะตัว ฉันได้ต่ออุปกรณ์เข้าด้วยกันโดยใช้องค์ประกอบรูปภาพ PNG, CSS, SVG และ Canvas

เครื่องดนตรีหลายชนิดต้องจัดการกับวิธีการโต้ตอบที่แตกต่างกัน (เช่น การคลิก การแตร และการดีดเครื่อง หรือสิ่งต่างๆ ที่คุณคาดว่าจะทำกับเครื่องดนตรี) ในขณะที่คงอินเทอร์เฟซเข้ากับเครื่องเสียงของ DinahMoe ไปพร้อมๆ กัน เราพบว่าเราต้องการมากกว่าแค่เมาส์อัพและเมาส์ดาวน์ของ JavaScript ก็สามารถให้ประสบการณ์การเล่นที่สวยงามได้

ในการจัดการกับรูปแบบนี้ทั้งหมด เราได้สร้างองค์ประกอบ “Stage” ที่ครอบคลุมพื้นที่ที่เล่นได้ เพื่อจัดการการคลิก การลาก และการดีดเครื่องดนตรีต่างๆ ทั้งหมด

เวที

เวทีเป็นตัวควบคุมที่เราใช้เพื่อตั้งค่าฟังก์ชันสำหรับเครื่องดนตรี เช่น การเพิ่มส่วนต่างๆ ของเครื่องมือที่ผู้ใช้จะโต้ตอบด้วย เมื่อเพิ่มการโต้ตอบ (เช่น "Hit") เราก็จะเพิ่มการโต้ตอบดังกล่าวลงในต้นแบบของ 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
};

การหาองค์ประกอบและตำแหน่งเมาส์

งานแรกของเราคือการแปลพิกัดของเมาส์ในหน้าต่างเบราว์เซอร์ให้สัมพันธ์กับองค์ประกอบ Stage ของเรา ในการดำเนินการดังกล่าว เราต้องพิจารณาตําแหน่งของหน้าเว็บ

เนื่องจากเราต้องการดูว่าองค์ประกอบใดที่สัมพันธ์กับหน้าต่างทั้งหน้าต่าง ไม่ใช่เพียงองค์ประกอบระดับบนเท่านั้น จึงมีความซับซ้อนมากกว่าการดูองค์ประกอบ PartnersTop และ {2/}Left วิธีที่ง่ายที่สุดคือการใช้ 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 ฟังก์ชัน alias() จะช่วยในการจัดการความซับซ้อนของการค้นหาตำแหน่งในแพลตฟอร์มต่างๆ ได้ดี แต่คุณยังคงต้องลบจำนวนเงินที่เลื่อน

เมื่อใดก็ตามที่มีการเลื่อนหรือปรับขนาดหน้าเว็บ ตําแหน่งขององค์ประกอบอาจเปลี่ยนไป เราสามารถฟังเหตุการณ์เหล่านี้และตรวจสอบตำแหน่งอีกครั้งได้ เหตุการณ์เหล่านี้จะเริ่มทำงานหลายครั้งเมื่อเลื่อนหรือปรับขนาดตามปกติ ดังนั้นในการใช้งานจริง วิธีที่ดีที่สุดคือการจำกัดความถี่ในการตรวจสอบตำแหน่งอีกครั้ง ซึ่งทำได้หลายวิธี แต่ HTML5 Rocks มีบทความสำหรับการดีเด้งเหตุการณ์การเลื่อนที่ใช้ requestAnimationFrame ซึ่งทำงานได้ดีในส่วนนี้

ก่อนที่เราจะจัดการกับการตรวจจับ Hit ตัวอย่างแรกนี้จะแสดงแค่ค่า 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 ใหม่และส่ง ID ของ div ที่ต้องการใช้เป็น Stage

//-- Create a new Stage object, for a div with id of "stage"
var stage = new Stage("stage");

การตรวจจับ Hit แบบง่าย

ใน JAM ที่ใช้ Chrome อินเทอร์เฟซเครื่องมือบางส่วนไม่ซับซ้อน แผ่นตีกลองของเราเป็นเพียงสี่เหลี่ยมผืนผ้าง่ายๆ ทำให้ตรวจสอบได้ง่ายหากคลิกอยู่ในขอบเขต

ดรัมแมชชีน

เราจะเริ่มจากสี่เหลี่ยมผืนผ้า เราจะสร้างประเภทฐานบางประเภท วัตถุรูปร่างแต่ละรายการจำเป็นต้องทราบขอบเขตของวัตถุและสามารถตรวจสอบได้ว่ามีจุดอยู่ภายในหรือไม่

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 เพื่อบันทึกเป็นโซน Hit

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 เป็น Hit หรือไม่ และส่งคืนค่าจริงหรือเท็จ

เรายังสามารถเพิ่มคลาส "แอคทีฟ" ลงในองค์ประกอบของพื้นที่งาน ซึ่งจะเปลี่ยนเคอร์เซอร์ของเมาส์เป็นตัวชี้เมื่อวางเมาส์เหนือสี่เหลี่ยมจัตุรัส

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 ที่ดีที่สุดบางส่วนที่ฉันได้เห็นมาจากไลบรารีเรขาคณิตของ Kevin Lindsey

โชคดีในการสร้าง JAM ด้วย Chrome เราไม่เคยต้องมาก่อนแค่วงกลมและสี่เหลี่ยมผืนผ้า อาศัยการผสมรูปทรงและการวางเลเยอร์เข้าไว้รับมือกับความซับซ้อนเป็นพิเศษ

รูปทรงของกลอง

วงกลม

ในการตรวจสอบว่าจุดอยู่ในกลองวงกลมหรือไม่ เราจะต้องสร้างรูปร่างฐานวงกลม แม้ว่าจะค่อนข้างคล้ายกับสี่เหลี่ยมผืนผ้า แต่ก็มีวิธีการกำหนดขอบเขตและการตรวจสอบว่าจุดอยู่ภายในวงกลมหรือไม่

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;
};

การเพิ่มคลาส Hit จะทริกเกอร์ภาพเคลื่อนไหว 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%;}
}

สตริง

ฟังก์ชัน GugarString จะใช้รหัส Canvas และวัตถุ 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;
};

ทางแยกและการดีด

พื้นที่ Hit สำหรับสตริงจะกลายเป็นกล่องอีกครั้ง การคลิกภายในช่องดังกล่าวควรจะทำให้สตริงเคลื่อนไหว แต่ใครอยากคลิกกีตาร์บ้าง

ในการเพิ่มการดีด เราต้องตรวจสอบจุดตัดของกล่องสตริงและเส้นที่เมาส์ของผู้ใช้เคลื่อนที่

หากต้องการระยะห่างระหว่างตำแหน่งปัจจุบันของเมาส์ที่เพียงพอ เราจะต้องชะลอความเร็วของเหตุการณ์การเคลื่อนที่ของเมาส์ สำหรับตัวอย่างนี้ เราจะตั้งค่าแฟล็กให้ละเว้นเหตุการณ์เลื่อนเมาส์เป็นเวลา 50 มิลลิวินาที

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);
};

ต่อไปเราจะอาศัยรหัสทางแยกที่ Kevin Lindsey เขียนไว้ เพื่อดูว่าเส้นการเคลื่อนที่ของเมาส์ตัดกับตรงกลางของสี่เหลี่ยมผืนผ้าหรือไม่

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;
}

ต่อไป เราจะวางตำแหน่งพื้นที่ Hit ของสตริง แล้วเพิ่มลงในองค์ประกอบ Stage

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);
  }
};

ในท้ายที่สุด ฟังก์ชันแสดงผลของเครื่องสายจะวนซ้ำสตริงทั้งหมดและเรียกใช้วิธีการแสดงผลของสตริงเหล่านั้น โค้ดจะทำงานตลอดเวลา ตามเวลาที่ requestAnimationFrame เห็นว่าพอดี คุณสามารถอ่านเพิ่มเติมเกี่ยวกับ requestAnimationFrame ได้ในบทความ requestAnimationFrame สำหรับแอนิเมชันอัจฉริยะของ Paul Ireland

ในแอปพลิเคชันจริง คุณอาจต้องตั้งค่าสถานะเมื่อไม่มีภาพเคลื่อนไหวเกิดขึ้นเพื่อหยุดวาดเฟรมผ้าใบใหม่

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 ทั่วไปไว้จัดการกับการโต้ตอบทั้งหมดของเราไม่ได้มีข้อเสีย ระบบคำนวณที่ซับซ้อนขึ้นและมีการจำกัดเหตุการณ์ตัวชี้เคอร์เซอร์โดยไม่ต้องเพิ่มโค้ดเพิ่มเติมเพื่อเปลี่ยนแปลง อย่างไรก็ตาม สำหรับ JAM ที่ใช้ Chrome แล้ว ประโยชน์ของการแยกเหตุการณ์ของเมาส์ออกจากองค์ประกอบแต่ละอย่างได้ผลดีมากๆ โดยช่วยให้เราทดลองการออกแบบอินเทอร์เฟซได้มากขึ้น สลับระหว่างวิธีทำให้องค์ประกอบเคลื่อนไหว ใช้ SVG เพื่อแทนที่รูปภาพของรูปทรงพื้นฐาน ปิดใช้พื้นที่ Hit ได้ง่ายๆ และอื่นๆ

หากต้องการดูการใช้งานจริงของกลองและตีกลอง ให้เริ่ม JAM ของคุณเอง แล้วเลือกกลองมาตรฐานหรือกีตาร์ไฟฟ้าแบบคลีนคลาสสิก

โลโก้ Jam