تخطَّ إلى المقالة

الحركة نُشر زمن القراءة 10 د

ما الذي ينبغي أن يفعله تقليل الحركة بواجهة مرسومة بـ WebGL أو canvas

القاعدة الشاملة في CSS التي يشحنها الجميع تقرّر كم تدوم الحركة. لا تستطيع أن تقرّر عند أي إطار تتوقّف، ولا أي تخطيط تتركه خلفها، ولا ما الذي يحمله canvas بعد أن تختفي حلقته — وفي واجهة يرسمها Three.js أو Rive أو سياق ثنائي الأبعاد، هذه الثلاثة هي السؤال كلّه.

باختصار

تحت prefers-reduced-motion: reduce، ينبغي لواجهة مرسومة بـ WebGL أو canvas أن تحمل إطارًا واحدًا كاملًا مستقرًّا وألّا تحتفظ بأي اشتراك في الإطارات: لا شيء يتحرّك، ولا شيء فارغ. والقاعدة الشاملة الشائعة في CSS لا تستطيع إنتاج تلك الحالة، لأن animation-duration: 0.01ms تقرّر كم تدوم الحركة وحسب — لا عند أي إطار مفتاحي تتوقّف، ولا ما يفعله تخطيط مثبَّت، ولا شيء البتّة عن بكسلات يرسمها زمن تشغيل JavaScript. في هذا الموقع يعلن كل نظام متحرّك واحدًا من أربعة أجوبة — off أو end-state أو shortened أو runs مع تبرير مكتوب — و61 فحصًا مُودَعًا في البوّابات عبر 17 كشفًا تؤكّد النصفين في آن واحد: لا مشترك في الإطارات محفوظ في أي مكان من الصفحة، وكل واحدة من لوحات canvas السبع ما تزال تحمل صورة مستقرّة.

ما ينبغي أن تخرج به

  1. القاعدة الشاملة تجيب عن سؤال واحد من الأسئلة الأربعة التي يطرحها التفضيل: animation-duration: 0.01ms تقرّر كم تدوم، لا عند أي إطار تحطّ، ولا في أي تخطيط تُترَك، ولا ما الذي يحمله canvas حين تتوقّف حلقته.
  2. نمطا الفشل متناظران، فأكّد النصفين في الفحص نفسه — لا مشترك في الإطارات محفوظ في أي مكان، ولا سطح مُترَك بلا صورة عليه.
  3. المدّة هي الرافعة الخطأ مع حركة ذات إطارات مفتاحية: طيّ دورة الرسم السلكي البالغة 18 ثانية إلى 0.01ms يُحطّ كل خطّ على الإطار المفتاحي الممحو، ولا يهزمها إلا animation-name: none.
  4. الحالة المستقرّة شيء تبنيه لا شيء تحصل عليه — إطار الواجهة في هذا الموقع كان نصف مرسوم بصمت إلى أن عادت لقطتان عند زمنين مثبَّتين 1.5 ث و9 ث متطابقتين بايتًا ببايت.
  5. التثبيت تخطيط لا حركة، فعلى reduce أن ينقضه: إطفاء التحويل المُمشَّط لشريط الرحلة وحده قصّ ثلاثة من فصوله الأربعة خارج حافة المستند.
  6. تقليل الحركة هو الحالة الأمينة الوحيدة لتدقيق موقع متحرّك، لأن جولة axe-core نفسها أبلغت عن 707 عقدة تباين خطيرة بلا التفضيل، و5 معه.

golden:webgl §7 · 4 checks

ما الذي ينبغي أن يفعله تقليل الحركة بواجهة canvas؟

جوابان يُشحنان كثيرًا وكلاهما خطأ. الأوّل يترك الواجهة تعمل، لأن استعلام وسائط في ملف أنماط لا يبلغ شيئًا يرسمه زمن تشغيل JavaScript داخل سياق معالج الرسوميات (GPU) — التفضيل مضبوط، والصفحة تبدو كما هي، والزائر الذي طلب حركة أقلّ يحصل عليها كاملة كما كانت. والثاني يزيل الواجهة، فيبادل صورة متحرّكة بفجوة، ويأخذ معه الانطباع الأوّل للصفحة كلّه.

