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 giữ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 làm việc mà chúng tôi đã tận dụng để thiết kế sao cho mang tính 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ó liên kết tải xuống để bạn có thể tải tất cả chú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 hoàn toàn không phải là kỹ sư âm thanh tại gskinner.com — nhưng hãy thử thách chúng tôi rồi vạch ra kế hoạch:

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

Chúng tôi cung cấp cho người dùng cơ hội khám phá API Web âm thanh thông qua bảng điều khiển công cụ áp dụng các bộ lọc và hiệu ứng âm thanh cho âm thanh 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á các thành phần đó giữa các ứng dụng
  • Cung cấp một số lựa chọn màu sắc để họ có thể vẽ các bài hát trông tuyệt vời
  • Cung cấp một thư viện để mọi người có thể nghe, yêu thích hoặc thậm chí là chỉnh sửa các tác phẩm của người khác

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

Chuyến đi phượt

Dữ liệu công cụ, hiệu ứng và lưới được hợp nhất và chuyển đổi tuần tự trên máy khách, 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, ví dụ như Socket.io. Dữ liệu này được gửi lại cho khách hàng cùng với sự đó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 đối chịu trách nhiệm hiển thị giao diện người dùng, mẫu và hiệu ứng trong khi phát nhiều người dùng.

Giao tiếp theo thời gian thực với socket cung cấp JavaScript trên ứng dụng khách và JavaScript trên máy chủ.

Sơ đồ máy chủ Technitone

Chúng tôi sử dụng Nút cho mọi khía cạnh của máy chủ. Đó là một máy chủ web tĩnh và máy chủ socket tất cả trong một. Express là một máy chủ web hoàn chỉnh được xây dựng hoàn toàn trên Nút. Trình quản lý thẻ của Google siêu có thể mở rộng, khả năng 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ỉ phải tập trung vào việc xây dựng ứng dụng của mình.

Bản minh hoạ nhiều người dùng (hay thực chất chỉ là ảnh chụp màn hình)

Bản minh hoạ này phải chạy từ máy chủ Nút và vì bài viết này không phải là một bản minh hoạ, nên chúng tôi đã đính kèm ảnh chụp màn hình 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 bản minh hoạ đó trên máy. Mỗi khi có người dùng mới truy cập vào phần cài đặt minh hoạ của bạn, một lưới mới sẽ được thêm vào và công việc của mọi người sẽ hiển thị cho nhau.

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

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

Dễ dàng đến mức nào? Xem video này.

