กรณีศึกษา - การสร้าง Technitone.com

Technitone - ประสบการณ์เสียงบนเว็บ

Technitone.com เป็นการนำ WebGL, Canvas, Web Sockets, CSS3, JavaScript, Flash และ Web Audio API ใหม่ใน Chrome มารวมเข้าด้วยกัน

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

ทีมผลิตของ gskinner.com

งาน

เราไม่ใช่วิศวกรเสียงที่ gskinner.com แต่ลองท้าทายเราดู เราจะหาวิธีแก้ปัญหาให้

  • ผู้ใช้วางโทนสีบนตารางกริด ซึ่ง "ได้รับแรงบันดาลใจ" จาก ToneMatrix ของ Andre
  • เสียงจะเชื่อมต่อกับเครื่องดนตรีที่ใช้แซมเปิล ชุดกลอง หรือแม้แต่เสียงที่ผู้ใช้บันทึกไว้เอง
  • ผู้ใช้ที่เชื่อมต่อหลายคนเล่นบนตารางกริดเดียวกันพร้อมกัน
  • …หรือเข้าสู่โหมดเล่นคนเดียวเพื่อสำรวจด้วยตัวเอง
  • เซสชันแบบคำเชิญช่วยให้ผู้ใช้จัดวงดนตรีและเล่นดนตรีร่วมกันได้ทันที

เราเปิดโอกาสให้ผู้ใช้สำรวจ Web Audio API ผ่านแผงเครื่องมือที่ใช้ตัวกรองและเอฟเฟกต์เสียงกับเสียง

Technitone โดย gskinner.com

นอกจากนี้

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

เรายังคงใช้การเปรียบเทียบกับตารางกริดที่คุ้นเคย ลอยอยู่ในพื้นที่ 3 มิติ เพิ่มเอฟเฟกต์แสง พื้นผิว และอนุภาคบางส่วน วางไว้ในอินเทอร์เฟซที่ขับเคลื่อนโดย CSS และ JS ที่ยืดหยุ่น (หรือแบบเต็มหน้าจอ)

ขับรถเที่ยว

ระบบจะรวมและจัดรูปแบบข้อมูลเครื่องมือ อุปกรณ์เสริม และตารางกริดในไคลเอ็นต์ จากนั้นส่งไปยังแบ็กเอนด์ Node.js ที่กําหนดเองเพื่อแก้ไขสําหรับผู้ใช้หลายคนในลักษณะเดียวกับ Socket.io ระบบจะส่งข้อมูลนี้กลับไปให้ไคลเอ็นต์โดยรวมข้อมูลของผู้เล่นแต่ละคนไว้ด้วย จากนั้นจะกระจายไปยังเลเยอร์ CSS, WebGL และ WebAudio ที่เกี่ยวข้องซึ่งมีหน้าที่แสดงผล UI, ตัวอย่างเพลง และเอฟเฟกต์ระหว่างการเล่นแบบหลายผู้ใช้

การสื่อสารแบบเรียลไทม์กับซ็อกเก็ตจะส่งข้อมูล JavaScript ไปยังไคลเอ็นต์และ JavaScript ไปยังเซิร์ฟเวอร์

แผนภาพเซิร์ฟเวอร์ Technitone

เราใช้ Node กับทุกแง่มุมของเซิร์ฟเวอร์ เป็นเว็บเซิร์ฟเวอร์แบบคงที่และเซิร์ฟเวอร์ซ็อกเก็ตแบบรวมอยู่ในที่เดียว สุดท้ายแล้วเราใช้ Express ซึ่งเป็นเว็บเซิร์ฟเวอร์ที่สมบูรณ์ซึ่งสร้างขึ้นจาก Node ทั้งหมด แพลตฟอร์มนี้ปรับขนาดได้อย่างมาก ปรับแต่งได้สูง และจัดการแง่มุมระดับล่างของเซิร์ฟเวอร์ให้คุณ (เช่นเดียวกับ Apache หรือ Windows Server) จากนั้นในฐานะนักพัฒนาแอป คุณก็จะต้องมุ่งเน้นที่การสร้างแอปพลิเคชันเท่านั้น

