Canvas-трансформации доступным языком

    Доброго времени суток, хабравчане! В этой статье я подробно расскажу вам о трансформации и вращении в javascripte. Матрица трансформаций, на первый взгляд, штука непонятная и многие ею пользуются даже не осознавая, что она делает на самом деле, используя готовые значения из интернета. На MDC об этом рассказано скудненько, а информацию в английской Википедии тяжело назвать общедоступной. Постараемся разобраться в этом вместе.


    Translate


    Translate — самый лёгкий метод из трансформирующих. Он всего лишь сдвигает все адресованные пиксели на указанные значения: ctx.translate(x,y). Нюансы работы с ним мы рассмотрим подробнее ниже.

    Экспериментируем с translate



    Scale


    Scale, как и translate, принимает в качестве аргументов x и y — значения, на которые надо умножить соответствующую ось. К примеру, при ctx.scale(2,3) всё будет отрисовываться в два раза шире и в три раза выше. Указав X=-1 мы отзеркалим изображение налево, указав y=-1 мы отзеркалим изображение вверх.

    Экспериментируем со scale



    Rotate


    Rotate принимает в качестве параметра угол в радианах, на который надо повернуть изображение вокруг точки опори (pivot), заданной методом translate (по-умолчанию 0:0). Перевести градусы в радианы можно с помощью простой формулы, которая является основой метода Number.degree в LibCanvas:
    Number.prototype.degree = function () {
    	return this * Math.PI / 180;
    };
    (60).degree() // 60 градусов в радианах
    

    Экспериментируем с rotate


    Если мы хотим вращать какой-то объект, например, картинку, необходимо правильно взаимодействовать методами rotate и translate, иначе мы никогда не попадём картинкой в нужное место. Самый простой способ осью вращения выбрать центр картинки и отрисовывать её в координаты (-width/2, -height/2). К примеру, мы хотим развернуть картинку размерами 50х50, находящуюся на координатах 100:100. Указываем translate в координату 125:125 и отрисовываем картинку в координату -25:-25. Альтернатива — использовать LibCanvas и метод rotatedImage(или drawImage в ближайшем будущем) и не напрягаться.

    Вращаем картинку



    setTransform


    ctx.setTransform(m11, m12, m21, m22, dx, dy);
    

    Рассмотрим по очереди, за что отвечает каждый из аргументов.
     dx,  dy — повторяют метод translate, смещая картинку на соответствующие значения.
    m11, m22 — повторяют метод scale, изменяя размер отрысовываемых пикселей.
    m12, m21 — более интересные. Каждый пиксель (x,y) смещается на y*m21 пикселей вправо и на x*m12 пикселей вниз.Это значит, что при m21=1 каждая следующая строчка будет смещена на 1 пиксель вправо, относительно предыдущей.

    Экспериментируем с setTransform


    Метод transform действует точно также, но в отличии от setTransform не обнуляет каждый раз предыдущую трансформацию, а накладывается поверх неё. Что можно из этого получить?

    Изометрическая карта


    Давайте из тайловой 2D-карты с видом сверху сделаем изометрическую с помощью простой трансформации. Самый простая фигура изометрической проекции — это ромб, в котором горизонтальная диагональ в два раза больше вертикальной (он шире в два раза чем выше). Его можно сделать в три шага.

    Смещаем его вправо при помощи m21=1, поднимаем вверх при помощи m12=-0.5 и сплюскиваем при помощи m22=0.5 (вы можете по шагам повторить это в песочнице).
    Но это будет изометрическая проекция с углом 26,565° к горизонтали. Если мы хотим настоящую изометрическую проекцию с углом 30° — необходимо слегка сплюснуть его по ширине, изменив ширину ячейки по оси Х, что легко высчитать следующим методом:

    Мы видим, что ячейка — это ромб ABCD с центром в точке O. Угол BAO — и есть угол, который надо из 26.6 сделать 30. Сейчас AO=BD, то есть высота равнобедренного трехугольника BAD равна его основе. Нам необходимо этот трехугольник сделать равносторонним (чтобы угол BAD стал равен 60 градусам), то есть уменьшить высоту на какой-то коэфициент. Допустим, AO = BD = 1 попугай. Тогда AO должна быть равна sqrt(AB2-BO2) = sqrt(1-0.25) = 0.866 попугая. Вот этот коэфициент мы и используем в нашей матрице:
    ctx.setTransform(0.866, -0.5, 0.866, 0.5, 0, 0)
    

    Смотрим, какая у нас получилась карта



    Надеюсь, все понятно описала. Задавайте вопросы, предлагайте, будем вместе пополнять топик.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 65
    • 0
      Очень подробно и понятно. Что касается изометрики, не совсем мне кажется доступно описано.
      Но так и потянуло открывать редактор и пробывать различные трансформации.
      • +2
        исправила. как теперь?
      • +1
        Спасибо, очень познавательно. А про кривые безье будет статья?
        • +2
          Кривые Безье — это намного проще, на статью не тянет. Глянем на MDC интерфейс:
          bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

          Тут главное разобраться за какую точку отвечает каждый из аргументов. Вы можете использовать LibCanvas PathBuilder для экспериментов.



          Оранжевая точка — это точка, из которой мы начинаем рисовать кривую, её положение показывает где закончился путь перед этим, например от аргумента moveTo или последняя точка предыдущего вызова bezierCurveTo.

          Аргументы x, y отвечают за изменения положения жёлтой точки, которая указывает куда следует проложить кривую, аргументы cp1x, cp1y за изменения положения контрольной розовой точки, а cp2x, cp2y – контрольной голубой.
          • +1
            Извините за не энциклопедическое объяснение)
            • 0
              Спасибо, понятно. Просто я уж коль вы разбираете в геометрических преобразованиях, хотел бы поинтересоваться как построить параллельную кривую, кривой бизье. Заданную 3мя точками. Надеюсь понятно что я имею в виду (например железнодорожные шпалы).
              • 0
                Для того, чтобы построить параллельную кривую необходимо сдвинуть на определённое значение х или у. Главное — сохранить растояние между всеми точками фигуры. К примеру, если Вам нужно построить параллельную кривую справа от первичной, то необходимо все иксы увеличить на N.
                • 0
                  Можно, но таким образом мы получим не равноценные окончания отрезков. Ладно, без примера сложно объяснить. Там нужно вычислять новые точки. У меня есть идея, думал может еще, что подскажите. Спасибо. Будет интересно нарисую.
                  • +1
                    Мне интересно. Особенно про пункт «неполноценные окончания». Что вы имеете ввиду? Если так критично сделать клона кривой — почему бы ее на нарисовать в буфер, а потом из него — две кривых
                    • 0
                      Буфер не нужен. Мне скорее требуется не клона, а кривую, которая будет полностью параллельна. Завтра я напишу комментарий тут, где приведу пример, и решение свое. Сегодня просто не успею.
                      • 0
                        Насколько я понял из описания, Вам нужна эквидистанта.
                        • 0
                          И так, у нас есть кривая, построенная по трем точкам.

                          Если мы просто сдвинем, то у нас получиться, что то такое.

                          На параллельные путь, ЖД не очень похоже.
                          Во что мне бы хотелось видеть.

                          Красная и синяя кривые.

                          Теперь опишу алгоритм как я это получаю.
                          Проводим отрезки АК и ВК в 3 точку кривой бизье.
                          Перпендик. им я строю прямые a и b. На этих прямых откладываю 2 равных отрезка AC и BD.
                          Это и будут новые точки кривой бизье. Но как найти 3ю точку.

                          Я провожу 2 параллельные прямые DF || BK и CF || AK.
                          И на их пересечении я нахожу точку F. Какая и является искомой. Вот так, я строю.
                          Сам алгорим я ещё не проверил(до этого я искал по другому, но оказалось ошибачно), может это и не правильно, или может есть ещё какой то более простой способ, поэтому и спрашиваю.

                          И ещё. Так как я не знаю как построить параллельные(может есть алгорим). То провожу перпендикулярные прямые, не только к точкам B и A. Но и в точку К я опускаю 2 перпендикуляра как к одной прямой так и к другой.

                            • 0
                              Во-первых, у Вас на картинках изображена квадратичная кривая Безье, это на всякий случай.

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

                              Но теперь, зная название, Вам будет гораздо проще найти информацию по предметной области. У меня, например, нашлась вот такая забавная статья.
                              • 0
                                Тут мне подсказывают, что кривая Безье второго порядка является сегментом параболы. Также нужно заметить, что эквидистанта к параболе не является параболой.
                                • 0
                                  картинки не открываются
                                  • 0
                                    Да, знаю. Сам сервис не работает. В понедельник, залью на другой.
                                    • 0
                                      Все заработало)
                • +3
                  Приятно когда девушки пишут такие статьи. Спасибо, только начал вникать в Canvas, будет полезно.
                  • +3
                    К тому же девушки, которым 19 лет o_O
                    • +6
                      В 19 лет нельзя осознать элементарную геометрию? :)
                      • +4
                        Вы знаете много 19-ти летних девушек, пишуших на хабр про элементарную геометрию в джаваскрипте? :-)
                        • –3
                          это не элементарная геометрия – это Аффинное преобразование
                          • +2
                            Рядовой пользователь, пользующийся API, об этом знать не должен. Потому и нужно осозновать элементарную геометрию, а не афинные преобразования. Вот если бы операторы нужно было явно задавать, то тогда да.
                    • 0
                      неправильно дано определение параметрам m11,m12,m21,m22.
                      m11 = scaleX * cos(alpha),
                      m12 = scaleY * sin(alpha),
                      m21 = -scaleX * sin(alpha),
                      m12 = scaleY * cos(alpha),
                      где scaleX,scaleY — сжатие растяжение вдоль соответствующей оси, а alpha — угол поворота.
                      • 0
                        И на сколько это поможет не понимающим людям понять, что делает каждый из аргументов? Цель этой статьи ведь не в том, чтобы процитировать технические определения, а обьяснить доступным языком, что можно было без заминки использовать трансформации
                        • 0
                          эта матрица является обобщением методов scale, translate и rotate(произведением матриц) и мне кажется если люди поняли как работают эти методы по отдельности, то их не нужно вводить в заблуждение, а дать точное описание параметров
                          • 0
                            Точное описание параметров можно найти в любой «умной» статье про трансформации. Они дают мало понимания менее опытным программистам. Важно понять, как оно работает, а не заучить технические определения
                            • 0
                              Хочу обратить внимание — я не спорю, что желательно глубокое понимание принципов трансформации, более того, сам достаточно хорошо разбираюсь в этом, но такие детали в ЭТОЙ статье только запутают разбирающегося. Любой желающий, прочтя нашу ветку комментариев в легкостью найдет более точные, но менее очевидные описания.
                              • 0
                                Я так же не спорю. Просто я не понимаю, то описание которое дал автор, мне кажется оно не корректным. Хотя для новичков — это старт, чтобы понять предмет трансформации.
                                • +1
                                  спросите у новичков, насколько точное определение помогает им понять предмет трансформации
                                  • –1
                                    Вы зануда.

                                    Автор молодец, доступно все объяснила, я не думаю что эта статья предназначается для опытных программистов, разбирающихся в терминологии.
                                    • 0
                                      я не претендую на последнюю истину, я просто написал точное определение из учебника, мне так легче понимается.

                                      ЗЫ. Это напоминает мне холивар: нужно ли программисту знание математики.
                                      • НЛО прилетело и опубликовало эту надпись здесь
                                        • +1
                                          нуу. для поворота есть метод rotate
                                          • 0
                                            когда работаешь с canvas напрямую, помогаю только формулы, а не подбор коэффициентом
                                • НЛО прилетело и опубликовало эту надпись здесь
                                  • +1
                                    немножко иксы-игреки перепутаны.
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                    • –2
                                      то что я написал это полный вариант, матрица трансформации — это перемножение трех матриц: поворота, растяжения и параллельного переноса.
                                      • НЛО прилетело и опубликовало эту надпись здесь
                                        • 0
                                          я подправил только коэффициенты m11-m22. Я вам говорю про матрицу трансформации на плоскости. Она имеет размер 3х3, и да на плоскости существую только поворот, сжатие и параллельный перенос.
                                          • НЛО прилетело и опубликовало эту надпись здесь
                                            • 0
                                              забыл про наклон упомянуть, его обычно не относят к базовой трансформации, хотя в матрицы трансформации он присутствует, в коэффициентах m11-m22

                                              Каюсь привел не до конца верные формулы.
                                          • 0
                                            и я повторюсь для меня проще сухие формулы, я ни кого не заставляю ими пользоваться.
                                            • НЛО прилетело и опубликовало эту надпись здесь
                                          • 0
                                            только поворота и растяжения. для переноса нужно вводить третюю координату.
                                    • +2
                                      "… он шире в два раза чем выше"

                                      После прочтения вспомнилось:
                                      Любая девушка может доказать, что крокодил более зеленый чем широкий...)))
                                      • 0
                                        Эх, жаль в канвасе нет матричных преобразований цвета пикселей… Руками это делать как-то чертовски долго выходит.
                                        • 0
                                          В тестировании матрицы ввел ошибочные параметры — канва упала, и перестала реагировать даже на корректные данные. Помог F5.
                                          • 0
                                            А что вы ввели? Но вообще, я вечером оберну в try-catch.
                                            • 0
                                              context.setTransform
                                              • 0
                                                4й параметр = 0
                                          • 0
                                            в изометрии между тайлами белые точки. как это побороть?
                                            • +1
                                              Накладывать с нахлестом в треть пикселя:
                                              ctx.drawImage(img,
                                              	128*sprite, 0, 128, 128,
                                              	64*x, 64*y, 64, 64
                                              );
                                              // =>
                                              ctx.drawImage(img,
                                              	128*sprite, 0, 128, 128,
                                              	64*x, 64*y, 64.3, 64.3
                                              );
                                              
                                              • 0
                                                ну так сделайте это в примере
                                            • 0
                                              Где же Ваши собственные фотографии на этот раз?
                                              • +3
                                                А к чему тут мои фотки?)
                                                • –2
                                                  Как к чему? Применять матрицу преобразований, очень удобно было бы.
                                              • +2
                                                Я сейчас для развлечения и изучения canvas пишу игру Scorch, мне там понадобилась совмещать трансформации, крутить вокруг выбранной точки, для этого просто написал Matrix2d

                                                Очень удобно пользоваться:

                                                new Matrix2d().
                                                	Rotate(angle, rotationPoint).
                                                	FlipY(rotationPoint).
                                                	CreateTransform(context);
                                                


                                                function Matrix2d () {
                                                	var _matrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
                                                	
                                                	var CalcOffset = function (matrix) {
                                                		var x = matrix[0][2] * matrix[0][0] + matrix[1][2] * matrix[0][1];
                                                		var y = matrix[0][2] * matrix[1][0] + matrix[1][2] * matrix[1][1];
                                                		
                                                		matrix[0][2] -= x;
                                                		matrix[1][2] -= y;
                                                		
                                                		return matrix;
                                                	}
                                                	
                                                	var Multiply = function (matrix) {
                                                		var mulMatrix = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
                                                		
                                                		for (var x = 0; x < 3; x++) {
                                                			for (var y = 0; y < 3; y++) {
                                                				var sum = 0;
                                                				
                                                				for (var dim = 0; dim < 3; dim++) {
                                                					sum += _matrix[y][dim] * matrix[dim][x];
                                                				}
                                                				
                                                				mulMatrix[y][x] = sum;
                                                			}
                                                		}
                                                		
                                                		_matrix = mulMatrix;
                                                	}
                                                	
                                                	this.Rotate = function (angle, point) {
                                                		var sin = Math.sin(angle);
                                                		var cos = Math.cos(angle);
                                                		
                                                		Multiply(CalcOffset([
                                                			[cos, -sin, point.x],
                                                			[sin, cos, point.y],
                                                			[0, 0, 1]
                                                		]));
                                                		
                                                		return this;
                                                	}
                                                	
                                                	this.FlipY = function (point) {
                                                		Multiply(CalcOffset([
                                                			[-1, 0, point.x],
                                                			[0, 1, point.y],
                                                			[0, 0, 1]
                                                		]));
                                                		
                                                		return this;
                                                	}
                                                	
                                                	this.FlipX = function (point) {
                                                		Multiply(CalcOffset([
                                                			[1, 0, point.x],
                                                			[0, -1, point.y],
                                                			[0, 0, 1]
                                                		]));
                                                		
                                                		return this;
                                                	}
                                                	
                                                	this.FlipXY = function (point) {
                                                		Multiply(CalcOffset([
                                                			[-1, 0, point.x],
                                                			[0, -1, point.y],
                                                			[0, 0, 1]
                                                		]));
                                                		
                                                		return this;
                                                	}
                                                	
                                                	this.CreateTransform = function (context) {
                                                		context.transform(
                                                			_matrix[0][0],
                                                			_matrix[1][0],
                                                			_matrix[0][1],
                                                			_matrix[1][1],
                                                			_matrix[0][2],
                                                			_matrix[1][2]);
                                                		
                                                		return this;
                                                	}
                                                }
                                                
                                                • 0
                                                  sylvester.jcoglan.com/ видели?)
                                                  • 0
                                                    Видел, но только для перемножений матриц брать её не имеет смысла. У них для 2d используется матрица 2x2 — у меня 3x3, что позволяет заодно и смещения считать, крутить вокруг нужной точки, отражать вокруг нужной точки. Плюс возможность цепочки трансформаций. Добавление новой функциональности сводится к добавлению 9 строк кода и новой матрицы в них.
                                                    В моём случае поддержка и расширение легче и, самое главное, кода намного меньше.
                                                    Да и условия лицензии не надо исполнять ;-)
                                                • 0
                                                  «сплюскиваем» =)

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.