Với 3 dòng JavaScript, chúng ta đã có một máy chủ web được thiết lập và chạy cù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 phần nữa để liên kết socket.io cho việc 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 theo dõi 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 chế độ đị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 (sự tích chập 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 làm cho bản ghi đó nghe được.

/**
 * 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 của chúng tôi trong Technitone là lên lịch. Thay vì đặt một khoảng thời gian hẹn giờ bằng với nhịp độ để xử lý âm thanh ở từng 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 cho âm thanh trong hàng đợi. Điều này cho phép API thực hiện trước công việc phân giải dữ liệu âm thanh cũng như xử lý bộ lọc và hiệu ứng trước khi chúng ta giao cho CPU thực sự nghe thấy. Khi nhịp âm thanh đó xuất hiện, nó đã có tất cả thông tin cần thiết để trình bày kết quả thực cho người nói.

Nhìn chung, mọi thứ cần được tối ưu hoá. Khi chúng tôi đẩy CPU quá mạnh, các quy trình đã bị bỏ qua (bật, nhấp, cào) để bắt kịp tiến độ. Chúng tôi đã nỗ lực hết sức để ngăn chặn công việc khó xử nếu bạn chuyển sang một thẻ khác trong Chrome.

Màn trình diễn ánh sáng

Phía trước và 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 đá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 cho GPU hoạt động cùng với bộ xử lý. Việc đạt được hiệu suất đó đi kèm với chi phí cho quá trình phát triển cần nhiều thời gian hơn đáng kể, nhưng khó khăn hơn rất nhiều trong việc học tập. Tuy nhiên, nếu bạn thực sự đam mê tương tác trên web và muốn ít bị hạn chế về hiệu suất nhất có thể, thì WebGL sẽ cung cấp một giải pháp tương đương với Flash.

Bản minh hoạ WebGL

Nội dung WebGL được hiển thị cho canvas (theo nghĩa đen là HTML5 Canvas) và bao gồm các khối xây dựng cốt lõi sau:

  • các đỉnh của đối tượng (hình học)
  • ma trận vị trí (tọa độ 3D)
    • chương trình đổ bóng (mô tả về hình dạng hình học, được liên kết trực tiếp tớ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 bối 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ẽ pixel vào canvas)

Quy trình cơ bản để kết xuất nội dung WebGL cho màn hình sẽ trông giống như sau:

  1. Đặt ma trận phối cảnh (điều chỉnh 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 điểm gốc trong toạ độ 3D mà các vị trí được đo tương ứng).
  3. Điền dữ liệu vào vùng đệm (vị trí đỉnh, màu sắc, hoạ tiết...) để 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ẽ để cho biết ngữ cảnh cần kích hoạt chương trình đổ bóng, chạy bằng dữ liệu và cập nhật canvas.

Ứng dụng sẽ hiển thị 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à hình thức...

// 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 đó vào 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

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

Ngay từ đầu, chúng tôi đã quyết định rằng người dùng nên 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ần "Bắt đầu". Nếu giao diện được tải – sẽ không có gì làm chậm giao diện.

Do đó, chúng tôi phải xem xét kỹ cách hướng dẫn người dùng lần đầu tiên tương tác. Chúng tôi đưa vào các gợi ý tinh tế, chẳng hạn như thay đổi thuộc tính con trỏ CSS dựa trên vị trí chuột 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ỏ đó sang con trỏ tay (vì con trỏ có thể tương tác bằng cách lập biểu đồ âm). Nếu nó di chuột trong khoảng trắng xung quanh lưới, chúng ta sẽ hoán đổi nó thành con trỏ chéo hướng (để biểu thị chúng có thể xoay hoặc nổ lưới thành các lớp).

Chuẩn bị cho buổi trình diễn

LESS (bộ xử lý trước của CSS) và CodeKit (phát triển web trên steroid) thực sự giúp giảm thời gian cần thiết để dịch các tệp thiết kế thành HTML/CSS bị loại bỏ. Các phần tử này cho phép chúng tôi 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, kết hợp (kết hợp) và thậm chí cả toán học!

Hiệu ứng sân khấu

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

Màu sắc Technitone.

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

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

HTML: Kiến thức cơ bản

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à 1 vùng màu hỗn hợp thứ ba. 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ĩ đến là hỗ trợ chuyển đổi CSS3 và ít yêu cầu HTTP nhất để minh hoạ.

<!-- 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 với phong cách

Chúng tôi sử dụng vị trí tuyệt đối để đặt từng khu vực vào đúng vị trí và điều chỉnh thuộc tính vị trí nền để căn chỉnh hình minh hoạ nền trong từng khu vực. Điều này làm cho tất cả các khu vực (mỗi khu vực có cùng một 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;
  }

Quá trình chuyển đổi tăng tốc GPU đã được áp dụng để theo dõi các sự kiện thay đổi màu. Chúng tôi đã tăng thời lượng và sửa đổi tốc độ đối với tệp .color-mixed để tạo ấn tượng rằng cần có thời gian để kết hợp các màu.

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

Truy cập vào HTML5Vui lòng truy cập vào dịch vụ hỗ trợ hiện tại về trình duyệt và đề xuất sử dụng khi chuyển đổi CSS3.

JavaScript: Làm cho công cụ này có hiệu quả

Việc chỉ định màu một cách linh hoạt rất đơn giản. Chúng tôi tìm kiếm DOM cho bất kỳ phần tử nào bằng lớp màu của chúng tôi và đặt màu nền dựa trên lựa chọn màu của người dùng. Chúng tôi á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 tôi tính toán giá trị màu hỗn 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 kiến trúc HTML/CSS: Tạo ra 3 tính năng của hộp thay đổi màu sắc

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

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

Độ trong của hình ảnh

Các hộp màu tạo ra các cạnh cứng nơi những màu khác nhau giao nhau. Điều này cản trở hiệu ứng ánh sáng thực tế 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ạ sao cho hình minh hoạ không bao giờ cho phép cạnh của các vùng màu hiển thị qua các vùng trong suốt.

Tô màu các cạnh của vùng

Lên kế hoạch xây dựng là rất quan trọng. Một phiên lập kế hoạch nhanh giữa nhà thiết kế, nhà phát triển và hoạ sĩ minh hoạ đã giúp nhóm hiểu được cách mọi thứ cần được xây dựng để có thể phối hợp hoạt động khi kết hợ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ề việc xây dựng CSS.

Tô màu các cạnh của vùng

Encore

Đối với người dùng không có Chrome, chúng tôi đặt mục tiêu chăm sóc tinh thần của ứng dụng vào một hình ảnh tĩnh duy nhất. Nút lưới trở thành yếu tố chính, các ô trong nền thể hiện mục đích của ứng dụng và phối cảnh trong ảnh phản chiếu gợi ý tại môi trường 3D sống động của lưới.

Tô màu các cạnh của 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, có lẽ chúng tôi sẽ sớm gặp bạn!