Аффинные преобразования в Web приложениях

Автор: | 05.02.2018

Введение
2D преобразования в элементе canvas
Матричные преобразования
Архитектура WebGL приложения
WebGL приложение управления положением рупорной антенны
Контрольные задания

Введение

В статье описываются особенности создания графических приложений для WEB, связанных с аффинными преобразованиями. В Web приложениях элементы интерфейса описывается в HTML файлах, а логика приложения – в коде javascript (JS).

При создании программных систем стремятся, чтобы описание данных, по возможности, было отделено от описания управляющих процедур. Это позволяет легко структурировать данные. Отделение данных от управляющих процедур обусловило создание новых языков, обеспечивающих описание данных и управление ими.

HTML (HyperText Markup Language) – язык разметки гипертекста для описания структуры Web-страницы. Основным компонентом HTML является тег (tag) – код, который командует Web-браузеру выполнить определенную задачу типа создания абзаца или вставки изображения. HTML не является языком программирования, но для организации динамических Web-страниц в него можно включать программы на языке Javascript, в пределах тегов <script> и </script>.

Основным инструментом работы и динамических изменений на HTML странице является DOM (Document Object Model) – объектная модель. Согласно DOM-модели,  объекты, описываемые в пределах HTML-тегов, структурированы иерархическим способом по мере вложенности один в другой. Ссылку на объекты можно обнаружить  в DOM с помощью JavaScript кода.

В HTML5 определен элемент <canvas> как «растровый холст, который может быть использован для отображения  2D графики.  Каждый холст имеет контекст рисования, для которого определены методы и свойства рисования. Объект canvas позволяет также получить WebGL контекст,  который обеспечивает 3D графику, используя возможности библиотеки OpenGL.

2D преобразования в элементе canvas

Ниже приведен простейший код приложения, который обеспечивает 2D-преобразования и рисование прямоугольника в Web-браузере (рис.1). В файле index.html на языке HTML определен элемент canvas – растровый холст на Web странице (прямоугольная двумерная сетка). Измерения пространственной области элемента canvas по ширине и высоте задаются в пикселах в так называемой оконной системе координат (рис.2). Верхний левый угол области Canvas имеет координаты x=0, y=0, ось X направлена вправо, ось X вниз.

Файл index.html

<html> 
     <head> 
         <script src="draw.js" type="text/javascript"> </script>
     </head> 
     <body onload="draw_b();"> 
  <canvas  id="b"  width="100" height="100" >  </canvas>
     </body> 
 </html>

Файл draw.js

var fi = 45.0;
fi = fi * Math.PI / 180.0;
var a = Math.cos (fi);        // a  d  0 /
var b = Math.sin (fi);        // b  e  0 /
var d = -Math.sin (fi);       // c  f  1 /
var e = Math.cos (fi);
var c = 10.0;
var f = 25.0;
function draw_b() {
  var b_canvas = document.getElementById("b");
  var b_context = b_canvas.getContext("2d");
  b_context.fillStyle = "#0000FF";
  b_context.fillRect(0, 0, b_canvas.width, b_canvas.height);
  b_context.fillStyle = "#FFFF00";
  //b_context.translate(c,f);
  //b_context.rotate(fi);
  //b_context.setTransform(a, b, d, e, 0, 0);
  //b_context.setTransform(1, 0, 0, 1, c, f);
  //b_context.setTransform(a, b, d, e, c, f); 
  //b_context.setTransform(a, b, d, e, c*a+f*b, c*d+f*e);
  b_context.fillRect(0, 0, 25, 10);
}

В файле draw.js на языке JavaScript получаем доступ по атрибуту id к элементу canvas и затем к объекту context, где определены методы преобразования пространства и свойства рисования. Вызов метода fillRect() рисует прямоугольник и заполняет его текущим цветом заливки. Прямоугольник задается левым верхним углом (0, 50), шириной (25) и высотой (10) в направлении слева направо и сверху вниз. Функции, которые обеспечивают геометрические преобразования, в программе закомментированы.

Взаимосвязь между  JS-кодом и  HTML -элементами реализуется через идентификаторы. В приложении описан элемент  canvas с идентификатором id=»b» и свойствами width=»100″ height=»100″. В JS коде доступ к этому элементу получаем через обращение document.getElementById(«b»).  Объект document находится в верхушке иерархии объектной модели. Через него получаем доступ к входящим объектам, их свойствам и методам.