الجواب هنا إطار واحد كامل، ثم لا شيء. تحت reduce تُرسَم الصورة الشخصية والظلّ النقطي (halftone) والوضعية المرساة جميعًا؛ ولا يُرسَم اختلاف المنظور بالمؤشّر ولا الأثر السائل خلفه؛ ولا يُحتفَظ بأي اشتراك في الإطارات. ويُقرَّر ذلك في شرطٍ واحد داخل زمن تشغيل الواجهة، حيث التفضيل واحد من أربعة أسباب مستقلّة للتوقّف، وفرع else هو ما يجعل النتيجة حالة مستقرّة لا حالة فارغة: ألغِ الاشتراك في أطوار الإطار، وأغلِق المؤشّر، ثم ارسم مرّة أخيرة إن كانت الواجهة ما تزال داخل بوّابتها.

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

  • 61 فحص بوّابة يذكر التفضيل من أصل 817 عبر 17 كشفًا
  • 7 لوحة canvas ما تزال تحمل صورة واجهة WebGL واحدة، وحقل نقاط واحد، و5 أسهم Rive
  • 0 مشترك في الإطارات محفوظ عند كل واحدة من 12 محطّة تمرير
  • 16 كتلة reduce في ملفات الأنماط موزّعة على 17 من أصل 42 ملفًا
محسوبة من الكشوف المُودَعة في tests/golden/expectations ومن تمشيط لـ src. الرقمان 61 و817 أسماء فحوص في الشجرة المتتبَّعة، لا ناتج تشغيل.

src/styles/base/reset.css:38-49

مفتاح الإيقاف صحيح، وهو يبلغ أقلّ ممّا تظنّ

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

أسبقية طبقات التتالي تنعكس مع الإعلانات المهمّة: الإعلان العادي في طبقة لاحقة هو الذي يفوز، والإعلان المهمّ في طبقة سابقة هو الذي يفوز. وترتيب الطبقات هو reset ثم tokens ثم base ثم layout ثم components ثم sections ثم utilities ثم overrides — فهذه أقوى قاعدة مهمّة في الملف كلّه، ويؤكّد اختبار وحدة نصفَي ذلك: الطبقة صفر هي reset وفيها سطر المدّة، والطبقة الأخيرة لا تحمل أي إعلان مهمّ البتّة.

القاعدة الشاملة، وسبب سكناها حيث تسكن. الكتلتان منقولتان حرفيًّا من المستودع.
/* src/styles/base/reset.css — طبقة التتالي الأولى. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    /* استثناء موثَّق: تفضيل المستخدم يعلو على التصميم */
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
 
/* For important declarations the layer order REVERSES: the declaration
   in the earliest layer wins (CSS Cascade 5, section 6.1). Nothing in a
   later layer can out-rank this at any specificity — and the correct
   escape hatch is therefore a NORMAL declaration in the last layer.
   Reaching for important there makes the rule weaker, not stronger. */

السطران اللذان لا يتعلّقان بالمدّة يحملان حِملًا. فـ0.01ms بدل 0s تُبقي حدثَي transitionend وanimationend، فتظلّ آلة الحالة التي تنتظر انتهاء انتقال تتقدّم؛ وanimation-iteration-count: 1 تمنع حركة معلَنة infinite من أن تصير ارتعاشًا سريعًا جدًّا عند 0.01ms، وهو نقيض ما طُلب. ويسجّل Chris Coyier، الذي شاع المقتطف على يده، في المنشور نفسه أنه لا يعمل إلا حيث تكون الحركة كلّها من CSS، وأنه مع التأثيرات التي تقودها JavaScript قد يجعل الحركات سريعة ومُدوِّخة.

ولوضع المفتاح أوّلًا كلفة صادقة، وهذا المستودع يسجّلها بدل أن يلتفّ حولها. طبقة التحميل مُواصَفة لتجيب shortened، عند 0.3 ث — وذلك غير قابل للبلوغ من أي طبقة لاحقة، لأن الإعلان العادي يخسر أمام المهمّ، والإعلان المهمّ في طبقة لاحقة أضعف منه بعد. فتحت reduce تكون العلامة مستقرّة لا مرسومة، والانبثاق لحظيّ. إنه تراجع متعمَّد عن المرجع، تُرك في مكانه لأن علوّ تفضيل المستخدم على التصميم هو النتيجة الصحيحة.

واستنتاج واحد مكانه هنا لا مكان قياس. ثلاث قواعد في طبقات لاحقة في هذا المستودع تضبط transition-duration: 0.01s تحت reduce، وبحكم قاعدة التتالي التي تذكرها الفقرة أعلاه، هي إعلانات عادية يهيمن عليها إعلان reset المهمّ. ولا تقرأ أي بوّابة مدّة انتقال محسوبة تحت reduce في أي مكان من العُدّة، فذلك يتبع المواصفة لا المتصفّح — وهو تحديدًا نوع القاعدة التي يراكمها مستودع ولا ينتبه أبدًا إلى أنها خاملة.

