موارد گوشه‌ای پیاده‌سازی شکل گوشه‌ای CSS در Blink

منتشر شده: ۱۹ فوریه ۲۰۲۶

یکی از ویژگی‌های CSS که کروم در سال ۲۰۲۵ ارائه کرد، corner-shape بود. این به شما امکان می‌دهد شکل یک گوشه را که دارای border-radius است با استفاده از کلمات کلیدی مانند bevel و scoop تعریف کنید. همچنین می‌توانید از یک تابع superellipse استفاده کنید که مقداری بین -Infinity و Infinity دریافت می‌کند.

برای مرور کلی این ویژگی و نحوه عملکرد آن ، به مقاله مفصل آمیت شین در Frontend Masters مراجعه کنید.

هنگام پیاده‌سازی این ویژگی در اوایل سال ۲۰۲۵، با چند چالش جالب با پیچیدگی‌های مختلف مواجه شدم. چیزهای زیادی در مورد ابربیضی‌ها، نقاشی حاشیه در Blink و استفاده از ریاضیات برداری برای گرافیک دوبعدی یاد گرفتم.

این سند برخی از چیزهایی را که من آموخته‌ام به اشتراک می‌گذارد، که ممکن است برای دیگران نیز جالب باشد.

تقارن اشکال محدب و مقعر

در حالی که مقادیر superellipse ( k ) به طور سنتی بین ۰ و Infinity قرار دارند، که در آن مقادیر بین ۰ و ۱ مقعر و بقیه محدب هستند (۱ bevel است)، مقادیر superellipse در مشخصات CSS بین -Infinity و Infinity قرار دارند و نشان دهنده ۲ k هستند. این امر تقارن ایجاد می‌کند زیرا هر مقدار مثبت مانند تصویر آینه‌ای همتای منفی خود به نظر می‌رسد.

با این حال، به طور پیش‌فرض، فرمول superellipse به این شکل کار نمی‌کند.

فرمول superellipse عبارت است از: x k + y k = 1 فرمول معکوس، x 1/k + y 1/k = 1 ، یک منحنی متقارن بصری ایجاد نمی‌کند.

برای مثال، با k برابر با 2 :

مقایسه منحنی‌های ابربیضی، که یک ابربیضی گرد (آبی)، یک ابربیضی قاشقی با فرمول متعارف (قرمز) و یک منحنی متقارن بصری (زرد) را نشان می‌دهد.
  • منحنی آبی نشان دهنده یک superellipse گرد ( y=x n ) است.
  • منحنی قرمز نشان دهنده یک superellipse scoop با فرمول متعارف ( y=x 1/n ) است.
  • منحنی زرد نشان دهنده منحنی‌ای است که از نظر بصری با منحنی آبی متقارن است ( y=1-(1-x) n ).

همانطور که نمودار نشان می‌دهد، شکل‌ها یکسان نیستند!

من بیشتر وارد جزئیات ریاضی آن نمی‌شوم، اما این موضوع به هنجارهای دوگانه و نحوه درک ما از انحنا مربوط می‌شود.

از نظر مشخصات و پیاده‌سازی، ما در اینجا چیزی بصری را نمایش می‌دهیم، بنابراین هنگام محاسبه اشکال مقعر از معادل‌های متقارن استفاده می‌کنیم. بقیه محاسبات روی اشکال محدب ( k>=1 یا مقادیر مثبت ابربیضی) انجام می‌شود.

فرمول فرم بسته

چالش بعدی نمایش منحنی یا محیط superellipse به شکل بسته است، فرمولی که از عملیات حسابی ساده ساخته شده است. این برای عملکرد ضروری است، که به سیستم اجازه می‌دهد رندر superellipse را به موتور گرافیکی تحویل دهد.

موتورهای گرافیکی مانند Skia با منحنی‌های Bezier آشنا هستند، بنابراین نمایش یک superellipse با تعداد کمی منحنی Bezier که محیط آن را تقریب می‌زنند، رندر کردن یک منحنی superellipse کارآمدتر می‌کند.