การสาธิตสำหรับผู้ใช้หลายคน (จริงๆ แล้วเป็นเพียงภาพหน้าจอ)

การสาธิตนี้ต้องทำงานจากเซิร์ฟเวอร์ Node และเนื่องจากบทความนี้ไม่ใช่เซิร์ฟเวอร์ดังกล่าว เราจึงใส่ภาพหน้าจอที่แสดงลักษณะของการสาธิตหลังจากที่คุณติดตั้ง Node.js กำหนดค่าเว็บเซิร์ฟเวอร์ และเรียกใช้แบบในเครื่อง ทุกครั้งที่ผู้ใช้ใหม่เข้าชมการติดตั้งเดโม ระบบจะเพิ่มตารางกริดใหม่และทุกคนจะเห็นงานของกันและกัน

ภาพหน้าจอของเดโม Node.js

โหนดนั้นใช้งานง่าย การใช้ Socket.io ร่วมกับคําขอ POST ที่กําหนดเองทําให้เราไม่ต้องสร้างกิจวัตรที่ซับซ้อนสําหรับการซิงค์ Socket.io จะจัดการเรื่องนี้อย่างโปร่งใส โดยส่ง JSON ไปรอบๆ

ง่ายแค่ไหน ดูวิดีโอนี้

เรามีเว็บเซิร์ฟเวอร์ที่พร้อมใช้งานด้วย Express จาก JavaScript 3 บรรทัด

//Tell  our Javascript file we want to use express.
var express = require('express');

//Create our web-server
var server = express.createServer();

//Tell express where to look for our static files.
server.use(express.static(__dirname + '/static/'));

เพิ่มอีก 2-3 บรรทัดเพื่อเชื่อมโยง socket.io สําหรับการสื่อสารแบบเรียลไทม์

var io = require('socket.io').listen(server);
//Start listening for socket commands
io.sockets.on('connection', function (socket) {
    //User is connected, start listening for commands.
    socket.on('someEventFromClient', handleEvent);

});

ตอนนี้เราจะเริ่มรอการเชื่อมต่อขาเข้าจากหน้า HTML

<!-- Socket-io will serve it-self when requested from this url. -->
<script type="text/javascript" src="/socket.io/socket.io.js"></script>

 <!-- Create our socket and connect to the server -->
 var sock = io.connect('http://localhost:8888');
 sock.on("connect", handleConnect);

 function handleConnect() {
    //Send a event to the server.
    sock.emit('someEventFromClient', 'someData');
 }
 ```

## Sound check

A big unknown was the effort entailed with using the Web Audio API. Our initial findings confirmed that [Digital Signal Processing](http://en.wikipedia.org/wiki/Digital_Signal_Processing) (DSP) is very complex, and we were likely in way over our heads. Second realization: [Chris Rogers](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html) has already done the heavy lifting in the API.
Technitone isn't using any really complex math or audioholicism; this functionality is easily accessible to interested developers. We really just needed to brush up on some terminology and [read the docs](https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html). Our advice? Don't skim them. Read them. Start at the top and end at the bottom. They are peppered with diagrams and photos, and it's really cool stuff.

If this is the first you've heard of the Web Audio API, or don't know what it can do, hit up Chris Rogers' [demos](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html). Looking for inspiration? You'll definitely find it there.

### Web Audio API Demo

Load in a sample (sound file)…

```js
/**
 * The XMLHttpRequest allows you to get the load
 * progress of your file download and has a responseType
 * of "arraybuffer" that the Web Audio API uses to
 * create its own AudioBufferNode.
 * Note: the 'true' parameter of request.open makes the
 * request asynchronous - this is required!
 */
var request = new XMLHttpRequest();
request.open("GET", "mySample.mp3", true);
request.responseType = "arraybuffer";
request.onprogress = onRequestProgress; // Progress callback.
request.onload = onRequestLoad; // Complete callback.
request.onerror = onRequestError; // Error callback.
request.onabort = onRequestError; // Abort callback.
request.send();