home-hero.css:329-353

المدّة هي الرافعة الخطأ — السؤال هو الإطار الذي تحطّ عليه

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

خاصّة واحدة تهزمها، واحدة لا غير. animation-name: none تُخرج الحركة من اللعب كلّيًّا، ولا تستطيع مدّة أن تُحيي حركة بلا اسم.

المهرب، والوضعية التي يستقرّ عليها. الإطار المفتاحي الممحو هو ما كانت القاعدة الشاملة ستختاره من تلقاء نفسها.
/* src/styles/sections/home-hero.css — the hero wireframe.
   The reset layer would collapse the 18 s cycle to 0.01 ms and land
   every stroke on the ERASED keyframe, leaving an empty board. */
@media (prefers-reduced-motion: reduce) {
  .wfx-e {
    animation-name: none;
    stroke-dashoffset: 0;
  }
 
  .wfx-g2,
  .wfx-g3,
  .wfx-g4 {
    display: none;
  }
}
 
/* تُترَك مجموعة واحدة مرسومة بالكامل، وهذا ما وُجدت اللوحة من أجله. */

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

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

motion-architecture.md:451-460

أربعة أجوبة، ولماذا يحتاج runs إلى تبرير مكتوب

التفضيل ليس قيمة منطقية، والمواصفة لا تصفه كذلك. تعرّفه Media Queries Level 5 بأنه كشف رغبة في حركة أقلّ على الصفحة، ويصف MDN واجب المؤلّف بأنه واجهة تُزيل الرسوم المتحرّكة القائمة على الحركة أو تقلّلها أو تستبدلها — ثلاث استجابات مسموحة لا واحدة. وهذا هو السند الخارجي لأي تصنيف، وهذا الموقع يعلن واحدًا من أربعة أجوبة لكل تأثير متحرّك.

المفردات التي يُسجَّل بها كل تأثير في هذا الموقع، وما يعنيه كل جواب في الشيفرة المشحونة لا في الخطّة.
الجواب ما الذي يعنيه أين يُستعمل هنا
off لا تبدأ أبدًا. يُرسَم العنصر في حالته النهائية المستقرّة تحوّل أيقونة القائمة، وسحب الشريط باللمس، واللوحة التي تتعقّب المؤشّر
end-state تُتخطّى الحركة ويُطبَّق إطارها الأخير فورًا واجهة WebGL، وأسهم Rive الخمسة، وحقل النقاط، وكل ظهور دخول
shortened تعمل، بمدّة مخفَّضة مسمّاة لانبثاق التحميل، ومسجَّلة بأنها غير قابلة للبلوغ من طبقة لاحقة
runs تعمل بلا تغيير، وتتطلّب تبريرًا مكتوبًا مشهد واحد لا غير في الموقع كلّه: فصل لون body

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

والـ runs الوحيد في الموقع تلاشٍ متقاطع للّون مربوط بموضع التمرير، وهو يستحقّ الاستثناء بقياس لا برأي: لا شيء ينزاح ولا يدور ولا يتغيّر حجمه، فلا حركة تُقلَّل. وقد قيس إطفاؤه فكسر الصفحة عند 390 بكسل، حيث يطوي الملف المخفَّض غلاف الحلقة إلى نافذة عرض واحدة، فيصير مصدر تقدّم الفصل صفرًا، وتُبقي الخلفية rgb(237, 245, 243) طوال الفصل كلّه. والبوّابة المُودَعة تؤكّد الشرط المعاكس، والحلقة مثبَّتة والمسافة صفر: يجب أن تظلّ خلفية body المحسوبة تُعتِم متجاوزةً قناة حمراء قدرها 120 عند رزمة الشهادات.

7 canvases · 3 runtimes · 15 consumers

نظامًا فنظامًا، ما الذي يفعله reduce فعلًا

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

