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

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

Technitone.com هو مزيج من WebGL وCanvas وWeb Sockets وCSS3 وJavaScript وFlash وWeb Audio API الجديدة في Chrome.

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

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

العمل الفني

نحن لا نفهم بأي شكل من الأشكال مهندسي صوت على الموقع الإلكتروني gskinner.com، ولكنّنا نعرِّفنا من خلال تحدي وسنضع خطة:

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

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

Technitone من gskinner.com

نجري أيضًا ما يلي:

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

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

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

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

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

الرسم التخطيطي لخادم Technitone

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

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

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

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

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

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

لدينا خادم ويب يعمل باستخدام Express، وذلك باستخدام 3 أسطر من JavaScript.

//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) لمتابعة الجدول الزمني، وبذلنا جهدًا كبيرًا لإيقاف كل العمليات المزعجة إذا انتقلت إلى علامة تبويب أخرى في Chrome.

عرض ضوئي

في الأمام والوسط، النفق الجسيمي للشبكة والنفق. هذه طبقة WebGL في Technitone.

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

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

يتم عرض محتوى WebGL في لوحة (حرفيًا، لوحة HTML5) ويتكون من الكتل البرمجية الإنشائية التالية:

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

تبدو العملية الأساسية لعرض محتوى 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 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

يتيح لنا Backban.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: بنية بسيطة ذات نمط

لقد استخدمنا ميزة تحديد الموضع المطلق لوضع كل منطقة في موقعها الجغرافي الصحيح وتم تعديل خاصية موضع الخلفية لمحاذاة الرسم التوضيحي للخلفية داخل كل منطقة. وهذا يجعل جميع المناطق (لكل منها صورة الخلفية نفسها)، تبدو كعنصر واحد.

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

يُرجى الانتقال إلى HTML5 لمعرفة التوافق الحالي مع المتصفِّح والاستخدام المقترَح لعمليات نقل 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.

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

عودة

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

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

إذا كنت مهتمًا بمعرفة المزيد عن Technitone، يُرجى متابعة مدونتنا.

الفرقة

شكرًا على القراءة، وربما نتفاعل معك قريبًا.