— Pixels Commander

[ In English, На русском ]

[:en]Control every pixel of your Web application flexibly keeping performance high with GLSL components[:ru]Больше гибкости и быстродействия в Web приложениях при помощи GLSL компонен[:]

[:en]5 years ago shaders transformed game graphics and became the technology behind all amazing VFX we see in computer games. Now they are ready to rock the Web. Shaders are little programs written in C-like language GLSL (OpenGL Shading Language) which are aimed for defining the state of vertices (vertex shaders) and pixels (fragment shaders) in OpenGL / WebGL context using math equations. GLSL compiles and runs at GPU achieving unprecedented performance for HTML/CSS world. Shaders are widely used in game development and 3d graphics apps providing unlimited abilities for implementing special effects and rendering techniques however for Web development GLSL is still underutilized despite wide browsers support. This article reviews real world shaders usage for Web UI development and provides some how-to`s on integrating GLSL component into your Web application.



First of all let`s get some clarity as for why shaders were introduced, how they look like and how do they work…

What are GLSL shaders?

With advance of videocards and raise of GPU power developers started to feel lack of customization capabilities for hardcoded OpenGL rendering algorithm. Shaders became programmable blocks of OpenGL rendering pipeline which allow to modify data coming through in order to implement special effects and customize rendering. They were added with OpenGL version 2.0 which was released in 2004 and was the biggest change to OpenGL specification since first one introduced in 1992.

As it was already mentioned there are two types of shaders: (vertex shaders) and fragment shaders. We see their position in a pipeline at a chart following.

Vertex shader executes first and modify geometry. After geometry is rasterized fragment shader is being executed for every fragment (pixel) of image defining it`s color. As Web UIs are mostly 2d we are not that interested in modifying geometry and will focus on fragment shaders.

Let`s have a step by step look at fragment shader which renders a circle:

 
1
float radius = 20.;
2
vec2 center = vec2(100., 100.);
3
4
void main() {
5
  float distanceToCenter = distance(gl_FragCoord.xy, center);
6
  float inCircle = float(distanceToCenter < radius);
7
  gl_FragColor = vec4(inCircle);
8
}

First we define constants for radius, center, then goes main function declaration which is a standard entry point for any shader. In the beginning of main we determine if current pixel is inside of circle. To achieve this let`s find distance from current pixel gl_FragCoord.xy to circle center (100, 100) and compare if it is less than radius. Thereafter inCircle going to be 1 if distance is less than radius and 0 if distance is greater. In last line we define color by assigning it`s value to a system variable (gl_FragColor). GLSL accepts RGBA as a color format, where every channel value is represented by number kept into range between 0 and 1. So when inCircle is equal to 0 we have 0 for all channels which means black pixel and vice versa if inCircle is 1 then we have a white pixel R=1, G=1, B=1, A=1.

Let`s break the barrier between you and GLSL. In the editor above try to change radius, center coordinates and the last line of main function to look like that gl_FragColor = vec4(.75, 1., .2, 1.0) * inCircle;.

This is how shaders work in a nutshell. Now we move on to a story about practical use case for GLSL web components.

Optimizing with CSS and it`s downsides

Once I was asked to optimize spinner in operation system we develop. Spinner lagged a lot when there were some processes running in background and pretty a lot of frames were missing. Of course having this kind of janks on spinner caused really bad user experience. I quickly figured out that component was implemented via spritesheet with background-position animated which caused component repaint on every frame.

Preloader with repaints

Obvious, that this kind of animation can not be fast (especially running on low-end devices many of our clients own) because new texture was painted and uploaded to GPU on every frame and we definitely should optimize it by changing the way animation is implemented. Basic approach would be to reimplement spinner animation using GPU accelerated CSS properties: scale / translate / skew / opacity). From these we can use opacity since every frame can be turned into separate GPU accelerated layer and then displayed / hidden by changing opacity. As a result we`ve got 150 div`s uploaded to GPU (with translateZ(0) hack) and displayed one by one by changing opacity from 0 to 1 and vice versa.

Not bad! We got rid of repaints and QA department confirmed performance improvement.