مقروء من المصدر المشحون لا من وثائق المواصفات — اثنتان منها ما تزالان تصفان جوابًا أقدم وأشدّ فراغًا لـ Rive كان سيترك خمس لوحات canvas فارغة.
النظام الفرعي الجواب ممّ تتألّف الحالة المستقرّة فحوص مُودَعة
واجهة WebGL، بـ Three.js end-state إطار واحد كامل عند فارق زمني صفر؛ أكثر من نصف اللوحة مضاء؛ لا مؤشّر مشترك 5
أسهم Rive، بـ Canvas2D end-state عشر خطوات، كلٌّ منها جزء من ستّين من الثانية، تُطبَّق وتُرسَم مرّة واحدة 3
حقل النقاط، بـ Canvas2D end-state التبعثر الساكن وكل نقطة مأخوذة، وحلقة المؤشّر مركونة خارج الحقل 5
ظهورات GSAP off غير مثبَّتة أصلًا: النص بلا قصّ، والماسحات غائبة، وكل شيء معلَّم بأنه ظهر 3
تمشيط ScrollTrigger off وضعية القسم المكتوبة — الحلقة مجمَّدة عند سالب 20 درجة من ملف الأنماط 6
تنعيم Lenis off لا يُنشَأ أصلًا: تمرير أصليّ، فوريّ، وكل دالّة ما تزال تعمل 1
رزمة الشهادات off ست بطاقات كاملة في عمود عادي، والشهادة أخيرة، بلا زرّ إيقاف 7

صفّان يستحقّان التوسيع، لأنهما يُظهران ما تكلّفه حالة مستقرّة كي تُبنى. جواب Rive معلَن كبيانات على العقد لا مقرَّرًا في فرع شرطي — عقد الأسهم المشحون يحمل settle — والاستقرار يعني تقديم آلة الحالة عشر مرّات، كل مرّة جزءًا من ستّين من الثانية، ثم الرسم مرّة واحدة. والحدّ ثابت لا حلقة تنتظر أن تكفّ الآلة عن التغيّر، لأن آلة حالة دائرية لا تبلغ ذلك الشرط أبدًا. وعشر خطوات تكفي لانتقال خمول بين حالتين كي يحطّ.

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

pinTime(1.5) against pinTime(9)

عيب نصف الإطار، والسؤال الذي أمسك به

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

  1. التنفيذ الذي يبدو صحيحًا تحت reduce، ألغِ الاشتراك في أطوار الإطار ثم استدعِ طور الكتابة مرّة واحدة. يرسم، ولوحة canvas ليست فارغة، ولقطة منها مقنعة.
  2. العيب الكامن الكتابة تركّب مع ما تركه آخر طور حساب. وحيث لم يعمل حساب قطّ، يكون ذلك قيمة uniform للزمن تساوي 0 ونسيج ضوضاء خلفية فارغًا — فكان الإطار المستقرّ ينقصه حقل الظلّ النقطي نفسه، وهو معظم ما تبدو عليه الواجهة.
  3. السؤال الذي أظهره طلبت العُدّة الحتمية لا الجمال. عادت لقطتان عند زمنين مثبَّتين 1.5 ث و9 ث متطابقتين بايتًا ببايت، والزمن المثبَّت الذي لا يغيّر شيئًا معناه قيمة uniform لا تصل إلى المظلِّل (shader).
  4. الإصلاح شغّل الحساب ثم الكتابة، عند فارق زمني صفر. كل مُخمِّد يحتفظ بقيمته، لأن التخميد عبر زمن معدوم يعيد القيمة التي بدأ بها، وساعة الزمن المنقضي لا تتقدّم. إنه الإطار نفسه مكتملًا لا الإطار التالي.
  5. البوّابة التي تجعل الادّعاء قابلًا للبرهان ثبّت 3 ثوانٍ، وارسم مرّة، ثم أكّد أن قيمة uniform للزمن في المشهد تقرأ 3. والتثبيت أوّلًا شرط لازم: تحت reduce يكون الزمن غير المثبَّت 0 بحقّ، فقراءته هناك لا تبرهن شيئًا في أي اتّجاه. ونصف الإطار يتركها عند 0 ونسيج الضوضاء فارغًا.
  6. اقرأ البكسلات، ولا تلتقط لقطة أبدًا مخزن الرسم غير محفوظ، فمحتويات canvas غير معرَّفة بمجرّد أن يأخذها المركِّب، ولقطة العنصر رمية عملة. والرسم وقراءة البكسلات في الدورة نفسها هما الالتقاط الأمين الوحيد.
كيف يُنتَج إطار مستقرّ واحد، وكيف أُمسك بالنسخة التي كانت تبدو عاملة فحسب.

والكشف حول ذلك الإصلاح يؤكّد أربعة أشياء وليس فيها صورة واحدة: أكثر من نصف اللوحة مضاء، والإطار إطار كامل، ولا إطار محفوظ ولا مؤشّر مشترك، وعدد الإطارات لم يتغيّر بعد 1,200 مللي ثانية. والأخير يهمّ أكثر ممّا يبدو — فالمُصيِّر الذي يواصل النبض بهدوء من تلقاء نفسه يجتاز كل فحص ساكن يمكنك كتابته عن الصورة.

قراءة البكسلات بدل مقارنة اللقطات هي الانضباط نفسه الذي تحتاجه لوحة canvas في اختبار الانحدار، وله أرقامه هنا: أرضية الضجيج في لقطة canvas.

home-journey.css:569-605

النصف الذي لا تبلغه أي قاعدة مدّة: التخطيط والحالة

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

وشريط الرحلة هو حيث كلّف ذلك قدرًا قابلًا للقياس من المحتوى. هو نظام أفقي — أربع بطاقات على مسار عرضه 4,052 بكسل داخل نافذة عرض 1,440 بكسل — والشيء الوحيد الذي يجلب الثلاثة الأخرى إلى المشهد هو تحويل مُمشَّط. وإطفاء ذلك التحويل عند reduce صحيح، لأن انزلاقًا جانبيًّا يقوده التمرير هو بالضبط الحركة التي يدور حولها التفضيل؛ لكن إطفاءه وحده قصّ ثلاثة أرباع القسم خلف الفيض الأفقي للصفحة. وقبل وجود قاعدة التخطيط، قيس المستند فخرج 14,884 بكسل بدل 18,709، والـ3,825 المفقودة كانت الفصول التي لم يكن أحد يستطيع بلوغها.

بلا تفضيل 18,709 بكسل
تحت reduce، كما يُشحن 15,506 بكسل
تحت reduce، قبل قاعدة التخطيط 14,884 بكسل
ارتفاع مستند الصفحة الرئيسية عند 1440 في 900، من فحص الرحلة المزدوجة في tools/golden/rest.mjs ومن الملاحظة في home-journey.css.الأشرطة بمقياس واحد مقارنةً بالمستند المتحرّك.

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

والنصف المتعلّق بالحالة يُنتج فشلًا أسوأ من بطاقة مفقودة. في شريط المشاريع، تفكّ كتلة reduce التصاق المنصّة، وتعيد الفيض الأفقي والالتقاط عند التمرير، وتضبط تحويل المسار على none، وتعيد touch-action إلى auto — كل ذلك حتى لا تترك سمةٌ قديمة، خلّفها تفضيل تغيّر في منتصف الجلسة قبل أن يفكّ زمن التشغيل بناءه، القارئَ داخل تثبيت بطول ثماني شاشات لا يستطيع التمرير خارجه. وإعادة touch-action ليست زينة: نصّ السحب لا يُثبَّت أبدًا تحت reduce، فقيمة pan-y موروثة كانت لتمنع الإيماءة الوحيدة التي وُجد البديل ليسمح بها.

والموقع الذي حلّ هذا محلّه كانت فيه أدقّ نسخة من هذا الصنف. انبثاق الانتقال بين الصفحات فيه يقصر من 0.68 ث إلى 0.3 ث تحت reduce، بينما يبقى انتقال الظهور المقترن به عند 0.68 ث، فلمدّة 380 مللي ثانية تقريبًا بعد أن تبدو الطبقة قد اختفت، كانت نقرات زائر تقليل الحركة تقع على طبقة غير مرئية تملأ نافذة العرض. والتفضيل المطبَّق جزئيًّا يفكّ تزامن توقيتين لم يكونا صحيحين إلا معًا، ولا يظهر ذلك في لقطة ولا لتدقيق آلي.

والشريط الذي تحميه تلك القاعدتان هو فهرس المشاريع، والأعمال التي خلفه هي المواقع المفاهيمية العشرة.

hero-swipe-lock.js:34-41

ما الذي يجب ألّا يطفئه reduce

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