// Use this context to create nodes, route everything together, etc.
var context = new webkitAudioContext();

// Feed this AudioBuffer into your AudioBufferSourceNode:
var audioBuffer = null;

function onRequestProgress (event) {
    var progress = event.loaded / event.total;
}

function onRequestLoad (event) {
    // The 'true' parameter specifies if you want to mix the sample to mono.
    audioBuffer = context.createBuffer(request.response, true);
}

function onRequestError (event) {
    // An error occurred when trying to load the sound file.
}

…ตั้งค่าการกำหนดเส้นทางแบบโมดูล…

/**
 * Generally you'll want to set up your routing like this:
 * AudioBufferSourceNode > [effect nodes] > CompressorNode > AudioContext.destination
 * Note: nodes are designed to be able to connect to multiple nodes.
 */

// The DynamicsCompressorNode makes the loud parts
// of the sound quieter and quiet parts louder.
var compressorNode = context.createDynamicsCompressor();
compressorNode.connect(context.destination);

// [other effect nodes]

// Create and route the AudioBufferSourceNode when you want to play the sample.

…ใช้เอฟเฟกต์รันไทม์ (การกรองโดยใช้การตอบสนองต่อแรงกระตุ้น)…

/**
 * Your routing now looks like this:
 * AudioBufferSourceNode > ConvolverNode > CompressorNode > AudioContext.destination
 */

var convolverNode = context.createConvolver();
convolverNode.connect(compressorNode);
convolverNode.buffer = impulseResponseAudioBuffer;

…ใช้เอฟเฟกต์รันไทม์อื่น (ล่าช้า)…

/**
 * The delay effect needs some special routing.
 * Unlike most effects, this one takes the sound data out
 * of the flow, reinserts it after a specified time (while
 * looping it back into itself for another iteration).
 * You should add an AudioGainNode to quieten the
 * delayed sound...just so things don't get crazy :)
 *
 * Your routing now looks like this:
 * AudioBufferSourceNode -> ConvolverNode > CompressorNode > AudioContext.destination
 *                       |  ^
 *                       |  |___________________________
 *                       |  v                          |
 *                       -> DelayNode > AudioGainNode _|
 */

var delayGainNode = context.createGainNode();
delayGainNode.gain.value = 0.7; // Quieten the feedback a bit.
delayGainNode.connect(convolverNode);

var delayNode = context.createDelayNode();
delayNode.delayTime = 0.5; // Re-sound every 0.5 seconds.
delayNode.connect(delayGainNode);

delayGainNode.connect(delayNode); // make the loop

…และทำให้ได้ยิน

/**
 * Once your routing is set up properly, playing a sound
 * is easy-shmeezy. All you need to do is create an
 * AudioSourceBufferNode, route it, and tell it what time
 * (in seconds relative to the currentTime attribute of
 * the AudioContext) it needs to play the sound.
 *
 * 0 == now!
 * 1 == one second from now.
 * etc...
 */

var sourceNode = context.createBufferSource();
sourceNode.connect(convolverNode);
sourceNode.connect(delayNode);
sourceNode.buffer = audioBuffer;
sourceNode.noteOn(0); // play now!

แนวทางการเล่นใน Technitone เป็นเรื่องเกี่ยวกับการจัดตารางเวลา แทนที่จะตั้งค่าช่วงเวลาของตัวจับเวลาเท่ากับจังหวะเพื่อประมวลผลเสียงทุกๆ บีท เราตั้งค่าช่วงเวลาที่สั้นลงซึ่งจัดการและกำหนดเวลาเสียงในคิว วิธีนี้ช่วยให้ API ทำงานล่วงหน้าในการแก้ไขข้อมูลเสียง รวมถึงประมวลผลฟิลเตอร์และเอฟเฟกต์ต่างๆ ก่อนที่จะให้ CPU ทำงานเพื่อทำให้เสียงดังขึ้น เมื่อถึงเวลานั้น อุปกรณ์จะมีข้อมูลทั้งหมดที่จำเป็นในการนำเสนอผลลัพธ์สุทธิต่อผู้พูด

