الإنشاء باستخدام Chrome

توفير قرميدات LEGO® على الويب متعدد الأجهزة

"Build with Chrome" (باستخدام Chrome)، إصدار تجربة مرحة لمستخدمي Chrome على أجهزة الكمبيوتر المكتبي تم إطلاقها في أستراليا في عام 2014، وقد أُعيدت في عام 2014 مع التوافر على مستوى العالم، وربطها بفيلم THE LEGO® MovieTM ، ودعم تمت إضافته مؤخرًا لأجهزة الجوّال. في هذه المقالة، سنشارك بعض الدروس المستفادة من المشروع، خاصةً فيما يتعلق بالانتقال من تجربة سطح المكتب فقط إلى حل متعدد الشاشات يدعم إدخال الماوس واللمس.

تاريخ الإنشاء باستخدام Chrome

تم إطلاق الإصدار الأول من "Build with Chrome" في أستراليا في عام 2012. أردنا توضيح مدى قوة الويب بطريقة جديدة تمامًا وتقديم Chrome إلى جمهور جديد تمامًا.

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

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

في عام 2013، قررنا توسيع نطاق التصميم باستخدام Chrome ليشمل تقنيات الويب الجديدة. ومن بين هذه التقنيات كان WebGL في Chrome لنظام Android والذي يسمح بطبيعة الحال بتطوير تطبيق Build with Chrome على الأجهزة الجوالة. للبدء، طوّرنا أولاً نماذج أولية تعمل باللمس قبل استفهام الأجهزة عن "أداة الإنشاء" لفهم سلوك الإيماءات والاستجابة الملموسة التي قد نواجهها من خلال المتصفح مقارنةً بتطبيق الأجهزة الجوّالة.

واجهة أمامية سريعة الاستجابة

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

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

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

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

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

لنتحدث قليلاً عن حجمي الشاشة وتجاربهما:

شاشة كبيرة مع دعم للماوس واللمس

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

مثال: عندما يكون الارتفاع المتاح أقل من 730 بكسل، يكون عنصر تحكم شريط التكبير/التصغير مخفيًا في وضع "الاستكشاف":

@media only screen and (max-height: 730px) {
    .zoom-slider {
        display: none;
    }
}

شاشة صغيرة، إمكانية اللمس فقط

يتم عرض هذا الإصدار على الأجهزة الجوّالة والأجهزة اللوحية الصغيرة (الجهازان المستهدفان Nexus 4 وNexus 7). يتطلّب هذا الإصدار إمكانية استخدام ميزة "اللمس المتعدد".

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

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

أداء WebGL ودعمه

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

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

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

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

كانت بعض الأجهزة تطلب منا إعادة النظر في بعض أدوات تظليل WebGL وتبسيطها، إلا أننا دائمًا ما نجد طريقة لحلها والمضي قدمًا.

التوافق مع الأجهزة التي لا تستخدم WebGL

أردنا أن يكون الموقع سهل الاستخدام إلى حد ما حتى لو كان جهاز الزائر لا يتوافق مع WebGL. أحيانًا تكون هناك طرق لتمثيل التصميم الثلاثي الأبعاد بطريقة مبسطة باستخدام حل لوحة الرسم أو ميزات CSS3D. للأسف لم نجد حلاً جيدًا بما يكفي لتكرار ميزات الإنشاء والاستكشاف ثلاثية الأبعاد بدون استخدام WebGL.

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

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

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

إدارة الأصول

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

في ميزة "استكشاف"، نحتاج إلى تحميل الكثير من الصور؛ الصور المصغَّرة للإبداعات وزخارف الخريطة للوحات الأساسية وأخيرًا الابتكارات الفعلية ثلاثية الأبعاد. ونحن نولي عناية إضافية للتأكّد من عدم حدوث تسرُّب في الذاكرة عندما نحمِّل صورًا جديدة باستمرار.

يتم تخزين الابتكارات الثلاثية الأبعاد بتنسيق ملف مخصص ويتم حزمه كصورة PNG. من خلال الاحتفاظ ببيانات الابتكارات الثلاثية الأبعاد مخزنة كصورة، يمكننا نقل البيانات مباشرةً إلى أدوات التظليل التي تعرض الابتكارات.

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

إدارة اتجاه الشاشة

ومن السهل أن تنسى مدى تغيُّر نسبة عرض إلى ارتفاع الشاشة عند الانتقال من الوضع العمودي إلى الوضع الأفقي أو العكس. أنت بحاجة إلى التفكير في هذا من البداية عند التكيف مع الأجهزة المحمولة.

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

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

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

