Честный Free Transform


Представляю вашему вниманию код, реализующий свободную трансформацию растра! Результатом работы этого, в целом, несложного метода является искаженное изображение точь-в-точь как в старом добром фотошопе.



С чего все началось?

Как-то давно я фантазировал об инструменте Free Transform. Мысль была такая: что если наш Free Transform — это (по некой идее) не тупое тягание вершин в 2d, а некая перспективная проекция нашей картинки, раскоряченной в 3d пространстве, да так, что вот как раз и выходит неправильный четырехугольник с корректной деформацией изображения по всей площади? Мысль показалась вполне себе близкой к реальности и я начал чертить, прикидывать, считать, снова чертить. Но у меня не выходило ровным счетом ничего похожего на правду.

Ну и чем все закончилось?

После многочисленных попыток лишить себя сна поисками тех самых координат XYZ, от которых я уже возьму нужные мне T (читаем доки), я вдруг понял, что нельзя быть таким честным — нужно просто подбирать, прикидывать, снова подбирать и опять примерять формулы нахождения сразу коэффициентов T. Поглядывая в свои «честные попытки», наиболее близкие по сути, я так или иначе пришел к результату, который в оформленном виде выглядит примерно так:


function freeTransform(image:BitmapData, canvas:Graphics, p1:Point, p2:Point, p3:Point, p4:Point):void
{
        // Соотношение длин диагоналей.
        var diagonalRatio:Number = Point.distance(p1, p3) / Point.distance(p2, p4);
 
        // A, B и C параметры уравнения прямой для диагонали,
        // соединяющей точки p1 и p3.
        var a1:Number = p1.y - p3.y;
        var b1:Number = p3.x - p1.x;
        var c1:Number = p1.x * p3.y - p3.x * p1.y;
 
        // A, B и C параметры уравнения прямой для диагонали,
        // соединяющей точки p2 и p4.
        var a2:Number = p2.y - p4.y;
        var b2:Number = p4.x - p2.x;
        var c2:Number = p2.x * p4.y - p4.x * p2.y;
 
        // Точка пересечения диагоналей.
        var intersection:Point = new Point();
            intersection.x = -(c1 * b2 - c2 * b1) / (a1 * b2 - a2 * b1);
            intersection.y = -(a1 * c2 - a2 * c1) / (a1 * b2 - a2 * b1);
 
        // Коэффициенты T, с помощью которых мы достигаем
        // нужного эффекта.
        var t1:Number = 1 / Point.distance(p3, intersection) * diagonalRatio;
        var t2:Number = 1 / Point.distance(p4, intersection);
        var t3:Number = 1 / Point.distance(p1, intersection) * diagonalRatio;
        var t4:Number = 1 / Point.distance(p2, intersection);
 
        // Заготавливаем данные, необходимые для отрисовки
        // треугольников.
        var vertices:Vector.<Number> = new Vector.<Number>();
            vertices.push(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y);
        var indices:Vector.<int> = new Vector.<int>();
            indices.push(0, 1, 2, 2, 3, 0);
        var uvtdata:Vector.<Number> = new Vector.<Number>();
            uvtdata.push(0, 0, t1, 1, 0, t2, 1, 1, t3, 0, 1, t4);
 
        // Рисуем два треугольника, представляющие собой
        // конечный результат.
        canvas.beginBitmapFill(image, null, false, true);
        canvas.drawTriangles(vertices, indices, uvtdata);
        canvas.endFill();
}

Итак, что мы имеем. У нас есть метод, который хочет от нас следующие данные:

image:BitmapData
Это и есть то изображение, которое мы подвергаем трансформации. Если вы желаете трансформировать не изображение, сделайте это изображением. Метод draw() вам в помощь.

canvas:Graphics
Это наш холст. В него мы будем отрисовывать результат, который представлен всего двумя треугольниками. В качестве этого аргумента нужно передавать ссылку на свойство graphics вашего шейпа, спрайта и т.д. (myCanvasShape.graphics, myCanvasSprite.graphics).

p1:Point, p2:Point, p3:Point, p4:Point
А это, собственно, список контрольных точек от верхней левой и по часовой стрелке.

Полагаю, этого кода и описания к нему достаточно, чтобы вы смогли управиться с этим самостоятельно.

Вот так это все выглядит:



А компот?!

Да, да. Остается один момент, помню. Метод производит корректную трансформацию только если четыре контрольные точки представляют собой выпуклый многоугольник. В том же фотошопе поступили следующим образом: они позволяют двигать точку так, чтобы получился угол больше 180 градусов, но если ты пытаешься завершить на этом трансформацию, они вываливают тебе алерт, мол, «алёэ! ты как себе это представляешь?!».

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