โดยรวมแล้ว ทุกอย่างต้องได้รับการเพิ่มประสิทธิภาพ เมื่อเราใช้ CPU หนักเกินไป ระบบจะข้ามกระบวนการ (ป๊อป คลิก ขีดข่วน) เพื่อไม่ให้เกิดความล่าช้า เราพยายามอย่างเต็มที่เพื่อหยุดความวุ่นวายทั้งหมดหากคุณเปลี่ยนไปใช้แท็บอื่นใน Chrome

การแสดงแสงสี

กริดและอุโมงค์อนุภาคจะแสดงอยู่ตรงกลาง นี่คือเลเยอร์ WebGL ของ Technitone

WebGL มีประสิทธิภาพเหนือกว่าวิธีการอื่นๆ ส่วนใหญ่ในการเรนเดอร์ภาพบนเว็บอย่างมาก โดยมอบหมายงานให้ GPU ทํางานร่วมกับโปรเซสเซอร์ ประสิทธิภาพที่เพิ่มขึ้นนี้มาพร้อมกับต้นทุนในการพัฒนาที่มากขึ้นอย่างเห็นได้ชัดและช่วงการเรียนรู้ที่ชันขึ้นมาก อย่างไรก็ตาม หากคุณหลงใหลในประสบการณ์การโต้ตอบบนเว็บอย่างแท้จริง และต้องการข้อจำกัดด้านประสิทธิภาพให้น้อยที่สุดเท่าที่จะเป็นไปได้ WebGL มีโซลูชันที่เทียบเท่ากับ Flash

การสาธิต WebGL

เนื้อหา WebGL จะแสดงผลเป็นผืนผ้าใบ (Canvas ของ HTML5) และประกอบด้วยองค์ประกอบพื้นฐานต่อไปนี้

  • จุดยอดของวัตถุ (เรขาคณิต)
  • เมทริกซ์ตำแหน่ง (พิกัด 3 มิติ)
    • Shader (คำอธิบายลักษณะเรขาคณิตที่ลิงก์กับ GPU โดยตรง)
    • บริบท ("ทางลัด" ไปยังองค์ประกอบที่ GPU อ้างอิงถึง)
    • บัฟเฟอร์ (ไปป์ไลน์สำหรับการส่งข้อมูลบริบทไปยัง GPU)
    • โค้ดหลัก (ตรรกะทางธุรกิจเฉพาะสําหรับอินเทอร์แอกทีฟที่ต้องการ)
    • เมธอด"draw" (เปิดใช้งาน Shader และวาดพิกเซลลงใน Canvas)

ขั้นตอนพื้นฐานในการแสดงผลเนื้อหา WebGL บนหน้าจอมีดังนี้

  1. ตั้งค่าเมทริกซ์มุมมอง (ปรับการตั้งค่าสำหรับกล้องที่มองเข้าไปในพื้นที่ 3 มิติ ซึ่งจะกำหนดระนาบภาพ)
  2. ตั้งค่าเมทริกซ์ตำแหน่ง (ประกาศจุดเริ่มต้นในพิกัด 3 มิติที่ระบบจะวัดตำแหน่งโดยสัมพันธ์กับจุดเริ่มต้นนั้น)
  3. เติมข้อมูล (ตำแหน่งเวิร์กเท็กซ์ สี พื้นผิว ฯลฯ) ลงในบัฟเฟอร์เพื่อส่งไปยังบริบทผ่านโปรแกรมเปลี่ยนสี
  4. ดึงและจัดระเบียบข้อมูลจากบัฟเฟอร์ด้วยเชดเดอร์ แล้วส่งไปยัง GPU
  5. เรียกใช้เมธอดวาดเพื่อบอกบริบทให้เปิดใช้งาน Shader, เรียกใช้กับข้อมูล และอัปเดตผืนผ้าใบ

การทำงานจริงจะมีลักษณะดังนี้

ตั้งค่าเมทริกซ์มุมมอง…

