دراسة حالة - إنشاء Technitone.com

Technitone: تجربة صوت على الويب

يجمع موقع Technitone.com بين WebGL ولوحة الرسم وWeb Sockets وCSS3 وJavaScript وFlash وواجهة Web Audio API الجديدة في Chrome.

ستتناول هذه المقالة كل جانب من جوانب الإنتاج: الخطة والخادم والأصوات والمرئيات وبعض سير العمل الذي نستفيد منه لتصميم المحتوى التفاعلي. تحتوي معظم الأقسام على مقتطفات رمز وإصدار تجريبي وتنزيل. في نهاية المقالة، يتوفّر رابط تنزيل يمكنك من خلاله الحصول على كل هذه الملفات كملف zip واحد.

فريق الإنتاج في gskinner.com

الأداء

نحن لسنا مهندسين صوت في gskinner.com، ولكن يمكننا مساعدتك في حلّ أي مشكلة تواجهك:

  • يرسم المستخدمون النغمات على شبكة، مستوحين ذلك من ToneMatrix من إنشاء أندريه.
  • يتم ربط النغمات بأدوات موسيقية أو مجموعات طبول أو حتى تسجيلات المستخدمين الخاصة.
  • يلعب عدة مستخدمين مرتبطين في الشبكة نفسها في الوقت نفسه.
  • أو الانتقال إلى وضع الاستكشاف الفردي
  • تتيح الجلسات الدعوية للمستخدمين تنظيم فرقة وإقامة حفلة موسيقية مفاجئة.

نقدّم للمستخدمين فرصة استكشاف واجهة برمجة التطبيقات Web Audio API من خلال لوحة أدوات تطبّق فلاتر صوتية وتأثيرات على نغمات الأصوات.

Technitone من gskinner.com

وننفّذ أيضًا ما يلي:

  • تخزين التأثيرات والتصميمات التي أنشأها المستخدمون كبيانات ومزامنة هذه البيانات على جميع الأجهزة
  • قدِّم بعض خيارات الألوان ليتمكّنوا من رسم أغانٍ رائعة المظهر.
  • توفير معرض يتيح للمستخدمين الاستماع إلى أعمال الآخرين أو إبداء الإعجاب بها أو حتى تعديلها

لقد تمسّكنا بالاستعارة الشائعة للشبكة، وجعلناها تطفو في مساحة ثلاثية الأبعاد، وأضفنا بعض تأثيرات الإضاءة والزخارف والجسيمات، ووضعناها في واجهة مرنة (أو ملء الشاشة) مستندة إلى CSS وJavaScript.

رحلة بالسيارة

يتم تجميع بيانات الأدوات والتأثيرات والشبكة وتسلسلها على العميل، ثم يتم إرسالها إلى الخلفية المخصّصة Node.js لحلّها لعدة مستخدمين باستخدام Socket.io. ويتم إرسال هذه البيانات مرة أخرى إلى العميل مع تضمين مساهمات كل لاعب، قبل توزيعها على طبقات CSS وWebGL وWebAudio ذات الصلة المسؤولة عن عرض واجهة المستخدم والعيّنات والتأثيرات أثناء تشغيل اللاعبين المتعدّدين.

ينقل التواصل في الوقت الفعلي مع مآخذ البيانات JavaScript على العميل وJavaScript على الخادم.

مخطّط خادم Technitone البياني

نستخدم Node في كل جانب من جوانب الخادم. وهو خادم ويب ثابت وخادم مآخذ التوصيل في خادم واحد. لقد انتهينا باستخدام Express، وهو خادم ويب كامل تم إنشاؤه بالكامل على Node. وهو قابل للتكيّف بشكل كبير، ويمكن تخصيصه بشكل كبير، ويعالج جوانب الخادم ذات المستوى المنخفض نيابةً عنك (تمامًا مثل Apache أو Windows Server). بعد ذلك، ما عليك سوى التركيز على إنشاء تطبيقك.

عرض توضيحي للاستخدام المتعدّد (حسنًا، هذه مجرد لقطة شاشة)

يتطلّب هذا العرض التجريبي تشغيله من خادم Node، وبما أنّ هذه المقالة ليست كذلك، لقد أدرجنا لقطة شاشة لشكل العرض التجريبي بعد تثبيت Node.js وضبط خادم الويب وتشغيله محليًا. في كل مرة يزور فيها مستخدم جديد الإصدار التجريبي من تطبيقك، ستتم إضافة شبكة جديدة ويصبح عمل كل مستخدم مرئيًا للآخرين.

لقطة شاشة لعرض Node.js التوضيحي

استخدام Node سهل. باستخدام مجموعة من Socket.io وطلبات POST المخصّصة، لم يكن علينا إنشاء إجراءات معقدة للمزامنة. يعالج Socket.io هذا الأمر بشكل شفاف، ويتم تمرير JSON.

ما مدى سهولة ذلك؟ شاهد هذا.

