Прямоугольник и квадрат в Figma
Если вы кликните на рабочую область, Figma создаст квадрат 100 на 100 пикселей серого цвета (#C4C4C4).
Если вы протянете по экрану с зажатым курсором, Фигма создаст прямоугольник, длины сторон которого будут пропорциональны пути курсора по вертикали и горизонтали.
Если вы протянете зажатым курсором по экрану с зажатым Shift, то Фигма создаст квадрат, длина стороны которого будет пропорционально пути курсора.
Как скруглить углы у прямоугольника читайте здесь.
Руководство по работе с изображениями в Figma
В этом гайде мы расскажем, как работать с картинками в Figma. Например, как менять их размер или яркость, добавлять текст и работать со слоями. Все инструкции подходят как для веб-версии, так и для десктопного варианта программы.

освойте профессию
графический дизайнер с нуля
Содержание
- Как создать рабочую область
- Как добавить изображение в Figma
- Как повернуть изображение
- Как масштабировать или обрезать изображение
- Как поменять яркость и контрастность изображений
- Как добавить текст на изображение
- Как работать со слоями
- Как добавить рамку к изображению
- Как добавить эффекты
- Как поменять форму изображения
- Удаляем фон
Как создать рабочую область
20 месяцев
профессия графический дизайнер с нуля до про
Станьте графическим дизайнером, обучаясь на реальных задачах

Для работы с картинками нужно создать новую рабочую область. Это можно сделать на главной странице, нажав на значок «+» или кнопку Design file:
Дальше в работе нужно будет использовать разные сочетания клавиш, например:
Здесь важно помнить, что клавиша Ctrl используется на компьютерах с системой Windows, а на macOS вместо нее будет использоваться клавиша Cmd.
20 месяцев
профессия графический дизайнер с нуля до про
Станьте графическим дизайнером, обучаясь на реальных задачах

Как добавить изображение в Figma
Первый способ
Используйте сочетание клавиш Shift+Ctrl+K в открытой рабочей области. В появившемся окне найдите нужное изображение и нажмите кнопку «Открыть»:
Теперь выбранное изображение появится в рабочей области:
Также можно открыть картинку через верхнее меню, но сочетанием клавиш получается быстрее. Через меню это можно сделать так:
Второй способ
Открываем папку с нужной картинкой, зажимаем ее левой клавишей мыши и перетаскиваем в рабочую область в Figma:
Этим способом картинка вставляется в исходном размере. Чтобы его изменить, нужно будет использовать настройки.
Третий способ
Этот способ полезен, если нужно скопировать изображение из интернета прямо в Figma. Выбираем картинку и нажимаем на нее правой кнопкой мыши. В появившемся меню жмем «Скопировать изображение»:
Возвращаемся в рабочую область Figma и вставляем картинку с помощью опции Paste here (Вставить) или сочетания Ctrl+V:
Четвертый способ
Его можно использовать, если нужно вставить изображение в необычную форму. Для этого:
- Выделяем объект, в который нужно вставить картинку.
- Справа в меню Fill (Заливка) открываем окно с редактированием цвета
- В нем нажимаем на иконку Image (Изображение).
В настройках появится кнопка Choose Image (Изменить изображение), нажав на которую можно будет поместить изображение внутрь фигуры.
Как повернуть изображение
Если положение картинки в рабочем пространстве вам не подходит, и ее нужно повернуть на 180 градусов, выделите картинку курсором и слева в блоке Fill нажмите на иконку Rotate 90º (Вращать) до тех пор, пока картинка не повернется так, как вам нужно:
Как масштабировать или обрезать изображение
В настройках есть четыре способа масштабирования изображений:
Если в настройках картинки выбрана опция Fill, то можно потянуть за любой край фрейма (прямоугольника, и картинка будет автоматически подстраиваться под него:
Если в выпавшем меню выбрать Fit (Подогнать), то картинка будет подстраиваться по высоте, чтобы ее было видно во фрейме целиком:
Если в выпавшем меню выбрать Crop (Обрезать), то картинку можно кадрировать — обрезать изображение и зафиксировать любую его часть:
Если выбрать Tile (Плитка), то можно заполнить миниатюрами картинки весь прямоугольник:
профессия | 20 месяцев
графический дизайнер с нуля до про

Станьте дизайнером, который нужен в маркетинге, PR, IT. Дадим не только знания, но и реальный опыт в профессии
Как поменять яркость и контрастность изображений
В Figma меньше инструментов для работы с яркостью и контрастностью, чем в Photoshop, но несколько инструментов есть. Выделите нужное изображение в рабочей области, откройте вкладку Fill в меню справа и нажмите на квадратик с изображением.
В выпавшем окне появятся ползунки с настройками экспозиции, контрастности, температуры и других эффектов. Если за них потянуть, то можно получить разные результаты:
В Figma есть несколько категорий настроек:
- Exposure (Экспозиция)
- Contrast (Контраст)
- Saturation (Насыщенность)
- Temperature (Температура)
- Tint (Оттенок)
- Highlights (Интенсивность света)
- Shadows (Интенсивность тени)

как сделать визитку в Figma
Как добавить текст на изображение
Мы вставили картинку, выбрали ее ориентацию, теперь можно добавить текст. Нажмите на иконку «Т» (Text) в верхнем меню или используйте горячую клавишу Т:
В появившемся поле можно написать текст, а в блоке справа отредактировать его и выбрать:
- шрифт;
- размер текста;
- тип выравнивания (по правому краю, по левому краю или по середине);
- интервал между символами и строками.
Как работать со слоями
Если текст не видно на макете, то можно создать дополнительный слой. Для этого в блоке Fill нужно нажать на «+»:
По умолчанию он появится в формате Solid (Заливка), но его можно поменять на Gradient (Градиент) или даже другую картинку. Проще всего будет сделать заливку одним цветом.
Если на нашем макете выбрать черную заливку с прозрачностью 50%, то мы получим хорошо видимый текст и просматриваемую картинку.
Слоев на изображении может быть сколько угодно, можно менять их порядок, стиль, цвет и прозрачность.
Как добавить рамку к изображению
Для добавления рамки нужно кликнуть на изображение и в блоке слева найти компонент Stroke (Черта). Дальше нажимаем на значок «+», и появится рамка, созданная по умолчанию:
В этом же блоке можно редактировать рамку: менять ее толщину, цвет, прозрачность и расположение:
Рамки работают, как слои — их тоже можно накладывать друг на друга.
Как добавить эффекты
Для примера возьмем красный прямоугольник. Выделим его в рабочей области и во вкладке Effects (Эффекты) в блоке справа нажмем на «+», чтобы открылись настройки эффектов:
В Figma есть несколько базовых эффектов:
- Drop shadow (Падающая тень)
- Inner shadow (Внутренняя тень)
- Layer blur (Размытие объекта)
- Background blur (Размытие фона)
Эффект Drop shadow обозначается иконкой с солнышком. При нажатии на нее откроются настройки: отступы, размытие, цвет тени и ее прозрачность. Можно двигать ползунки и смотреть, как это работает:
Следующий эффект — Inner shadow или внутренняя тень. Его настройки похожи на Drop shadow, только тень будет падать не на фон за объектом, а внутрь объекта. Так будет создаваться ощущение, что объект находится на заднем плане:
Layer blur размывает весь объект. Степень размытия можно настроить, выбрав значение от 0 до 100 в панели настроек:
Чтобы увидеть эффект Background blur, нужно сделать прямоугольник немного прозрачным, например, на 50%. А вторым слоем под прямоугольником расположить другой объект, например, круг. Внутри прямоугольника будут размываться все объекты, находящиеся на нижних слоях. Так создается эффект матового стекла:

как сделать эффект неона в Figma
Как поменять форму изображения
Если изображению нужно придать необычную форму, которая будет сложгее, чем квадрат или круг, то такую фигуру можно нарисовать от руки или загрузить, как второе изображение:
Например, можно взять фото и наложить его на фигуру. Важно, чтобы фотография, которую вы хотите обрезать, была именно сверху. Затем нужно зажать Ctrl, выдеить оба объекта и сверху на панели нажать на иконку маски:
Если кадр встал внутри фигуры не так, как нужно, можно выделить фотографию на панели слоев и сдвинуть в нужное положение.
Удаляем фон
Самый простой способ удаления фона — с помощью плагина Remove BG. Скачать его можно на сайте Figma Community, а потом зарегистрироваться на странице remove.bg.
На сайте нужно получить ключ: нажать на иконку своего аккаунта справа вверху → My account → API Keys → + New API Keys → Create API Keys и скопировать его.
Идем в Figma: сверху слева нажимаем на Main menu → Plugins → Find more plugins → в поиске ищем Remove BG → Set API Key → вставляем скопированный ключ.
На холст вставляем изображение, в котором нужно удалить фон, выделяем его и правой кнопкой мыши открываем меню. Ищем нужный нам плагин и жмем Run (Действовать):
Немного ждем и получаем мопса без фона:
профессия графический дизайнер с нуля до про
Маркетинг, PR, IT — мы не знаем, какую сферу вы выберете, когда станете графическим дизайнером. Но знаем, что вы сможете им стать, получив реальный опыт. Тот, который оценят работодатели
Как в Figma сделать объёмную фигуру с помощью теней
Коротко и ясно рассказываем, как быстро добиться интересного эффекта для вашего проекта.


Иллюстрация: Оля Ежак для Skillbox Media

Вячеслав Лазарев
Редактор. Пишет про дизайн, редактирует книги, шутит шутки, смотрит аниме.
Плагины в Figma помогают ускорить работу и быстро добавить интересные эффекты в ваш проект. В этой инструкции рассказываем, как с помощью Beautiful Shadows и Noise сделать объёмную фигуру с текстурой для вашего макета.
Перед чтением инструкции скачайте и установите плагины Beautiful Shadows и Noise:
Подготовка фигуры
- Сделайте любую фигуру. Мы будем рассматривать эффект на примере круга, но он может сработать и с квадратом, и с петлёй, и с чем угодно ещё.
- Если вы используете круг, то удалите его заливку в панели настроек в блоке Fill и добавьте обводку чёрного цвета в блоке Stroke.
- Кликните правой кнопкой мыши по фигуре и перейдите в Plugins → Beautiful Shadows.
- В появившемся окне плагина кликните на иконку , укажите белый цвет тени и вместо Drop Shadow выберите Inner Shadow.
- Закройте окно с редактированием цвета, поставьте источник света в левый нижний угол. Яркость света советуем поставить примерно на 55–60%.
- Скопируйте получившуюся фигуру и у копии в панели настроек в блоке Stroke укажите непрозрачность заливки 1%.
- Кликните правой кнопкой мыши по копии фигуры и откройте Beautiful Shadows.
- В окне плагина кликните на иконку , укажите серый цвет тени.
- Источник света поставьте в правый верхний угол, а яркость укажите на 75%.


Самые полные и полезные инструкции, которые помогут вам освоить все функции графического редактора.

Каталог эффектов в Figma, которые помогут сделать ваш проект интереснее.
Больше интересного про дизайн в нашем телеграм-канале. Подписывайтесь!
Больше о Figma
- Как в Figma сделать объёмный градиент для букв
- Как в Figma сделать объёмную фигуру из градиентов
- Как превратить макет из Figma в рабочий сайт
- Обновление в Figma: новый Auto Layout, вариативные шрифты, тёмная тема
- 5 лайфхаков в Figma, которые помогут работать быстрее
- Как делать варианты элементов интерфейса
- Как создать тёмную тему
Как сделать рамку редактирования как в Figma-е
Привет, хабр. В этой статье я бы хотел рассказать, как построить рамку редактирования, наподобие той, которая используется в редакторах figma, adobe illustrator и во множестве других графических редакторах. В основном рамка редактирования является составной частью графического редактора. Она может изменять расположение объекта, его масштаб и угол поворота.


Статья разделена на две части на теорию и практику. В теоретической части Я расскажу основы линейной алгебры необходимые для понимания мат. операций, применяющиеся к рамке. В практической части дана реализация рамки редактирования на языке javascript, описаны мат. операции над рамкой и его составных частей, разобраны алгоритмы выполнения программы. Реализация рамки в статье редактирует такие фигуры как полигон, смайлик и фотография. Реализация этих фигур, также рассматривается в статье.
Теория
Геометрический смысл скалярного произведения.
Скалярное произведение записывается следующей формулой:

Чтобы понять, что это формула обозначает, вспомним, что такое проекция вектора на вектор. Проекция вектора A на вектор B — называется число, равное длине проецирования вектора A на вектор B под углом 90 градусов. Если переформулировать определение под “рабоче-крестьянский язык”, то выйдет, что проекция вектора A на вектор B — это длина тени, откладываемая вектором А на вектор B под углом 90 градусов. Графически это выглядит следующим образом:

Скалярное произведение тесно связано с проекцией векторов. Так как геометрический смысл скалярного произведения векторов A и B — это проекция вектора A на вектор B умноженное на длину вектора B, или наоборот проекция вектора B на вектор A умноженное на длину вектора A:

Если из двух векторов один из них имеет длину равную единице, то скалярное произведение векторов равна проекции вектора на единичный вектор:

Матрицы
Матрицы, используемые для изменения рамки, берутся из стандартного курса компьютерной графики:

Если перечисленные матрицы умножить на произвольную фигуру с внутренней совокупностью точек, то внутренние точки не выйдут за края фигуры и будут пропорционально изменены вместе с “родительской” фигурой. Например, пусть фигура — это прямоугольник, а внутреннюю совокупность точек составляет треугольник, тогда умножая все точки на вышеописанные матрицы, получим пропорционально изменённые точки:

где под pi подразумеваются точки прямоугольника и внутренней фигуры.
Последовательное использование матриц для решения задач
Для комплексной работы с матрицами рассмотрим такую задачу. Есть прямоугольник расположенный на некотором расстоянии от центра системы координат:

Необходимо увеличить его по длине в два раза и по ширине в три раза. Изменение размеров должно происходить относительно его точки t. Задача решается в три этапа. На первом этапе точки прямоугольника перемещаются на вектор -t . На втором происходит изменение масштаба рамки. И в заключении рамку нужно переместить в исходное положение, переместив все его точки на вектор t. Уравнение, которое получается для решения задачи следующие:

Где точка pi — это точка прямоугольника с индексом i.
Если проиллюстрировать этапы изменения рамки, то выглядит это следующим образом:
- перемещение всех точек на вектор -t
- перемещение всех точек на вектор -t

- увеличение масштаба прямоугольника по длине в 2 раза и по ширине в 3 раза

- перемещение всех точек на вектор t

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

Практика
В практической части реализуется следующий функционал рамки:
- Перемещение;
- Масштабирование по стороне относительно центра или уголка;
- Поворот относительно своего центра;
- Откат изменений.
Фигуры, которые рамка редактирования изменяет:
Код написан на javascript и разбирается по пунктам. Те функции, методы и члены классов, которые требуют разъяснений в их работе разбираются детально. Если хотите увидеть полностью код, пролистайте статью до конца.
Линейная алгебра
Класс вектор
class Vector < constructor(x, y) < this.x = x; this.y = y >static getVector(p2, p1) < // создание вектора по двум точкам return new Vector(p2.x - p1.x, p2.y - p1.y) >static getNormal(v) < // нормаль вектора return new Vector( v.y, -v.x ) >static getRotationAngle(v) < // угол поворота относитльно оси X const axisY = const dot = Vector.dotProduct(v, axisY) let angle = Math.acos(v.x / v.length()) angle = dot > 0 ? angle : -angle return angle > length() < return Math.sqrt(this.x * this.x + this.y * this.y) >normalize() < // нормализация, делает длину вектора равной единице const len = Math.sqrt(this.x * this.x + this.y * this.y) this.x = this.x / len this.y = this.y / len >negative() < // поворот вектора на 180 градусов this.x = -this.x this.y = -this.y >multi(k) < // умножения на коэффицент this.x = this.x * k; this.y = this.y * k >static dotProduct(v1, v2) < // скалярное произведение return v1.x * v2.x + v1.y * v2.y >>
Класс Vector, имеет статичный метод getRotationAngle. Он возвращает угол поворота вектора относительно оси x:

Алгоритм нахождения этого угла следующий:
- Находим угол через арккосинус косинуса вектора v:

- Арккосинус угла имеет минимальное значение 0 градусов и максимальное значение 180 градусов. Этот диапазон не даёт точного значения угла поворота, т.к. полной оборот — это 360 градусов. Поэтому нужно расширить диапазон возможных значений угла поворота. Для этого определяем арифметический знак скалярного произведение вектора v на единичный вектор axisY, который является сонаправленным оси Y. Если знак положительный, проекция вектора v откладывается на вектор axisY. Это означает, что вектор v находится в 1-ой или во 2-ой четверти системы координат, и диапазон возможных значений угла поворота будет находиться в диапазоне от 0 до 180 градусов:
если знак отрицательный, то вектор v проецируется на противоположное направление вектора axisY. И это означает, что вектор v находится в 3-ей или 4-ой четверти системы координат, и возможные значений угла поворота будут в диапазоне от 0 до -180 градусов: 
Класс матрица
class Matrix < constructor(a, b, c, d, e, f) < this.a = a; this.c = c; this.e = e this.b = b; this.d = d; this.f = f >multiMatrix(m) < // умножение на матрицу const a = this.a * m.a + this.c * m.b const b = this.b * m.a + this.d * m.b const c = this.a * m.c + this.c * m.d const d = this.b * m.c + this.d * m.d const e = this.a * m.e + this.c * m.f + this.e const f = this.b * m.e + this.d * m.f + this.f return new Matrix(a, b, c, d, e, f) >multiVector(p) < // умножение на вектор или точку return < x: this.a * p.x + this.c * p.y + this.e, y: this.b * p.x + this.d * p.y + this.f >> clone() < // создание копии матрицы return new Matrix(this.a, this.b, this.c, this.d, this.e, this.f) >>
Метод clone возвращает копию объекта класса Matrix. На протяжении всего проекта, реализация метода clone любого класса возвращает копию объекта класса.
Фигуры
У всех фигур должен быть одинаковый интерфейс по обращению к свойствам или методам. Это позволит рамке обращаться к определённому интерфейсу, не зная о множестве реализаций фигур. Реализация в проекте общего интерфейса представлен классом AbstractFigure. Чтобы представить иерархию классов унаследованных от класса AbstractFigure, отобразим диаграмму классов:

Класс AbstractFigure
class AbstractFigure < clockwise = false constructor() < >clone() < return AbstractFigure() >setClockwise(val) < this.clockwise = val >clockwise() < return this.clockwise >prop() < return < x: 0, y: 0, width: 0, height: 0 >> multiMatrix(mat) < >draw() < >>
Свойство clockwise используется для всех дочерних классов. Это свойство определяет по какому направлению рисовать фигуру по часовой стрелке или против часовой стрелки.
Метод prop возвращает свойства фигуры — расположение фигуры по координатам x, y, длину и ширину. Позиция x и y показывает расположение фигуры относительно левого нижнего угла.
Класс Polygon
class Polygon extends AbstractFigure < constructor(points = []) < super() this.points = Object.assign(points) >clone() < // создание копии Polygon const polygon = new Polygon(this.points) return polygon >prop() < // свойства Polygon: позиция фигуры по x,y; длина; ширина let minX = Number.MAX_VALUE; let maxX = Number.MIN_VALUE let minY = Number.MAX_VALUE; let maxY = Number.MIN_VALUE this.points.forEach(p => < if (p.x >maxX) < maxX = p.x >if (p.x < minX) < minX = p.x >if (p.y > maxY) < maxY = p.y >if (p.y < minY) < minY = p.y >>) return < x: minX, y: minY, width: maxX - minX, height: maxY - minY >> multiMatrix(mat) < // умножение на матрицу this.points = this.points.map(( p ) =>mat.multiVector( p )) > draw() < ctx.save() ctx.beginPath() this.points.forEach((p, ind) => < if (ind === 0) < ctx.moveTo(p.x, p.y) >else < ctx.lineTo(p.x, p.y) >>) ctx.stroke() ctx.restore() > >
Класс хранит точки points, которые соединяются при рисовании через метод draw. Изменение точек происходит через метод multiMatrix. В методе происходит перемножение каждой точки на матрицу mat и результат перемножения перезаписывает в переменную points.
Класс Ellipse
class Ellipse extends AbstractFigure < constructor(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) < super() this.x = x; this.y = y; // center this.radiusX = radiusX this.radiusY = radiusY this.rotation = rotation // поворот элипса this.startAngle = startAngle this.endAngle = endAngle super.setClockwise(!counterclockwise) >clone() < // создание копии Ellipse const ellipse = new Ellipse(this.x, this.y, this.radiusX, this.radiusY, this.rotation, this.startAngle, this.endAngle, !super.clockwise()) return ellipse >prop() < // свойства Ellipse: позиция фигуры по x,y; длина; ширина return < x: this.x - this.radiusX, y: this.y - this.radiusY, width: this.radiusX * 2, height: this.radiusY * 2 >> multiMatrix(mat) < const sin = Math.sin( this.rotation ) const cos = Math.cos( this.rotation ) const newCenter = mat.multiVector( < x: this.x, y: this.y >) // ось x let vectorX = (new Matrix(cos, sin, -sin, cos, 0, 0)).multiVector(< x: this.radiusX, y: 0>) let extremeX = < x: this.x + vectorX.x, y: this.y + vectorX.y >let newExtremeX = mat.multiVector(extremeX) const newRadiusX = Vector.getVector(newExtremeX, newCenter).length() // ось y let vectorY = (new Matrix(cos, sin, -sin, cos, 0, 0)).multiVector(< x: 0, y: this.radiusY>) let extremeY = < x: this.x + vectorY.x, y: this.y + vectorY.y >let newExtremeY = mat.multiVector(extremeY) const newRadiusY = Vector.getVector(newExtremeY, newCenter).length() // подсчет rotation const newRotation = Vector.getRotationAngle(Vector.getVector(newExtremeX, newCenter)) // Записываем результат this.x = newCenter.x this.y = newCenter.y this.radiusX = newRadiusX this.radiusY = newRadiusY this.rotation = newRotation > draw() < ctx.save() ctx.beginPath() ctx.ellipse(this.x, this.y, this.radiusX, this.radiusY, this.rotation, this.startAngle, this.endAngle, !super.clockwise()) ctx.stroke() ctx.restore() >>
Конструктор Ellipse принимает стандартные свойства, которые нужны для рисования ellipse в canvas:

- radiusX, radiusY — радиусы по осям X, Y;
- rotation — поворот эллипса, значение в радианах;
- startAngle — угол поворота по оси X, обозначающая начала рисования;
- endAngle — угол поворота по оси X, обозначающая завершения рисования;
- counterclockwise — булевая переменная, если значение true рисуем против часовой стрелки, если false по часовой стрелке.
Класс Ellipse реализует метод multiMatrix по следующему алгоритму:
- Находим по текущим свойствам объекта класса Ellipse переменные, которые потребуются для дальнейших расчетов:


- Перемножаем текущие значения точек на матрицу mat:

- Рассчитываем переменные из раннее перемноженных точек для обновления свойств класса объекта:

- Перезаписываем текущие значения объекта класса переменными с пометкой в начале new.
Класс PaintedImage
class PaintedImage extends AbstractFigure < constructor(img, x, y, width, height) < super() this.mat = new Matrix(1, 0, 0, 1, 0, 0) this.img = img this.x = x; this.y = y; this.width = width; this.height = height >clone() < // создание копии PaintedImage const paintedImage = new PaintedImage(this.img, this.x, this.y, this.width, this.height) paintedImage.mat = this.mat.clone() return paintedImage >prop() < // свойства PaintedImage: позиция фигуры по x,y; длина; ширина return < x: this.x, y: this.y, width: this.width, height: this.height >> multiMatrix(mat) < this.mat = mat.multiMatrix( this.mat ).clone() >draw() < ctx.beginPath() ctx.save() const mat = this.mat ctx.transform(mat.a, mat.b, mat.c, mat.d, mat.e, mat.f) ctx.drawImage(this.img, this.x, this.y, this.width, this.height) ctx.restore() >>
В классе PaintedImage метод multiMatrix перемножает результат предыдущей матрицы, сохраненной в this.mat, на аргумент mat. И при рисовании методом draw, рассчитанная матрица применяется для трансформации контекста canvas.
Класс Container
class Container extends AbstractFigure < constructor(figures = []) < super() this.figures = figures >clone() < const figureClone = this.figures.map((figure) =>figure.clone()) return new Container(figureClone) > setClockwise(val) < this.figures.forEach((figure) =>< figure.setClockwise( super.clockwise() == val ? figure.clockwise : !figure.clockwise ) >) super.setClockwise(val) > prop() < let minX = Number.MAX_VALUE; let minY = Number.MAX_VALUE; let maxWidth = Number.MIN_VALUE; let maxHeight = Number.MIN_VALUE this.figures.forEach(figure => < const prop = figure.prop() if (prop.x < minX) < minX = prop.x >if (prop.y < minY) < minY = prop.y >if (prop.width > maxWidth) < maxWidth = prop.width >if (prop.height > maxHeight) < maxHeight = prop.height >>) return < x: minX, y: minY, width: maxWidth, height: maxHeight >> multiMatrix(mat) < this.figures.forEach(( figure ) =>< figure.multiMatrix( mat ) >) > draw() < this.figures.forEach(( figure ) =>< figure.draw() >) > >
Класс Container — это класс, реализующий объект, который хранит в себе массив фигур. В данной статье класс контейнер применяется для создания фигуры смайлика. Составные части контейнера для смайлика — это эллипсы и полуэллипсы.
Класс Frame — рамка редактирования
Инцилизация
class Frame < constructor(width = 0, height = 0) < this.width = width; this.height = height; this.points = [ , , , ] > clockwise() < let indexByMinX = 0 for (let i = 0; i < this.points.length; ++i) < // Узнаем индекс точки c наименьшим x if(this.points[indexByMinX].x >this.points[i].x) < indexByMinX = i >> const beginIndex = (indexByMinX + 3) % this.points.length const middleIndex = indexByMinX const lastIndex = (indexByMinX + 1) % this.points.length const v1 = Vector.getVector( this.points[middleIndex], this.points[beginIndex] ) const v2 = Vector.getVector( this.points[lastIndex], this.points[middleIndex] ) const multiVec = v1.x * v2.y - v2.x * v1.y // Векторное произведение return multiVec < 0 >>
При инициализации класса Frame определяются первоначальные свойства рамки. Это его размеры width, height, и его точки points, образующие при рисовании прямоугольник. Последовательность точек points влияют на обход прямоугольника. Обход может быть по или против часовой стрелки. Метод clockwise определяет это.
class Frame < figure = new Polygon() // фигура, которая редектируется . adjustToFigure() < // обвалакивает фигуру рамкой const prop = this.figure.prop() this.width = prop.width this.height = prop.height this.points = [ , , , ] > setFigure(figure) < this.figure = figure.clone() this.adjustToFigure() >transition(v) < . this.figure.multiMatrix( mat ) >rotation(downMouse, upMouse) < . this.figure.multiMatrix( mat ) >multiBySide(index, v, shift = false) < . this.figure.multiMatrix( mat ) this.figure.setClockwise( this.clockwise() ) >draw( mouse = ) < this.figure.draw() . >>
Фигуру, которую должна редактировать рамка, передается методом setFigure. Переданную фигуру, класс объекта Frame копирует и, вызовом метода adjustToFigure, подстраивается под свойства prop фигуры.
Все изменения рамки представляются в виде матриц, которые передаются методу multiMatrix объекта figure.
Составные части рамки
// Рамка редактирования class Frame < // свойства painted изменяются после вызова метода draw paintedCircles = new Array(4).fill().map(() =>(new Path2D())) // круги для поворота рамки paintedSides = new Array(4).fill().map(() => new Path2D()) paintedRect = new Path2D() // область перемещения рамки cornerWidth = 7.5 // ширина уголка paintedCorners = new Array(4) . containedInRect(mouse) < return ctx.isPointInPath( this.paintedRect, mouse.x, mouse.y ) >containedInCorner(mouse) < for (let i = 0; i < this.paintedCorners.length; ++i) < if (ctx.isPointInPath( this.paintedCorners[i], mouse.x, mouse.y )) < return i >> return -1 > containedInCircle(mouse) < for (let i = 0; i < this.paintedCircles.length; ++i) < if (ctx.isPointInPath( this.paintedCircles[i], mouse.x, mouse.y )) < return true >> return false > containedInSide(mouse) < for (let i = 0; i < this.paintedSides.length; ++i) < if (ctx.isPointInStroke( this.paintedSides[i], mouse.x, mouse.y )) < return i >> return -1 > . draw( mouse = ) < this.figure.draw() const points = this.clockwise() ? this.points.reverse() : this.points let selectedMouse = false // влияет на перекрашивание компонента // уголки ctx.save() const center = < x: ( points[2].x + points[0].x ) / 2, y: ( points[2].y + points[0].y ) / 2 >const width = Vector.getVector( points[1], points[0] ).length() const height = Vector.getVector( points[2], points[1] ).length() const rotationAngle = Vector.getRotationAngle( Vector.getVector( points[1], points[0] ) ) const sin = Math.sin( rotationAngle ) const cos = Math.cos( rotationAngle ) const cornerPos = [ < x: center.x - width / 2, y: center.y - height / 2 >, < x: center.x + width / 2, y: center.y - height / 2 >, < x: center.x + width / 2, y: center.y + height / 2 >, < x: center.x - width / 2, y: center.y + height / 2 >, ] cornerPos.forEach((p, ind) => < const p1 = < x: p.x - this.cornerWidth / 2, y: p.y - this.cornerWidth / 2 >const p2 = < x: p1.x + this.cornerWidth, y: p1.y >const p3 = < x: p2.x, y: p2.y + this.cornerWidth >const p4 = < x: p3.x - this.cornerWidth, y: p3.y >const cornerPoints = [p1, p2, p3, p4].map((p) => < let mat = new Matrix(1, 0, 0, 1, center.x, center.y) mat = mat.multiMatrix(new Matrix(cos, sin, -sin, cos, 0, 0)) mat = mat.multiMatrix(new Matrix(1, 0, 0, 1, -center.x, -center.y)) return mat.multiVector( p ) >) this.paintedCorners[ind] = new Path2D() cornerPoints.forEach((p, index) => < if (index == 0) < this.paintedCorners[ind].moveTo(p.x, p.y) >else < this.paintedCorners[ind].lineTo(p.x, p.y) >>) ctx.fillStyle = ctx.isPointInPath( this.paintedCorners[ind], mouse.x, mouse.y ) && !selectedMouse ? "#6200EA" : "#757575"; if ( !selectedMouse ) < selectedMouse = ctx.isPointInPath( this.paintedCorners[ind], mouse.x, mouse.y ) >ctx.fill( this.paintedCorners[ind] ) >) ctx.restore() // стороны ctx.save() for (let i = 0; i < this.paintedSides.length; ++i) < const p1 = points[i] const p2 = points[(i + 1) % 4] this.paintedSides[i] = new Path2D() this.paintedSides[i].moveTo(p1.x, p1.y) this.paintedSides[i].lineTo(p2.x, p2.y) ctx.strokeStyle = ctx.isPointInStroke( this.paintedSides[i], mouse.x, mouse.y ) && !selectedMouse ? "#6200EA" : "#75757599"; if ( !selectedMouse ) < selectedMouse = ctx.isPointInStroke( this.paintedSides[i], mouse.x, mouse.y ) >ctx.stroke( this.paintedSides[i] ) > ctx.restore() // прямоугольник this.paintedRect.painted = new Path2D() points.forEach((p, ind) => < if (ind == 0) < this.paintedRect.moveTo(p.x, p.y) >else < this.paintedRect.lineTo(p.x, p.y) >>) // круги ctx.save() const d = 5 const angle = Math.PI / 4 // 45 градусов const mat = new Matrix(angle, angle, -angle, angle, 0, 0) for (let i = 0; i < 4; ++i) < this.paintedCircles[i] = new Path2D() const p1 = points[i] const p2 = points[(i + 1) % 4] const sideV = Vector.getVector(p2, p1) sideV.normalize() sideV.multi(20) const circleV = mat.multiVector(sideV) const circleP = this.paintedCircles[i].arc(circleP.x, circleP.y, d, 0, 2 * Math.PI) ctx.fillStyle = ctx.isPointInPath( this.paintedCircles[i], mouse.x, mouse.y ) ? "#6200EA" : "#757575"; ctx.fill( this.paintedCircles[i] ) > ctx.restore() > >
Рамка состоит из следующих составных элементов: круги, стороны, уголки и прямоугольник. Каждая из перечисленных фигур во время перетаскивания курсором мыши изменяет рамку:
- Круги поворачивают рамку;
- стороны масштабируют рамку по длине или ширине относительно центра или углов рамки;
- уголки масштабируют рамку одновременно по длине и ширине относительно противоположного угла рамки;
- прямоугольник перемещает все точки рамки.
Методы, начинающиеся с contained проверяют нахождение точки в одной из составных элементов рамки. Проверка происходит среди прорисованных элементов painted. В случае успешной проверки возвращается число большее -1 или булевое значение true, возвращаемое значение зависит от вызываемого метода. Например, containedInSide возвращает число, указывающее индекс стороны рамки, в которой находится точка, тогда как метод containedInRect возвращает булевое значение.
Каждый элемент прорисовывается, вызовом метода draw, и сохраняется в переменных начинающихся с painted. Все прорисовки, которые реализуются в методе, по мнению автора, очевидны за исключением прорисовки уголков. Алгоритм прорисовки уголков:
- Откладываем относительно центра рамки четыре равноудаленных точки по x на ширину рамки деленной на два и по y на длину рамки деленную на два:

- Строим уголки по найденным точкам:

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

- последовательно умножаем матрицы для поворота уголков относительно центра рамки и рассчитанную матрицу умножаем на точки уголков:


- рисуем результат.
transition
class Frame < . transition(v) < const mat = new Matrix(1, 0, 0, 1, v.x, v.y) this.points = this.points.map(( p ) =>mat.multiVector( p )) this.figure.multiMatrix( mat ) > >
Перемещение рамки осуществляется методом transition. Аргумент функции v указывает на какой вектор переместить рамку. В теле функции создаётся матрица перемещения, которая умножается на точки рамки и дочернюю фигуру. Результаты умножения сохраняются.
class Frame < . rotation(downMouse, upMouse) < const center = < x: ( this.points[2].x + this.points[0].x ) / 2, y: (this.points[2].y + this.points[0].y) / 2 >const axisX = Vector.getVector( downMouse, center ) axisX.normalize() const axisY = Vector.getNormal( axisX ) const rotationV = Vector.getVector(upMouse, center) const proj = Vector.dotProduct(axisX, rotationV) let angle = Math.acos( proj / rotationV.length()) angle = Vector.dotProduct(axisY, rotationV) > 0 ? -angle : angle const sin = Math.sin(angle) const cos = Math.cos(angle) let mat = new Matrix(1, 0, 0, 1, center.x, center.y) mat = mat.multiMatrix(new Matrix(cos, sin, -sin, cos, 0, 0)) mat = mat.multiMatrix(new Matrix(1, 0, 0, 1, -center.x, -center.y)) this.points = this.points.map(( p ) => mat.multiVector( p )) this.figure.multiMatrix( mat ) > >
Метод rotation поворачивает рамку относительно центра. Для определения поворота рамки в аргументы функции передаются две точки. Первая точка downMouse определяется при нажатии ЛКМ, вторая upMouse после опускания ЛКМ или перемещения курсора мыши. Описание математического алгоритма метода rotation:
- Откладываем от центра рамки к точке downMouse вектор с названием vectorX. Нормируем vectorX, и получившийся вектор называем vectorY. Данные вектора образуют произвольную систему координат:

- От центра рамки к точке upMouse откладываем вектор с именем rotationV. Находим угол angle между произвольной осью vectorX и rotationV по алгоритму из класса Vector метода getRotationAngle:

- Последовательно перемножаем матрицы для поворота рамки:


- Умножаем точки на получившуюся матрицу.
multiBySide
class Frame < . multiBySide(index, v, shift = false) < const center = < x: ( this.points[2].x + this.points[0].x ) / 2, y: (this.points[2].y + this.points[0].y) / 2 >const prevSideInterval = 3 const referencePoint = shift ? : this.points[ (index + prevSideInterval) % this.points.length ] const sideVector = Vector.getVector( this.points[index], this.points[ (index + 1) % this.points.length ] ) const adjacentVector = Vector.getVector( this.points[ (index + prevSideInterval) % this.points.length ], this.points[index] ) const sideNormal = Vector.getNormal( sideVector ) sideNormal.normalize() sideNormal.negative() let dif = shift ? 2 * Vector.dotProduct(v, sideNormal) : Vector.dotProduct(v, sideNormal) dif = this.clockwise() ? -dif : dif const angle = Vector.getRotationAngle(adjacentVector) const width = adjacentVector.length() let multiX = 1 + dif / width // чтобы рамкка при минимальном размере, стремящимся к нулю, не ломалась и не исчезала if (multiX > 0) < multiX = multiX < 0.001 ? 0.001 : multiX >else < multiX = multiX >-0.001 ? -0.001 : multiX > const sin = Math.sin(angle) const cos = Math.cos(angle) let mat = new Matrix(1, 0, 0, 1, referencePoint.x, referencePoint.y) mat = mat.multiMatrix(new Matrix(cos, sin, -sin, cos, 0, 0)) mat = mat.multiMatrix(new Matrix(multiX, 0, 0, 1, 0, 0)) mat = mat.multiMatrix(new Matrix(cos, -sin, sin, cos, 0, 0)) mat = mat.multiMatrix(new Matrix(1, 0, 0, 1, -referencePoint.x, -referencePoint.y)) const newPoints = this.points.map(( p ) => mat.multiVector( p )) this.points = newPoints this.figure.multiMatrix( mat ) this.figure.setClockwise( this.clockwise() ) > >
Масштабирование через сторону рамки выполняется методом multiBySide. В аргументы функции передаются index — это индекс, перетаскиваемой стороны, v — вектор, на который сторону перетащили, shift — это булевая переменная, указывающая относительно чего рамка масштабируется, если shift равен true, то относительно центра рамки, если false, то относительно угла рамки. Математические этапы масштабирования рамки следующие:
- Находим опорную точку referencePoint. Если переменная shift равна true, то опорная точка равна центру рамки, если false, то точка определяется из угла рамки. Нахождение этого угла происходит по индексу стороны рамки, по которому происходит масштабирование. Индекс стороны совпадает с одним из индексов угла рамки. Увеличение значения индекса на три дает индекс опорной точки:
Взятие именно этой точки в качестве опорной, дает возможность масштабирования рамки по уголку. - Рассчитываем и нормализуем нормаль стороны — sideNormal. Проецируем вектор перемещения v на эту нормаль, результат записывается в переменную dif. Результатом проекции является число на сколько изменилась сторона рамки:

- Берем предыдущую сторону перетаскиваемой стороны, если обходим точки points по часовой стрелке, и делаем из этой стороны вектор adjacentVector. Находим угол, на который он повернут относительно оси X:

- Подсчитываем переменную multiX. Переменная multiX — число, во сколько раз новая рамка больше или меньше предыдущей:

- Последовательное умножаем матрицы для масштабирования рамки:


multiByCorner
class Frame < . multiByCorner(index, v) < this.multiBySide(index, v) const nextSideInterval = 3 this.multiBySide((index + nextSideInterval) % this.paintedSides.length, v) >>
Масштабирование через уголок рамки аналогичен масштабированию через сторону. Метод multiByCorner принимает индекс перетаскиваемого уголка и вектор, на который следует переместить этот уголок. В теле метода multiByCorner вызываются два метода multiBySide. Они масштабируют рамку по тем сторонам, которые прилегают к уголку рамки.
class Frame < . clone() < const frameClone = new Frame() frameClone.points = Object.assign(this.points) frameClone.figure = this.figure.clone() return frameClone >. >
Метод сlone возвращает копию рамки. Копируются точки рамки и фигура figure.
Выполнение программы
В начале инициализируется canvas с контекстом для рисования в 2d. Этот контекст в дальнейшем используют методы draw:
const canvas = document.querySelector("canvas"); const rect = canvas.parentNode.getBoundingClientRect() canvas.width = rect.width canvas.height = rect.height const ctx = canvas.getContext("2d"); ctx.lineWidth = 3;
К кнопкам с названиями “полигон”, “окружность”, “картинка” привязываем callback функции, инициализирующие глобальные переменные, рамку — frame и фигуру — figure. Фигура инициализируется относительно центра canvas и передается через метод setFigure объекту frame:
function resetBtn() < polygonBtn.classList.remove('active') circleBtn.classList.remove('active') imageBtn.classList.remove('active') >let frame = new Frame(200 ,200) // инцилизация frame polygonBtn.onclick = () => < savedFrame = [] ctx.clearRect(0, 0, canvas.width, canvas.height); frame = new Frame(200 ,200) let points = [ , , , , , ] points = points.map((p) => < return (< x: canvas.width / 2 + p.x - 75, y: canvas.height / 2 + p.y - 100 >) // центрируем >) const polygon = new Polygon(points) frame.setFigure( polygon ) frame.draw() resetBtn() polygonBtn.classList.add('active') > circleBtn.onclick = () => < savedFrame = [] ctx.clearRect(0, 0, canvas.width, canvas.height); frame = new Frame(200 ,200) let ellipses = [ new Ellipse(50, 50, 50, 50, 0, 0, Math.PI * 2, true), new Ellipse(50, 50, 35, 35, 0, 0, Math.PI, false), new Ellipse(35, 40, 5, 5, 0, 0, Math.PI * 2, true), new Ellipse(65, 40, 5, 5, 0, 0, Math.PI * 2, true), ] // центрируем ellipses = ellipses.map((ellipse) =>< const halfWidth = 100 / 2 const halfHeight = 100 / 2 ellipse.x = ellipse.x + canvas.width / 2 - halfWidth ellipse.y = ellipse.y + canvas.height / 2 - halfHeight return ellipse >) const container = new Container(ellipses) frame.setFigure( container ) frame.draw() resetBtn() circleBtn.classList.add('active') > imageBtn.onclick = () => < savedFrame = [] ctx.clearRect(0, 0, canvas.width, canvas.height); frame = new Frame(200 ,200) const img = new Image() img.src = 'sonic.webp' const width = 300 const height = 150 const paintedImage = new PaintedImage(img, canvas.width / 2 - width / 2, canvas.height / 2 - height / 2, 300, 150) frame.setFigure( paintedImage ) frame.draw() resetBtn() imageBtn.classList.add('active') >circleBtn.click()
callback функция события нажатия мыши сохраняет текущее состояние рамки, тип изменения рамки, которое должно последовать при перемещении курсора мыши, и координаты нажатия мыши. Все эти свойства сохраняются в объекте savedProp:
let savedProp = undefined addEventListener("mousedown", (evt) => < const rect = canvas.getBoundingClientRect(); const mouse = < x: evt.clientX - rect.left, y: evt.clientY - rect.top >const cornerInd = frame.containedInCorner(mouse) if (cornerInd > -1) < savedFrame.push( frame.clone() ) savedProp = < type: 'multiByCorner', cornerInd, downMouse: mouse, frame: frame.clone() >return > const sideInd = frame.containedInSide(mouse) if (sideInd > -1) < savedFrame.push( frame.clone() ) savedProp = < type: 'multiBySide', sideInd, downMouse: mouse, shift: evt.shiftKey, frame: frame.clone() >return > if (frame.containedInCircle(mouse)) < savedFrame.push( frame.clone() ) savedProp = < type: 'rotation', downMouse: mouse, frame: frame.clone() >return > if (frame.containedInRect(mouse)) < savedFrame.push( frame.clone() ) savedProp = < type: 'transition', downMouse: mouse, frame: frame.clone() >> >)
При отпускании нажатой мыши срабатывает callback, который присваивает переменной savedProp значение undefined:
let savedProp = undefined addEventListener("mouseup", (evt) => < savedProp = undefined >)
Перемещение курсора мыши реализует изменение рамки в случае, если propState не undefined. Вектор перемещение зажатой мыши откладывается от координат сохраненной позиции нажатия мыши к координатам текущей позиции курсора мыши. Отложенный вектор передается в методы frame для изменения рамки:
addEventListener("mousemove", (evt) => < ctx.clearRect(0, 0, canvas.width, canvas.height); const rect = canvas.getBoundingClientRect(); const mouse = < x: evt.clientX - rect.left, y: evt.clientY - rect.top >if (savedProp) < frame = savedProp.frame.clone() const v = Vector.getVector(mouse, savedProp.downMouse) if (savedProp.type === 'multiByCorner') < frame.multiByCorner(savedProp.cornerInd, v) >else if (savedProp.type === 'multiBySide') < frame.multiBySide(savedProp.sideInd, v, savedProp.shift) >else if (savedProp.type === 'transition') < frame.transition(v) >else if (savedProp.type === 'rotation') < frame.rotation( savedProp.downMouse, mouse ) >> frame.draw(mouse) >)
Все изменения над рамкой можно откатить по нажатию кнопки “откатить изменения”. Реализация функционала следующее:
let savedFrame = [] . polygonBtn.onclick = () => < savedFrame = [] >circleBtn.onclick = () => < savedFrame = [] >imageBtn.onclick = () => < savedFrame = [] >backBtn.onclick = () => < if (savedFrame.length >0) < ctx.clearRect(0, 0, canvas.width, canvas.height); frame = savedFrame.pop() frame.draw() >> addEventListener("mousedown", (evt) =>
Заключение
На этом у меня всё. Надеюсь статья была полезной и Вы подтянули знания линейной алгебры и компьютерной графики, и сможете использовать наработки данной статьи в своих будущих проектах.
Код всего проекта
Page Title --> --> body < display: flex; margin: 0; >.wrapper-canvas < width: 80%; height: 100vh; background-color: #E0F7FA; >.wrapper-btns < display: flex; flex-direction: column; width: 20%; height: 100vh; background-color: #00838F; >button < border-color: #B39DDB; background-color: #6200EA; color: #fff; height: 50px; >button.active < background-color: #FFD54F; color: black; >#backBtn
- рамка редактирования
- figma
- компьютерная графика
- линейная алгебра
- графический редактор