// Aspect ratio (usually based off the viewport,
// as it can differ from the canvas dimensions).
var aspectRatio = canvas.width / canvas.height;

// Set up the camera view with this matrix.
mat4.perspective(45, aspectRatio, 0.1, 1000.0, pMatrix);

// Adds the camera to the shader. [context = canvas.context]
// This will give it a point to start rendering from.
context.uniformMatrix4fv(shader.pMatrixUniform, 0, pMatrix);

…ตั้งค่าเมทริกซ์ตำแหน่ง…

// This resets the mvMatrix. This will create the origin in world space.
mat4.identity(mvMatrix);

// The mvMatrix will be moved 20 units away from the camera (z-axis).
mat4.translate(mvMatrix, [0,0,-20]);

// Sets the mvMatrix in the shader like we did with the camera matrix.
context.uniformMatrix4fv(shader.mvMatrixUniform, 0, mvMatrix);

…กำหนดเรขาคณิตและลักษณะบางอย่าง…

// Creates a square with a gradient going from top to bottom.
// The first 3 values are the XYZ position; the last 4 are RGBA.
this.vertices = new Float32Array(28);
this.vertices.set([-2,-2, 0,    0.0, 0.0, 0.7, 1.0,
                   -2, 2, 0,    0.0, 0.4, 0.9, 1.0,
                    2, 2, 0,    0.0, 0.4, 0.9, 1.0,
                    2,-2, 0,    0.0, 0.0, 0.7, 1.0
                  ]);

// Set the order of which the vertices are drawn. Repeating values allows you
// to draw to the same vertex again, saving buffer space and connecting shapes.
this.indices = new Uint16Array(6);
this.indices.set([0,1,2, 0,2,3]);

…เติมข้อมูลลงในบัฟเฟอร์และส่งไปยังบริบท…

// Create a new storage space for the buffer and assign the data in.
context.bindBuffer(context.ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ARRAY_BUFFER, this.vertices, context.STATIC_DRAW);

// Separate the buffer data into its respective attributes per vertex.
context.vertexAttribPointer(shader.vertexPositionAttribute,3,context.FLOAT,0,28,0);
context.vertexAttribPointer(shader.vertexColorAttribute,4,context.FLOAT,0,28,12);

// Create element array buffer for the index order.
context.bindBuffer(context.ELEMENT_ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ELEMENT_ARRAY_BUFFER, this.indices, context.STATIC_DRAW);

…และเรียกใช้เมธอด draw

// Draw the triangles based off the order: [0,1,2, 0,2,3].
// Draws two triangles with two shared points (a square).
context.drawElements(context.TRIANGLES, 6, context.UNSIGNED_SHORT, 0);

อย่าลืมล้างแคนวาสในทุกเฟรมหากไม่ต้องการให้ภาพที่อิงตามอัลฟ่าซ้อนทับกัน

The Venue

นอกจากตารางกริดและอุโมงค์อนุภาคแล้ว องค์ประกอบ UI อื่นๆ ทั้งหมดสร้างขึ้นใน HTML / CSS และตรรกะแบบอินเทอร์แอกทีฟใน JavaScript

ตั้งแต่เริ่มต้น เราตัดสินใจว่าผู้ใช้ควรโต้ตอบกับตารางกริดได้อย่างรวดเร็วที่สุด ไม่มีหน้าจอเริ่มต้น ไม่มีวิธีการ ไม่มีบทแนะนำ มีเพียง "เริ่มเลย" หากอินเทอร์เฟซโหลดแล้ว ก็ไม่ควรมีอะไรที่ทำให้ช้าลง

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

เตรียมพร้อมสำหรับการแสดง

LESS (โปรแกรมประมวลผลข้อมูล CSS ก่อนการคอมไพล์) และ CodeKit (เครื่องมือพัฒนาเว็บที่มีประสิทธิภาพสูง) ช่วยประหยัดเวลาในการแปลงไฟล์การออกแบบเป็น HTML/CSS เวอร์ชันตัวอย่างได้อย่างมาก ซึ่งช่วยให้เราจัดระเบียบ เขียน และเพิ่มประสิทธิภาพ CSS ได้อย่างหลากหลายมากขึ้น โดยใช้ตัวแปร มิกซ์อิน (ฟังก์ชัน) และแม้แต่คณิตศาสตร์