function isConvex(p1:Point, p2:Point, p3:Point):Boolean
{
        var a:Point = new Point(p1.x - p2.x, p1.y - p2.y);
        var b:Point = new Point(p3.x - p2.x, p3.y - p2.y);
 
        return ((a.x * b.y - a.y * b.x) <= 0);
}

p1:Point, p2:Point, p3:Point
Если смотреть изнутри угла, эти три точки будут идти по часовой стрелке. Вершиной, соответственно, является точка p2. Метод возвращает true, если угол меньше 180 градусов, и false в противном случае.

С помощью этого метода следует проверить все 4 угла:


var convexTest1:Boolean = isConvex(p4, p1, p2);
var convexTest2:Boolean = isConvex(p1, p2, p3);
var convexTest3:Boolean = isConvex(p2, p3, p4);
var convexTest4:Boolean = isConvex(p3, p4, p1);

Эти четыре условия проверяют каждый угол четырехугольника p1-p2-p3-p4 на предмет «выпуклости», ну т.е. чтобы каждый угол был меньше 180 градусов (true).

Однако если противоположные углы p1 и p3 будут вогнуты оба, то это тоже допустимо! Ведь в контексте 4-угольника это просто означает, что рамка флипнулась по диагонали (которая проходит через p2 и p4). То же самое применимо и к углам p2 и p4.

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


if (convexTest1 == convexTest3 && convexTest2 == convexTest4)
{
        // вызываем наш метод freeTransform(…)
}
else
{
        // сообщаем пользователю, что он гонит!
}


Не верю!

Жалуются люди, говорят, мол, обманул я их про «точь-в-точь». Настаивают. Проверить религия не позволяет, но чуют они подвох всем сердцем своим. И трансформация какая-то не интуитивная, и на дисплейсмент больше похоже. Другое дело фотошоп, говорят, там таки по-настоящему, говорят.

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

  • +15

Комментарии (18)

0
drawTriangles
Из этого целые 3Д движки без stage 3d делают, а он всего-лишь картинку этим плющит :)
  • ADF
  • ADF
+2
Целый движок любой дурак сделает, а вот картинку сплющить! :)
+1
drawTriangles наше все!
0
А в Фотошопе ещё когда на Ентер жмёшь, картинка сглаживается )
0
Ну в смысле — у тебя не сглаженное изображение?
0
Сравнил с Фотошопом — у тебя лучше )
А картинка 1 в 1 масштабом вставлена, не уменьшал?

И вопрос по практическому применению — для каких целей?
0
В демке изображение имеет размер 640х480. Соответственно, в стартовой позиции оно уже несколько уменьшено.

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

В играх мне впервые пришлось использовать такой метод при создании уровней для моей игры :) в ней встречается длинная кладка камней по ломаной. По определенным соображениям они должны прилегать плотно. Также по дизайну допускается трапецевидная форма камней. Разумеется можно сделать это с помощью разбиения изображения на множество прямоугольников. Этот метод применим и, пожалуй, единственно возможен, когда мы делаем изгибание растра по сплайну (дорожка, речка, грунт в каком-то платформере и все такое). В моем случае лучший результат дает честный Free Transform.
0
В демке изображение имеет размер 640х480. Соответственно, в стартовой позиции оно уже несколько уменьшено.
Значит всё-таки схитрил мальца )
Нужно 1 к 1 с Шопом сравнивать.
0
Все досужие разговоры были исключительно о геометрии смещения, а тут все «точь-в-точь»! И никаких фокусов :) кости в молоке ищешь.
0
В геометрию я не лезу, потому что мало чего в этом понимаю, говорю лишь за конечный результат )

Я заскриншотил картинку с твоей флешки, вставил в фотошоп, потягал — вижу, что результат хуже, хвалю тебя )
А потом оказывается, что ты «скрыл лишние пиксели», предварительно уменьшив картинку = чувство обмана )
+1
Это ты зря :) увеличение с последующим уменьшением — нормальная практика! Вот тебе, потягай:
0
Круто, спасибо! :)
+2
Ну и передавай Graphics, а не Shape или Sprite, в чем проблема?
По факту — круто.
  • ryzed
  • ryzed
0
Спасибо! :)
Ну вот хотел я это предложить, но почему-то счел лютым костылем. Разве норм будет?
0
Норм, конечно, это обычный класс.
0
Оукей! Иду менять.
0
Это, конечно, всё хорошо, но трансформация какая-то неинтуитивная. Перемещая точку по горизонтали, изображение начинает дополнительно искажаться еще и по вертикали. Неужели, в ФШ это также работает?
0
Ты удивишься :)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.