خوشبختانه، با استفاده از رگرسیون نمادین ، می‌توانیم فرمولی پیدا کنیم که نیمی از یک گوشه محدب را به عنوان یک منحنی بزیر مکعبی واحد نشان دهد.

یک منحنی بزیه مکعبی چهار نقطه دارد:

  • نقطه اول ( 0, 1 ) است.
  • آخرین نقطه، نیم‌گوشه واقعی ابربیضی است: 0.5 1/k ,0.5 1/k .
  • اولین نقطه کنترل در همان سطح نقطه شروع امتداد می‌یابد: ( a, 1 ).
  • نقطه کنترل دوم، قطر نیم گوشه است: (0.5 1/k - b,0.5 1/k + b) .

مقدار نیم‌گوشه که در اینجا استفاده شده، یک مختصات بسیار مهم است که قرار است در محاسبات بعدی از آن استفاده کنیم.

که در آن a و b با استفاده از رگرسیون نمادین از k محاسبه می‌شوند.

تصویرسازی نقاط کنترل نگاشت شده بر روی یک منحنی.
برای نمایش، به این Codepen مراجعه کنید.

محاسبه این چهار نقطه و رسم منحنی بزیر مکعبی بین آنها، یک نیم گوشه محدب بسته با k داده شده را فراهم می‌کند. سپس می‌توانیم نتایج را بچرخانیم تا بقیه گوشه را پر کند، به گوشه‌های دیگر اعمال کنیم و آنها را برعکس کنیم تا معادل‌های مقعر رسم شوند.

بدون اینکه بیشتر وارد جزئیات ریاضی شویم، فرمول محاسبه a و b به این صورت است:

p0 = 1.2430920942724248
p1 = 2.010479023614843
p2 = 0.32922901179443753
p3 = 0.2823023142212073
p4 = 1.3473704261055421
p5 = 2.9149468637949814
p6 = 0.9106507102917086

s = log2(k)
slope = p0 + (p6 - p0) * 0.5 * (1 + tanh(p5 * (s - p1)))
base = 1 / (1 + exp(slope * p1))
logistic = 1 / (1 + exp(slope * (p1 - s)))

a = (logistic - base) / (1 - base)
b =  p2 * exp(-p[3] * (s ^ p4))

حاشیه‌ها و سایه‌ها

علاوه بر محاسبه مسیر محیط گوشه، سیستم همچنین محاسبه می‌کند که وقتی به سمت داخل (یک حاشیه یا یک box-shadow درج شده) یا به سمت خارج (یک outline یا یک box-shadow معمولی) جابجا می‌شود، چگونه به نظر می‌رسد. در کتابخانه‌های گرافیکی مرسوم، این کار با کشیدن خطوط انجام می‌شود.

با این حال، حاشیه‌ها و سایه‌ها در CSS ویژگی‌های رندرینگی دارند که با Stroke متفاوت است:

  • مرزها یکنواخت نیستند.
  • برای مثال، حاشیه بالا می‌تواند ۱۰ پیکسل و حاشیه سمت راست ۵ پیکسل باشد، و گوشه بین آنها قرار گیرد.
  • علاوه بر این، آنها به جای اینکه به هر دو طرف بروند، به سمت داخل می‌روند.
  • سایه‌ها و خطوط بیرونی دقیقاً مانند یک ضربه قلم مو رندر نمی‌شوند.
  • در عوض، آنها طوری تنظیم می‌شوند که گوشه‌ها تیز به نظر برسند.

در حالی که مسیر معمول رندر حاشیه و سایه برای مقادیر corner-shape که گرد یا محدب‌تر از آن هستند (مثلاً squircle ) به خوبی کار می‌کند، و می‌تواند برای شکل‌هایی که مقعرتر از یک scoop هستند، ۹۰ درجه بچرخد، این پیش‌فرض برای مقادیر corner-shape بین -۱ و ۱ کار نمی‌کند، زیرا جابجایی حاشیه یا سایه به موازات لبه، گوشه‌ای ایجاد می‌کند که به نظر می‌رسد عرض ناهمواری دارد.