Для тестирования приложения, которое состоит из модулей кода на языках HTML и JavaScript(JS), удобно использовать online редактор CodePen. При запуске его в WEB-браузере открываются 3 окна для ввода кода на языках HTML, CSS и JS, а внизу размещается окно для отображения результата.

Окно для CSS кода уменьшено, т.к. его нет в рассматриваемом приложении.

 

Ниже приведены результаты тестовых запусков программы с различными сочетаниями функций преобразования.

Из рисунка 3 видим, что положительное направление вращения по часовой стрелке. Результаты запуска приложения с подключением функций rotate и translate (рис.3-6) показывают, что конечный результат элементарных преобразований зависит не только от значений параметров функций (они в обоих случаях одинаковы), но и последовательности их использования. На рис. 7-8 получены соответственно аналогичные результаты для функции setTransform. Она объединяет элементарные преобразования в матрице.

Судя из рисунков 3-6 после выполнения каждой из функций (rotate или translate) каждое последующее преобразование происходит относительно новой (текущей) системы координат.

Из рисунков 7-8 мы пока ничего не можем сказать, какая система координат будет активна после выполнения функции setTransform. Для определения этого был выполнен тестовый пример. После функции setTransform вызываем функцию translate (25 0). Результат (рис.9-10) показывает, что прямоугольник передвигается в направлении вдоль оси X текущей системы координат.

Матричные преобразования

Многие веб-разработчики игнорируют матрицу аффинных преобразований, полагая её слишком сложной для понимания и используя взамен простейшие функции для трансформации типа rotate и  translate. И совершенно зря, матрица преобразований обладает широкими возможностями, вдобавок, в том или ином виде поддерживаются всеми браузерами, а значит, её применение даёт кроссбраузерный код. Сама матрица имеет размер 3х3 и в общем виде записывается так:

Роль каждого коэффициента матрицы становится понятна из следующего. Рассмотрим схему и  уравнения для пересчета координат точки в новой СК [4].

В матричном представлении уравнения запишутся так:

Обобщим формулы преобразований движения к формулам аффинного преобразования плоскости:

Однородные координаты позволяют описать преобразования матрицей размером:


Обратите внимание, что около синуса угла изменился знак, поскольку вывод системы уравнений выполнялся для системы координат, ось Y которой направлена вверх, а положительный угол против часовой стрелки.

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

Результирующая матрица, полученная произведением двух исходных матриц преобразования, представляет собой совмещение элементарных преобразований. Независимо от количества элементарных преобразований в последовательности, можно всегда произвести совмещение так, чтобы только одна матрица 3×3 представляла всю последовательность преобразований.

Еще больший эффект использования матрицы аффинных преобразований можно получить взамен  простейших функций для трансформации при создании 3D-графики на Web-странице. Переход из одной прямолинейной координатной системы к другой описывается в общем случае системой уравнений с соответствующим матричным представлением:

Архитектура WebGL приложения

WebGL (Web-based Graphics Library) – технология, которая обеспечивает 3D-графику в браузере. Три кита лежат в основе WebGL технологии – HTML,  JavaScript и  OpenGL.Для создания WebGL-приложения на базе WebGL API необходимо:

  1. Создать на HTML5 элемент canvas.
  2. Получить контекст элемента canvas.
  3. Инициализировать рабочую область отображения графики.
  4. Инициализировать шейдер с параметрами.
  5. Создать буфер данных вершин модели.
  6. Создать матрицу преобразования вершин модели на экран.
  7. Нарисовать графику.

Ниже приведен фрагмент (по пунктам 5 и 6) простейшего WebGL приложения, который обеспечивает рисование белого прямоугольника на черном фоне (рис.12). СК для WebGL приведена на рисунке 13.

Фрагмент приложения simpleWebGL

function initMatrices()
{
   modelViewMatrix = new Float32Array(
     [1, 0, 0, 0,
      0, 1, 0, 0, 
      0, 0, 1, 0, 
      0, 0, -5, 1]);
   projectionMatrix = new Float32Array(
     [1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 0, 0, 
      0, 0, 0, 1]);  
}
 
