1 /** 2 * Surface 3 */ 4 module d2d.sdl2.Surface; 5 6 import std.algorithm; 7 import std.range; 8 import std..string; 9 import d2d.sdl2; 10 11 /** 12 * Surfaces are a rectangular collection of pixels 13 * Surfaces are easy to work with and edit and can be blitted on to another surface 14 * Surfaces can also be converted to textures which are more efficient but less flexible 15 * Surfaces are handled in software as opposed to textures which are handled in hardware 16 * Surfaces can be used, but when used repeatedly and stored, textures should be preferred 17 * Surface draw methods do not respect alpha, but surface blitting does; to draw with alpha, draw to another surface, and blit to desired surface 18 */ 19 class Surface { 20 21 private SDL_Surface* surface; 22 23 /** 24 * Returns the raw SDL data of this object 25 */ 26 @property SDL_Surface* handle() { 27 return this.surface; 28 } 29 30 /** 31 * Gets the surfaces dimensions as a vector with width as the x component and height as the y component 32 */ 33 @property iVector dimensions() { 34 return new iVector(this.surface.w, this.surface.h); 35 } 36 37 /** 38 * Sets the alpha modifier for the surface 39 * Alpha modification works by multiplying the alphaMultiplier / 255 into the surface pixels 40 */ 41 @property void alphaMod(ubyte alphaMultiplier) { 42 ensureSafe(SDL_SetSurfaceAlphaMod(this.surface, alphaMultiplier)); 43 } 44 45 /** 46 * Gets the alpha modifier for the surface 47 * Alpha modification works by multiplying the alphaMultiplier / 255 into the surface pixels 48 */ 49 @property ubyte alphaMod() { 50 ubyte alphaMultiplier; 51 ensureSafe(SDL_GetSurfaceAlphaMod(this.surface, &alphaMultiplier)); 52 return alphaMultiplier; 53 } 54 55 /** 56 * Sets the surface's blend mode 57 */ 58 @property void blendMode(SDL_BlendMode bMode) { 59 ensureSafe(SDL_SetSurfaceBlendMode(this.surface, bMode)); 60 } 61 62 /** 63 * Gets the surface's blend mode 64 */ 65 @property SDL_BlendMode blendMode() { 66 SDL_BlendMode bMode; 67 ensureSafe(SDL_GetSurfaceBlendMode(this.surface, &bMode)); 68 return bMode; 69 } 70 71 /** 72 * Sets the clip boundaries for the surface 73 * Anything put on the surface outside of the clip boundaries gets discarded 74 */ 75 @property void clipRect(iRectangle clipArea) { 76 ensureSafe(SDL_SetClipRect(this.surface, (clipArea is null) ? null : clipArea.handle)); 77 } 78 79 /** 80 * Gets the clip boundaries for the surface 81 * Anything put on the surface outside of the clip boundaries gets discarded 82 */ 83 @property iRectangle clipRect() { 84 SDL_Rect clipArea; 85 SDL_GetClipRect(this.surface, &clipArea); 86 return new iRectangle(clipArea.x, clipArea.y, clipArea.w, clipArea.h); 87 } 88 89 /** 90 * Sets the color modifier for the surface 91 * Color modification works by multiplying the colorMultiplier / 255 into the surface pixels 92 */ 93 @property void colorMod(Color colorMultiplier) { 94 ensureSafe(SDL_SetSurfaceColorMod(this.surface, colorMultiplier.r, 95 colorMultiplier.g, colorMultiplier.b)); 96 } 97 98 /** 99 * Gets the color modifier for the surface 100 * Color modification works by multiplying the colorMultiplier / 255 into the surface pixels 101 */ 102 @property Color colorMod() { 103 Color colorMultiplier; 104 ensureSafe(SDL_GetSurfaceColorMod(this.surface, &colorMultiplier.r, 105 &colorMultiplier.g, &colorMultiplier.b)); 106 return colorMultiplier; 107 } 108 109 /** 110 * Creates an RGB surface given at least a width and a height 111 */ 112 this(int width, int height, int depth = 32, uint flags = 0, uint Rmask = 0, 113 uint Gmask = 0, uint Bmask = 0, uint Amask = 0) { 114 loadLibSDL(); 115 this.surface = ensureSafe(SDL_CreateRGBSurface(flags, width, height, 116 depth, Rmask, Gmask, Bmask, Amask)); 117 } 118 119 /** 120 * Creates an RGB surface given at least a width, height, and an SDL_PixelFormatEnum 121 */ 122 this(int width, int height, uint format, int depth = 32, uint flags = 0) { 123 loadLibSDL(); 124 this.surface = ensureSafe(SDL_CreateRGBSurfaceWithFormat(flags, width, 125 height, depth, format)); 126 } 127 128 /** 129 * Creates a surface from another surface but with a different pixel format 130 */ 131 this(Surface src, SDL_PixelFormat* fmt, uint flags = 0) { 132 loadLibSDL(); 133 this.surface = ensureSafe(SDL_ConvertSurface(src.handle, fmt, flags)); 134 } 135 136 /** 137 * Creates a surface from another surface but with a different pixel format 138 */ 139 this(Surface src, uint fmt, uint flags = 0) { 140 loadLibSDL(); 141 this.surface = ensureSafe(SDL_ConvertSurfaceFormat(src.handle, fmt, flags)); 142 } 143 144 /** 145 * Creates a surface from a BMP file path; for other image formats, use loadImage 146 */ 147 this(string bmpFilePath) { 148 loadLibSDL(); 149 this.surface = ensureSafe(SDL_LoadBMP(bmpFilePath.toStringz)); 150 } 151 152 /** 153 * Creates a surface from an already existing SDL_Surface 154 */ 155 this(SDL_Surface* alreadyExisting) { 156 this.surface = alreadyExisting; 157 } 158 159 /** 160 * Ensures that SDL can properly dispose of the surface 161 */ 162 ~this() { 163 SDL_FreeSurface(this.surface); 164 } 165 166 /** 167 * Saves the surface as a BMP with the given file name 168 */ 169 void saveBMP(string fileName) { 170 ensureSafe(SDL_SaveBMP(this.surface, fileName.toStringz)); 171 } 172 173 /** 174 * Blits another surface onto this surface 175 * Takes the surface to blit, the slice of the surface to blit, and where on this surface to blit to 176 * Is faster than a scaled blit to a rectangle 177 */ 178 void blit(Surface src, iRectangle srcRect, int dstX, int dstY) { 179 SDL_Rect dst = SDL_Rect(dstX, dstY, 0, 0); 180 ensureSafe(SDL_BlitSurface(src.handle, (srcRect is null) ? null 181 : srcRect.handle, this.surface, &dst)); 182 } 183 184 /** 185 * Does a scaled blit from another surface onto this surface 186 * Takes the surface to blit, the slice of the surface to blit, and the slice on this surface of where to blit to 187 * Is slower than the blit to a location 188 */ 189 void blit(Surface src, iRectangle srcRect, iRectangle dstRect) { 190 ensureSafe(SDL_BlitScaled(src.handle, (srcRect is null) ? null 191 : srcRect.handle, this.surface, (dstRect is null) ? null : dstRect.handle)); 192 } 193 194 /** 195 * Fills a rectangle of the surface with the given color 196 * Due to how SDL surfaces work, all other drawing functions on surface are built with this one 197 */ 198 void fill(iRectangle destination, Color color) { 199 ensureSafe(SDL_FillRect(this.surface, (destination is null) ? null 200 : destination.handle, SDL_MapRGBA(this.surface.format, color.r, 201 color.g, color.b, color.a))); 202 } 203 204 /** 205 * Draws a point on the surface with the given color 206 */ 207 void draw(int x, int y, Color color) { 208 this.fill(new iRectangle(x, y, 1, 1), color); 209 } 210 211 /** 212 * Draws a point on the surface with the given color 213 */ 214 void draw(iVector point, Color color) { 215 this.draw(point.x, point.y, color); 216 } 217 218 /** 219 * Draws a line on the surface with the given color 220 */ 221 void draw(iVector first, iVector second, Color color) { 222 if (first.x == second.x) { 223 this.fill(new iRectangle(first.x, first.y, 1, second.y - first.y), color); 224 } 225 else if (first.y == second.y) { 226 this.fill(new iRectangle(first.x, first.y, second.x - first.x, 1), color); 227 } 228 else { 229 foreach (x; iota(first.x, second.x, second.x > first.x ? 1 : -1)) { 230 //Iterating through x and using point slope form 231 immutable intersection = (second.y - first.y) / (second.x - first.x) * ( 232 x - first.x) + first.y; 233 this.draw(new iVector(x, intersection), color); 234 } 235 } 236 } 237 238 /** 239 * Draws a line on the surface with the given color 240 */ 241 void draw(iSegment line, Color color) { 242 this.draw(line.initial, line.terminal, color); 243 } 244 245 /** 246 * Draws a polygon on the surface with the given color 247 */ 248 void draw(uint sides)(iPolygon!sides toDraw, Color color) { 249 foreach (polygonSide; toDraw.sides) { 250 this.draw(polygonSide, color); 251 } 252 } 253 254 /** 255 * Draws a rectangle on the surface 256 */ 257 void draw(iRectangle rect, Color color) { 258 this.draw!4(rect.toPolygon(), color); 259 } 260 261 /** 262 * Draws the given bezier curve with numPoints number of points on the curve 263 * More points is smoother but slower 264 */ 265 void draw(uint numPoints = 100)(BezierCurve!(int, 2) curve, Color color) { 266 Vector!(int, 2)[] points = cast(Vector!(int, 2)[])(curve.getPoints!numPoints); 267 foreach (i; 0 .. points.length - 1) { 268 this.draw(new iSegment(points[i], points[i + 1]), color); 269 } 270 } 271 272 /** 273 * Draws the ellipse bounded by the given box between the given angles in radians 274 * More points generally means a slower but more well drawn ellipse 275 */ 276 void draw(uint numPoints = 100)(iRectangle bounds, double startAngle, double endAngle) { 277 immutable angleStep = (endAngle - startAngle) / numPoints; 278 iVector previousPoint; 279 iVector currentPoint = new iVector(-1); 280 foreach (i; 0 .. numPoints + 1) { 281 immutable currentAngle = startAngle + angleStep * i; 282 currentPoint.x = cast(int)(bounds.extent.x * cos(currentAngle) / 2); 283 currentPoint.y = cast(int)(bounds.extent.y * sin(currentAngle) / 2); 284 currentPoint += bounds.center; 285 if (previousPoint !is null) { 286 draw(previousPoint, currentPoint); 287 } 288 previousPoint = new iVector(currentPoint); 289 } 290 } 291 292 /** 293 * Draws the ellipse bounded by the given box between the given angles in radians with the given color 294 * More points generally means a slower but more well drawn ellipse 295 */ 296 void draw(uint numPoints = 100)(iRectangle bounds, double startAngle, 297 double endAngle, Color color) { 298 this.performWithColor(color, { 299 this.draw!numPoints(bounds, startAngle, endAngle); 300 }); 301 } 302 303 /** 304 * Fills the ellipse bounded by the given box between the given angles in radians 305 * Fills the ellipse between the arc endpoints: fills ellipse as arc rather than filling as ellipse (not a pizza slice) 306 * More points generally means a slower but more well drawn ellipse 307 */ 308 void fill(uint numPoints = 100)(iRectangle bounds, double startAngle, double endAngle) { 309 immutable angleStep = (endAngle - startAngle) / numPoints; 310 iPolygon!numPoints ellipseSlice = new iPolygon!numPoints(); 311 foreach (i; 0 .. numPoints) { 312 immutable currentAngle = startAngle + angleStep * i; 313 ellipseSlice.vertices[i] = bounds.center + new iVector( 314 cast(int)(bounds.extent.x * cos(currentAngle) / 2), 315 cast(int)(bounds.extent.y * sin(currentAngle) / 2)); 316 } 317 this.fill!numPoints(ellipseSlice); 318 } 319 320 /** 321 * Fills the ellipse bounded by the given box between the given angles in radians with the given color 322 * Fills the ellipse between the arc endpoints: fills ellipse as arc rather than filling as ellipse (not a pizza slice) 323 * More points generally means a slower but more well drawn ellipse 324 */ 325 void fill(uint numPoints = 100)(iRectangle bounds, double startAngle, 326 double endAngle, Color color) { 327 this.performWithColor(color, { 328 this.fill!numPoints(bounds, startAngle, endAngle); 329 }); 330 } 331 332 /** 333 * Fills a polygon on the surface with the given color 334 */ 335 void fill(uint sides)(iPolygon!sides toDraw, Color color) { 336 iRectangle bounds = bound(toDraw); 337 int[][int] intersections; //Stores a list of x coordinates of intersections accessed by the y value 338 foreach (polygonSide; toDraw.sides) { 339 foreach (y; bounds.initialPoint.y .. bounds.bottomLeft.y) { 340 //Checks that the y value exists within the segment 341 if ((y - polygonSide.initial.y) * (y - polygonSide.terminal.y) > 0) { 342 continue; 343 } 344 //If the segment is a horizontal line at this y, draws the horizontal line and then breaks 345 if (y == polygonSide.initial.y && polygonSide.initial.y == polygonSide.terminal.y) { 346 this.draw(new iSegment(polygonSide.initial, polygonSide.terminal), color); 347 continue; 348 } 349 //Vertical lines 350 if (polygonSide.initial.x == polygonSide.terminal.x) { 351 intersections[y] ~= polygonSide.initial.x; 352 continue; 353 } 354 iVector sideDirection = polygonSide.direction; 355 immutable dy = y - polygonSide.initial.y; 356 intersections[y] ~= (dy * sideDirection.x + polygonSide.initial.x * sideDirection.y) / sideDirection 357 .y; 358 359 } 360 } 361 foreach (y, xValues; intersections) { 362 foreach (i; 0 .. xValues.sort.length - 1) { 363 this.draw(new iSegment(new iVector(xValues[i], y), 364 new iVector(xValues[i + 1], y)), color); 365 } 366 } 367 } 368 369 } 370 371 /** 372 * Uses the SDL_Image library to create a non-bmp image surface 373 */ 374 Surface loadImage(string imagePath) { 375 loadLibSDL(); 376 loadLibImage(); 377 return new Surface(ensureSafe(IMG_Load(imagePath.toStringz))); 378 } 379 380 /** 381 * Returns a surface that fits the given rectangle 382 * Fits the original surface within the returned surface to be as large as it can while maintaining aspect ratio 383 * Also centers the original surface within the returned surface 384 */ 385 Surface scaled(Surface original, int desiredW, int desiredH) { 386 Surface scaledSurface = new Surface(desiredW, desiredH, SDL_PIXELFORMAT_RGBA32); 387 iVector newDimensions = cast(iVector)(cast(dVector) original.dimensions * min( 388 cast(double) desiredW / original.dimensions.x, 389 cast(double) desiredH / original.dimensions.y)); 390 iRectangle newLoc = new iRectangle((desiredW - newDimensions.x) / 2, 391 (desiredH - newDimensions.y) / 2, newDimensions.x, newDimensions.y); 392 scaledSurface.blit(original, null, newLoc); 393 return scaledSurface; 394 }