برای مثال، گرفتن یک گوشه bevel و جابجا کردن حاشیه به اندازه چند پیکسل به هر دو طرف، یک جلوه "شکم" ایجاد می‌کند، که در آن وسط گوشه پهن‌تر از کناره‌ها به نظر می‌رسد.

برای در نظر گرفتن این موضوع، هدف ایجاد جلوه‌ای است که مانند یک ضربه عمل کند - نقطه نرمال منحنی گوشه را در ابتدا پیدا کنید و طول آن را به اندازه عرض border یا shadow-spread تنظیم کنید.

خوشبختانه، این فقط برای زیربیضی‌ها (بین bevel و round) لازم است، زیرا هایپربیضی‌هایی مانند squircle همانطور که انتظار می‌رود کار می‌کنند.

برای یافتن نرمال یک منحنی زیربیضی، کافی است نرمال منحنی درجه دوم متناظر آن را پیدا کنیم، زیرا زیربیضی‌ها و معادل‌های منحنی درجه دوم آنها نزدیک به یکدیگر هستند.

با استفاده از همان نیم‌گوشه محاسبه‌شده قبلی، می‌توانید یک منحنی درجه دوم با همان نقطه میانی پیدا کنید، نقطه کنترل درجه دوم آن را استخراج کنید و از آنجا محاسبه نرمال ساده است.

خط نرمال با همان طول border-width یا shadow-spread ادامه می‌یابد و سپس منحنی حاصل را با لبه‌ها (لبه داخلی برای حاشیه، لبه بیرونی برای سایه) برش می‌دهد تا یک مسیر پیوسته ایجاد شود.

تصویرسازی یک گوشه با حاشیه، که نشان می‌دهد چگونه خط عمود برای تعریف شکل حاشیه امتداد یافته است.
این مثال را در CodePen ببینید.

روش‌های ریاضی دقیق‌تری برای محاسبه‌ی مماس برای یک superellipse وجود دارد، اما این روش کارآمد است و نتایج مناسبی برای رندر کردن حاشیه‌ها و سایه‌ها ارائه می‌دهد.

رنگ‌ها به هم می‌پیوندند

یک بخش جالب از نقاشی که در مرورگرها اتفاق می‌افتد، در CSS نامشخص است. این نقاشی، حاشیه‌هایی را رندر می‌کند که رنگ‌ها یا سبک‌های غیریکنواختی دارند. به عنوان مثال، جایی که عنصر شما دارای یک حاشیه بالایی سبز رنگ و یک حاشیه سمت راست زرد رنگ است. در این موارد، خط برش ، یک خط برش است که بین گوشه مربوطه از لبه حاشیه و گوشه مربوطه از لبه padding قرار می‌گیرد. این خط، مرز بین لبه‌های مجاور را ایجاد می‌کند. اگرچه مشخص نشده است، اما رندر آن تا حدودی بین مرورگرها سازگار است.

نحوه‌ی پیاده‌سازی این قابلیت در Blink (و در سایر مرورگرها) به شرح زیر است. لبه‌ای که قرار است رنگ‌آمیزی شود، مانند یک چندضلعی که از روی فارسی عبور می‌کند، به صورت خام برش داده می‌شود و به گونه‌ای محاسبه می‌شود که شامل لبه‌ی مربوطه باشد، اما هیچ یک از لبه‌های دیگر را شامل نشود. این کار از خونریزی و رنگ‌آمیزی یکی از لبه‌های دیگر با سبک و رنگ نادرست جلوگیری می‌کند.