Few days later I was asked to apply the same optimization for similar spinner having another color and size. That moment it became clear that CSS solution is not flexible at all. We need to load one more spritesheet with correct size / color, and also we need to keep additionally 150 textures in video memory which is 150*120*120*4=8,64 mb based on spinner 150 frames and 120px dimensions multiplied by 4 bytes per pixel (32bit RGBA). If we will need one more size or color — we create third spritesheet and load 8.6 mb more to GPU and so on… Sounds like a problem which can be solved by using GLSL shader which provides way better flexibility.

GLSL implementation

Advantage of GLSL — ability to control every pixel of surface and wide parametrization abilities, and of course shaders run on GPU by default which means no hacks there. Let`s have a look at step by step implementation of such a spinner in GLSL to have a taste of water. To render such a spinner we need to draw an arc with dynamically changing angle and some rotation. For this we will need a bit of math. Let`s start with circle we already did by making this piece of code a bit more scalable.

 
1
float PI = 3.14159;
2
vec2 center = u_resolution * 0.5;
3
float radius = min(u_resolution.x, u_resolution.y) * .5;
4
5
float circle(vec2 coord, vec2 center, float radius) {
6
  float distanceToCenter = distance(coord, center);
7
  return step(distanceToCenter, radius);
8
}
9
10
void main() {
11
  vec2 coord = vec2(gl_FragCoord);
12
  float isFilled = circle(coord, center, radius);
13
  gl_FragColor = vec4(1. - isFilled);
14
}

Now we use uniform variable u_resolution sent from JS which reflects size of current WebGL context which makes it possible to calculate center of current surface and distance from center to boundaries in order to draw the biggest circle possible for a current canvas. Also we moved detection of is point in circle to a separate function and inverted colors by substracting isFilled from 1.

Further on this circle we should cut a sector defining start angle and end angle for it. Essentially here we should determine «If current point lies between angles A and B«. This is not that easy to determine as it looks like at the first sight. Pitfalls and solution are described well here, I saw more elegant solutions but they have more disadvantages than this one.

So we define two new functions — isAngleBetween and sector which calculates angle between X axis and line from center to current pixel and return 1 if this angle is between start and end angles of sector required. Now by multiplying result of circle and sector we`ve got an arc.

 
1
bool isAngleBetween(float target, float angle1, float angle2) {
2
  float startAngle = min(angle1, angle2);
3
  float endAngle = max(angle1, angle2);
4
5
  if (endAngle - startAngle < 0.1) {
6
    return false;
7
  }
8
9
  target = mod((360. + (mod(target, 360.))), 360.);
10
  startAngle = mod((3600000. + startAngle), 360.);
11
  endAngle = mod((3600000. + endAngle), 360.);
12
13
  if (startAngle < endAngle) return startAngle <= target && target <= endAngle;
14
  return startAngle <= target || target <= endAngle;
15
}
16
17
float sector(vec2 coord, vec2 center, float startAngle, float endAngle) {
18
  vec2 uvToCenter = coord - center;
19
  float angle = degrees(atan(uvToCenter.y, uvToCenter.x));
20
  if (isAngleBetween(angle, startAngle, endAngle)) {
21
    return 1.0;
22
  } else {
23
    return 0.;
24
  }
25
}
26
27
void main() {
28
  vec2 coord = vec2(gl_FragCoord);
29
  float isFilled = circle(coord, center, radius) * sector(coord, center, 0., 75.);
30
  gl_FragColor = vec4(1. - isFilled);
31
}

Next let`s cut inner radius of circle and move arc rendering to a separate function.

 
1
float arc(vec2 uv, vec2 center, float startAngle, float endAngle, float innerRadius, float outerRadius) {
2
  float result = 0.0;
3
  result = sector(uv, center, startAngle, endAngle) * circle(uv, center, outerRadius) * (1.0 - circle(uv, center, innerRadius));
4
  return result;
5
}
6
7
void main() {
8
  vec2 coord = vec2(gl_FragCoord);
9
  float width = 7.;
10
  float outerRadius = min(u_resolution.x, u_resolution.y) * .5;
11
  float innerRadius = outerRadius - width;
12
  float isFilled = arc(coord, center, 0., 75., innerRadius, outerRadius);
13
  gl_FragColor = vec4(1. - isFilled);
14
}