يتم التحكم في معظم تنسيق المحتوى بواسطة CSS، ولكن يلزم تنفيذ بعض العناصر ذات الصلة بالاتجاه في JavaScript. وجدنا أنه لم يكن هناك حل جيد عبر الأجهزة لاستخدام window.orientation لتحديد الاتجاه، ولذلك في النهاية كنا نقارن window.innerDisplay وwindow.innerHeight لتحديد اتجاه الجهاز.

if( window.innerWidth > window.innerHeight ){
  //landscape
} else {
  //portrait
}

إضافة دعم اللمس

وتُعد إضافة الدعم باللمس إلى محتوى الويب أمرًا سهلاً على نحو معقول. تعمل ميزات التفاعل الأساسي، مثل حدث النقر، بالطريقة نفسها على أجهزة الكمبيوتر المكتبي والأجهزة التي تعمل باللمس، ولكن في ما يتعلّق بالتفاعلات الأكثر تقدّمًا، عليك أيضًا التعامل مع أحداث اللمس، وهي "البدء باللمس" و"النقل باللمس" و"اللمس". تتناول هذه المقالة أساسيات كيفية استخدام هذه الأحداث. لا يتيح متصفّح Internet Explorer أحداث اللمس، ولكنه يستخدم بدلاً من ذلك أحداث المؤشر (pointerdown وpointmove وdownup). تم إرسال أحداث المؤشر إلى W3C لتوحيدها، إلا أنّه لا يتم تنفيذها حاليًا إلا في Internet Explorer.

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

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

ابدأ بتهيئة كائن لتخزين الإدخال وأضف أداة معالجة الحدث touchstart. ونُطلق على كل معالِج من معالجات الأحداث الاسم event.preventDefault(). والهدف من ذلك هو منع المتصفِّح من مواصلة معالجة حدث اللمس، ما قد يتسبب في حدوث سلوك غير متوقَّع، مثل الانتقال للأسفل أو للأعلى في الصفحة بأكملها.

var input = {dragStartX:0, dragStartY:0, dragX:0, dragY:0, dragDX:0, dragDY:0, dragging:false};
plateContainer.addEventListener('touchstart', onTouchStart);

function onTouchStart(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
    //start listening to all needed touchevents to implement the dragging
    document.addEventListener('touchmove', onTouchMove);
    document.addEventListener('touchend', onTouchEnd);
    document.addEventListener('touchcancel', onTouchEnd);
  }
}

function onTouchMove(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragging(event.touches[0].clientX, event.touches[0].clientY);
  }
}

function onTouchEnd(event) {
  event.preventDefault();
  if( event.touches.length === 0){
    handleDragStop();
    //remove all eventlisteners but touchstart to minimize number of eventlisteners
    document.removeEventListener('touchmove', onTouchMove);
    document.removeEventListener('touchend', onTouchEnd);
    //also listen to touchcancel event to avoid unexpected behavior when switching tabs and some other situations
    document.removeEventListener('touchcancel', onTouchEnd);
  }
}

لا يتم إجراء التخزين الفعلي للمدخلات في معالِجات الأحداث، ولكن بدلاً من ذلك باستخدام معالجات منفصلة: handDragStart وhandDragging وhandDragStop. وذلك لأننا نريد أن نكون قادرين على استدعاء هذه الأشياء من معالِجات أحداث الماوس أيضًا. ضع في اعتبارك أنه على الرغم من أنه غير مرجح، فقد يستخدم المستخدم اللمس والماوس في نفس الوقت. وبدلاً من التعامل مع هذه الحالة بشكل مباشر، نحرص على ألا يؤدي ذلك إلى أي مشاكل.

function handleDragStart(x ,y ){
  input.dragging = true;
  input.dragStartX = input.dragX = x;
  input.dragStartY = input.dragY = y;
}

function handleDragging(x ,y ){
  if(input.dragging) {
    input.dragDX = x - input.dragX;
    input.dragDY = y - input.dragY;
    input.dragX = x;
    input.dragY = y;
  }
}

function handleDragStop(){
  if(input.dragging) {
    input.dragging = false;
    input.dragDX = 0;
    input.dragDY = 0;
  }
}