باستخدام 3 أسطر من JavaScript، يمكننا تشغيل خادم ويب باستخدام 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/'));

مزيد من الخطوات لربط 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 على تحديد المواعيد. بدلاً من ضبط فاصل زمني للموقّت يساوي معدّل الإيقاع لمعالجة الأصوات في كل نبضة، نضبط فاصلاً زمنيًا أصغر يدير الأصوات ويحدّد جدولاً زمنيًا لها في قائمة الانتظار. يتيح ذلك لواجهة برمجة التطبيقات تنفيذ العمل المُسبَق المتمثل في تحليل بيانات الصوت ومعالجة الفلاتر والتأثيرات قبل أن نطلب من وحدة المعالجة المركزية تحويل الصوت إلى ملف قابل للتشغيل. وعندما تأتي هذه النقطة أخيرًا، يكون لدى الجهاز كل المعلومات التي يحتاج إليها لتقديم النتيجة النهائية للمكبّرات.

بشكل عام، كان يجب تحسين كل شيء. عندما ضغطنا على وحدات المعالجة المركزية (CPU) بشكلٍ كبير، تمّ تخطي العمليات (الظهور والنقر والتنقّل) للحفاظ على الجدول الزمني، وبذلنا جهدًا كبيرًا في إيقاف كل هذه العمليات إذا قفزت إلى علامة تبويب أخرى في Chrome.

عرض ضوئي

في المقدّمة، تظهر الشبكة ونفق الجسيمات. هذه هي طبقة WebGL في Technitone.

يقدّم WebGL أداءً أفضل بكثير من معظم الأساليب الأخرى لعرض العناصر المرئية على الويب، وذلك من خلال تكليف وحدة معالجة الرسومات بالعمل مع المعالج. ويرتبط هذا التحسين في الأداء بتكلفة تطوير أكثر تعقيدًا مع منحنى تعلُّم أكثر انحدارًا. ومع ذلك، إذا كنت من المهتمين حقًا بالعناصر التفاعلية على الويب وتريد الحدّ من القيود المفروضة على الأداء قدر الإمكان، يوفّر WebGL حلًا مشابهًا لخدمة Flash.

عرض WebGL التوضيحي

يتم عرض محتوى WebGL على لوحة (لوحة HTML5) ويتألف من العناصر الأساسية التالية:

  • رؤوس العناصر (الأشكال الهندسية)
  • مصفوفات مواضع العناصر (الإحداثيات الثلاثية الأبعاد)
    • Shaders (وصف لمظهر الأشكال الهندسية، مرتبط مباشرةً بوحدة معالجة الرسومات)
    • السياق ("الاختصارات" إلى العناصر التي تشير إليها وحدة معالجة الرسومات)
    • المخزن المؤقت (مسارات لنقل بيانات السياق إلى وحدة معالجة الرسومات)
    • الرمز الرئيسي (منطق النشاط التجاري الخاص بالتفاعل المطلوب)
    • طريقة"الرسم" (تفعّل تأثيرات التظليل وترسم وحدات البكسل على اللوحة)

في ما يلي العملية الأساسية لعرض محتوى WebGL على الشاشة:

  1. ضبط مصفوفة المنظور (لضبط إعدادات الكاميرا التي ترصد المساحة الثلاثية الأبعاد، وتحديد مستوى الصورة)
  2. اضبط مصفوفة المواضع (حدِّد مصدرًا في الإحداثيات الثلاثية الأبعاد التي يتم قياس المواضع بالنسبة إليها).
  3. ملء المخزن المؤقت بالبيانات (موضع الرأس واللون والقوام…) لنقلها إلى السياق من خلال برامج التظليل
  4. استخراج البيانات من المخزن المؤقت وتنظيمها باستخدام برامج التظليل ونقلها إلى وحدة معالجة الرسومات
  5. يمكنك استدعاء طريقة الرسم لإخبار السياق بتفعيل تأثيرات التظليل وتشغيلها مع البيانات وتعديل اللوحة.

في ما يلي مثال عملي على ذلك:

اضبط مصفوفة المنظور…

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

في كل إطار، تذكَّر محو اللوحة إذا كنت لا تريد أن تتكدس العناصر المرئية المستندة إلى شفافية ألفا فوق بعضها.

مكان انعقاد فعاليات

بالإضافة إلى الشبكة وقناة الجسيمات، تم إنشاء كل عنصر آخر من عناصر واجهة المستخدم باستخدام HTML / CSS والمنطق التفاعلي في JavaScript.

منذ البداية، قرّرنا أن يتفاعل المستخدمون مع الشبكة في أسرع وقت ممكن. ما مِن شاشة بداية أو تعليمات أو أدلة توجيهية، بل ما عليك سوى النقر على "بدء". إذا تم تحميل الواجهة، من المفترض ألا يكون هناك أي شيء يبطئها.