Arc is quite pixelated. This happens because step function sharply discard values below given. Let`s change it for smoothstep to get some antialiasing there.

 
1
float circle(vec2 coord, vec2 center, float radius) {
2
  float distanceToCenter = distance(coord, center);
3
//Before
4
//return step(distanceToCenter, radius);
5
//After
6
  return smoothstep(distanceToCenter - 2., distanceToCenter, radius);
7
}

Now we are about to start with implementing animation which really need us to keep eye on details. Spinner animation can be decomposed into two: arc edges movement and whole spinner rotation. Let`s begin with moving arc edges. As we see from original spinner:

  • Front edge accelerate during whole round.
  • At the moment when front edge ends it`s turn rear edge start moving immediately having maximum speed (same as front edge had) and then decelerates at the end of round.

These smooth acceleration / deceleration easings could be implemented by using sine function. Passing reminder from dividing current time by Pi and substrating Pi/2 as argument. In this way time will always be in the range from -Pi/2 to Pi/2.

Sine graph for spinner easings

Let`s keep time in variable named periodicTime. For front edge of arc we are interested in limting time in the range from -Pi/2 to 0, and for rear edge from 0 to Pi/2. Let`s implement this clamping of periodicTime using GLSL function clamp and save it to variables startX, endX which we pass to sine function. Also for negative sine values expected for startX according to graph we add 1 to avoid negative angles. Start / end angles in degrees we get by multiplying sine results by 360.

 
1
void main() {
2
  vec2 coord = vec2(gl_FragCoord);
3
  float width = 7.;
4
  float halfPI = PI * .5;
5
  float periodicTime = mod(u_time, PI) - halfPI;
6
7
  float outerRadius = min(u_resolution.x, u_resolution.y) * .5;
8
  float innerRadius = outerRadius - width;
9
  
10
  float startX = clamp(periodicTime, -halfPI, 0.);
11
  float endX = clamp(periodicTime, 0., halfPI);
12
  
13
  float angleVariation = sin(startX) + 1.;
14
  float endAngleVariation = sin(endX);
15
  
16
  float startAngle = 360. * angleVariation;
17
  float endAngle = 360. * endAngleVariation;
18
19
  float isFilled = arc(coord, center, - startAngle, - endAngle, innerRadius, outerRadius);
20
  gl_FragColor = vec4(1. - isFilled);
21
}

Wow. Spinner is almost there! Next we add a rotation. Rotation accelerates in the beginning of the round and breaks in the end which corresponds to sine graph in the range of -Pi/2 to Pi/2, however with two times longer period than we have for edges. Also let`s rotate spinner in order animation to start at top of round by substracting 90 degrees from startAngle/endAngle.

 
1
//main
2
float rotation = 180. * (sin(periodicTime) + 1.);
3
4
float startAngle = 360. * angleVariation + rotation - 90.;
5
float endAngle = 360. * endAngleVariation + rotation - 90.;
6
7
float isFilled = arc(coord, center, - startAngle, - endAngle, innerRadius, outerRadius);
8
gl_FragColor = vec4(1. - isFilled);

So what is left is to set correct color. We can use RGB color value (45,121,184) for this but should write rgb function which converts byte — length channels to channels in the range of 0 — 1. Also let`s make backgroundColor customizable without forgetting about colors inversion.

 
1
float width = 2.;
2
vec4 color = rgb(45., 121., 184.);
3
vec4 backgroundColor = vec4(1.);
4
float innerRadius = outerRadius - width;
5
float isFilled = arc(coord, center, - startAngle, - endAngle, innerRadius, outerRadius);
6
gl_FragColor = (backgroundColor - (backgroundColor - color) * isFilled);

Try to change value of width variable and arguments passed to rgb function, this will immediately lead to changes in spinner size and color. So, now we have customizable spinner animated with GPU which is already more than we can achieve with CSS however let`s go further in order to understand the power of GLSL to control every pixel flexibly keeping performance high.

