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