เอฟเฟกต์ระยะ

เราใช้การเปลี่ยน CSS3 และ backbone.js เพื่อสร้างเอฟเฟกต์ง่ายๆ ที่ช่วยทำให้แอปพลิเคชันมีชีวิตชีวาและแสดงภาพซึ่งบ่งบอกถึงเครื่องมือที่ผู้ใช้กำลังใช้

สีของ Technitone

Backbone.js ช่วยให้เราจับเหตุการณ์การเปลี่ยนแปลงสีและใช้สีใหม่กับองค์ประกอบ DOM ที่เหมาะสมได้ การเปลี่ยน CSS3 ที่เร่งด้วย GPU จัดการการเปลี่ยนแปลงสไตล์สีโดยแทบไม่ส่งผลกระทบต่อประสิทธิภาพ

การเปลี่ยนสีส่วนใหญ่ในองค์ประกอบอินเทอร์เฟซสร้างขึ้นจากการเปลี่ยนสีพื้นหลัง เหนือสีพื้นหลังนี้ เราได้วางภาพพื้นหลังที่มีพื้นที่โปร่งใสในจุดยุทธศาสตร์เพื่อให้สีพื้นหลังแสดงออกมา

HTML: รากฐาน

เราต้องใช้พื้นที่สี 3 รายการสำหรับเดโม ได้แก่ พื้นที่สีที่ผู้ใช้เลือก 2 รายการ และพื้นที่สีผสมอีก 1 รายการ เราสร้างโครงสร้าง DOM ที่ง่ายที่สุดเท่าที่นึกออกซึ่งรองรับการเปลี่ยน CSS3 และคำขอ HTTP น้อยที่สุดสำหรับภาพประกอบ

<!-- Basic HTML Setup -->
<div class="illo color-mixed">
  <div class="illo color-primary"></div>
  <div class="illo color-secondary"></div>
</div>

CSS: โครงสร้างแบบง่ายที่มีสไตล์

เราใช้การวางตำแหน่งแบบสัมบูรณ์เพื่อวางแต่ละภูมิภาคในตำแหน่งที่ถูกต้อง และปรับพร็อพเพอร์ตี้ background-position เพื่อจัดแนวภาพพื้นหลังภายในแต่ละภูมิภาค ซึ่งจะทำให้ภูมิภาคทั้งหมด (แต่ละภูมิภาคมีภาพพื้นหลังเดียวกัน) ดูเหมือนองค์ประกอบเดียว

.illo {
  background: url('../img/illo.png') no-repeat;
  top:        0;
  cursor:     pointer;
}
  .illo.color-primary, .illo.color-secondary {
    position: absolute;
    height:   100%;
  }
  .illo.color-primary {
    width:                350px;
    left:                 0;
    background-position:  top left;
  }
  .illo.color-secondary {
    width:                355px;
    right:                0;
    background-position:  top right;
  }

ใช้ทรานซิชันที่เร่งด้วย GPU ซึ่งคอยฟังเหตุการณ์การเปลี่ยนแปลงสี เราได้เพิ่มระยะเวลาและแก้ไข easing ใน .color-mixed เพื่อให้รู้สึกว่าสีผสมกันอยู่

/* Apply Transitions To Backgrounds */
.color-primary, .color-secondary {
  -webkit-transition: background .5s linear;
  -moz-transition:    background .5s linear;
  -ms-transition:     background .5s linear;
  -o-transition:      background .5s linear;
}

.color-mixed {
  position:           relative;
  width:              750px;
  height:             600px;
  -webkit-transition: background 1.5s cubic-bezier(.78,0,.53,1);
  -moz-transition:    background 1.5s cubic-bezier(.78,0,.53,1);
  -ms-transition:     background 1.5s cubic-bezier(.78,0,.53,1);
  -o-transition:      background 1.5s cubic-bezier(.78,0,.53,1);
}