function createModel(gl) {
       var vertexBuffer;
       vertexBuffer = gl.createBuffer();
       gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
       var verts = [                      //              |Y
                    .5, .5, 0.0,          //1          2  |  1
                    -.5, .5, 0.0,         //2        -----|-----X
                    .5, -.5, 0.0,         //3          4  |  3
                    -.5, -.5, 0.0         //4             |
                    ];
       gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
       var square = {buffer:vertexBuffer, vertSize:3, nVerts:4, primtype:gl.TRIANGLE_STRIP};
        return square;
    }

WebGL приложение управления положением рупорной антенны

Ниже рассмотрена техника использования матрицы аффинных преобразований при создании 3D-графики на Web-странице (рис.14) на примере выполнения геометрических преобразований рупорной антенны  через управление изменением любого из 5 параметров ее положения (рис.15) перемещением  мышки ( событие «mousemove»). В программе, описанной ниже, при перемещении мышки меняются 2 угла  – альфа и бета.

Через OpenGL предоставляется возможность описать модель в глобальной системе координат XYZ. Результирующая матрица преобразований (рис.16) от начального положения антенны,  при котором СК антенны совпадает с положением глобальной СК (рис.17), до ее конечного положения в пространстве (см. рис.15) определяется через последовательность перемножения  матриц элементарных преобразований (см. табл.). Последовательность преобразований  реализуется с учетом того, что всегда активна глобальная система координат.

 

Ниже приведен кода WebGL приложения, который обеспечивает визуализацию и управления положением рупорной антенны (см. рис.14).

Приложение Rupor

Файл index.html

<html>
<head>
<script src="draw.js" type="text/javascript"> </script>
</head>
<body onload="onLoad();">
<canvas id="webgl"   width="500" height="500"></canvas>
<script id="shader-fs" type="x-shader/x-fragment">
precision mediump float;
varying vec4 vColor;
void main(void) {
gl_FragColor = vColor;
}
</script>
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 vertexPos;
attribute vec4 aVertexColor;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
varying vec4 vColor;
void main(void) {
gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPos, 1.0);
vColor = aVertexColor;
}
</script>
</body>
</html>

Файл draw.js

function onLoad() {
var canvas = document.getElementById("webgl");
var gl = initWebGL(canvas);
initViewport(gl, canvas);
initShader(gl);
createModel(gl);
drawScene();
canvas.addEventListener("mousemove", mouseMoveEvent, false);
canvas.addEventListener("mousedown", mouseDownEvent, false);
canvas.addEventListener("mouseup",   mouseUpEvent, false);
}
function initWebGL(canvas) {
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
return gl;
}
function initViewport(gl, canvas) {
gl.viewport(0, 0, canvas.width, canvas.height);
}
var shaderProgram, shaderVertexPositionAttribute, shaderVertexColorAttribute, shaderProjectionMatrixUniform, shaderModelViewMatrixUniform;

function initShader(gl) {
var fragmentShader = createShader(gl, "fragmentShaderSource");
var vertexShader = createShader(gl, "vertexShaderSource");
shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
shaderVertexPositionAttribute = gl.getAttribLocation(shaderProgram, "vertexPos");
gl.enableVertexAttribArray(shaderVertexPositionAttribute);
shaderVertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
gl.enableVertexAttribArray(shaderVertexColorAttribute);
shaderProjectionMatrixUniform = gl.getUniformLocation(shaderProgram, "projectionMatrix");
shaderModelViewMatrixUniform = gl.getUniformLocation(shaderProgram, "modelViewMatrix");
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
}
}
function createShader(gl, id) {
var shaderScript = document.getElementById(id);
if (!shaderScript) {
return null;
}
var str = "";
var k = shaderScript.firstChild;
while (k) {
if (k.nodeType == 3)
str += k.textContent;
k = k.nextSibling;
}
var shader;
if (shaderScript.type == "x-shader/x-fragment") {
shader = gl.createShader(gl.FRAGMENT_SHADER);
}
else if (shaderScript.type == "x-shader/x-vertex") {
shader = gl.createShader(gl.VERTEX_SHADER);
}
else {return null;}
gl.shaderSource(shader, str);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
var rightface;
var colorsrightface;
var downface;
var colorsdownface;
var leftface;
var colorsleftface;
var upperface;
var colorsupperface;

//antena v10-----v9
//     v6------v5 |
//  v1--|-----v0| |
//  |   |     | | v12
//  |  v7-----|-v8
//  |         |
//  v2--------v3

function createModel(gl) { 
  // модель поверхности справа
       var vertexBuffer1 = gl.createBuffer();
       gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer1);
       var vertices = [ 0.40, 0.20, 0.0,  // v0
                     0.40, -0.20, 0.0,    // v3
                  0.20 ,0.10, -0.40,    // v5
                  0.20, -0.10, -0.40,   // v8
                  0.20, 0.10, -0.80,    // v9
                  0.20, -0.10, -0.80    // v12
                    ];
       gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
      rightface = {buffer: vertexBuffer1, vertSize:3, nVerts:6, primtype:gl.TRIANGLE_STRIP};
colorsrightface = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, colorsrightface);
        var colors = []
        for (var i=0; i < 6; i++) {
          colors = colors.concat([1.0, 0.0, 0.0, 1.0]);
        }
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
       colorsrightface = {buffer:colorsrightface, vertSize:4, nVerts:6, primtype:gl.TRIANGLE_STRIP};
