Nghiên cứu điển hình – Xây dựng Technitone.com

Sean Middleditch
Sean Middleditch
Technitone – trải nghiệm âm thanh trên web.

Technitone.com là sự kết hợp của WebGL, Canvas, Web Sockets, CSS3, Javascript, Flash và Web Audio API mới trong Chrome.

Bài viết này sẽ đề cập đến mọi khía cạnh của quá trình sản xuất: kế hoạch, máy chủ, âm thanh, hình ảnh và một số quy trình mà chúng tôi tận dụng để thiết kế cho tính năng tương tác. Hầu hết các phần đều chứa đoạn mã, bản minh hoạ và tệp tải xuống. Ở cuối bài viết, có một đường liên kết tải xuống để bạn có thể tải tất cả các tệp này xuống dưới dạng một tệp zip.

Nhóm sản xuất gskinner.com.

Chương trình biểu diễn

Chúng tôi không phải là kỹ sư âm thanh tại gskinner.com — nhưng hãy thử thách chúng tôi và chúng tôi sẽ tìm ra một kế hoạch:

  • Người dùng đưa các tông màu lên lưới,"lấy cảm hứng" từ ToneMatrix của Andre
  • Các âm sắc được kết nối với các nhạc cụ được lấy mẫu, bộ trống hoặc thậm chí là bản ghi âm của chính người dùng
  • Nhiều người dùng đã kết nối chơi trên cùng một lưới đồng thời
  • …hoặc chuyển sang chế độ chơi một mình để tự khám phá
  • Phiên mời cho phép người dùng tổ chức một ban nhạc và chơi nhạc ngẫu hứng

Chúng tôi mang đến cho người dùng cơ hội khám phá Web Audio API thông qua một bảng công cụ áp dụng các bộ lọc và hiệu ứng âm thanh cho âm sắc của họ.

Technitone của gskinner.com

Chúng tôi cũng:

  • Lưu trữ các thành phần và hiệu ứng của người dùng dưới dạng dữ liệu và đồng bộ hoá dữ liệu đó trên các ứng dụng
  • Cung cấp một số lựa chọn màu sắc để trẻ có thể vẽ những bài hát trông thật ngầu
  • Cung cấp một thư viện để mọi người có thể nghe, yêu thích hoặc thậm chí chỉnh sửa tác phẩm của người khác

Chúng tôi vẫn giữ nguyên phép ẩn dụ lưới quen thuộc, thả lưới đó vào không gian 3D, thêm một số hiệu ứng ánh sáng, hoạ tiết và hạt, đặt lưới đó trong giao diện CSS và JS linh hoạt (hoặc toàn màn hình).

Chuyến đi đường

Dữ liệu về công cụ, hiệu ứng và lưới được hợp nhất và chuyển đổi tuần tự trên ứng dụng, sau đó được gửi đến phần phụ trợ Node.js tuỳ chỉnh của chúng tôi để phân giải cho nhiều người dùng theo Socket.io. Dữ liệu này được gửi lại cho ứng dụng cùng với nội dung đóng góp của từng người chơi, trước khi được phân tán đến các lớp CSS, WebGL và WebAudio tương ứng chịu trách nhiệm kết xuất giao diện người dùng, mẫu và hiệu ứng trong quá trình phát nhiều người dùng.

Giao tiếp theo thời gian thực với các ổ cắm cấp dữ liệu JavaScript trên máy khách và JavaScript trên máy chủ.

Sơ đồ máy chủ Technitone

Chúng tôi sử dụng Node cho mọi khía cạnh của máy chủ. Đây là một máy chủ web tĩnh và máy chủ ổ cắm của chúng tôi tích hợp trong một. Express là công cụ mà chúng ta đã sử dụng. Đây là một máy chủ web đầy đủ được xây dựng hoàn toàn trên Node. Nó có khả năng mở rộng cực tốt, có thể tuỳ chỉnh cao và xử lý các khía cạnh máy chủ cấp thấp cho bạn (giống như Apache hoặc Windows Server). Sau đó, với tư cách là nhà phát triển, bạn chỉ cần tập trung vào việc xây dựng ứng dụng.

Bản minh hoạ nhiều người dùng (được rồi, thực sự đây chỉ là ảnh chụp màn hình)