Let`s make pixel`s color dependent on current angle of rotation and arc width dependent on distance to mouse pointer.

 
1
float distanceToMouse = distance(u_mouse, gl_FragCoord.xy) * 0.085;
2
3
float width = 50. - clamp(5. * distanceToMouse, 5., 45.); 
4
5
vec4 color = rgb(255. * (sin(periodicTime) + 1.), 60. * distanceToMouse / 2., 160.) * (radius / distance(gl_FragCoord.xy, center));
6
7
vec4 backgroundColor = vec4(0.);
8
9
float innerRadius = outerRadius - width;
10
float isFilled = arc(coord, center, - startAngle, - endAngle, innerRadius, outerRadius);
11
gl_FragColor = (backgroundColor - (backgroundColor - color) * isFilled);

Try moving mouse over spinner. This kind of things is not possible to implement with CSS.

Deploying to project

GLSL-Component was created as a solution for integration GLSL components into Web application. Just place your shader code between glsl-component tags.

Conclusion

Some components can be implemented using GLSL way more efficiently than they can be made with CSS. This is also promoted by wide support of WebGL across all modern browsers. Flexibility, control over every pixel, high performance of GLSL are advantages which may inspire you to contribute into developing GLSL components for Web applications. GLSL — is not only a good addition to your UI developer profile but also a great treatment against JavaScript fatigue.

What is next?

Reading The Book of Shaders may be a good start.[:ru]В свое время шейдеры перевернули мир игровой графики и теперь они готовы перевернуть Web. Шейдеры — небольшие программы на сиподобном языке GLSL (OpenGL Shading Language) которые позволяют описывать состояние вершин (вершинные шейдеры) или пикселов (пиксельные шейдеры) в контексте OpenGL (или WebGL) с помощью математических функций. GLSL компилируется и выполняется на GPU с беспрецендентной для HTML/CSS производительностью. Как правило шейдеры применяются для разработки игр и компьютерной графики, а использование их в UI компонентах незаслуженно обходится стороной. В данной статье рассматривается опыт использования GLSL при разработке Web приложений.


Для начала давайте получим некоторую ясность относительно того зачем нужны, как работают и выглядят шейдеры…

Что такое GLSL шейдеры?

С ростом производительности видеокарт разработчикам графических приложений перестало хватать стандартных возможностей OpenGL и появилась необходимость программно задавать алгоритм рендеринга. Шейдеры и стали программируемыми звеньями конвеера отрисовки OpenGL которые позволяют модифицировать проходящие через них данные с целью реализации специальных эффектов. Они были добавлены в OpenGL версии 2.0, который вышел в 2004 году став самым большим апдейтом OpenGL с момента утверждения первой спецификации в 1992 году.

Как уже говорилось шейдеры бывают двух видов — вершинные (vertex shader) и пиксельные (fragment shader). На рисунке мы видим их положение в конвеере.

Первым выполняется вершинный шейдер который позволяет менять геометрию и затем после ее растеризации для каждого фрагмента (пиксела) изображения будет выполнен пиксельный шейдер. Так как для преимущественно двухмерных UI работать с геометрией нам не особо интересно сфокусируем внимание на последних.

Рассмотрим пример пиксельного (fragment) шейдера описывающего круг:

 

1
float radius = 20.;
2
vec2 center = vec2(100., 100.);
3
4
void main() {
5
  float distanceToCenter = distance(gl_FragCoord.xy, center);
6
  float inCircle = float(distanceToCenter < radius);
7
  gl_FragColor = vec4(inCircle);
8
}

Как уже говорилось, данная программа выполняется на GPU для каждого пиксела WebGL контекста и определяет его цвет. Рассмотрим по шагам ее листинг… В начале мы определеям константы radius, center используемые программой, затем следует декларация функции main с которой начинается выполнение любого шейдера. В начале функции main находим лежит ли текущий пиксель в окружности. Для этого найдем расстояние от текущей точки экрана gl_FragCoord.xy до центра (100, 100) и сравним его с заданным радиусом. Соответственно inCircle будет 1 если расстояние меньше радиуса и 0 если расстояние больше и точка лежит за пределами круга. В последней строке определим значение цвета для текущего пиксела записывая цвет в системную переменную (gl_FragColor). Цвет в GLSL задается в формате RGBA, где значение для каждого канала лежит в пределах от 0 до 1. Таким образом при inCircle равном 0 мы будем иметь ноль по всем каналам цвета т.е. черный пиксель, и наоборот при inCircle равном 1 пиксел будет белым R=1, G=1, B=1, A=1.