//  модель поверхности снизу
var vertexBuffer2 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer2);
var vertices = [-0.40,-0.20,0.0,   // v2
0.40,-0.20,0.0,      // v3
-0.20,-0.10,-0.40,    // v7
0.20,-0.10,-0.40,     // v8
-0.20,-0.10,-0.80,    // v11
0.20,-0.10,-0.80     // v12
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
downface = {buffer:vertexBuffer2, vertSize:3, nVerts:6, primtype:gl.TRIANGLE_STRIP};
colorsdownface = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorsdownface);
var colors = []
for (var i=0; i < 6; i++) {
colors = colors.concat([1.0, 1.0, 1.0, 1.0]);
}
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
colorsdownface = {buffer:colorsdownface, vertSize:4, nVerts:6, primtype:gl.TRIANGLE_STRIP};

//  модель поверхности слева
var vertexBuffer3 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer3);
var vertices = [ -0.40, 0.20, 0.0,   // v1
-0.40, -0.20, 0.0,     // v2
-0.20 ,0.10, -0.40,     // v6
-0.20, -0.10, -0.40,    // v7
-0.20, 0.10, -0.80,     // v10
-0.20, -0.10, -0.80     // v11
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
leftface = {buffer:vertexBuffer3, vertSize:3, nVerts:6, primtype:gl.TRIANGLE_STRIP};
colorsleftface = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorsleftface);
var colors = []
for (var i=0; i < 6; i++) {
colors = colors.concat([1.0, 0.0, 0.0, 1.0]);
}
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
colorsleftface = {buffer:colorsleftface, vertSize:4, nVerts:6, primtype:gl.TRIANGLE_STRIP};

//  модель поверхности сверху
var vertexBuffer4 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer4);
var vertices = [-0.40,0.20,0.0,       // v1
0.40,0.20,0.0,          // v0
-0.20,0.10,-0.40,        // v6
0.20,0.10,-0.40,        // v5
-0.20,0.10,-0.80,        // v10
0.20,0.10,-0.80         // v9
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
upperface = {buffer:vertexBuffer4, vertSize:3, nVerts:6, primtype:gl.TRIANGLE_STRIP};
colorsupperface = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorsupperface);
var colors = []
for (var i=0; i < 6; i++) {
colors = colors.concat([1.0, 1.0, 1.0, 1.0]);
}
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
colorsupperface = {buffer:colorsupperface, vertSize:4, nVerts:6, primtype:gl.TRIANGLE_STRIP};
}
function drawScene() {
gl.clearColor(0.8, 0.8, 1.0, 1.0);
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
initMatrices();
draw(gl, rightface, colorsrightface);
draw(gl, downface, colorsdownface);
draw(gl, leftface, colorsleftface);
draw(gl, upperface, colorsupperface);
}
var A = 45.0;
A = A * Math.PI / 180.0; 
var B = -45.0;
B = B * Math.PI / 180.0; 
var C = 45.0;
C = C * Math.PI / 180.0;
var h = 1.0;
var p = 0.0;
var lastX = 0 , lastY = 0;
var mouseState=false; 
function initMatrices()
{
   modelViewMatrix = new Float32Array(
     [Math.cos(C)*Math.cos(A),  Math.sin(C)*Math.cos(B), -Math.sin(A)*Math.cos(C), 0,
      -Math.sin(C)*Math.cos(A), Math.cos(C)*Math.cos(B), Math.sin(C)*Math.sin(A), 0,
      Math.cos(B)*Math.sin(A),      -Math.sin(B),        Math.cos(B)*Math.cos(A), 0,
      h*Math.cos(B)*Math.sin(A), -h*Math.sin(B)+p,    h*Math.cos(B)*Math.cos(A), 1]); 
   projectionMatrix = new Float32Array(
     [1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 0, 0, 
      0, 0, 0, 1]); 
}
function draw(gl, obj, colobj) {
gl.useProgram(shaderProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, obj.buffer);
gl.vertexAttribPointer(shaderVertexPositionAttribute, obj.vertSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, colobj.buffer);
gl.vertexAttribPointer(shaderVertexColorAttribute, colobj.vertSize, gl.FLOAT, false, 0, 0);
gl.uniformMatrix4fv(shaderProjectionMatrixUniform,false, projectionMatrix);
gl.uniformMatrix4fv(shaderModelViewMatrixUniform,false, modelViewMatrix);
gl.drawArrays(obj.primtype, 0, obj.nVerts);
}
function mouseMoveEvent(e) {
if (mouseState==true) {
A-=(lastX-e.pageX)*0.01;
B-=(lastY-e.pageY)*0.01;
lastX = e.pageX;
lastY = e.pageY;
drawScene();
}
}