محاسبه این چندضلعی تاکنون نسبتاً ساده بود، زیرا با گوشه‌های گرد منظم، نواحی گوشه هرگز نمی‌توانند همپوشانی داشته باشند. با این حال، این موضوع با بیضی‌های زیرین و به طور خاص با بیضی‌های فوق مقعر (مقادیر منفی superellipse ) تغییر می‌کند. این‌ها می‌توانند اشکال بسیار جالبی ایجاد کنند که چندضلعی‌های تقاطع ساده را بسیار مستعد همپوشانی و "خونریزی" می‌کند.

سی‌اس‌اس زیر را در نظر بگیرید:

.weird {
  width: 200px;
  height: 200px;
  corner-shape: scoop round;
  border-radius: 80% 20% / 50% 50%;
  border-width: 10px;
  border-color: orange purple black blue;
  border-style: solid dotted;
}
نمونه‌ای از یک عنصر CSS با حاشیه‌های غیریکنواخت، که لبه‌های نارنجی، بنفش-نقطه‌ای، مشکی و آبی-نقطه‌ای را نشان می‌دهد.

ما می‌خواهیم هر لبه (نارنجی، بنفش-نقطه‌ای، مشکی، آبی-نقطه‌ای) را جداگانه برش دهیم و سپس مسیر را رسم کنیم.

برای رسیدن به این هدف بدون همپوشانی با هر یک از سه گوشه دیگر، برش دقیق ضروری است.

برای مثال، لبه نارنجی (بالا) را در نظر بگیرید.

پیدا کردن یک چندضلعی دقیق که کل آن لبه را شامل شود و به لبه‌های بنفش، زرد یا حتی سیاه نفوذ نکند، دشوار است. برخی از اشکال دیگر چالش برانگیزتر هستند.

این فرآیند شامل سه کلیپس است.

اولین کلیپ شامل کل لبه، به همراه گوشه کامل (بدون فارسی بر) است. برای مثال:

شکلی با گوشه‌های بریده‌شده که نمایانگر دو گوشه (یکی به شکل قاشق و یکی به شکل دایره) است.

این شامل دو گوشه (یکی scoop و یکی گرد) است که لبه‌های بسیار کمی بین آنها وجود دارد و در انتها به هم متصل شده‌اند.

شروع از این شکل، همپوشانی با لبه مقابل را از بین می‌برد و اکنون فقط دو تیغه فارسی باقی می‌مانند.

این امر با برش دادن از این گوشه، یک چندضلعی که از بین گوشه‌های border-edge و padding-edge عبور می‌کند و در لحظه‌ای که قرار است با لبه تلاقی کند، متوقف می‌شود، حاصل می‌شود:

نمایشی از مناطقی که باید کوتاه شوند.

سیستم نقطه‌ای را پیدا می‌کند که در آن خطی از لبه مرزی تا لبه بالشتک، با مماس منحنی از نقطه شروع مربوطه (در صورت مقعر بودن منحنی) تلاقی می‌کند.

اگر آن نقطه درون ناحیه رندر شده باشد، فرآیند در آنجا متوقف می‌شود و در امتداد آن مماس ادامه می‌یابد تا دوباره به کادر مرزی برسد و یک چهارضلعی را تکمیل کند.

در غیر این صورت، یک مثلث ساده را می‌توان قیچی کرد.

خلاصه

این پلتفرم وب، قدرت بیان قابل توجهی را در اختیار طراحان و توسعه‌دهندگان وب قرار می‌دهد. گاهی اوقات یک ویژگی CSS که یک مقدار عددی واحد می‌گیرد، پیچیدگی قابل توجهی را در زیر کاپوت پنهان می‌کند تا رندر آن به طور دقیق و مداوم انجام شود.

ویژگی corner-shape پیچیدگی شگفت‌انگیزی داشت. هدف این مستندات کمک به توسعه‌دهندگان آینده‌ای است که روی این ویژگی، در Blink، مرورگرهای دیگر یا مشخصات آن کار می‌کنند.