1// Aseprite
2// Copyright (C) 2020-2022 Igara Studio S.A.
3// Copyright (C) 2001-2017 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/editor/transform_handles.h"
13
14#include "app/pref/preferences.h"
15#include "app/ui/editor/editor.h"
16#include "app/ui/skin/skin_theme.h"
17#include "base/pi.h"
18#include "os/surface.h"
19
20#include <algorithm>
21
22namespace app {
23
24using namespace app::skin;
25using namespace ui;
26
27static const int HANDLES = 8;
28
29static struct HandlesInfo {
30 // These indices are used to calculate the position of each handle.
31 //
32 // handle.x = (corners[i1].x + corners[i2].x) / 2
33 // handle.y = (corners[i1].y + corners[i2].y) / 2
34 //
35 // Corners reference (i1, i2):
36 //
37 // 0,0 0,1 1,1
38 // 0,3 1,2
39 // 3,3 3,2 2,2
40 //
41 int i1, i2;
42 // The angle bias of this specific handle.
43 double angle;
44 // The exact handle type ([0] for scaling, [1] for rotating).
45 HandleType handle[2];
46} handles_info[HANDLES] = {
47 { 1, 2, (PI * 0.0 / 180.0), { ScaleEHandle, SkewEHandle } },
48 { 1, 1, (PI * 45.0 / 180.0), { ScaleNEHandle, RotateNEHandle } },
49 { 0, 1, (PI * 90.0 / 180.0), { ScaleNHandle, SkewNHandle } },
50 { 0, 0, (PI * 135.0 / 180.0), { ScaleNWHandle, RotateNWHandle } },
51 { 0, 3, (PI * 180.0 / 180.0), { ScaleWHandle, SkewWHandle } },
52 { 3, 3, (PI * 225.0 / 180.0), { ScaleSWHandle, RotateSWHandle } },
53 { 3, 2, (PI * 270.0 / 180.0), { ScaleSHandle, SkewSHandle } },
54 { 2, 2, (PI * 315.0 / 180.0), { ScaleSEHandle, RotateSEHandle } },
55};
56
57HandleType TransformHandles::getHandleAtPoint(Editor* editor, const gfx::Point& pt, const Transformation& transform)
58{
59 auto theme = SkinTheme::get(editor);
60 os::Surface* gfx = theme->parts.transformationHandle()->bitmap(0);
61 double angle = transform.angle();
62
63 auto corners = transform.transformedCorners();
64
65 std::vector<gfx::Point> screenPoints;
66 getScreenPoints(editor, corners, screenPoints);
67
68 int handle_rs[2] = { gfx->width()*2, gfx->width()*3 };
69 for (int i=0; i<2; ++i) {
70 int handle_r = handle_rs[i];
71 for (size_t c=0; c<HANDLES; ++c) {
72 if (inHandle(pt,
73 (screenPoints[handles_info[c].i1].x+screenPoints[handles_info[c].i2].x)/2,
74 (screenPoints[handles_info[c].i1].y+screenPoints[handles_info[c].i2].y)/2,
75 handle_r, handle_r,
76 angle + handles_info[c].angle)) {
77 return handles_info[c].handle[i];
78 }
79 }
80 }
81
82 // Check if the cursor is in the pivot
83 if (visiblePivot(angle) && getPivotHandleBounds(editor, transform, corners).contains(pt))
84 return PivotHandle;
85
86 return NoHandle;
87}
88
89void TransformHandles::drawHandles(Editor* editor, ui::Graphics* g,
90 const Transformation& transform)
91{
92 double angle = transform.angle();
93
94 auto corners = transform.transformedCorners();
95
96 std::vector<gfx::Point> screenPoints;
97 getScreenPoints(editor, corners, screenPoints);
98
99 const gfx::Point origin = editor->bounds().origin();
100
101#if 0 // Uncomment this if you want to see the bounds in red (only for debugging purposes)
102 { // Bounds
103 gfx::Point
104 a(transform.bounds().origin()),
105 b(transform.bounds().point2());
106 a = editor->editorToScreen(a) - origin;
107 b = editor->editorToScreen(b) - origin;
108 g->drawRect(gfx::rgba(255, 0, 0), gfx::Rect(a, b));
109
110 a = transform.pivot();
111 a = editor->editorToScreen(a) - origin;
112 g->drawRect(gfx::rgba(255, 0, 0), gfx::Rect(a.x-2, a.y-2, 5, 5));
113 }
114 { // Rotated bounds
115 const gfx::Point& a = screenPoints[0] - origin;
116 const gfx::Point& b = screenPoints[1] - origin;
117 const gfx::Point& c = screenPoints[2] - origin;
118 const gfx::Point& d = screenPoints[3] - origin;
119
120 ui::Paint paint;
121 paint.style(ui::Paint::Stroke);
122 paint.antialias(true);
123 paint.color(gfx::rgba(255, 0, 0));
124
125 gfx::Path p;
126 p.moveTo(a.x, a.y);
127 p.lineTo(b.x, b.y);
128 p.lineTo(c.x, c.y);
129 p.lineTo(d.x, d.y);
130 p.close();
131
132 g->drawPath(p, paint);
133 }
134 // Mouse active areas
135 {
136 auto theme = SkinTheme::get(editor);
137 auto gfx = theme->parts.transformationHandle()->bitmap(0);
138
139 int handle_rs[2] = { gfx->width()*2, gfx->width()*3 };
140 for (int i=0; i<2; ++i) {
141 int handle_r = handle_rs[i];
142 for (size_t c=0; c<HANDLES; ++c) {
143 int x = (screenPoints[handles_info[c].i1].x+screenPoints[handles_info[c].i2].x)/2 - origin.x;
144 int y = (screenPoints[handles_info[c].i1].y+screenPoints[handles_info[c].i2].y)/2 - origin.y;
145 adjustHandle(x, y, handle_r, handle_r, angle + handles_info[c].angle);
146 g->drawRect(
147 gfx::rgba(255, 0, 0),
148 gfx::Rect(x, y, handle_r, handle_r));
149 }
150 }
151 }
152#endif
153
154 // Draw corner handle
155 for (size_t c=0; c<HANDLES; ++c) {
156 drawHandle(
157 editor, g,
158 (screenPoints[handles_info[c].i1].x+screenPoints[handles_info[c].i2].x)/2 - origin.x,
159 (screenPoints[handles_info[c].i1].y+screenPoints[handles_info[c].i2].y)/2 - origin.y,
160 angle + handles_info[c].angle);
161 }
162
163 // Draw the pivot
164 if (visiblePivot(angle)) {
165 gfx::Rect pivotBounds = getPivotHandleBounds(editor, transform, corners);
166 auto theme = SkinTheme::get(editor);
167 os::Surface* part = theme->parts.pivotHandle()->bitmap(0);
168
169 g->drawRgbaSurface(part,
170 pivotBounds.x - origin.x,
171 pivotBounds.y - origin.y);
172 }
173}
174
175void TransformHandles::invalidateHandles(Editor* editor, const Transformation& transform)
176{
177 auto theme = SkinTheme::get(editor);
178 double angle = transform.angle();
179
180 auto corners = transform.transformedCorners();
181
182 std::vector<gfx::Point> screenPoints;
183 getScreenPoints(editor, corners, screenPoints);
184
185 // Invalidate each corner handle.
186 for (size_t c=0; c<HANDLES; ++c) {
187 os::Surface* part = theme->parts.transformationHandle()->bitmap(0);
188 int u = (screenPoints[handles_info[c].i1].x+screenPoints[handles_info[c].i2].x)/2;
189 int v = (screenPoints[handles_info[c].i1].y+screenPoints[handles_info[c].i2].y)/2;
190
191 adjustHandle(u, v, part->width(), part->height(), angle + handles_info[c].angle);
192
193 editor->invalidateRect(gfx::Rect(u, v, part->width(), part->height()));
194 }
195
196 // Invalidate area where the pivot is.
197 if (visiblePivot(angle)) {
198 gfx::Rect pivotBounds = getPivotHandleBounds(editor, transform, corners);
199 os::Surface* part = theme->parts.pivotHandle()->bitmap(0);
200
201 editor->invalidateRect(
202 gfx::Rect(pivotBounds.x, pivotBounds.y,
203 part->width(), part->height()));
204 }
205}
206
207gfx::Rect TransformHandles::getPivotHandleBounds(Editor* editor,
208 const Transformation& transform,
209 const Transformation::Corners& corners)
210{
211 auto theme = SkinTheme::get(editor);
212 gfx::Size partSize = theme->parts.pivotHandle()->size();
213 gfx::Point screenPivotPos = editor->editorToScreen(gfx::Point(transform.pivot()));
214
215 screenPivotPos.x += editor->projection().applyX(1) / 2;
216 screenPivotPos.y += editor->projection().applyY(1) / 2;
217
218 return gfx::Rect(
219 screenPivotPos.x-partSize.w/2,
220 screenPivotPos.y-partSize.h/2,
221 partSize.w,
222 partSize.h);
223}
224
225bool TransformHandles::inHandle(const gfx::Point& pt, int x, int y, int gfx_w, int gfx_h, double angle)
226{
227 adjustHandle(x, y, gfx_w, gfx_h, angle);
228
229 return (pt.x >= x && pt.x < x+gfx_w &&
230 pt.y >= y && pt.y < y+gfx_h);
231}
232
233void TransformHandles::drawHandle(Editor* editor, Graphics* g, int x, int y, double angle)
234{
235 auto theme = SkinTheme::get(editor);
236 os::Surface* part = theme->parts.transformationHandle()->bitmap(0);
237
238 adjustHandle(x, y, part->width(), part->height(), angle);
239
240 g->drawRgbaSurface(part, x, y);
241}
242
243void TransformHandles::adjustHandle(int& x, int& y, int handle_w, int handle_h, double angle)
244{
245 angle = base::fmod_radians(angle + PI) + PI;
246 const int angleInt = std::clamp<int>(std::floor(8.0 * angle / (2.0*PI) + 0.5), 0, 8) % 8;
247
248 // Adjust x,y position depending the angle of the handle
249 switch (angleInt) {
250
251 case 0:
252 y = y-handle_h/2;
253 break;
254
255 case 1:
256 y = y-handle_h;
257 break;
258
259 case 2:
260 x = x-handle_w/2;
261 y = y-handle_h;
262 break;
263
264 case 3:
265 x = x-handle_w;
266 y = y-handle_h;
267 break;
268
269 case 4:
270 x = x-handle_w;
271 y = y-handle_h/2;
272 break;
273
274 case 5:
275 x = x-handle_w;
276 break;
277
278 case 6:
279 x = x-handle_w/2;
280 break;
281
282 case 7:
283 // x and y are correct
284 break;
285 }
286}
287
288bool TransformHandles::visiblePivot(double angle) const
289{
290 return (Preferences::instance().selection.pivotVisibility() || angle != 0);
291}
292
293void TransformHandles::getScreenPoints(
294 Editor* editor,
295 const Transformation::Corners& corners,
296 std::vector<gfx::Point>& screenPoints) const
297{
298 gfx::Point main = editor->mainTilePosition();
299
300 screenPoints.resize(corners.size());
301 for (size_t c=0; c<corners.size(); ++c)
302 screenPoints[c] = editor->editorToScreenF(
303 gfx::PointF(corners[c].x+main.x,
304 corners[c].y+main.y));
305}
306
307} // namespace app
308