وقد تطلّب ذلك منّا النظر بعناية في كيفية توجيه المستخدم لأول مرة خلال تفاعلاته. لقد أدرجنا إشارات دقيقة، مثل تغيير خاصية مؤشر CSS استنادًا إلى موضع الماوس لدى المستخدم ضمن مساحة WebGL. إذا كان المؤشر فوق الشبكة، سنبدّله إلى مؤشر يد (لأنّه يمكنه التفاعل من خلال وضع نغمات). وإذا تم تمرير مؤشر الماوس فوق المساحة الفارغة حول الشبكة، يتم استبداله بمؤشر متقاطع اتجاهي (للإشارة إلى إمكانية تدوير الشبكة أو تقسيمها إلى طبقات).

الاستعداد للعرض

إنّ LESS (معالج CSS مُسبَق) وCodeKit (أداة تطوير ويب فعّالة) يقلّلان بشكلٍ كبير من الوقت الذي يستغرقه تحويل ملفات التصميم إلى HTML/CSS. تتيح لنا هذه الأدوات تنظيم CSS وكتابته وتحسينه بطريقة أكثر تنوعًا، وذلك من خلال الاستفادة من المتغيّرات والعناصر المُدمَجة (الدوال) وحتى العمليات الحسابية.

تأثيرات المرحلة

باستخدام عمليات النقل في CSS3 وbackbone.js، أنشأنا بعض التأثيرات البسيطة التي تساعد في إضفاء الحيوية على التطبيق وتزويد المستخدمين بإشارات مرئية تشير إلى الأداة التي يستخدمونها.

ألوان Technitone

تسمح لنا Backbone.js برصد أحداث تغيير اللون وتطبيق اللون الجديد على عناصر DOM المناسبة. تعالج عمليات النقل في CSS3 التي تُسرَّع باستخدام وحدة معالجة الرسومات تغييرات نمط الألوان بدون التأثير في الأداء أو التأثير فيه بشكل بسيط.

تم إنشاء معظم عمليات انتقال الألوان في عناصر الواجهة من خلال تغيير ألوان الخلفية. فوق لون الخلفية هذا، نضع صور خلفية تتضمّن مساحات شفافة بشكل استراتيجي للسماح للون الخلفية بالظهور.

HTML: الأساس

احتجنا إلى ثلاث مناطق ألوان للعرض التجريبي: منطقتَي ألوان يختارها المستخدم ومنطقة ألوان مختلطة ثالثة. لقد أنشأنا أبسط بنية 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;
  }

تم تطبيق عمليات انتقال سريعة باستخدام وحدة معالجة الرسومات تستمع إلى أحداث تغيير اللون. لقد زدنا المدة وعدّلنا السرعة في موضع 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 عن أي عنصر يتضمّن فئة اللون ونضبط لون الخلفية استنادًا إلى اختيارات المستخدم للألوان. نطبّق تأثير الانتقال على أي عنصر في 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: إضفاء طابع شخصي على ثلاثة مربّعات متغيّرة الألوان

كان هدفنا هو إنشاء تأثير إضاءة ممتع وواقعي يحافظ على سلامته عند وضع ألوان متباينة في مناطق ألوان مجاورة.

تسمح ملفات PNG بسعة 24 بت بظهور لون خلفية عناصر HTML من خلال المناطق الشفافة في الصورة.

صور شفافة

تُنشئ المربّعات الملونة حوافًا قاسية عند تلاقِي ألوان مختلفة. يؤثر ذلك في تأثيرات الإضاءة الواقعية، وكان أحد أكبر التحديات عند تصميم الرسم التوضيحي.

مناطق الألوان

كان الحلّ هو تصميم الرسم التوضيحي بحيث لا يسمح أبدًا بظهور حواف مناطق الألوان من خلال المناطق الشفافة.

حواف مناطق الألوان

كان التخطيط لبناء النموذج أمرًا بالغ الأهمية. من خلال جلسة تخطيط سريعة بين المصمّم والمطوّر والمصوّر، تمكّن الفريق من فهم كيفية إنشاء كل العناصر كي تعمل معًا عند تجميعها.

اطّلِع على ملف Photoshop كمثال على كيفية استخدام أسماء الطبقات لتوفير معلومات عن بنية CSS.

حواف مناطق الألوان

Encore

بالنسبة إلى المستخدمين الذين لا يستخدمون Chrome، وضعنا هدفًا لتلخيص جوهر التطبيق في صورة ثابتة واحدة. أصبحت عقدة الشبكة هي العنصر الرئيسي، وتشير مربّعات الخلفية إلى الغرض من التطبيق، ويشير المنظور المعروض في الانعكاس إلى البيئة الثلاثية الأبعاد الغامر للشبكة.

حواف منطقة الألوان

إذا أردت الاطّلاع على مزيد من المعلومات عن Technitone، يمكنك متابعتنا على مدونتنا.

الفرقة

نشكرك على القراءة، وسنتواصل معك قريبًا لمناقشة هذه المسألة.