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 }