1 /*
2  * DSFML - The Simple and Fast Multimedia Library for D
3  *
4  * Copyright (c) 2013 - 2017 Jeremy DeHaan (dehaan.jeremiah@gmail.com)
5  *
6  * This software is provided 'as-is', without any express or implied warranty.
7  * In no event will the authors be held liable for any damages arising from the
8  * use of this software.
9  *
10  * Permission is granted to anyone to use this software for any purpose,
11  * including commercial applications, and to alter it and redistribute it
12  * freely, subject to the following restrictions:
13  *
14  * 1. The origin of this software must not be misrepresented; you must not claim
15  * that you wrote the original software. If you use this software in a product,
16  * an acknowledgment in the product documentation would be appreciated but is
17  * not required.
18  *
19  * 2. Altered source versions must be plainly marked as such, and must not be
20  * misrepresented as being the original software.
21  *
22  * 3. This notice may not be removed or altered from any source distribution
23  */
24 
25 /**
26  * $(U Shape) is a drawable class that allows to define and display a custom
27  * convex shape on a render target.
28  *
29  * It's only an abstract base, it needs to be specialized for concrete types of
30  * shapes (circle, rectangle, convex polygon, star, ...).
31  *
32  * In addition to the attributes provided by the specialized shape classes, a
33  * shape always has the following attributes:
34  * $(UL
35  * $(LI a texture)
36  * $(LI a texture rectangle)
37  * $(LI a fill color)
38  * $(LI an outline color)
39  * $(LI an outline thickness))
40  *
41  * $(PARA Each feature is optional, and can be disabled easily:)
42  * $(UL
43  * $(LI the texture can be null)
44  * $(LI the fill/outline colors can be Color.Transparent)
45  * $(LI the outline thickness can be zero))
46  *
47  * $(PARA You can write your own derived shape class, there are only two
48  * abstract functions to override:)
49  * $(UL
50  * $(LI `getPointCount` must return the number of points of the shape)
51  * $(LI `getPoint` must return the points of the shape))
52  *
53  * See_Also:
54  * $(RECTANGLESHAPE_LINK), $(CIRCLESHAPE_LINK), $(CONVEXSHAPE_LINK),
55  * $(TRANSFORMABLE_LINK)
56  */
57 module dsfml.graphics.shape;
58 
59 import dsfml.system.vector2;
60 
61 import dsfml.graphics.color;
62 import dsfml.graphics.drawable;
63 import dsfml.graphics.primitivetype;
64 import dsfml.graphics.rect;
65 import dsfml.graphics.rendertarget;
66 import dsfml.graphics.renderstates;
67 import dsfml.graphics.texture;
68 import dsfml.graphics.transformable;
69 import dsfml.graphics.vertexarray;
70 
71 import std.typecons : Rebindable;
72 
73 /**
74  * Base class for textured shapes with outline.
75  */
76 class Shape : Drawable, Transformable
77 {
78     mixin NormalTransformable;
79 
80     protected this()
81     {
82         m_vertices = new VertexArray(PrimitiveType.TrianglesFan,0);
83         m_outlineVertices = new VertexArray(PrimitiveType.TrianglesStrip,0);
84     }
85 
86     private
87     {
88         // Texture of the shape
89         Rebindable!(const(Texture)) m_texture;
90         // Rectangle defining the area of the source texture to display
91         IntRect m_textureRect;
92         // Fill color
93         Color m_fillColor;
94         // Outline color
95         Color m_outlineColor;
96         // Thickness of the shape's outline
97         float m_outlineThickness = 0;
98         // Vertex array containing the fill geometry
99         VertexArray m_vertices;
100         // Vertex array containing the outline geometry
101         VertexArray m_outlineVertices;
102         // Bounding rectangle of the inside (fill)
103         FloatRect m_insideBounds;
104         // Bounding rectangle of the whole shape (outline + fill)
105         FloatRect m_bounds;
106     }
107 
108     @property
109     {
110         /**
111          * The sub-rectangle of the texture that the shape will display.
112          *
113          * The texture rect is useful when you don't want to display the whole
114          * texture, but rather a part of it. By default, the texture rect covers
115          * the entire texture.
116          */
117         IntRect textureRect(IntRect rect)
118         {
119             m_textureRect = rect;
120             updateTexCoords();
121             return rect;
122         }
123         /// ditto
124         IntRect textureRect() const
125         {
126             return m_textureRect;
127         }
128     }
129 
130     @property
131     {
132         /**
133          * The fill color of the shape.
134          *
135          * This color is modulated (multiplied) with the shape's texture if any.
136          * It can be used to colorize the shape, or change its global opacity.
137          * You can use `Color.Transparent` to make the inside of the shape
138          * transparent, and have the outline alone. By default, the shape's fill
139          * color is opaque white.
140          */
141         Color fillColor(Color color)
142         {
143             m_fillColor = color;
144             updateFillColors();
145             return color;
146         }
147         /// ditto
148         Color fillColor() const
149         {
150             return m_fillColor;
151         }
152     }
153 
154     @property
155     {
156         /**
157          * The outline color of the shape.
158          *
159          * By default, the shape's outline color is opaque white.
160          */
161         Color outlineColor(Color color)
162         {
163             m_outlineColor = color;
164             updateOutlineColors();
165             return color;
166         }
167         /// ditto
168         Color outlineColor() const
169         {
170             return m_outlineColor;
171         }
172     }
173 
174     @property
175     {
176         /**
177          * The thickness of the shape's outline.
178          *
179          * Note that negative values are allowed (so that the outline expands
180          * towards the center of the shape), and using zero disables the
181          * outline. By default, the outline thickness is 0.
182          */
183         float outlineThickness(float thickness)
184         {
185             m_outlineThickness = thickness;
186             update();
187             return thickness;
188         }
189         /// ditto
190         float outlineThickness() const
191         {
192             return m_outlineThickness;
193         }
194     }
195 
196     @property
197     {
198         /**
199          * The total number of points in the shape.
200          */
201         abstract uint pointCount();
202     }
203 
204     /**
205      * Get the global bounding rectangle of the entity.
206      *
207      * The returned rectangle is in global coordinates, which means that it
208      * takes in account the transformations (translation, rotation, scale, ...)
209      * that are applied to the entity. In other words, this function returns the
210      * bounds of the sprite in the global 2D world's coordinate system.
211      *
212      * Returns: Global bounding rectangle of the entity.
213      */
214     FloatRect getGlobalBounds()
215     {
216         return getTransform().transformRect(getLocalBounds());
217     }
218 
219     /**
220      * Get the local bounding rectangle of the entity.
221      *
222      * The returned rectangle is in local coordinates, which means that it
223      * ignores the transformations (translation, rotation, scale, ...) that are
224      * applied to the entity. In other words, this function returns the bounds
225      * of the entity in the entity's coordinate system.
226      *
227      * Returns: Local bounding rectangle of the entity.
228      */
229     FloatRect getLocalBounds() const
230     {
231         return m_bounds;
232     }
233 
234     /**
235      * Get a point of the shape.
236      *
237      * The result is undefined if index is out of the valid range.
238      *
239      * Params:
240      * 	index = Index of the point to get, in range [0 .. getPointCount() - 1]
241      *
242      * Returns: Index-th point of the shape.
243      */
244     abstract Vector2f getPoint(uint index) const;
245 
246     /**
247      * Get the source texture of the shape.
248      *
249      * If the shape has no source texture, a NULL pointer is returned. The
250      * returned pointer is const, which means that you can't modify the texture
251      * when you retrieve it with this function.
252      *
253      * Returns: The shape's texture.
254      */
255     const(Texture) getTexture() const
256     {
257         return m_texture;
258     }
259 
260     /**
261      * Change the source texture of the shape.
262      *
263      * The texture argument refers to a texture that must exist as long as the
264      * shape uses it. Indeed, the shape doesn't store its own copy of the
265      * texture, but rather keeps a pointer to the one that you passed to this
266      * function. If the source texture is destroyed and the shape tries to use
267      * it, the behaviour is undefined. texture can be NULL to disable texturing.
268      *
269      * If resetRect is true, the TextureRect property of the shape is
270      * automatically adjusted to the size of the new texture. If it is false,
271      * the texture rect is left unchanged.
272      *
273      * Params:
274      * 	texture	  = New texture
275      * 	resetRect = Should the texture rect be reset to the size of the new
276      *              texture?
277      */
278     void setTexture(const(Texture) texture, bool resetRect = false)
279     {
280         if((texture !is null) && (resetRect || (m_texture is null)))
281         {
282             textureRect = IntRect(0, 0, texture.getSize().x, texture.getSize().y);
283         }
284 
285         m_texture = (texture is null)? null:texture;
286     }
287 
288     /**
289      * Draw the shape to a render target.
290      *
291      * Params:
292      * 		renderTarget	= Target to draw to
293      * 		renderStates	= Current render states
294      */
295     override void draw(RenderTarget renderTarget, RenderStates renderStates)
296     {
297         renderStates.transform = renderStates.transform * getTransform();
298         renderStates.texture = m_texture;
299         renderTarget.draw(m_vertices, renderStates);
300 
301         // Render the outline
302         if (m_outlineThickness != 0)
303         {
304             renderStates.texture = null;
305             renderTarget.draw(m_outlineVertices, renderStates);
306         }
307     }
308 
309     /**
310      * Recompute the internal geometry of the shape.
311      *
312      * This function must be called by the derived class everytime the shape's
313      * points change (ie. the result of either `getPointCount` or `getPoint` is
314      * different).
315      */
316     protected void update()
317     {
318         // Get the total number of points of the shape
319         uint count = pointCount();
320         if (count < 3)
321         {
322             m_vertices.resize(0);
323             m_outlineVertices.resize(0);
324             return;
325         }
326 
327         m_vertices.resize(count + 2); // + 2 for center and repeated first point
328 
329         // Position
330         for (uint i = 0; i < count; ++i)
331         {
332             m_vertices[i + 1].position = getPoint(i);
333         }
334         m_vertices[count + 1].position = m_vertices[1].position;
335 
336         // Update the bounding rectangle
337         m_vertices[0] = m_vertices[1]; // so that the result of getBounds() is correct
338         m_insideBounds = m_vertices.getBounds();
339 
340         // Compute the center and make it the first vertex
341         m_vertices[0].position.x = m_insideBounds.left + m_insideBounds.width / 2;
342         m_vertices[0].position.y = m_insideBounds.top + m_insideBounds.height / 2;
343 
344         // Color
345         updateFillColors();
346 
347         // Texture coordinates
348         updateTexCoords();
349 
350         // Outline
351         updateOutline();
352     }
353 
354     private
355     {
356         Vector2f computeNormal(Vector2f p1, Vector2f p2)
357         {
358             Vector2f normal = Vector2f(p1.y - p2.y, p2.x - p1.x);
359             float length = sqrt(normal.x * normal.x + normal.y * normal.y);
360             if (length != 0f)
361             {
362                 normal /= length;
363             }
364             return normal;
365         }
366 
367         float dotProduct(Vector2f p1, Vector2f p2)
368         {
369             return (p1.x * p2.x) + (p1.y * p2.y);
370         }
371 
372         //update methods
373         void updateFillColors()
374         {
375             for(uint i = 0; i < m_vertices.getVertexCount(); ++i)
376             {
377                 m_vertices[i].color = m_fillColor;
378             }
379         }
380 
381         void updateTexCoords()
382         {
383 
384             for (uint i = 0; i < m_vertices.getVertexCount(); ++i)
385             {
386                 float xratio = (m_vertices[i].position.x - m_insideBounds.left) / m_insideBounds.width;
387                 float yratio = (m_vertices[i].position.y - m_insideBounds.top) / m_insideBounds.height;
388 
389                 m_vertices[i].texCoords.x = m_textureRect.left + m_textureRect.width * xratio;
390                 m_vertices[i].texCoords.y = m_textureRect.top + m_textureRect.height * yratio;
391 
392             }
393         }
394 
395         void updateOutline()
396         {
397             uint count = m_vertices.getVertexCount() - 2;
398             m_outlineVertices.resize((count + 1) * 2);
399 
400             for (uint i = 0; i < count; ++i)
401             {
402                 uint index = i + 1;
403 
404                 // Get the two segments shared by the current point
405                 Vector2f p0 = (i == 0) ? m_vertices[count].position : m_vertices[index - 1].position;
406                 Vector2f p1 = m_vertices[index].position;
407                 Vector2f p2 = m_vertices[index + 1].position;
408 
409                 // Compute their normal
410                 Vector2f n1 = computeNormal(p0, p1);
411                 Vector2f n2 = computeNormal(p1, p2);
412 
413                 // Make sure that the normals point towards the outside of the shape
414                 // (this depends on the order in which the points were defined)
415                 if (dotProduct(n1, m_vertices[0].position - p1) > 0)
416                     n1 = -n1;
417                 if (dotProduct(n2, m_vertices[0].position - p1) > 0)
418                     n2 = -n2;
419 
420                 // Combine them to get the extrusion direction
421                 float factor = 1f + (n1.x * n2.x + n1.y * n2.y);
422                 Vector2f normal = (n1 + n2) / factor;
423 
424                 // Update the outline points
425                 m_outlineVertices[i * 2 + 0].position = p1;
426                 m_outlineVertices[i * 2 + 1].position = p1 + normal * m_outlineThickness;
427             }
428 
429             // Duplicate the first point at the end, to close the outline
430             m_outlineVertices[count * 2 + 0].position = m_outlineVertices[0].position;
431             m_outlineVertices[count * 2 + 1].position = m_outlineVertices[1].position;
432 
433             // Update outline colors
434             updateOutlineColors();
435 
436             // Update the shape's bounds
437             m_bounds = m_outlineVertices.getBounds();
438         }
439 
440         void updateOutlineColors()
441         {
442             for (uint i = 0; i < m_outlineVertices.getVertexCount(); ++i)
443             {
444                 m_outlineVertices[i].color = m_outlineColor;
445             }
446         }
447     }
448 }
449 
450 unittest
451 {
452     //meant to be inherited. Unit test?
453 }