โปรดไปที่ HTML5please เพื่อดูการรองรับของเบราว์เซอร์ในปัจจุบันและการใช้งานที่แนะนำสำหรับทรานซิชัน CSS3

JavaScript: การใช้ให้ได้ผล

การกำหนดสีแบบไดนามิกนั้นง่ายมาก เราจะค้นหาองค์ประกอบที่มีคลาสสีใน DOM และตั้งค่า background-color ตามสีที่ผู้ใช้เลือก เราใช้เอฟเฟกต์ทรานซิชันกับองค์ประกอบใดก็ได้ใน DOM โดยการเพิ่มคลาส ซึ่งจะสร้างสถาปัตยกรรมที่เบา ยืดหยุ่น และปรับขนาดได้

function createPotion() {

    var primaryColor = $('.picker.color-primary > li.selected').css('background-color');
    var secondaryColor = $('.picker.color-secondary > li.selected').css('background-color');
    console.log(primaryColor, secondaryColor);
    $('.illo.color-primary').css('background-color', primaryColor);
    $('.illo.color-secondary').css('background-color', secondaryColor);

    var mixedColor = mixColors (
            parseColor(primaryColor),
            parseColor(secondaryColor)
    );

    $('.color-mixed').css('background-color', mixedColor);
}

เมื่อเลือกสีหลักและสีรองแล้ว เราจะคำนวณค่าสีผสมและกำหนดค่าที่ได้ให้กับองค์ประกอบ DOM ที่เหมาะสม

// take our rgb(x,x,x) value and return an array of numeric values
function parseColor(value) {
    return (
            (value = value.match(/(\d+),\s*(\d+),\s*(\d+)/)))
            ? [value[1], value[2], value[3]]
            : [0,0,0];
}

// blend two rgb arrays into a single value
function mixColors(primary, secondary) {

    var r = Math.round( (primary[0] * .5) + (secondary[0] * .5) );
    var g = Math.round( (primary[1] * .5) + (secondary[1] * .5) );
    var b = Math.round( (primary[2] * .5) + (secondary[2] * .5) );

    return 'rgb('+r+', '+g+', '+b+')';
}

ภาพประกอบสำหรับสถาปัตยกรรม HTML/CSS: การสร้างบุคลิกให้กับกล่องเปลี่ยนสี 3 ช่อง

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

PNG 24 บิตช่วยให้สีพื้นหลังขององค์ประกอบ HTML แสดงผ่านบริเวณที่โปร่งใสของรูปภาพได้

แผ่นใสของรูปภาพ

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

ภูมิภาคสี

ทางออกคือการออกแบบภาพให้ขอบของบริเวณสีไม่แสดงผ่านพื้นที่โปร่งใส

ขอบของภูมิภาคสี

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

ดูไฟล์ Photoshop เป็นตัวอย่างว่าการตั้งชื่อเลเยอร์สามารถสื่อสารข้อมูลเกี่ยวกับการสร้าง CSS ได้อย่างไร

ขอบของภูมิภาคสี

Encore

สําหรับผู้ใช้ที่ไม่มี Chrome เรามีเป้าหมายที่จะกลั่นสาระสำคัญของแอปพลิเคชันเป็นภาพนิ่งภาพเดียว โหนดตารางกริดกลายเป็นจุดสนใจหลัก ไทล์พื้นหลังสื่อถึงวัตถุประสงค์ของแอปพลิเคชัน และมุมมองที่ปรากฏในการสะท้อนเป็นนัยถึงสภาพแวดล้อม 3 มิติที่สมจริงของตารางกริด

ขอบของภูมิภาคสี

หากสนใจดูข้อมูลเพิ่มเติมเกี่ยวกับ Technitone โปรดติดตามบล็อกของเรา

วงดนตรี

ขอขอบคุณที่อ่าน หวังว่าจะได้เล่นดนตรีกับคุณเร็วๆ นี้