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 | |
22 | namespace app { |
23 | |
24 | using namespace app::skin; |
25 | using namespace ui; |
26 | |
27 | static const int HANDLES = 8; |
28 | |
29 | static 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 | |
57 | HandleType 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 | |
89 | void 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 | |
175 | void 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 | |
207 | gfx::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 | |
225 | bool 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 | |
233 | void 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 | |
243 | void 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 | |
288 | bool TransformHandles::visiblePivot(double angle) const |
289 | { |
290 | return (Preferences::instance().selection.pivotVisibility() || angle != 0); |
291 | } |
292 | |
293 | void 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 | |