Bản minh hoạ này yêu cầu chạy trên máy chủ Node và vì bài viết này không phải là một máy chủ Node, nên chúng tôi đã cung cấp ảnh chụp màn hình về giao diện của bản minh hoạ sau khi bạn cài đặt Node.js, định cấu hình máy chủ web và chạy máy chủ đó trên máy. Mỗi khi người dùng mới truy cập vào bản cài đặt minh hoạ của bạn, một lưới mới sẽ được thêm vào và mọi người đều có thể xem công việc của nhau.

Ảnh chụp màn hình của bản minh hoạ Node.js

Node rất dễ sử dụng. Khi sử dụng kết hợp Socket.io và các yêu cầu POST tuỳ chỉnh, chúng ta không phải xây dựng các quy trình phức tạp để đồng bộ hoá. Socket.io xử lý việc này một cách minh bạch; JSON được truyền xung quanh.

Dễ đến mức nào? Hãy xem video này.

Với 3 dòng mã JavaScript, chúng ta đã có một máy chủ web đang chạy bằng Express.

//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/'));

Một vài bước nữa để liên kết socket.io để giao tiếp theo thời gian thực.

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

});

Bây giờ, chúng ta chỉ cần bắt đầu nghe các kết nối đến từ trang 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.
}

…thiết lập tính năng định tuyến mô-đun…