Давайте разрушим барьер между вами и GLSL. В редакторе выше попробуйте изменить радиус и координаты центра а так же последнюю строку функции main на gl_FragColor = vec4(.75, 1., .2, 1.0) * inCircle;.

Это то как работают пиксельные шейдеры вкратце, а теперь перейдем к повествованию о решении практической задачи.

Оптимизация через CSS и ее недостатки

Однажды меня попросили заняться оптимизацией спиннера в нашей операционной системе. Спиннер заметно подергивался в моменты фоновых нагрузок и это создавало весьма неприятное ощущение. Быстро выяснилось, что компонент анимирован через spritesheet и смену background-position что вызывало перерисовку (repaint) содержимого на каждый кадр.

 

Preloader with repaints

Очевидно, что быстро это работать не может (тем более с учетом низкой производительности наших устройств) из-за загрузки новой текстуры в видео память при смене кадра и напрашивается оптимизация. Первое что приходит в голову — это избавиться от репэинта воспроизведя спиннер с использованием аппаратно ускоренных CSS свойств: scale / translate / skew / opacity (подробнее). Из перечисленных трансформаций подойдет opacity. Каждый кадр из spritesheet можно представить в виде отдельного проброшенного на GPU слоя и затем отображать поочередно эти слои изменяя их прозрачность. В результате получим 150 div`ов которые браузер отсылает как отдельные текстуры на GPU (благодаря использованию translateZ хака) и отображает эти слои циклически изменяя opacity с 0 на 1 и наоборот.

Что же, неплохо! Мы избавились от репэинтов и QA отдел подтвердил улучшение быстродействия.

Спустя несколько дней меня попросили сделать то же самое в другой части системы для спиннера другого цвета и другого размера. Тут начали очевидно проступать недостатки CSS решения. Во первых нам потребуется создать и загружать еще один spritesheet с соответствующим изображением, во вторых держать дополнительно 150 текстур — кадров в видеопамяти. Куда более гибким решением в этом случае могло бы быть использование GLSL шейдера.

GLSL реализация

Преимущество GLSL — возможность контроля над каждым пикселом поверхности и широкие возможности параметризации, не говоря уже про то что выполняются шейдеры на GPU по умолчанию. Давайте по шагам разберем создание спиннера аналогичного данному с помощью GLSL.

Можно представить, что для воспроизведения такого спиннера нам потребуется рисовать арку с динамически изменяющимся углом. Тут нам потребуется немного математики.

Начнем с окружности которую мы уже рассматривали в начале статьи и приведем ту программу в чуть более масштабируемый вид.

 

1
float PI = 3.14159;
2
vec2 center = u_resolution * 0.5;
3
float radius = min(u_resolution.x, u_resolution.y) * .5;
4
5
float circle(vec2 coord, vec2 center, float radius) {
6
  float distanceToCenter = distance(coord, center);
7
  return step(distanceToCenter, radius);
8
}
9
10
void main() {
11
  vec2 coord = vec2(gl_FragCoord);
12
  float isFilled = circle(coord, center, radius);
13
  gl_FragColor = vec4(1. - isFilled);
14
}

Теперь мы используем присланную из JS кода переменную u_resolution которая отражает размер текущего WebGL контекста. Это позволяет вычислить центр поверхности и расстояние от центра до края, таким образом нарисовать максимально большую окружность для текущего контекста. Так же вынесли в отдельную функцию circle проверку «входит ли координата в круг?» и инвертировали цвета отнимая isFilled от единицы.

Дальше на этой окружности нужно отсечь сектор задавая начальный и конечный угол. Тут мы решаем задачу «Лежит ли текущая точка между углами A и B?«. Задача не так проста как может показаться на первый взгляд и неплохо описана тут, конечно я встречал и более элегантные решения, но они имели больше недостатков чем это.

Итак мы вводим две новые функции — isAngleBetween и sector, определяющую угол от центра до текущей точки и возвращающую 1 если текущая точка лежит между начальным и конечным углом дуги. Перемножая выход функций circle и sector получим дугу.

 

1
bool isAngleBetween(float target, float angle1, float angle2) {
2
  float startAngle = min(angle1, angle2);
3
  float endAngle = max(angle1, angle2);
4
5
  if (endAngle - startAngle < 0.1) {
6
    return false;
7
  }
8
9
  target = mod((360. + (mod(target, 360.))), 360.);
10
  startAngle = mod((3600000. + startAngle), 360.);
11
  endAngle = mod((3600000. + endAngle), 360.);
12
13
  if (startAngle < endAngle) return startAngle <= target && target <= endAngle;
14
  return startAngle <= target || target <= endAngle;
15
}
16
17
float sector(vec2 coord, vec2 center, float startAngle, float endAngle) {
18
  vec2 uvToCenter = coord - center;
19
  float angle = degrees(atan(uvToCenter.y, uvToCenter.x));
20
  if (isAngleBetween(angle, startAngle, endAngle)) {
21
    return 1.0;
22
  } else {
23
    return 0.;
24
  }
25
}
26
27
void main() {
28
  vec2 coord = vec2(gl_FragCoord);
29
  float isFilled = circle(coord, center, radius) * sector(coord, center, 0., 75.);
30
  gl_FragColor = vec4(1. - isFilled);
31
}

Следующим шагом отсечем внутренний радиус и вынесем формирование арки в отдельную функцию.

1
float arc(vec2 uv, vec2 center, float startAngle, float endAngle, float innerRadius, float outerRadius) {
2
  float result = 0.0;
3
  result = sector(uv, center, startAngle, endAngle) * circle(uv, center, outerRadius) * (1.0 - circle(uv, center, innerRadius));
4
  return result;
5
}
6
7
void main() {
8
  vec2 coord = vec2(gl_FragCoord);
9
  float width = 7.;
10
  float outerRadius = min(u_resolution.x, u_resolution.y) * .5;
11
  float innerRadius = outerRadius - width;
12
  float isFilled = arc(coord, center, 0., 75., innerRadius, outerRadius);
13
  gl_FragColor = vec4(1. - isFilled);
14
}

Видим края нашей арки несколько пикселизованы, это происходит из-за применения оператора step который резко отсекает значения меньше заданного. Давайте заменим его на smoothstep что бы получить эффект сглаживания краев.

1
float circle(vec2 coord, vec2 center, float radius) {
2
  float distanceToCenter = distance(coord, center);
3
//Before
4
//return step(distanceToCenter, radius);
5
//After
6
  return smoothstep(distanceToCenter - 2., distanceToCenter, radius);
7
}

Перейдем к части требующей самой детальной проработки — анимации, которая опять же задается с помощью математических функций. Анимацию спиннера в примере можно декомпозировать на две: движение краев арки и вращение всего спиннера. Давайте сначала реализуем движение краев арки. Как видно по исходному спиннеру:

  • Передний край разгоняется на протяжении всего оборота.
  • В момент когда передний край завершает оборот, задний край начинает вращение сразу с максимальной скоростью, замедляясь к окончанию своего оборота.

Ускорение и замедление можно реализовать использованием функции синуса в пределе от -Pi/2 до Pi/2 передавая в виде X текущее время, точнее остаток от его деления на Pi отняв от него Pi/2. Таким образом время всегда будет в нужном промежутке от -Pi/2 до Pi/2.

 

Sine graph for spinner easings

Время храним в переменной periodicTime. Для переднего края нас будет интересовать синус на промежутке от -Pi/2 до 0, а для заднего края от 0 до Pi/2. Реализуем подобное отсечение periodicTime с помощью GLSL функции clamp (переменные startX, endX). Для отрицательных величин синуса добавим 1 и переведем startX/endX в градусы умножив на 360.

1
void main() {
2
  vec2 coord = vec2(gl_FragCoord);
3
  float width = 7.;
4
  float halfPI = PI * .5;
5
  float periodicTime = mod(u_time, PI) - halfPI;
6
7
  float outerRadius = min(u_resolution.x, u_resolution.y) * .5;
8
  float innerRadius = outerRadius - width;
9
  
10
  float startX = clamp(periodicTime, -halfPI, 0.);
11
  float endX = clamp(periodicTime, 0., halfPI);
12
  
13
  float angleVariation = sin(startX) + 1.;
14
  float endAngleVariation = sin(endX);
15
  
16
  float startAngle = 360. * angleVariation;
17
  float endAngle = 360. * endAngleVariation;
18
19
  float isFilled = arc(coord, center, - startAngle, - endAngle, innerRadius, outerRadius);
20
  gl_FragColor = vec4(1. - isFilled);
21
}

Вуаля. Спиннер почти готов! Добавим вращение. Оно ускоряется в начале периода и тормозит в конце, что соответствует графику синуса на периоде от -Pi/2 до Pi/2, однако с в два раза меньшей периодичностью. Так же перевернем спиннер на 90 градусов изменив формулу startAngle/endAngle что бы вращение начиналось в верхней части.

1
//main
2
float rotation = 180. * (sin(periodicTime) + 1.);
3
4
float startAngle = 360. * angleVariation + rotation - 90.;
5
float endAngle = 360. * endAngleVariation + rotation - 90.;
6
7
float isFilled = arc(coord, center, - startAngle, - endAngle, innerRadius, outerRadius);
8
gl_FragColor = vec4(1. - isFilled);

Осталось задать корректный цвет. Сделаем это через RGB код цвета — 45,121,184. Для чего добавим соответствующую функцию преобразования побайтного rgb в единичный вектор. Так же не забываем про инверсию цвета и сделаем backgroundColor изменяемым.

1
float width = 2.;
2
vec4 color = rgb(45., 121., 184.);
3
vec4 backgroundColor = vec4(1.);
4
float innerRadius = outerRadius - width;
5
float isFilled = arc(coord, center, - startAngle, - endAngle, innerRadius, outerRadius);
6
gl_FragColor = (backgroundColor - (backgroundColor - color) * isFilled);

Попробуйте поменять значение переменной width и аргументы отправляемые в функцию rgb, это моментально отразится на цвете спиннера. Итого у нас строящийся на GPU параметрически настраиваемый спиннер — это уже лучше чем можно было сделать через HTML/CSS. но не будем останавливаться на достигнутом… Попробуем лучше осознать тот факт, что GLSL в отличие от CSS позволяет управлять каждым пикселом при этом не теряя производительности.

Добавим зависимость цвета от угла вращения и зависимость толщины спиннера от расстояния до мыши.

 
1
float distanceToMouse = distance(u_mouse, gl_FragCoord.xy) * 0.085;
2
3
float width = 50. - clamp(5. * distanceToMouse, 5., 45.); 
4
5
vec4 color = rgb(255. * (sin(periodicTime) + 1.), 60. * distanceToMouse / 2., 160.) * (radius / distance(gl_FragCoord.xy, center));
6
7
vec4 backgroundColor = vec4(0.);
8
9
float innerRadius = outerRadius - width;
10
float isFilled = arc(coord, center, - startAngle, - endAngle, innerRadius, outerRadius);
11
gl_FragColor = (backgroundColor - (backgroundColor - color) * isFilled);

Поводите мышью по поверхности спиннера. У вас вряд ли получится реализовать это на CSS.

Размещение в проекте

GLSL-Component создавался специально для интеграции GLSL компонент в Web приложения. Просто поместите код шейдера между тегами glsl-component.

Заключение

Некоторые типы компонент можно эффективно реализовывать с помощью пиксельных шейдеров чему способствует широкая поддержка WebGL в современных браузерах. Гибкость, контроль над каждым пикселом, высокая скорость работы GLSL — это те приемущества которые могут вдохновить вас на вклад в развитие GLSL компонент для Web. GLSL — может быть не только существенным дополнением к вашему резюме разработчика UI, но и отличным лекарством от JavaScript fatigue.

Что дальше?

Хорошим стартом в изучении GLSL может стать чтение The Book of Shaders.[:]

0 comments
Submit comment