function mouseDownEvent(e) {
mouseState = true;
lastX = e.pageX;
lastY = e.pageY;
} 
 function mouseUpEvent(e) {
mouseState = false;
}

Контрольные задания

  1. Запустить приложение simpleCanvas (см. код ниже). Приложение обеспечивает появление квадратиков в месте, указанном курсором мышки (рис.18).
  2. Модифицировать приложение, обеспечив появление квадратиков под углом 45 градусов (рис.19), используя функции rotate и translate. В качестве подсказки, посмотрите как решается задача поворота объекта относительно произвольной точки.
  3. Модифицировать приложение, обеспечив появление  квадратиков под углом 45 градусов (см. рис.19), используя функцию setTransform.
  4. Модифицировать приложение, обеспечив появление  квадратиков с пошаговым изменением угла при каждом click (рис.20).
  5. Модифицировать приложение, обеспечив вращение квадратика по часовой стрелке при перемещении курсора мышки (при нажатой клавише мышки) вправо и вращение квадратика против часовой стрелки при перемещении курсора мышки влево (рис.21).
  6. Курсор должен быть привязан к центру квадратика. Квадратик заменить окружностью (рис.22).

 

Приложение simpleCanvas

Файл index.html

<html> 
     <head> 
         <script src="draw.js" type="text/javascript"> </script>
     </head> 
     <body onload="init();"> 
  <canvas  id="b"  width="500" height="500" >  </canvas>
     </body> 
 </html>

Файл draw.js

var fi = -45.0;
fi = fi * Math.PI / 180.0;
var canvas;
var context;
var mouseX;
var mouseY;
function init(){
canvas = document.getElementById("b");
context = canvas.getContext("2d");
canvas.addEventListener("click", setMousePosition, false);
//canvas.addEventListener("mousemove", setMousePosition, false);
mouseX = 0;
mouseY = 0;
update();
}
function update() {
//context.clearRect(0, 0, canvas.width, canvas.height);
//context.rect(mouseX, mouseY, 100, 100,true);
context.rect(0, 0, 100, 100,true);
context.fill();
//requestAnimationFrame(update);
}

function setMousePosition(e) {
mouseX = e.clientX;
mouseY = e.clientY;
context.beginPath();
context.fillStyle = '#'+Math.random().toString(16).slice(-6);
// rotate 45 degrees clockwise
//context.rotate(fi);
//context.save();//сохраняет текущее состояние на вершине стека
context.translate(mouseX,mouseY);
context.rotate(fi);
update();
context.rotate(-fi);
context.translate(-mouseX,-mouseY);
//context.restore();//устанавливает состояние с вершины стека
}

7. Запустить приложение Rupor (см. рис.14).

8. Модифицировать его в приложение simpleWebGL (см. рис.12).

9. Обеспечить поворот квадрата вокруг оси Z на любой заданный через переменные угол (рис.23).

10. Обеспечить вращение квадрата вокруг оси Z при перемещении (события «mousedown» и «mousemove») мышки влево и вправо.

 

Автор: Николай Свирневский

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *