CSS والتخطيطاتالرسوم المتحركة

استخدام CSS Houdini: تطوير تأثيرات مخصصة غير متوفرة في CSS التقليدي

استخدام CSS Houdini: تطوير تأثيرات مخصصة غير متوفرة في CSS التقليدي

جرّب أن تفعل شيئاً يبدو بسيطاً: تحريك زاوية في conic-gradient() بسلاسة عند hover. تُعرّف custom property باسم --kat: 0deg، وتضيف transition: --kat 0.4s، وتُمرّر الماوس فوق العنصر – ولا يحدث شيء. يتغيّر التدرّج بشكل مفاجئ ومباشر، وكأن transition غير موجود أصلاً. هذه ليست مشكلة في كودك. بالنسبة للمتصفح، --kat مجرد سلسلة نصية (string) غير شفافة – لا يعرف أنها زاوية، ولذلك لا توجد طريقة لحساب القيم البينية (interpolation) بين 0deg و180deg. يستطيع المتصفح حساب القيم البينية للأرقام والألوان والأطوال، لأنه يعرف أنواعها. أما custom properties، على عكس خصائص CSS المدمجة، فليس لها أي نوع محدد. وهنا بالضبط، في هذه الفجوة، يأتي دور CSS Houdini.

ما هو Houdini فعلياً

هذا سوء فهم شائع: Houdini ليست تقنية واحدة، ولا حتى مواصفة (specification) واحدة. إنها مظلة تجمع عدة مقترحات مستقلة في W3C، تربطها فكرة واحدة – كشف أجزاء من محرك العرض (تحليل القيم، تخطيط الصفحة، الرسم، الحركة) كانت في السابق مخفية تماماً عن المطوّر. بدلاً من انتظار ظهور خاصية جاهزة في CSS تقوم بالضبط بما تحتاجه، تحصل على نقطة تدخّل منخفضة المستوى (low-level hook) في مرحلة محددة من هذه العملية.

هذه النقاط تُنفَّذ في صورة worklets – وحدات JavaScript صغيرة، قريبة من الناحية المفاهيمية من Web Workers، لكنها مصمّمة خصيصاً لتعمل ضمن pipeline العرض (rendering). يعمل الـ worklet خارج الخيط الرئيسي (main thread)، وليس له وصول إلى window أو document أو DOM، ويتواصل فقط عبر عقد (contract) محدد بدقة، مثل paint(ctx, size, properties). هذا ليس قيداً ناتجاً عن كسل واضعي المواصفة، بل قرار معماري مقصود. بفضل هذا العزل، يستطيع المحرك تشغيل الـ worklets بالتوازي، على خيوط منفصلة، وتخزين النتيجة مؤقتاً (caching) دون خطر أن يلامس كودك شيئاً لا ينبغي له لمسه. هذا العزل نفسه هو السبب في أنك لن تجد داخل الـ worklet لا Date ولا performance.now() ولا fetch – فتقييد الوصول إلى الوقت الدقيق والشبكة هو حماية مقصودة من هجمات من نوع timing/fingerprinting انطلاقاً من كود يُعنى برسم الصفحة.

أجزاء هذه المظلة اليوم في مراحل مختلفة تماماً: جزء منها أصبح منذ فترة طويلة ضمن Baseline، وجزء آخر يعمل فقط في Chrome، وجزء ثالث لا يزال، بعد عقد كامل، مجرد تجربة (experiment). هذا التمييز جوهري – ومعظم المقالات التعريفية عن Houdini تتجاهله، وتتعامل مع الموضوع بأكمله وكأنه تقنية واحدة جاهزة للاستخدام.

الجزء الوحيد من Houdini الذي يستحق الاستخدام فعلياً اليوم: @property

تحلّ CSS Properties and Values API بالضبط المشكلة التي بدأنا بها هذا المقال. تُسجّل custom property بنوع محدد (syntax)، وقيمة ابتدائية، ومعلومة عمّا إذا كانت موروثة (inherited) أم لا – ومن هذه اللحظة يتعامل المتصفح معها كقيمة كاملة قابلة للتحريك (animatable)، لا كسلسلة نصية.

html

<div class="ring"></div>

css