/**
 * 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.

…áp dụng hiệu ứng thời gian chạy (convolution sử dụng phản hồi xung)…

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

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

…áp dụng một hiệu ứng thời gian chạy khác (độ trễ)…

/**
 * 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

…rồi phát âm.

/**
 * 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!

Phương pháp phát trong Technitone là lên lịch. Thay vì đặt khoảng thời gian hẹn giờ bằng tốc độ để xử lý âm thanh mỗi nhịp, chúng ta thiết lập một khoảng thời gian nhỏ hơn để quản lý và lên lịch âm thanh trong hàng đợi. Điều này cho phép API thực hiện công việc trước khi phân giải dữ liệu âm thanh và xử lý các bộ lọc và hiệu ứng trước khi chúng ta giao nhiệm vụ cho CPU thực sự phát âm thanh. Khi nhịp đó xuất hiện, nó đã có tất cả thông tin cần thiết để trình bày kết quả cuối cùng cho loa.

Nhìn chung, mọi thứ đều cần được tối ưu hoá. Khi chúng tôi đẩy CPU quá mạnh, các quy trình sẽ bị bỏ qua (nhấn, nhấp, cào) để theo kịp lịch trình; chúng tôi đã nỗ lực rất nhiều để ngăn chặn mọi sự cố nếu bạn chuyển sang một thẻ khác trong Chrome.

Chương trình trình diễn ánh sáng

Ở vị trí trung tâm là lưới và đường hầm hạt. Đây là lớp WebGL của Technitone.

WebGL mang lại hiệu suất vượt trội hơn đáng kể so với hầu hết các phương pháp kết xuất hình ảnh khác trên web, bằng cách giao nhiệm vụ cho GPU hoạt động cùng với bộ xử lý. Mức tăng hiệu suất đó đi kèm với chi phí phát triển phức tạp hơn đáng kể với độ dốc học tập cao hơn nhiều. Tuy nhiên, nếu bạn thực sự đam mê tính năng tương tác trên web và muốn hạn chế tối đa các hạn chế về hiệu suất, thì WebGL là một giải pháp có thể so sánh với Flash.

Bản minh hoạ WebGL

Nội dung WebGL được kết xuất thành một canvas (nghĩa là Canvas HTML5) và bao gồm các khối xây dựng cốt lõi sau:

  • đỉnh đối tượng (hình học)
  • ma trận vị trí (tọa độ 3D)
    • chương trình đổ bóng (mô tả về giao diện hình học, được liên kết trực tiếp với GPU)
    • ngữ cảnh ("lối tắt" đến các phần tử mà GPU tham chiếu đến)
    • vùng đệm (quy trình truyền dữ liệu ngữ cảnh đến GPU)
    • mã chính (logic nghiệp vụ dành riêng cho hoạt động tương tác mong muốn)
    • phương thức"vẽ" (kích hoạt chương trình đổ bóng và vẽ các pixel lên canvas)

Quy trình cơ bản để kết xuất nội dung WebGL lên màn hình như sau:

  1. Đặt ma trận phối cảnh (điều chỉnh chế độ cài đặt cho máy ảnh nhìn vào không gian 3D, xác định mặt phẳng hình ảnh).
  2. Đặt ma trận vị trí (khai báo một gốc trong toạ độ 3D mà các vị trí được đo lường tương ứng).
  3. Điền dữ liệu vào vùng đệm (vị trí đỉnh, màu sắc, hoạ tiết, v.v.) để truyền đến ngữ cảnh thông qua chương trình đổ bóng.
  4. Trích xuất và sắp xếp dữ liệu từ vùng đệm bằng chương trình đổ bóng rồi truyền dữ liệu đó vào GPU.
  5. Gọi phương thức vẽ để yêu cầu ngữ cảnh kích hoạt chương trình đổ bóng, chạy với dữ liệu và cập nhật canvas.

Giao diện thực tế sẽ như sau:

Đặt ma trận phối cảnh…

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

…đặt ma trận vị trí…

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

…xác định một số hình học và giao diện…

// 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]);

…điền dữ liệu vào vùng đệm và truyền dữ liệu đó đến ngữ cảnh…

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

…và gọi phương thức vẽ

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

Mỗi khung hình, hãy nhớ xoá canvas nếu bạn không muốn hình ảnh dựa trên alpha xếp chồng lên nhau.

Nơi xử án

Ngoài lưới và đường hầm hạt, mọi thành phần giao diện người dùng khác đều được tạo bằng HTML / CSS và logic tương tác bằng JavaScript.

Ngay từ đầu, chúng tôi đã quyết định người dùng phải tương tác với lưới nhanh nhất có thể. Không có màn hình chờ, không có hướng dẫn, không có hướng dẫn, chỉ có "Bắt đầu". Nếu giao diện được tải, thì không có gì làm chậm giao diện.

Điều này đòi hỏi chúng tôi phải xem xét kỹ lưỡng cách hướng dẫn người dùng mới trong quá trình tương tác. Chúng tôi đã đưa vào các tín hiệu tinh tế, chẳng hạn như thay đổi thuộc tính con trỏ CSS dựa trên vị trí con trỏ của người dùng trong không gian WebGL. Nếu con trỏ nằm trên lưới, chúng ta sẽ chuyển con trỏ đó thành con trỏ tay (vì chúng có thể tương tác bằng cách vẽ các tông màu). Nếu con trỏ di chuột vào khoảng trắng xung quanh lưới, chúng ta sẽ hoán đổi con trỏ đó thành con trỏ chữ thập hướng (để cho biết người dùng có thể xoay hoặc mở rộng lưới thành các lớp).

Chuẩn bị cho chương trình

LESS (một trình xử lý trước CSS) và CodeKit (phát triển web trên steroid) thực sự giúp giảm thời gian dịch các tệp thiết kế thành HTML/CSS được rút gọn. Các tính năng này cho phép chúng ta sắp xếp, viết và tối ưu hoá CSS theo cách linh hoạt hơn nhiều — tận dụng các biến, thành phần kết hợp (hàm) và thậm chí là toán học!

Hiệu ứng sân khấu

Bằng cách sử dụng hiệu ứng chuyển đổi CSS3backbone.js, chúng tôi đã tạo ra một số hiệu ứng rất đơn giản giúp ứng dụng trở nên sống động và cung cấp cho người dùng các hàng đợi hình ảnh cho biết họ đang sử dụng công cụ nào.

Màu sắc của Technitone.

Backbone.js cho phép chúng ta nắm bắt các sự kiện thay đổi màu sắc và áp dụng màu mới cho các phần tử DOM thích hợp. Các hiệu ứng chuyển đổi CSS3 tăng tốc GPU đã xử lý các thay đổi về kiểu màu sắc mà hầu như không ảnh hưởng đến hiệu suất.

Hầu hết các hiệu ứng chuyển màu trên các thành phần giao diện đều được tạo bằng cách chuyển đổi màu nền. Trên màu nền này, chúng ta đặt hình nền có các vùng trong suốt mang tính chiến lược để màu nền có thể tỏa sáng.

HTML: Nền tảng

Chúng ta cần 3 vùng màu cho bản minh hoạ: 2 vùng màu do người dùng chọn và vùng màu thứ ba là màu hỗn hợp. Chúng tôi đã xây dựng cấu trúc DOM đơn giản nhất mà chúng tôi có thể nghĩ ra để hỗ trợ các hiệu ứng chuyển đổi CSS3 và ít yêu cầu HTTP nhất cho hình minh hoạ của chúng tôi.

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

CSS: Cấu trúc đơn giản có kiểu

Chúng tôi đã sử dụng tính năng định vị tuyệt đối để đặt từng khu vực ở đúng vị trí và điều chỉnh thuộc tính background-position để căn chỉnh hình minh hoạ nền trong từng khu vực. Điều này khiến tất cả các vùng (mỗi vùng có cùng hình nền) trông giống như một phần tử duy nhất.

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

Các hiệu ứng chuyển đổi tăng tốc GPU đã được áp dụng để nghe các sự kiện thay đổi màu. Chúng ta đã tăng thời lượng và sửa đổi tốc độ chuyển đổi trên .color-mixed để tạo cảm giác rằng cần có thời gian để các màu sắc kết hợp với nhau.

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

Hãy truy cập vào HTML5please để biết thông tin hỗ trợ trình duyệt hiện tại và cách sử dụng được đề xuất cho hiệu ứng chuyển đổi CSS3.

JavaScript: Làm cho nó hoạt động

Việc chỉ định màu động rất đơn giản. Chúng ta tìm kiếm trong DOM bất kỳ phần tử nào có lớp màu và đặt màu nền dựa trên lựa chọn màu của người dùng. Chúng ta áp dụng hiệu ứng chuyển đổi cho bất kỳ phần tử nào trong DOM bằng cách thêm một lớp. Điều này tạo ra một cấu trúc nhẹ, linh hoạt và có thể mở rộng.

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

Sau khi chọn màu chính và màu phụ, chúng ta sẽ tính toán giá trị màu kết hợp của chúng và gán giá trị thu được cho phần tử DOM thích hợp.

// 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+')';
}

Minh hoạ cho Cấu trúc HTML/CSS: Tạo cá tính cho ba hộp chuyển màu

Mục tiêu của chúng tôi là tạo ra hiệu ứng ánh sáng thú vị và chân thực, duy trì tính toàn vẹn khi các màu tương phản được đặt trong các vùng màu liền kề.

Tệp PNG 24 bit cho phép màu nền của các phần tử HTML hiển thị thông qua các vùng trong suốt của hình ảnh.

Hình ảnh trong suốt

Các hộp màu tạo ra các cạnh cứng khi các màu khác nhau gặp nhau. Điều này gây cản trở hiệu ứng ánh sáng chân thực và là một trong những thách thức lớn hơn khi thiết kế hình minh hoạ.

Vùng màu

Giải pháp là thiết kế hình minh hoạ để không bao giờ cho phép các cạnh của vùng màu hiển thị qua các vùng trong suốt.

Màu cạnh vùng

Việc lập kế hoạch cho bản dựng là rất quan trọng. Một buổi lập kế hoạch nhanh giữa nhà thiết kế, nhà phát triển và họa sĩ minh hoạ đã giúp nhóm hiểu được cách xây dựng mọi thứ để hoạt động cùng nhau khi được lắp ráp.

Hãy xem tệp Photoshop làm ví dụ về cách đặt tên lớp có thể truyền đạt thông tin về cấu trúc CSS.

Màu cạnh vùng

Encore

Đối với những người dùng không có Chrome, chúng tôi đặt mục tiêu chắt lọc bản chất của ứng dụng thành một hình ảnh tĩnh. Nút lưới trở thành nhân vật chính, các ô nền gợi nhắc đến mục đích của ứng dụng và phối cảnh xuất hiện trong hình phản chiếu gợi ý về môi trường 3D sống động của lưới.

Tô màu cạnh vùng.

Nếu bạn muốn tìm hiểu thêm về Technitone, hãy theo dõi blog của chúng tôi.

Ban nhạc

Cảm ơn bạn đã đọc bài viết này. Có thể chúng ta sẽ sớm cùng nhau sáng tác!