عند عمل رسوم متحركة تستند إلى touchmove، غالبًا ما يكون من المفيد أيضًا تخزين حركة دلتا منذ آخر حدث. على سبيل المثال، استخدمنا هذا كمَعلمة لسرعة الكاميرا عند التحرك على جميع لوحات القاعدة في ميزة "استكشاف"، نظرًا لأنك لا تسحب لوحات القاعدة، بل أنت تحرّك الكاميرا فعلاً.

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );

  //execute animation based on input.dragDX, input.dragDY, input.dragX or input.dragY
 /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX=0;
  input.dragDY=0;
}

مثال مضمَّن: سحب عنصر باستخدام أحداث اللمس. يتم تطبيق طريقة التنفيذ نفسها كما هي الحال عند سحب الخريطة الثلاثية الأبعاد في Build with Chrome: http://cdpn.io/qDxvo

إيماءات اللمس المتعدّد

هناك العديد من أُطر العمل أو المكتبات، مثل Hammer أو QuoJS، التي يمكنها الاهتمام بتبسيط إدارة إيماءات اللمس المتعدّد، ولكن إذا كنت تريد الجمع بين عدة إيماءات والتحكّم بشكل كامل، من الأفضل أحيانًا إجراء ذلك من البداية.

لإدارة إيماءات التصغير والتدوير، نخزّن المسافة والزاوية بين إصبعين عند وضع الإصبع الثاني على الشاشة:

//variables representing the actual scale/rotation of the object we are affecting
var currentScale = 1;
var currentRotation = 0;

function onTouchStart(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
  }else if( event.touches.length === 2 ){
    handleGestureStart(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY );
  }
}

function handleGestureStart(x1, y1, x2, y2){
  input.isGesture = true;
  //calculate distance and angle between fingers
  var dx = x2 - x1;
  var dy = y2 - y1;
  input.touchStartDistance=Math.sqrt(dx*dx+dy*dy);
  input.touchStartAngle=Math.atan2(dy,dx);
  //we also store the current scale and rotation of the actual object we are affecting. This is needed to support incremental rotation/scaling. We can't assume that an object is always the same scale when gesture starts.
  input.startScale=currentScale;
  input.startAngle=currentRotation;
}

في حدث التحريك باللمس، يتم قياس المسافة والزاوية بين هذين الإصبعين باستمرار. بعد ذلك، يتمّ استخدام الفرق بين مسافة البداية والمسافة الحالية لضبط المقياس، ويُستخدم الفرق بين زاوية البداية وزاوية التيار الحالي لتحديد الزاوية.

function onTouchMove(event) {
  event.preventDefault();
  if( event.touches.length  === 1){
    handleDragging(event.touches[0].clientX, event.touches[0].clientY);
  }else if( event.touches.length === 2 ){
    handleGesture(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY );
  }
}

function handleGesture(x1, y1, x2, y2){
  if(input.isGesture){
    //calculate distance and angle between fingers
    var dx = x2 - x1;
    var dy = y2 - y1;
    var touchDistance = Math.sqrt(dx*dx+dy*dy);
    var touchAngle = Math.atan2(dy,dx);
    //calculate the difference between current touch values and the start values
    var scalePixelChange = touchDistance - input.touchStartDistance;
    var angleChange = touchAngle - input.touchStartAngle;
    //calculate how much this should affect the actual object
    currentScale = input.startScale + scalePixelChange*0.01;
    currentRotation = input.startAngle+(angleChange*180/Math.PI);
    //upper and lower limit of scaling
    if(currentScale<0.5) currentScale = 0.5;
    if(currentScale>3) currentScale = 3;
  }
}

من المحتمل أن تستخدم مقياس تغيير المسافة بين كل حدث باللمس بالطريقة نفسها المستخدمة في المثال مع السحب، ولكن غالبًا ما يكون هذا النهج أكثر فائدة عندما تريد حركة مستمرة.

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );
  //execute transform based on currentScale and currentRotation
  /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX=0;
  input.dragDY=0;
}

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

مثال مضمّن: تدوير كائن وتغيير حجمه في الوضع الثنائي الأبعاد. على غرار الطريقة التي يتم بها تنفيذ الخريطة في ميزة "استكشاف": http://cdpn.io/izloq

دعم الماوس واللمس على الجهاز نفسه

تتوفّر حاليًا العديد من أجهزة الكمبيوتر المحمولة، مثل Chromebook Pixel، التي تتيح إدخال الماوس واللمس. قد يؤدي ذلك إلى حدوث بعض السلوكيات غير المتوقعة إذا لم تكن حريصًا.

ومن المهم جدًا ألا تكتفي باكتشاف الدعم باللمس ثمّ تتجاهل إدخال الماوس، بل تتيح لك بدلاً من ذلك دعمهما في الوقت نفسه.