@property --kat {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.ring {
width: 200px;
height: 200px;
border-radius: 50%;
background: conic-gradient(from var(--kat), #ff4d4d, #4d79ff, #4dff88, #ff4d4d);
transition: --kat 0.6s ease;
}
.ring:hover {
--kat: 180deg;
}

هذا المثال يفعل فعلياً شيئاً لا يستطيع CSS العادي فعله – فبدون @property، لن يعمل transition أعلاه على الإطلاق، تماماً كما في السيناريو الذي فتحنا به هذا المقال. تخبر syntax: '<angle>' المتصفح أن --kat زاوية، فيستطيع حساب القيم البينية بسلاسة؛ أما inherits: false فتمنع الوراثة العرضية من قِبل العناصر الفرعية، وهو أمر يُعد غالباً مصدراً لأخطاء يصعب تتبعها عند استخدام custom properties.

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

javascript

if ('registerProperty' in CSS) {
CSS.registerProperty({
name: '--kat',
syntax: '<angle>',
inherits: false,
initialValue: '0deg',
});
}

هذا هو الجزء الوحيد من Houdini الذي يحمل اليوم حالة Baseline – يعمل بشكل أصلي (native) في Chrome، وFirefox (منذ الإصدار 128)، وSafari (منذ 16.4)، دون الحاجة إلى polyfills ودون @supports. لو كان عليك أن تتذكر وتطبّق شيئاً واحداً فقط من كل Houdini، فليكن هذا بالتحديد.

CSS Paint API: worklet يرسم فعلياً

تسمح لك Paint API باستبدال background بدالة paint() تَرسم مباشرة على العنصر، مستخدمةً واجهة مختصرة تشبه Canvas 2D (تُدعى PaintRenderingContext2D) – بدون fillText أو drawImage أو إمكانية قراءة البكسلات. ما يميّز هذا الأسلوب عن الرسم العادي على <canvas> هو inputProperties: يُعلن الـ worklet عن custom properties التي يريد مراقبتها، ويُعاد رسمه تلقائياً فقط عندما تتغيّر إحداها – دون سطر واحد من JavaScript مخصص للاستماع إلى الأحداث (event listening).

في المثال التالي نمط من الخطوط المائلة التحذيرية، تتحكّم في زاويتها الخاصية نفسها التي سجّلناها سابقاً --kat (أو بالأحرى نسختها المحلية --stripe-angle) – وهو ما يوضّح كيف يكمّل @property وPaint API بعضهما البعض بشكل طبيعي.

html

<div class="hazard"></div>

css

@property --stripe-angle {
syntax: '<angle>';
inherits: false;
initial-value: 45deg;
}
.hazard {
width: 100%;
height: 120px;
--stripe-angle: 45deg;
background: paint(hazardStripes);
transition: --stripe-angle 0.4s ease;
}
.hazard:hover {
--stripe-angle: 135deg;
}

javascript

// main.js
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('hazard-stripes.js');
}

javascript

// hazard-stripes.js
class HazardStripesPainter {
static get inputProperties() {
return ['--stripe-angle'];
}
paint(ctx, size, properties) {
const angle = properties.get('--stripe-angle').to('deg').value;
const stripeWidth = 24;
const { width, height } = size;
const diagonal = Math.sqrt(width ** 2 + height ** 2) * 2;
ctx.save();
ctx.translate(width / 2, height / 2);
ctx.rotate((angle * Math.PI) / 180);
ctx.translate(-diagonal, -diagonal);
let stripeIndex = 0;
for (let x = 0; x < diagonal * 2; x += stripeWidth) {
ctx.fillStyle = stripeIndex % 2 === 0 ? '#111111' : '#f5c400';
ctx.fillRect(x, 0, stripeWidth, diagonal * 2);
stripeIndex++;
}
ctx.restore();
}
}
registerPaint('hazardStripes', HazardStripesPainter);

يستحق الانتباه هنا سطر properties.get('--stripe-angle').to('deg').value – وهو CSS Typed OM، عنصر آخر من نفس عائلة الـ API. فبدلاً من تحليل السلسلة النصية يدوياً (parseFloat، وقصّ "deg")، تحصل على قيمة مُصنّفة (typed) من نوع CSSUnitValue، تقوم بتحويلها بشكل صريح إلى الوحدة التي تحتاجها. وبما أن زاوية الخطوط المرسومة تعتمد فقط على custom property المُعلَنة في inputProperties، يعرف المحرك بالضبط متى يجب إعادة رسم العنصر – فتغيير خاصية CSS غير مرتبطة في مكان آخر من الصفحة لن يُشغّل paint() من جديد.

Layout API وAnimation Worklet: واقع عام 2026

هنا يبدأ الجزء الذي تصمت عنه معظم المقالات التعريفية عن Houdini. CSS Layout API – الركيزة الثالثة من نفس المواصفة، والتي تسمح بتعريف خوارزمية تخطيط (layout) خاصة بك، أي ما يعادل display: grid مخصص خاص بك – موجودة كمشروع منذ عام 2015، ولا تزال، بعد عقد كامل، تجريبية. لم يُطبّقها أي متصفح مستقر بشكل أصلي (native)؛ وهي متاحة فقط خلف أعلام تجريبية (flags) أو ضمن origin trials في Chrome. إذا صادفت مقالاً يعرض display: layout(name) وكأنه حل جاهز للاستخدام، تحقّق من تاريخ النشر – فالأرجح أنه عرض توضيحي (demo) من origin trial يعود لسنوات مضت، وليس شيئاً يمكنك تشغيله اليوم في متصفح المستخدم.

كان الهدف من Animation Worklet منح تحكّم كامل في الحركات المدفوعة بالتمرير (scroll)، تُنفَّذ خارج الخيط الرئيسي. لكن الواقع أنه مدعوم فقط في Chromium. والأهم من ذلك – أن حالة الاستخدام (use case) الرئيسية له لم تعد سبباً للجوء إلى Houdini، لأن CSS حصل على حل أصلي (native) لنفس المشكلة: تسمح لك اليوم animation-timeline: scroll() بربط الحركة بالتمرير دون سطر واحد من JavaScript ودون worklets، مع دعم متزايد في المتصفحات. هذا مثال جيد على نمط أوسع: جزء من الأسباب التي دفعت لظهور Houdini أصلاً انتقل مع الوقت مباشرة إلى مواصفة CSS نفسها، بدلاً من أن يبقى واجهة برمجية منخفضة المستوى للمطورين.

متى يكون هذا منطقياً عملياً

  • استخدم @property بثقة، دون أي احتياطات – فهي تحمل حالة Baseline، ولا تحتاج إلى @supports ولا إلى polyfill.
  • تعامل مع Paint API كتحسين تدريجي (progressive enhancement) – غلّفها بـ @supports(background: paint(x)) مع حل بديل (fallback) معقول بـ CSS خالص (مثلاً تدرّج لوني عادي) لأجل Firefox، أو استخدم polyfill باسم css-paint-polyfill إذا كان التأثير يجب أن يعمل في كل مكان.
  • لا تخطط لميزة إنتاجية اعتماداً على Layout API – فهي لا تزال تجربة بلا دعم حقيقي من المتصفحات، جيدة للتجربة والمرح، لا لخريطة الطريق (roadmap).
  • تذكّر أن paint() ليست CSS «مجانياً» – إنها كود JavaScript خاص بك، يُعاد تشغيله في كل مرة تتغيّر فيها إحدى inputProperties المُعلَنة. الحسابات المكلفة (توليد الضوضاء (noise)، الأنماط الإجرائية المعقّدة) يجب تحسينها تماماً كما تُحسّن حلقة الرسم (render loop) على <canvas> – فمن السهل هنا أن تبني، دون قصد، حركة تعيد رسم العنصر بأكمله إطاراً تلو إطار على الخيط الرئيسي لتخطيط الصفحة (layout main thread).

الخلاصة

Houdini ليست قراراً واحداً بـ «نستخدمها أم لا» – بل ثلاثة أو أربعة قرارات منفصلة، لكل منها درجة مخاطرة مختلفة. أولاً، تحلّ @property مشكلة حقيقية ومحددة (تحريك custom properties) وهي اليوم آمنة للاستخدام تماماً مثل أي خاصية CSS أخرى – إنها الجزء الوحيد من هذا النظام البيئي الذي يستحق التطبيق دون تردد. ثانياً، تمنحك Paint API قوة حقيقية (worklet يُعاد رسمه فقط عند تغيّر التبعيات المُعلَنة، دون استماع يدوي للأحداث)، لكنها تتطلب خطة واعية للمتصفحات التي لا تدعمها – فهي أداة للتحسين التدريجي، لا للميزات الحرجة. ثالثاً، يُعدّ Layout API وAnimation Worklet مثالاً جيداً على أن وجود المواصفة بحد ذاته لا يضمن التبنّي (adoption) – فبعد سنوات، لا يزال أحدهما ينتظر التطبيق، بينما استُبدل الآخر جزئياً بحل CSS أصلي أبسط. قبل أن تلجأ إلى Houdini، تحقّق في أي من هذه السلال الثلاث يقع بالضبط الجزء الذي تحتاجه فعلاً.