22 сентября 2010 в 04:34

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

HTML*
Доброго времени суток, хабравчане! В этой статье я подробно расскажу вам о трансформации и вращении в 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)

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



Надеюсь, все понятно описала. Задавайте вопросы, предлагайте, будем вместе пополнять топик.
Аня @Nutochka
карма
253,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (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
          только поворота и растяжения. для переноса нужно вводить третюю координату.
          • 0
            перенос это самая простая трансформация. Она имеется даже в R1.
  • +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
    «сплюскиваем» =)

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