قفل التمرير
ليس نظام حركة، فلا يطفئه reduce — فقفل التمرير إمكانية تنقّل وقد طلبها الزائر صراحةً. أمّا تبديلا التسمية إلى جانبه فحركة، وينطويان إلى تبديل لحظيّ. والقفل نفسه محسوب بالمرجعيات لدى مالك التمرير، فيستطيع هذا الزرّ والقائمة أن يحمل كلٌّ منهما واحدًا بلا أن يحرّر قفل الآخر.
التثبيت
تخطيط، فعلى reduce أن ينقضه لا أن يوقف تحريكه. وفحص سهولة الوصول المُودَع ينقل تأكيدًا لم يجرِه الموقع القديم قطّ: تحت reduce، والقائمة مفتوحة، لا يغيّر 1,200 بكسل من عجلة الفأرة ولا ضغطة End ولا ضغطة PageDown موضعَ التمرير، وEscape يحرّره ويدع الصفحة تبلغ قاعها.
التمرير الناعم
وضع مختلف لا وضع معطَّل. في الموقع الذي حلّ هذا محلّه، قيس الخيار البديهي — إيقاف المنعِّم — فترك الصفحة غير قابلة للتمرير إطلاقًا: 0 بكسل من العجلة، و0 بكسل من End. ولهذا كانت رقعته تستطلع حتى 600 إطار بحثًا عن متغيّر عامّ ثم تُتلف النسخة بدلًا من ذلك. أمّا هنا فلا يُنشَأ المنعِّم أصلًا تحت reduce، والتمرير أصليّ وكامل.
زرّ الإيقاف
غائب لا خامل. رزمة الشهادات توزّع ست بطاقات على إيقاع 2,200 مللي ثانية، بتوزيع مدّته 600 مللي ثانية ودورة 13.2 ث، وتحت reduce لا تُنصَب أصلًا — فالزرّ الذي كان سيوقفها يُزال لا يُعطَّل. والعنصر الحاضر الذي لا يفعل شيئًا أسوأ من غيابه: يأخذ محطّة تنقّل واسمًا مقروءًا وضغطة.
تلاشي اللون المتقاطع
التأثير الوحيد في هذا الموقع الذي يعلن runs. لا شيء ينزاح ولا يدور ولا يتغيّر حجمه، والحجّة مقيسة لا مُدَّعاة — فمع إطفاء التأثير عند 390 بكسل صارت مسافة الفصل نفسها صفرًا، وحملت الصفحة لونًا مسطّحًا واحدًا بطولها كلّه. وتقليل الحركة مفعَّل افتراضيًّا في حصّة كبيرة من الهواتف، وهذا افتراض عمل لهذا المستودع لا رقم قاسه بنفسه.
خمس آليات، والتعليل الذي يحسم كلًّا منها. ثلاث من الخمس تقع داخل مكوّنات نصفها الآخر يجيب التفضيل فعلًا.

ولحالة التمرير نسخة في المكتبة أيضًا، وقد تحرّكت. النسخة المثبَّتة هنا، 1.1.20، لا تحتوي أي إشارة إلى الاستعلام البتّة — فكان عدم إنشائها أصلًا هو الجواب الوحيد المتاح. أمّا Lenis الحالي فيشحن respectReducedMotion مفعَّلًا افتراضيًّا ويأخذ موقفًا ثالثًا: أبقِ النسخة تعمل مع إجبار استيفائها على واحد إلى واحد، كي ينجو تزامن WebGL مع DOM، واجعل عمليات التمرير البرمجية لحظية. ثلاثة أجوبة قابلة للدفاع عن تفضيل واحد، وهي حجّة التصنيف مصغَّرة.

وهل يستحقّ المنعِّم وجوده أصلًا سؤال قياس منفصل، وهو Lenis مقابل التمرير الأصليّ، مقيسًا.

ولجواب القفزة اللحظية في الروابط المرساة عواقبه على التركيز والتاريخ — ما الذي يكسره رابط مرسًى بتمرير ناعم.

axe-core 4.11.0 · 144 scans · 2026-08-24

كيف تبرهن ذلك، والـ707 التي صارت 5

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

والرحلة المزدوجة الحيّة هي النصف الأصعب، لأن التفضيل قد يتغيّر في منتصف الجلسة. تشغيل reduce يفكّ 26 من أصل 41 من ScrollTriggers وكل فاصل تثبيت معها، وهنا تذهب الـ3,203 بكسل من ارتفاع المستند؛ وإطفاؤه يعيد بناءها، بلا تأثير مسرَّب ولا مكرَّر عبر الرحلة، وبلا شيء متروك مقصوصًا في أي اتّجاه. أمّا إزاحة التمرير فلا تنجو منها — 9,645 بكسل، ثم 8,004، ثم 9,716 — وذلك الانحراف البالغ 71 بكسل ينقل ثلاثة ظهورات أخرى تحت الطيّة إلى مجموعة ما لم يُطلَق بعد.

أسماء الفحوص المُودَعة تحت tests/golden/expectations، محسوبة بمرور واحد على الكشوف. هذه تأكيدات في الشجرة المتتبَّعة، لا ناتج تشغيل.
الكشف الفحوص فيه التي تذكر التفضيل
certificate 123 7
webgl 72 5
projects 52 6
rive 48 3
particles 38 5
rest 20 7
accessibility 20 9
الكشوف الـ17 كلّها 817 61

وهذا ليس ادّعاءً بلاغيًّا. رقم axe-core المنشور لهذا الموقع — 5 مخالفات، axe-core 4.11.0، و72 وثيقة بعرضين لمجموع 144 فحصًا، قيست في 24 أغسطس 2026 — يحمل شرطًا، والشرط يحمل حِملًا: prefers-reduced-motion: reduce، بعد تمرير كل وثيقة من طرفها إلى طرفها. وبدونه، أبلغت الأداة نفسها على الموقع نفسه عن 707 عقدة تباين لونيّ خطيرة عبر 60 مسارًا. والجولتان تغطّيان عددًا مختلفًا من المسارات، فهما الأداة نفسها قبل شرط مصحَّح وبعده، لا مقارنة مضبوطة.

وكل واحدة من تلك الـ707 كانت عنصرًا أُمسك به في منتصف ظهوره. يركّب axe العنصر شبه الشفّاف على ما خلفه، فيُقرَأ لون على لون شبه مطابق، ويقيس عنوان يتلاشى داخلًا 1.02:1. ولم يُعفِ التصحيح شيئًا: نجا منه عيبان حقيقيان فأُصلحا — بطاقة دراسة حالة تطبع عند 4.36:1 عبر 612 عقدة وفي وثائقها الـ33 كلّها، وعلامة مائية في صفحة 404 عند 1.25:1 — والخمس الباقية عنصر واحد على ورق الشهادة، عند 1.33:1 حيث ينطبق 3:1، مسموح به بالاسم وبالمحدِّد عبر ثلاثة مسارات وعرضين.

والموقع الذي حلّ هذا محلّه هو المثال المضادّ لهذا كلّه، وأرقامه أدلّة على ذلك البناء القديم لا على هذا. تمشيط للاستعلام عبر محرّكه يعيد صفرًا. تحت reduce عملت نبضة GL فيه 51 مرّة في ثلاث ثوانٍ مقابل 52 بدونه، وبقي خطّه الزمني العامّ في GSAP عند مقياس زمن 1 عبر ظهوراته الـ836 كلّها، وعملت ScrollTriggers الـ61 فيه بلا تغيير، ونوع واحد لا غير من لوحات Rive توقّف — أيقونة القائمة في شريط التنقّل، 70 منها في الموقع كلّه — بينما واصلت 520 غيرها اللعب. أمّا المعالجة التي وُجدت فعلًا فكانت رقعة من 106 أسطر تستطلع متغيّرًا عامًّا، و24 كتلة في ملفات الأنماط بسياستين متنافرتين. ليس أن أحدًا لم يحاول، بل أن التفضيل أُجيب حيث كانت الإجابة سهلة، وتُرك بلا جواب حيث كانت الحركة فعلًا.

ونصف تلك القصّة المتعلّق بالتباين مقالة قائمة بذاتها — لماذا تكون جولة axe على موقع متحرّك زائفة الإيجاب في معظمها.

أسئلة

هل يعني prefers-reduced-motion ألّا تكون هناك حركة البتّة؟

لا. تعرّفه Media Queries Level 5 بأنه كشف رغبة في حركة أقلّ على الصفحة، ويصف MDN مهمّة المؤلّف بأنها واجهة تُزيل الرسوم المتحرّكة القائمة على الحركة أو تقلّلها أو تستبدلها — ثلاثة خيارات لا واحد. وقد أوصى WebKit عند تقديمه الميزة سنة 2017 بتقديم حركة بديلة أبسط، أو مؤشّر بصريّ آخر ينقل المعنى المقصود. وفي هذا الموقع يصير ذلك أربعة أجوبة معلَنة لكل تأثير، والجواب الذي يعمل بلا تغيير كان لا بدّ من تبريره كتابةً.

هل ينبغي إزالة واجهة WebGL كلّيًّا تحت تقليل الحركة؟

لا، وذلك هو نمط الفشل الثاني لا الخيار الآمن. تحت reduce تُصيّر واجهة هذا الموقع إطارًا واحدًا كاملًا ثم تتوقّف: الصورة الشخصية والظلّ النقطي والوضعية المرساة حاضرة، واختلاف المنظور بالمؤشّر والأثر السائل غائبان، ولا اشتراك في الإطارات محفوظ. والفحوص المُودَعة تؤكّد أن أكثر من نصف اللوحة مضاء، وأن الإطار إطار كامل — إذ يجب أن يبلغ زمن مثبَّت قدره 3 ثوانٍ قيمة uniform الخاصّة بالمظلِّل، لأن نصف الإطار يتركها عند 0 مع نسيج ضوضاء فارغ — وأن لا مؤشّر مشترك، وأن المُصيِّر لا ينبض من تلقاء نفسه خلال الـ1,200 مللي ثانية التالية.

هل تكفي القاعدة الشاملة 0.01ms وحدها؟

هي لازمة وهي مشحونة هنا، في طبقة التتالي الأولى حيث لا يمكن هزيمة إعلانها المهمّ. وهي تجيب عن سؤال واحد من الأسئلة الأربعة التي يطرحها التفضيل. لا تستطيع أن تقرّر عند أي إطار تحطّ حركة متوقّفة: فطيّ دورة الرسم السلكي في هذا الموقع البالغة 18 ثانية إلى 0.01ms يُحطّ كل خطّ على الإطار المفتاحي الممحو، ولا يصلح ذلك إلا animation-name: none. ولا تبلغ التخطيط، حيث تظلّ اللوحة اللاصقة تثبّت ويظلّ المسار الأفقي يقصّ، ولا تبلغ شيئًا يرسمه GSAP أو Three.js أو Rive أو منعِّم تمرير.

لماذا 0.01ms بدل 0s، ولماذا عدد التكرارات؟

0.01ms تُبقي حدثَي transitionend وanimationend، فتظلّ شيفرة JavaScript التي تنتظر انتهاء انتقال قبل إزالة عنصر أو تقديم آلة حالة تعمل، بينما قد يتخطّى الصفر الحقيقي الحدث فيحبس ذلك المنطق. وضبط عدد التكرارات على 1 لازم لأن حركة معلَنة infinite لا تتوقّف عند مدّة 0.01ms — بل تصير ارتعاشًا سريعًا جدًّا، وهو نقيض الطلب. السطران كلاهما ينتميان إلى المقتطف، ولا واحد منهما يقول شيئًا عن الإطار الذي تتوقّف عليه الحركة.

هل يتولّى GSAP أو Lenis هذا عنّي؟

GSAP لا يتولّاه، عن قصد: توثيقه يقول إن احترام التفضيل يتطلّب شيفرة مقصودة، ويعرض gsap.matchMedia بشرط reduce يضبط مثاله المشروح المدّة فيه على صفر. أمّا Lenis فيتولّاه الآن — إذ تحترم إصداراته الحديثة التفضيل افتراضيًّا، فتعطّل التنعيم بإجبار الاستيفاء على واحد إلى واحد مع إبقاء النسخة حيّة كي ينجو تزامن WebGL مع DOM. والنسخة المثبَّتة في هذا المستودع، 1.1.20، لا تحتوي أي إشارة إلى الاستعلام البتّة، ولهذا كان جواب الموقع ألّا يُنشئ المنعِّم أصلًا تحت reduce. تحقّق من نسختك المثبَّتة قبل أن تعتمد على المكتبة.

المصادر

مقيسة في هذا المستودع

  • src/runtime/webgl/core/webgl-runtime.js شرط تشغيل الواجهة، والإطار المستقرّ المُصيَّر عند فارق زمني صفر، والملاحظة عن سبب تعذّر التقاط مخزن الرسم بعد حدوثه.
  • src/styles/base/reset.css مفتاح الإيقاف نفسه، في طبقة التتالي الأولى، مع الاستثناء الموثَّق على سطر المدّة.
  • src/runtime/rive/rive-instance.js جواب settle كبيانات على العقد، والخطوات العشر الثابتة التي تبلغه بلا دوران على آلة حالة دائرية.
  • tools/golden/webgl.mjs قسم reduce: ثبّت زمنًا، وصيّر مرّة، واقرأ البكسلات، وأكّد أن الإطار كامل وأن لا إطار محفوظ.
  • contracts/published-measurements.json رقم axe-core مع شرطه، والملاحظة التي تسجّل ما أبلغت عنه الجولة نفسها بلا ضبط التفضيل.

مُقابَلة بـ

Alaa Abbod

بقلم

علاء عبّود

مطوّر إبداعي — هيرنه، ألمانيا

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

Alaa Abbod

أتشحن شيئًا يرسم إطاراته بنفسه؟

الواجهة المبنيّة بـ WebGL أو Rive أو سياق ثنائي الأبعاد تحتاج أن يُصمَّم جوابها لتقليل الحركة إلى جانبها، لا أن يُركَّب عليها لاحقًا — لأن الحالة المستقرّة شيء تبنيه. فإن كنت عند هذه المرحلة، فهي محادثة قصيرة يستحقّ أن تُجرى مبكرًا.

من فضلك أدر جهازك،
هذا الموقع مبني عموديًا.