إذا كنت لا تستخدم event.preventDefault() في معالِجات أحداث اللمس، سيتم أيضًا تنشيط بعض أحداث الماوس التي تمت محاكاتها، وذلك للحفاظ على استمرار عمل معظم المواقع الإلكترونية المحسّنة التي لا تعمل باللمس. على سبيل المثال، قد يتم إطلاق هذه الأحداث بنقرة واحدة على الشاشة بترتيب سريع وبهذا الترتيب:

  1. بداية اللمس
  2. حركة اللمس
  3. اللمس
  4. تمرير الماوس
  5. mousemove
  6. تمرير الماوس
  7. تمرير الماوس
  8. نقرة

إذا كان لديك تفاعلات أكثر تعقيدًا، فقد تتسبب أحداث الماوس هذه في حدوث سلوك غير متوقع وتؤدي إلى فوضى عملية التنفيذ. غالبًا ما يكون من الأفضل استخدام event.preventDefault() في معالِجات أحداث اللمس وإدارة إدخال الماوس في معالِجات أحداث منفصلة. يجب الانتباه إلى أنّ استخدام event.preventDefault() في معالِجات أحداث اللمس سيؤدي أيضًا إلى منع بعض السلوكيات التلقائية، مثل التمرير وحدث النقر.

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

<meta name="viewport" content="width=device-width,user-scalable=no">

وتذكّر أنّه عند استخدام هذه الميزة، الأمر متروك لك لجعل الموقع قابلاً للقراءة على جميع أحجام الشاشات، نظرًا لأن المستخدم لن يتمكن من التكبير بالقرب منك.

الإدخال بالماوس واللمس ولوحة المفاتيح

في الوضع "استكشاف" ثلاثية الأبعاد، أردنا أن تكون هناك ثلاث طرق للتنقل على الخريطة: الماوس (السحب)، واللمس (السحب، والتصغير أو التكبير بإصبعين والتدوير) ولوحة المفاتيح (التنقّل باستخدام مفاتيح الأسهم). تعمل كل طُرق التنقّل هذه بشكل مختلف قليلاً، لكننا استخدمنا النهج نفسه في جميع هذه الطرق، حيث حدَّدنا ضبط المتغيّرات في معالِجات الأحداث ونطبّق ذلك في حلقة requestAnimationFrame. لا يجب أن يعرف حلقة requestAnimationFrame الطريقة المستخدمة للتنقل.

كمثال، يمكننا تعيين حركة الخريطة (dragDX وragDY) باستخدام طرق الإدخال الثلاثة جميعها. في ما يلي طريقة تنفيذ لوحة المفاتيح:

document.addEventListener('keydown', onKeyDown );
document.addEventListener('keyup', onKeyUp );

function onKeyDown( event ) {
  input.keyCodes[ "k" + event.keyCode ] = true;
  input.shiftKey = event.shiftKey;
}

function onKeyUp( event ) {
  input.keyCodes[ "k" + event.keyCode ] = false;
  input.shiftKey = event.shiftKey;
}

//this needs to be called every frame before animation is executed
function handleKeyInput(){
  if(input.keyCodes.k37){
    input.dragDX = -5; //37 arrow left
  } else if(input.keyCodes.k39){
    input.dragDX = 5; //39 arrow right
  }
  if(input.keyCodes.k38){
    input.dragDY = -5; //38 arrow up
  } else if(input.keyCodes.k40){
    input.dragDY = 5; //40 arrow down
  }
}

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );
  //because keydown events are not fired every frame we need to process the keyboard state first
  handleKeyInput();
  //implement animations based on what is stored in input
   /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX = 0;
  input.dragDY = 0;
}

مثال مضمَّن: استخدام الماوس واللمس ولوحة المفاتيح للتنقّل: http://cdpn.io/catlf

ملخّص

إنّ تكييف تطبيق Build with Chrome مع الأجهزة التي تعمل باللمس ذات الأحجام المختلفة للشاشات شكّل تجربة تعليمية. لم يكن لدى الفريق خبرة كبيرة في القيام بهذا المستوى من التفاعل على الأجهزة التي تعمل باللمس وتعلمنا الكثير طوال العملية.

وتبين أنّ التحدي الأكبر كان كيفية حل تجربة المستخدم والتصميم. وتمثلت التحديات الفنية في إدارة أحجام الشاشات المتعددة، وأحداث اللمس، ومشاكل الأداء.

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

والآن، إذا لم يسبق لك ذلك، فانطلق وأنشئ شيئًا رائعًا!