├── .gitignore ├── LICENSE ├── README.md ├── docs ├── tinamou.html └── tinamou │ ├── actions.html │ ├── core.html │ ├── exception.html │ ├── fontmanager.html │ ├── imagemanager.html │ ├── keyboard.html │ ├── mouse.html │ ├── painter.html │ ├── scene.html │ ├── scenemanager.html │ ├── sharedinfo.html │ ├── soundmanager.html │ ├── tools.html │ ├── transition.html │ ├── tween.html │ └── windowmanager.html ├── src ├── tinamou.nim └── tinamou │ ├── actions.nim │ ├── core.nim │ ├── exception.nim │ ├── fontmanager.nim │ ├── imagemanager.nim │ ├── keyboard.nim │ ├── mouse.nim │ ├── painter.nim │ ├── scene.nim │ ├── scenemanager.nim │ ├── sharedinfo.nim │ ├── soundmanager.nim │ ├── tools.nim │ ├── transition.nim │ ├── tween.nim │ └── windowmanager.nim ├── tests ├── nim.cfg └── test.nim └── tinamou.nimble /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | nimcache/ 3 | *.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Double_oxygeN 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tinamou 2 | 3 | Game Library in Nim with SDL2 4 | 5 | ## How to Use 6 | 7 | 1. Prepare this library. 8 | - Add `requires "tinamou"` in your `.nimble` file. 9 | 1. import `tinamou` module. 10 | 1. Create scenes. (See **How to Make Scenes**) 11 | 1. Do `startGame`. 12 | 1. That is all! 13 | 14 | ## Documentation 15 | 16 | See https://gh.double-oxygen.net/tinamou/tinamou.html 17 | 18 | ## How to Make Scenes 19 | 20 | All you have to do first in order to make a scene is extend `BaseScene`. 21 | 22 | ```nim 23 | # Example 24 | import tinamou 25 | import colors 26 | 27 | type 28 | ExScene = ref object of BaseScene 29 | count: Natural 30 | 31 | ExScene2 = ref object of BaseScene 32 | 33 | ExScene3 = ref object of BaseScene 34 | 35 | ``` 36 | 37 | Then override the methods. 38 | 39 | ```nim 40 | proc newExScene(): ExScene = 41 | new result 42 | 43 | method init(self: ExScene, tools: Tools, info: SharedInfo) = 44 | # Scene initialization should be written in `init` method. 45 | self.count = 0 46 | 47 | tools.imageManager.setImage("imageName", "res/img/example.png") 48 | tools.imageManager.setSprite("spriteName", "res/img/sprite.png", spriteWidth = 32, spriteHeight = 32) 49 | 50 | tools.soundManager.setMusic("bgmName", "res/music/example.ogg") 51 | tools.soundManager.setEffect("seName", "res/se/example.wav") 52 | 53 | tools.soundManager.getMusic("bgmName").play() 54 | 55 | method update(self: ExScene, tools: Tools, actions: Actions): Transition = 56 | # What the scene do each frames should be written in `update` method. 57 | # This method returns scene transition. 58 | result = stay() 59 | 60 | inc self.count 61 | 62 | if actions.keyboard.isPressed(SPACE): 63 | result = next("ex2") 64 | elif self.count >= 1200: 65 | result = next("ex3") 66 | elif actions.mouse.isPressed(RIGHT): 67 | tools.soundManager.getEffect("seName").play() 68 | 69 | method draw(self: ExScene, painter: Painter, tools: Tools, actions: Actions) = 70 | # Output to the screen should be written in `draw` method. 71 | painter.clear(colWhite) 72 | 73 | painter.drawImage(tools.imageManager.getImage("imageName"), 100, 50, 100, 80) 74 | painter.drawImage(tools.imageManager.getImage("spriteName"), 200, 10, spriteNum = 2) 75 | 76 | painter.setFont("res/fonts/example.ttf", 48) 77 | painter.text($actions.mouse.getPosition(), 150, 50).fill(colBlack) 78 | 79 | ``` 80 | 81 | And then register all scenes into scene manager. 82 | When starting the game, give the second argument of `startGame` the initial scene ID. 83 | 84 | ```nim 85 | when isMainModule: 86 | let scenes = newSceneManager() 87 | scenes.setScene("ex1", newExScene()) 88 | scenes.setScene("ex2", newExScene2()) 89 | scenes.setScene("ex3", newExScene3()) 90 | 91 | startGame(sceneManager = scenes, firstSceneId = "ex1", title = "Tinamou Example", width = 1200, height = 800, showFPS = true) 92 | 93 | ``` 94 | 95 | ## License 96 | 97 | MIT 98 | See LICENSE. 99 | 100 | Copyright (c) 2018 Double_oxygeN 101 | -------------------------------------------------------------------------------- /docs/tinamou.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Module tinamou 20 | 1183 | 1184 | 1185 | 1186 | 1211 | 1212 | 1213 | 1214 |
1215 |
1216 |

Module tinamou

1217 |
1218 |
1219 | 1223 |
1224 | Search: 1226 |
1227 |
1228 | Group by: 1229 | 1233 |
1234 |
    1235 |
  • 1236 | Imports 1237 |
      1238 | 1239 |
    1240 |
  • 1241 | 1242 |
1243 | 1244 |
1245 | 1255 |
1256 | 1257 |
1258 | 1263 |
1264 |
1265 |
1266 | 1267 | 1268 | 1269 | -------------------------------------------------------------------------------- /docs/tinamou/scene.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Module scene 20 | 1183 | 1184 | 1185 | 1186 | 1211 | 1212 | 1213 | 1214 |
1215 |
1216 |

Module scene

1217 |
1218 |
1219 | 1223 |
1224 | Search: 1226 |
1227 |
1228 | Group by: 1229 | 1233 |
1234 | 1263 | 1264 |
1265 |
1266 |
1267 |

Scene

1268 |
1269 |

Imports

1270 |
1271 | colors, sdl2, painter, tools, actions, exception, sharedinfo, transition 1272 |
1273 |
1274 |

Types

1275 |
1276 |
BaseScene = ref object of RootObj
1277 |   
1278 |
1279 | When you create scenes, extend it and implement methods. 1280 | 1281 |
1282 | 1283 |
1284 |
1285 |

Methods

1286 |
1287 |
method init(self: BaseScene; tools: Tools; info: SharedInfo) {.
base, raises: [], tags: []
.}
1288 |
1289 | Scene initialization. Please implement it! 1290 | 1291 |
1292 |
method update(self: BaseScene; tools: Tools; actions: Actions): Transition {.
base, 1293 | raises: [], tags: []
.}
1294 |
1295 | Update the scene. Returns the transition. Please implement it! 1296 | 1297 |
1298 |
method draw(self: BaseScene; painter: Painter; tools: Tools; actions: Actions) {.
base, 1299 | raises: [], tags: []
.}
1300 |
1301 | Output states to the window using painter. Please implement it! 1302 | 1303 |
1304 | 1305 |
1306 | 1307 |
1308 |
1309 | 1310 |
1311 | 1316 |
1317 |
1318 |
1319 | 1320 | 1321 | 1322 | -------------------------------------------------------------------------------- /docs/tinamou/scenemanager.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Module scenemanager 20 | 1183 | 1184 | 1185 | 1186 | 1211 | 1212 | 1213 | 1214 |
1215 |
1216 |

Module scenemanager

1217 |
1218 |
1219 | 1223 |
1224 | Search: 1226 |
1227 |
1228 | Group by: 1229 | 1233 |
1234 | 1264 | 1265 |
1266 |
1267 |
1268 |

Manager for Multiple Scenes

1269 |
1270 |

Imports

1271 |
1272 | tables, scene, transition, exception 1273 |
1274 |
1275 |

Types

1276 |
1277 |
SceneManager = ref object of RootObj
1278 |   table: TableRef[SceneId, BaseScene]
1279 | 
1280 |
1281 | 1282 | 1283 |
1284 | 1285 |
1286 |
1287 |

Procs

1288 |
1289 |
proc newSceneManager(): SceneManager {.
raises: [], tags: []
.}
1290 |
1291 | Create new SceneManager. 1292 | 1293 |
1294 |
proc setScene(self: SceneManager; name: SceneId; scene: BaseScene) {.
raises: [], tags: []
.}
1295 |
1296 | Set scene with key string. 1297 | 1298 |
1299 |
proc getScene(self: SceneManager; name: SceneId): BaseScene {.
1300 | raises: [TinamouException], tags: []
.}
1301 |
1302 | Get scene by key string. If no scenes are found, then throw exception. 1303 | 1304 |
1305 | 1306 |
1307 | 1308 |
1309 |
1310 | 1311 |
1312 | 1317 |
1318 |
1319 |
1320 | 1321 | 1322 | 1323 | -------------------------------------------------------------------------------- /docs/tinamou/sharedinfo.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Module sharedinfo 20 | 1183 | 1184 | 1185 | 1186 | 1211 | 1212 | 1213 | 1214 |
1215 |
1216 |

Module sharedinfo

1217 |
1218 |
1219 | 1223 |
1224 | Search: 1226 |
1227 |
1228 | Group by: 1229 | 1233 |
1234 |
    1235 |
  • 1236 | Types 1237 | 1242 |
  • 1243 |
  • 1244 | Lets 1245 | 1250 |
  • 1251 | 1252 |
1253 | 1254 |
1255 |
1256 |
1257 |

Information shared between two scenes.

1258 |
1259 |

Types

1260 |
1261 |
SharedInfo = ref object of RootObj
1262 |   
1263 |
1264 | Shared informations. Extend it and set what you want to share as fields. 1265 | 1266 |
1267 | 1268 |
1269 |
1270 |

Lets

1271 |
1272 |
NOSHARE: SharedInfo = new SharedInfo
1273 |
1274 | Use it when no information is shared. 1275 | 1276 |
1277 | 1278 |
1279 | 1280 |
1281 |
1282 | 1283 |
1284 | 1289 |
1290 |
1291 |
1292 | 1293 | 1294 | 1295 | -------------------------------------------------------------------------------- /src/tinamou.nim: -------------------------------------------------------------------------------- 1 | ## Tinamou Game Library 2 | 3 | import 4 | tinamou.core, 5 | tinamou.scene, 6 | tinamou.transition, 7 | tinamou.tools, 8 | tinamou.actions, 9 | tinamou.sharedinfo, 10 | tinamou.scenemanager, 11 | tinamou.imagemanager, 12 | tinamou.soundmanager, 13 | tinamou.windowmanager, 14 | tinamou.fontmanager, 15 | tinamou.painter, 16 | tinamou.keyboard, 17 | tinamou.mouse, 18 | tinamou.tween, 19 | tinamou.exception 20 | 21 | export 22 | core, 23 | scene.BaseScene, scene.init, scene.update, scene.draw, 24 | transition.Transition, transition.stay, transition.next, transition.final, transition.reset, 25 | tools.Tools, tools.imageManager, tools.soundManager, tools.windowManager, tools.fontManager, 26 | actions.Actions, actions.mouse, actions.keyboard, 27 | sharedinfo.SharedInfo, sharedinfo.NOSHARE, 28 | scenemanager.SceneManager, scenemanager.newSceneManager, scenemanager.setScene, 29 | imagemanager.Image, imagemanager.ImageManager, imagemanager.getSrc, imagemanager.isSprite, 30 | imagemanager.getSpriteSize, imagemanager.setImage, imagemanager.setSprite, imagemanager.getImage, 31 | soundmanager.Music, soundmanager.SoundEffect, soundmanager.SoundManager, 32 | soundmanager.play, soundmanager.stopMusic, soundmanager.stopAllEffects, 33 | soundmanager.setMusic, soundmanager.setEffect, soundmanager.getMusic, soundmanager.getEffect, 34 | soundmanager.setMusicVolume, soundmanager.setEffectVolume, soundmanager.getMusicVolume, soundmanager.getEffectVolume, 35 | windowmanager.WindowManager, windowmanager.getWindowSize, windowManager.setWindowSize, 36 | windowmanager.getResolution, windowmanager.setResolution, windowManager.isFullScreen, 37 | windowmanager.setFullScreen, windowmanager.alert, 38 | fontmanager.Font, fontmanager.FontManager, fontmanager.setFont, fontmanager.getFont, 39 | painter, 40 | keyboard.Keyboard, keyboard.KeyName, keyboard.isPressed, keyboard.isDown, keyboard.isReleased, keyboard.getPressingKeyNames, 41 | mouse.Mouse, mouse.MouseButton, mouse.isPressed, mouse.isDown, mouse.isReleased, mouse.getPosition, 42 | tween, 43 | exception.TinamouException, exception.`$`, exception.getErrorCode 44 | 45 | 46 | when isMainModule: 47 | type 48 | TestScene = ref object of BaseScene 49 | 50 | proc newTestScene: TestScene = 51 | new result 52 | 53 | let scenes = newSceneManager() 54 | scenes.setScene("test", newTestScene()) 55 | 56 | echo "This file was compiled on ", CompileDate, " at ", CompileTime 57 | 58 | startGame scenes, "test", "foobar", 600, 400 59 | -------------------------------------------------------------------------------- /src/tinamou/actions.nim: -------------------------------------------------------------------------------- 1 | ## User Input Managers 2 | 3 | import 4 | sdl2, 5 | keyboard, 6 | mouse 7 | 8 | type 9 | Actions* = ref object of RootObj 10 | mouse: Mouse 11 | keyboard: Keyboard 12 | 13 | proc newActions*(): Actions = 14 | ## Create new actions. 15 | new result 16 | result.mouse = newMouse() 17 | result.keyboard = newKeyboard() 18 | 19 | proc mouse*(self: Actions): Mouse = self.mouse 20 | proc keyboard*(self: Actions): Keyboard = self.keyboard 21 | 22 | proc update*(self: Actions, event: Event) = 23 | ## Update actions. 24 | self.keyboard.update(event) 25 | self.mouse.update(event) 26 | 27 | proc frameEnd*(self: Actions) = 28 | ## Tell keyboard the end of a frame. 29 | frameEnd self.keyboard 30 | frameEnd self.mouse 31 | -------------------------------------------------------------------------------- /src/tinamou/core.nim: -------------------------------------------------------------------------------- 1 | ## Tinamou Core Utilities 2 | 3 | import 4 | # std 5 | math, 6 | times, 7 | 8 | # sdl2 9 | sdl2, 10 | sdl2.image, 11 | sdl2.ttf, 12 | sdl2.mixer, 13 | 14 | # tinamou 15 | exception, 16 | scene, 17 | scenemanager, 18 | painter, 19 | windowmanager, 20 | tools, 21 | actions, 22 | sharedinfo, 23 | transition 24 | 25 | const 26 | framesPerSecond: int = 61 27 | oneFrameInterval: float = 1e3 / framesPerSecond.toFloat() # [ms] 28 | errorLogFileName: string = "TinamouError.log" 29 | 30 | type 31 | ExceptionHandler* = proc (exception: ref TinamouException, msg: string, tools: Tools) 32 | 33 | proc defaultExceptionHandler*(exception: ref TinamouException, msg: string, tools: Tools) = 34 | tools.windowManager.alert(message = "Tinamou caught an exception!\pFor details, please see " & errorLogFileName & ".", title = "Tinamou Error") 35 | 36 | var 37 | exceptionHandler: ExceptionHandler = defaultExceptionHandler 38 | 39 | proc calcFPSMaker(startTicks: uint32, interval: int = 30): (proc (ticks: uint32): float) = 40 | var 41 | prevTicks: uint32 = startTicks 42 | currentFPS: float = 0 43 | ticksHistory: seq[uint32] = @[] 44 | 45 | return proc (ticks: uint32): float = 46 | ticksHistory.add(ticks - prevTicks) 47 | prevTicks = ticks 48 | 49 | if ticksHistory.len >= interval: 50 | currentFPS = (ticksHistory.len * 1000).float / sum(ticksHistory).float 51 | ticksHistory = @[] 52 | 53 | return currentFPS 54 | 55 | var 56 | quitDialogButtons: array[2, MessageBoxButtonData] = [ 57 | MessageBoxButtonData(flags: SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, buttonid: 0, text: "cancel"), 58 | MessageBoxButtonData(flags: SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, buttonid: 1, text: "OK")] 59 | quitDialogData: MessageBoxData = 60 | MessageBoxData(flags: SDL_MESSAGEBOX_WARNING, window: nil, title: "Confirmation", message: "Are you sure you want to quit?", numbuttons: 2, buttons: cast[ptr MessageBoxButtonData](addr quitDialogButtons), colorScheme: nil) 61 | 62 | template embed*(path: string): RWopsPtr = 63 | ## Embed resource from path. 64 | ## 65 | ## **Example:** 66 | ## :: 67 | ## method init(self: ExScene, tools: Tools) = 68 | ## tools.imageManager.setImage "sampleImg", embed(currentSourcePath().splitFile().dir / "../../res/img/sample_img.png") 69 | const file = staticRead(path) 70 | rwFromConstMem(file.cstring, file.len) 71 | 72 | proc startGame*(sceneManager: SceneManager; firstSceneId: SceneId; title: string; width, height: int = 600; showFPS: bool = false) = 73 | ## Start the game 74 | 75 | if not sdl2.init(INIT_VIDEO or INIT_AUDIO or INIT_TIMER or INIT_EVENTS): 76 | raise newTinamouException(INIT_ERROR_CODE, "SDL2 Initialization failed. " & $sdl2.getError()) 77 | defer: sdl2.quit() 78 | 79 | if image.init(IMG_INIT_JPG or IMG_INIT_PNG) == 0: 80 | raise newTinamouException(INIT_ERROR_CODE, "SDL2_Image Initialization failed. " & $sdl2.getError()) 81 | defer: image.quit() 82 | 83 | if not ttfInit(): 84 | raise newTinamouException(INIT_ERROR_CODE, "SDL2_ttf Initialization failed. " & $sdl2.getError()) 85 | defer: ttfQuit() 86 | 87 | if openAudio(mixer.MIX_DEFAULT_FREQUENCY, mixer.MIX_DEFAULT_FORMAT, 2, 1024) < 0: 88 | raise newTinamouException(INIT_ERROR_CODE, "SDL2_mixer Initialization failed. " & $sdl2.getError()) 89 | defer: 90 | closeAudio() 91 | mixer.quit() 92 | 93 | # allocate channels for playing sound effects 94 | discard allocateChannels(16) 95 | 96 | if not sdl2.setHint("SDL_RENDER_SCALE_QUALITY", "best"): 97 | raise newTinamouException(INIT_TEXTURE_FILTERING_ERROR_CODE, "Linear texture filtering could not be enabled. " & $sdl2.getError()) 98 | 99 | let window = createWindow( 100 | title = title, 101 | x = SDL_WINDOWPOS_CENTERED, y = SDL_WINDOWPOS_CENTERED, 102 | w = width.cint, h = height.cint, 103 | flags = SDL_WINDOW_SHOWN) 104 | if window.isNil: 105 | raise newTinamouException(WINDOW_CREATION_ERROR_CODE, "Window could not be created. " & $sdl2.getError()) 106 | defer: destroy window 107 | 108 | let renderer = window.createRenderer(index = -1, 109 | flags = Renderer_Accelerated or Renderer_PresentVsync) 110 | if renderer.isNil: 111 | raise newTinamouException(RENDERER_CREATION_ERROR_CODE, "Renderer could not be created. " & $sdl2.getError()) 112 | defer: destroy renderer 113 | 114 | # Initialize tools. 115 | let 116 | painter: Painter = newTPainter(renderer) 117 | tools: Tools = newTools(window, renderer) 118 | actions: Actions = newActions() 119 | defer: destroy tools 120 | 121 | tools.windowManager.setResolution(width = width, height = height) 122 | 123 | let calcFPS = calcFPSMaker(getTicks()) 124 | 125 | # Declare variables. 126 | var 127 | q: bool = false 128 | e: Event = sdl2.defaultEvent 129 | currentScene: BaseScene 130 | ticks: uint32 131 | intervalTime: float = 0'f32 132 | 133 | try: 134 | currentScene = sceneManager.getScene(firstSceneId) 135 | currentScene.init(tools, NOSHARE) 136 | 137 | ticks = getTicks() 138 | 139 | # Start game-loop. 140 | while not q: 141 | 142 | # Event handling. 143 | while e.pollEvent(): 144 | 145 | if e.kind == QuitEvent: 146 | ## TODO: quit handling 147 | var buttonId: cint 148 | if showMessageBox(messageboxdata = addr quitDialogData, buttonid = buttonId) < 0: 149 | raise newTinamouException(WINDOW_CREATION_ERROR_CODE, "Could not show quit dialog.") 150 | elif buttonId == 1: 151 | q = true 152 | 153 | actions.update(e) 154 | 155 | # Drawing. 156 | if intervalTime >= 0: 157 | currentScene.draw(painter, tools, actions) 158 | painter.present() 159 | 160 | # Update states. 161 | let 162 | transition = currentScene.update(tools, actions) 163 | 164 | if transition.isStay(): 165 | discard 166 | 167 | elif transition.isNext(): 168 | # TODO: transition animation is not implemented yet 169 | let 170 | nextSceneId = transition.getNextSceneId() 171 | sharedInfo = transition.getSharedInfo() 172 | currentScene = sceneManager.getScene(nextSceneId) 173 | currentScene.init(tools, sharedInfo) 174 | 175 | elif transition.isFinal(): 176 | q = true 177 | 178 | elif transition.isReset(): 179 | currentScene = sceneManager.getScene(firstSceneId) 180 | currentScene.init(tools, NOSHARE) 181 | 182 | else: 183 | raise newTinamouException(UNKNOWN_TRANSITION_ERROR_CODE, "Unknown transition.") 184 | 185 | # FPS control 186 | intervalTime += oneFrameInterval 187 | let 188 | endTicks: uint32 = getTicks() 189 | restTime: float = intervalTime - (endTicks.int - ticks.int).toFloat() 190 | if restTime > 0: 191 | sdl2.delay(restTime.toInt().uint32) 192 | intervalTime = 0 193 | else: 194 | intervalTime = restTime 195 | ticks = getTicks() 196 | 197 | if showFPS: 198 | let fps = calcFPS(ticks).round(2) 199 | window.setTitle title & " (FPS = " & $fps & ")" 200 | 201 | frameEnd actions 202 | 203 | except TinamouException: 204 | 205 | let 206 | currentException: ref TinamouException = cast[ref TinamouException](getCurrentException()) 207 | currentExceptionMsg: string = getCurrentExceptionMsg() 208 | 209 | try: 210 | let logFile: File = open(filename = errorLogFileName, mode = fmAppend) 211 | defer: close logFile 212 | 213 | logFile.write "Tinamou caught an exception!\pat: " & times.now().format("dd/MMM/yyyy HH:mm:ss ('GMT'z)") & "\p" & currentExceptionMsg & "\p" 214 | if stackTraceAvailable(): logFile.write getStackTrace(currentException) 215 | logFile.write "\p" 216 | 217 | exceptionHandler(currentException, currentExceptionMsg, tools) 218 | 219 | except IOError: 220 | 221 | stderr.write "Tinamou caught an exception!\p" & currentExceptionMsg & "\p" 222 | if stackTraceAvailable(): stderr.write getStackTrace(currentException) 223 | stderr.write errorLogFileName & " file could not open." 224 | 225 | proc setExceptionHandler*(handler: ExceptionHandler): ExceptionHandler = 226 | ## Set exception handler. 227 | result = exceptionHandler 228 | exceptionHandler = handler 229 | -------------------------------------------------------------------------------- /src/tinamou/exception.nim: -------------------------------------------------------------------------------- 1 | ## Exception Handling 2 | 3 | type 4 | ErrorCode = distinct range[1000..9999] 5 | 6 | TinamouException* = object of Exception 7 | errorCode: ErrorCode 8 | 9 | proc `$`*(errorCode: ErrorCode): string {.borrow.} 10 | 11 | template newTinamouException*(code: ErrorCode, message: string): untyped = 12 | var 13 | e: ref TinamouException 14 | e = TinamouException.newException(message & " (Error Code: " & $code & ")") 15 | e.errorCode = code 16 | if e.isNil: assert false 17 | e 18 | 19 | proc getErrorCode*(self: TinamouException): ErrorCode = self.errorCode 20 | proc getErrorCode*(self: ref TinamouException): ErrorCode = self.errorCode 21 | 22 | const 23 | # 10xx: SDL2 Error 24 | INIT_ERROR_CODE* = 1001.ErrorCode 25 | INIT_TEXTURE_FILTERING_ERROR_CODE* = 1002.ErrorCode 26 | WINDOW_CREATION_ERROR_CODE* = 1003.ErrorCode 27 | RENDERER_CREATION_ERROR_CODE* = 1004.ErrorCode 28 | RENDERER_CONFIG_ERROR_CODE* = 1005.ErrorCode 29 | FULLSCREEN_ERROR* = 1006.ErrorCode 30 | 31 | # 15xx: Font Error 32 | FONT_LOAD_ERROR_CODE* = 1501.ErrorCode 33 | 34 | # 16xx: Image Error 35 | IMAGE_LOAD_ERROR_CODE* = 1601.ErrorCode 36 | 37 | # 17xx: Sound Error 38 | BGM_LOAD_ERROR_CODE* = 1701.ErrorCode 39 | SE_LOAD_ERROR_CODE* = 1702.ErrorCode 40 | BGM_PLAY_ERROR_CODE* = 1711.ErrorCode 41 | SE_PLAY_ERROR_CODE* = 1712.ErrorCode 42 | 43 | # 18xx: Implementation Error 44 | UNKNOWN_TRANSITION_ERROR_CODE* = 1801.ErrorCode 45 | SCENE_NOT_FOUND_ERROR_CODE* = 1802.ErrorCode 46 | INVALID_TRANSITION_ERROR_CODE* = 1803.ErrorCode 47 | FONT_NOT_FOUND_ERROR_CODE* = 1851.ErrorCode 48 | FONT_NEVER_SET_ERROR_CODE* = 1852.ErrorCode 49 | IMAGE_NOT_FOUND_ERROR_CODE* = 1861.ErrorCode 50 | BGM_NOT_FOUND_ERROR_CODE* = 1871.ErrorCode 51 | SE_NOT_FOUND_ERROR_CODE* = 1872.ErrorCode 52 | -------------------------------------------------------------------------------- /src/tinamou/fontmanager.nim: -------------------------------------------------------------------------------- 1 | ## Font Manager 2 | 3 | import 4 | tables, 5 | 6 | sdl2, 7 | sdl2.ttf, 8 | 9 | exception 10 | 11 | type 12 | Font* = ref object of RootObj 13 | font: ttf.FontPtr 14 | 15 | FontSrc = ref object of RootObj 16 | src: string 17 | savedFonts: TableRef[Positive, Font] 18 | 19 | FontManager* = ref object of RootObj 20 | table: TableRef[string, FontSrc] 21 | 22 | proc newFont(path: string, fontSize: Positive): Font = 23 | ## Create new font. 24 | new result 25 | if ttfWasInit(): 26 | result.font = openFont(path, fontSize.cint) 27 | if result.font.isNil: 28 | raise newTinamouException(FONT_LOAD_ERROR_CODE, "Failed to load font " & path & ".") 29 | 30 | else: 31 | raise newTinamouException(INIT_ERROR_CODE, "SDL2_TTF was not initialized.") 32 | 33 | proc getFontPtr*(self: Font): ttf.FontPtr = self.font 34 | 35 | proc newFontSrc(path: string): FontSrc = 36 | ## Create new font source. 37 | new result 38 | result.src = path 39 | result.savedFonts = newTable[Positive, Font]() 40 | 41 | proc getFont(self: FontSrc, size: Positive): Font = 42 | ## Get font of given font size. 43 | if self.savedFonts.hasKey(size): 44 | result = self.savedFonts[size] 45 | else: 46 | result = newFont(self.src, size) 47 | self.savedFonts.add(size, result) 48 | 49 | proc newFontManager*: FontManager = 50 | ## Create new font manager. 51 | new result 52 | result.table = newTable[string, FontSrc]() 53 | 54 | proc setFont*(self: FontManager, name, path: string): FontManager {.discardable.} = 55 | ## Set a font. 56 | if not self.table.hasKey(name): 57 | self.table.add(name, newFontSrc(path)) 58 | 59 | proc getFont*(self: FontManager, name: string, size: Positive): Font = 60 | ## Get a font. 61 | ## This method requires font size. 62 | if self.table.hasKey(name): 63 | return self.table[name].getFont(size) 64 | else: 65 | raise newTinamouException(FONT_NOT_FOUND_ERROR_CODE, "Font '" & name & "' was not registered.") 66 | 67 | proc destroy*(self: FontManager) = 68 | ## Free font manager resources. 69 | for src in self.table.values: 70 | for font in src.savedFonts.values: 71 | ttf.close font.font 72 | 73 | clear src.savedFonts 74 | clear self.table 75 | -------------------------------------------------------------------------------- /src/tinamou/imagemanager.nim: -------------------------------------------------------------------------------- 1 | ## Image Manager 2 | 3 | import 4 | tables, 5 | 6 | sdl2, 7 | sdl2.image, 8 | 9 | exception 10 | 11 | type 12 | ImageKind = enum 13 | source, sprite 14 | 15 | Image* = ref object of RootObj 16 | src: string 17 | texture: TexturePtr 18 | width*, height*: int 19 | case kind: ImageKind 20 | of sprite: 21 | spriteWidth, spriteHeight: int 22 | spriteColumns: int 23 | of source: 24 | discard 25 | 26 | ImageManager* = ref object of RootObj 27 | table: TableRef[string, Image] 28 | renderer: RendererPtr 29 | 30 | proc newImage(renderer: RendererPtr, path: static[string]): Image = 31 | ## Create new image. 32 | new result 33 | result.src = path 34 | 35 | let surface = image.load(path) 36 | if surface.isNil: raise newTinamouException(IMAGE_LOAD_ERROR_CODE, "Image " & path & " could not be loaded. " & $sdl2.getError()) 37 | defer: freeSurface surface 38 | 39 | result.texture = renderer.createTextureFromSurface(surface) 40 | result.width = surface.w 41 | result.height = surface.h 42 | result.kind = source 43 | 44 | proc newImageFromRW(renderer: RendererPtr, src: RWopsPtr): Image = 45 | ## Create new image from data. 46 | new result 47 | result.src = "#!EMBEDDED" 48 | 49 | let surface = image.load_RW(src, 1) 50 | if surface.isNil: raise newTinamouException(IMAGE_LOAD_ERROR_CODE, "Image could not be loaded. " & $sdl2.getError()) 51 | defer: freeSurface surface 52 | 53 | result.texture = renderer.createTextureFromSurface(surface) 54 | result.width = surface.w 55 | result.height = surface.h 56 | result.kind = source 57 | 58 | proc newSprite(renderer: RendererPtr, path: static[string], spriteWidth, spriteHeight: static[int]): Image = 59 | ## Create new sprite. 60 | new result 61 | result.src = path 62 | 63 | let surface = image.load(path) 64 | if surface.isNil: raise newTinamouException(IMAGE_LOAD_ERROR_CODE, "Image " & path & " could not be loaded. " & $sdl2.getError()) 65 | defer: freeSurface surface 66 | 67 | result.texture = renderer.createTextureFromSurface(surface) 68 | result.width = spriteWidth 69 | result.height = spriteHeight 70 | result.kind = sprite 71 | result.spriteWidth = spriteWidth 72 | result.spriteHeight = spriteHeight 73 | result.spriteColumns = surface.w div spriteWidth 74 | 75 | proc newSpriteFromRW(renderer: RendererPtr, src: RWopsPtr, spriteWidth, spriteHeight: static[int]): Image = 76 | ## Create new sprite from data. 77 | new result 78 | result.src = "#!EMBEDDED" 79 | 80 | let surface = image.load_RW(src, 1) 81 | if surface.isNil: raise newTinamouException(IMAGE_LOAD_ERROR_CODE, "Image could not be loaded. " & $sdl2.getError()) 82 | defer: freeSurface surface 83 | 84 | result.texture = renderer.createTextureFromSurface(surface) 85 | result.width = spriteWidth 86 | result.height = spriteHeight 87 | result.kind = sprite 88 | result.spriteWidth = spriteWidth 89 | result.spriteHeight = spriteHeight 90 | result.spriteColumns = surface.w div spriteWidth 91 | 92 | proc getSrc*(self: Image): string = self.src 93 | proc getTexture*(self: Image): TexturePtr = self.texture 94 | proc isSprite*(self: Image): bool = self.kind == sprite 95 | proc getSpriteSize*(self: Image): tuple[width, height, columns: int] = 96 | if self.kind == sprite: 97 | (width: self.spriteWidth, height: self.spriteHeight, columns: self.spriteColumns) 98 | else: 99 | (width: self.width, height: self.height, columns: 0) 100 | 101 | proc destroy(self: Image) = 102 | ## Free image resources. 103 | destroy self.texture 104 | 105 | proc newImageManager*(renderer: RendererPtr): ImageManager = 106 | ## Create new image manager. 107 | new result 108 | result.renderer = renderer 109 | result.table = newTable[string, Image]() 110 | 111 | proc setImage*(self: ImageManager; name, path: static[string]): ImageManager {.discardable.} = 112 | ## Set an image. 113 | if not self.table.hasKey(name): 114 | self.table.add(name, newImage(self.renderer, path)) 115 | 116 | proc setImage*(self: ImageManager, name: static[string], src: RWopsPtr): ImageManager {.discardable.} = 117 | ## Set an image from embedded data. 118 | if not self.table.hasKey(name): 119 | self.table.add(name, newImageFromRW(self.renderer, src)) 120 | 121 | proc setSprite*(self: ImageManager; name, path: static[string]; spriteWidth, spriteHeight: static[int]): ImageManager {.discardable.} = 122 | ## Set a sprite image. 123 | if not self.table.hasKey(name): 124 | self.table.add(name, newSprite(self.renderer, path, spriteWidth, spriteHeight)) 125 | 126 | proc setSprite*(self: ImageManager; name: static[string]; src: RWopsPtr, spriteWidth, spriteHeight: static[int]): ImageManager {.discardable.} = 127 | ## Set a sprite image from embedded data. 128 | if not self.table.hasKey(name): 129 | self.table.add(name, newSpriteFromRW(self.renderer, src, spriteWidth, spriteHeight)) 130 | 131 | proc getImage*(self: ImageManager; name: string): Image = 132 | ## Get an image. 133 | if self.table.hasKey(name): 134 | return self.table[name] 135 | else: 136 | raise newTinamouException(IMAGE_NOT_FOUND_ERROR_CODE, "Image '" & name & "' was not registered.") 137 | 138 | proc destroy*(self: ImageManager) = 139 | ## Free image manager resources. 140 | for image in self.table.values: 141 | destroy image 142 | 143 | clear self.table 144 | -------------------------------------------------------------------------------- /src/tinamou/keyboard.nim: -------------------------------------------------------------------------------- 1 | ## Keyboard Input 2 | 3 | import 4 | sdl2 5 | 6 | type 7 | KeyName* {.pure.} = enum 8 | KEYA, KEYB, KEYC, KEYD, KEYE, KEYF, KEYG, KEYH, KEYI, KEYJ, KEYK, KEYL, KEYM, 9 | KEYN, KEYO, KEYP, KEYQ, KEYR, KEYS, KEYT, KEYU, KEYV, KEYW, KEYX, KEYY, KEYZ, 10 | DIGIT0, DIGIT1, DIGIT2, DIGIT3, DIGIT4, DIGIT5, DIGIT6, DIGIT7, DIGIT8, DIGIT9, 11 | COMMA, PERIOD, SLASH, SEMICOLON, COLON, MINUS, AT, LEFTBRACKET, RIGHTBRACKET, UNDERSCORE, 12 | LEFT, UP, RIGHT, DOWN, 13 | ENTER, SPACE, SHIFT, BACKSPACE, ESCAPE, CTRL, ALT, META, 14 | F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, 15 | UNKNOWN 16 | 17 | DetailedKeyState = enum 18 | up = 0, down, pressed, released 19 | 20 | Keyboard* = ref object of RootObj 21 | keyStates: array[KeyName, DetailedKeyState] 22 | 23 | proc toKeyName(keyCode: cint): KeyName = 24 | ## Convert key code to key name. 25 | result = case keyCode 26 | of K_a: KeyName.KEYA 27 | of K_b: KeyName.KEYB 28 | of K_c: KeyName.KEYC 29 | of K_d: KeyName.KEYD 30 | of K_e: KeyName.KEYE 31 | of K_f: KeyName.KEYF 32 | of K_g: KeyName.KEYG 33 | of K_h: KeyName.KEYH 34 | of K_i: KeyName.KEYI 35 | of K_j: KeyName.KEYJ 36 | of K_k: KeyName.KEYK 37 | of K_l: KeyName.KEYL 38 | of K_m: KeyName.KEYM 39 | of K_n: KeyName.KEYN 40 | of K_o: KeyName.KEYO 41 | of K_p: KeyName.KEYP 42 | of K_q: KeyName.KEYQ 43 | of K_r: KeyName.KEYR 44 | of K_s: KeyName.KEYS 45 | of K_t: KeyName.KEYT 46 | of K_u: KeyName.KEYU 47 | of K_v: KeyName.KEYV 48 | of K_w: KeyName.KEYW 49 | of K_x: KeyName.KEYX 50 | of K_y: KeyName.KEYY 51 | of K_z: KeyName.KEYZ 52 | of K_0, K_KP_0: KeyName.DIGIT0 53 | of K_1, K_KP_1: KeyName.DIGIT1 54 | of K_2, K_KP_2: KeyName.DIGIT2 55 | of K_3, K_KP_3: KeyName.DIGIT3 56 | of K_4, K_KP_4: KeyName.DIGIT4 57 | of K_5, K_KP_5: KeyName.DIGIT5 58 | of K_6, K_KP_6: KeyName.DIGIT6 59 | of K_7, K_KP_7: KeyName.DIGIT7 60 | of K_8, K_KP_8: KeyName.DIGIT8 61 | of K_9, K_KP_9: KeyName.DIGIT9 62 | of K_COMMA: KeyName.COMMA 63 | of K_PERIOD: KeyName.PERIOD 64 | of K_SLASH: KeyName.SLASH 65 | of K_SEMICOLON: KeyName.SEMICOLON 66 | of K_COLON: KeyName.COLON 67 | of K_MINUS: KeyName.MINUS 68 | of K_AT: KeyName.AT 69 | of K_LEFTBRACKET: KeyName.LEFTBRACKET 70 | of K_RIGHTBRACKET: KeyName.RIGHTBRACKET 71 | of K_UNDERSCORE: KeyName.UNDERSCORE 72 | of K_LEFT: KeyName.LEFT 73 | of K_UP: KeyName.UP 74 | of K_RIGHT: KeyName.RIGHT 75 | of K_DOWN: KeyName.DOWN 76 | of K_RETURN, K_RETURN2, K_KP_ENTER: KeyName.ENTER 77 | of K_SPACE: KeyName.SPACE 78 | of K_LSHIFT, K_RSHIFT: KeyName.SHIFT 79 | of K_BACKSPACE: KeyName.BACKSPACE 80 | of K_ESCAPE: KeyName.ESCAPE 81 | of K_LCTRL, K_RCTRL: KeyName.CTRL 82 | of K_LALT, K_RALT: KeyName.ALT 83 | of K_LGUI, K_RGUI: KeyName.META 84 | of K_F1: KeyName.F1 85 | of K_F2: KeyName.F2 86 | of K_F3: KeyName.F3 87 | of K_F4: KeyName.F4 88 | of K_F5: KeyName.F5 89 | of K_F6: KeyName.F6 90 | of K_F7: KeyName.F7 91 | of K_F8: KeyName.F8 92 | of K_F9: KeyName.F9 93 | of K_F10: KeyName.F10 94 | of K_F11: KeyName.F11 95 | of K_F12: KeyName.F12 96 | else: KeyName.UNKNOWN 97 | 98 | proc newKeyboard*(): Keyboard = 99 | ## Create new keyboard. 100 | new result 101 | 102 | proc isDown*(self: Keyboard, keyNames: varargs[KeyName]): bool = 103 | ## Check if any key of key names is down or pressed now. 104 | result = false 105 | for keyName in keyNames: 106 | if self.keyStates[keyName] == down or self.keyStates[keyName] == pressed: return true 107 | 108 | proc isPressed*(self: Keyboard, keyNames: varargs[KeyName]): bool = 109 | ## Check if any key of key names is now pressed. 110 | result = false 111 | for keyname in keyNames: 112 | if self.keyStates[keyName] == pressed: return true 113 | 114 | proc isReleased*(self: Keyboard, keyNames: varargs[KeyName]): bool = 115 | ## Check if any key of keynames is now released. 116 | result = false 117 | for keyName in keyNames: 118 | if self.keyStates[keyName] == released: return true 119 | 120 | proc update*(self: Keyboard, event: Event) = 121 | ## Update keyboard state by event. 122 | case event.kind 123 | of KeyDown: 124 | if not self.isDown(event.key.keysym.sym.toKeyName): 125 | self.keyStates[event.key.keysym.sym.toKeyName] = pressed 126 | of KeyUp: 127 | self.keyStates[event.key.keysym.sym.toKeyName] = released 128 | else: 129 | discard 130 | 131 | proc frameEnd*(self: Keyboard) = 132 | ## Tell keyboard the end of a frame. 133 | for keyName in KeyName: 134 | if self.keyStates[keyName] == pressed: 135 | self.keyStates[keyName] = down 136 | elif self.keyStates[keyName] == released: 137 | self.keyStates[keyName] = up 138 | 139 | proc getPressingKeyNames*(self: Keyboard): seq[KeyName] = 140 | ## Get pressing key names. 141 | for keyName in KeyName: 142 | if self.isPressed keyName: result.safeAdd keyName 143 | -------------------------------------------------------------------------------- /src/tinamou/mouse.nim: -------------------------------------------------------------------------------- 1 | ## Mouse Input 2 | 3 | import 4 | sdl2 5 | 6 | type 7 | DetailedKeyState = enum 8 | up = 0, down, pressed, released 9 | 10 | MouseButton* {.pure.} = enum 11 | LEFT, RIGHT, MIDDLE, UNKNOWN 12 | 13 | Mouse* = ref object of RootObj 14 | buttonStates: array[MouseButton, DetailedKeyState] 15 | x, y: int # negative position means the pointer is out of window 16 | 17 | proc toMouseButton(button: uint8): MouseButton = 18 | ## Convert button code to button name. 19 | result = case button 20 | of BUTTON_LEFT: MouseButton.LEFT 21 | of BUTTON_RIGHT: MouseButton.RIGHT 22 | of BUTTON_MIDDLE: MouseButton.MIDDLE 23 | else: MouseButton.UNKNOWN 24 | 25 | proc newMouse*(): Mouse = 26 | ## Create new mouse. 27 | new result 28 | result.x = -1 29 | result.y = -1 30 | 31 | proc update*(self: Mouse, event: Event) = 32 | ## Update mouse state by event. 33 | case event.kind 34 | of MouseButtonDown: 35 | self.buttonStates[event.button.button.toMouseButton] = pressed 36 | self.x = event.button.x 37 | self.y = event.button.y 38 | of MouseButtonUp: 39 | self.buttonStates[event.button.button.toMouseButton] = released 40 | self.x = event.button.x 41 | self.y = event.button.y 42 | of MouseMotion: 43 | self.x = event.motion.x 44 | self.y = event.motion.y 45 | else: 46 | discard 47 | 48 | proc frameEnd*(self: Mouse) = 49 | ## Tell keyboard the end of a frame. 50 | for button in MouseButton: 51 | if self.buttonStates[button] == pressed: 52 | self.buttonStates[button] = down 53 | elif self.buttonStates[button] == released: 54 | self.buttonStates[button] = up 55 | 56 | proc isDown*(self: Mouse, buttons: varargs[MouseButton]): bool = 57 | ## Check if any button of buttons is down or pressed now. 58 | result = false 59 | for button in buttons: 60 | if (self.buttonStates[button] == down) or (self.buttonStates[button] == pressed): return true 61 | 62 | proc isPressed*(self: Mouse, buttons: varargs[MouseButton]): bool = 63 | ## Check if any button of buttons is now pressed. 64 | result = false 65 | for button in buttons: 66 | if self.buttonStates[button] == pressed: return true 67 | 68 | proc isReleased*(self: Mouse, buttons: varargs[MouseButton]): bool = 69 | ## Check if any button of buttons is now released. 70 | result = false 71 | for button in buttons: 72 | if self.buttonStates[button] == released: return true 73 | 74 | proc getPosition*(self: Mouse): tuple[x, y: int] = 75 | ## Get current mouse position. 76 | ## If mouse is out of window, then result is negative. 77 | return (x: self.x, y: self.y) 78 | -------------------------------------------------------------------------------- /src/tinamou/painter.nim: -------------------------------------------------------------------------------- 1 | ## Painter 2 | 3 | import 4 | colors, 5 | math, 6 | lenientops, 7 | 8 | sdl2, 9 | sdl2.gfx, 10 | sdl2.image, 11 | sdl2.ttf, 12 | 13 | imagemanager, 14 | fontmanager, 15 | exception 16 | 17 | type 18 | Painter* = ref object of RootObj 19 | renderer: RendererPtr 20 | font: Font 21 | 22 | OriginKind* {.pure.} = enum 23 | NW = 0, N, NE, W, C, E, SW, S, SE 24 | 25 | PaintableRect* = ref object of RootObj 26 | renderer: RendererPtr 27 | x, y, w, h: int16 28 | 29 | PaintableRoundRect* = ref object of PaintableRect 30 | radius: int16 31 | 32 | PaintableLine* = ref object of RootObj 33 | renderer: RendererPtr 34 | x0, x1, y0, y1: int16 35 | 36 | PaintableThickLine* = ref object of PaintableLine 37 | width: uint8 38 | 39 | PaintableCircle* = ref object of RootObj 40 | renderer: RendererPtr 41 | x, y, radius: int16 42 | 43 | PaintableArc* = ref object of PaintableCircle 44 | start, finish: int16 45 | 46 | PaintableEllipse* = ref object of RootObj 47 | renderer: RendererPtr 48 | x, y, rx, ry: int16 49 | 50 | PaintablePie* = ref object of PaintableCircle 51 | start, finish: int16 52 | 53 | PaintablePolygon*[N: static[int]] = ref object of RootObj 54 | renderer: RendererPtr 55 | xs, ys: array[N, int16] 56 | 57 | PaintableBezier*[N: static[int]] = ref object of RootObj 58 | renderer: RendererPtr 59 | xs, ys: array[N, int16] 60 | steps: cint 61 | 62 | PaintableText* = ref object of RootObj 63 | renderer: RendererPtr 64 | font: FontPtr 65 | x, y: int16 66 | str: string 67 | origin: OriginKind 68 | 69 | RGBAFillable = concept x 70 | x.fill(r = uint8, g = uint8, b = uint8, alpha = uint8) 71 | 72 | RGBAStrokeable = concept x 73 | x.stroke(r = uint8, g = uint8, b = uint8, alpha = uint8) 74 | 75 | proc toInt16(n: SomeInteger): int16 = n.int16 76 | proc toInt16(n: SomeReal): int16 = n.toInt.int16 77 | 78 | proc getOrigin[N0, N1: SomeNumber](origin: OriginKind, x, y: N0; width, height: N1): tuple[x, y: float] = 79 | case origin 80 | of OriginKind.NW, OriginKind.W, OriginKind.SW: result.x = x.float 81 | of OriginKind.N, OriginKind.C, OriginKind.S: result.x = x.float - width / 2 82 | of OriginKind.NE, OriginKind.E, OriginKind.SE: result.x = x.float - width.float 83 | 84 | case origin 85 | of OriginKind.NW, OriginKind.N, OriginKind.NE: result.y = y.float 86 | of OriginKind.W, OriginKind.C, OriginKind.E: result.y = y.float - height / 2 87 | of OriginKind.SW, OriginKind.S, OriginKind.SE: result.y = y.float - height.float 88 | 89 | proc newTPainter*(renderer: RendererPtr): Painter = 90 | ## Create new painter from given renderer. 91 | new result 92 | result.renderer = renderer 93 | result.font = nil 94 | 95 | proc clear*(self: Painter; r, g, b: uint8; alpha: uint8 = 255) = 96 | ## Clear the window. 97 | self.renderer.setDrawColor(r = r, g = g, b = b, a = alpha) 98 | self.renderer.clear() 99 | 100 | proc clear*(self: Painter, color: colors.Color, alpha: uint8 = 255) = 101 | ## Clear the window. 102 | let 103 | rgb = color.extractRGB() 104 | self.clear(r = rgb.r, g = rgb.g, b = rgb.b, alpha = alpha) 105 | 106 | proc clear*(self: Painter, color: sdl2.Color) = 107 | ## Clear the window. 108 | self.renderer.setDrawColor(color) 109 | self.renderer.clear() 110 | 111 | proc drawImage0(self: Painter, image: Image, srcRect: ptr Rect = nil, dstRect: ptr Rect, spriteNum: int, alpha: uint8, rotAngle: float = 0.0, rotCenter: ptr Point = nil) = 112 | ## Draw image. 113 | image.getTexture().setTextureAlphaMod(alpha) 114 | 115 | if image.isSprite: 116 | let 117 | spriteSize = image.getSpriteSize() 118 | origin: tuple[x, y: int] = (x: spriteSize.width * (spriteNum mod spriteSize.columns), y: spriteSize.height * (spriteNum div spriteSize.columns)) 119 | var 120 | actualSrcRect: Rect = if srcRect.isNil: 121 | (x: origin.x.cint, y: origin.y.cint, w: spriteSize.width.cint, h: spriteSize.height.cint) 122 | else: 123 | (x: (origin.x + srcRect.x).cint, y: (origin.y + srcRect.y).cint, w: (spriteSize.width - srcRect.x).cint, h: (spriteSize.height - srcRect.y).cint) 124 | 125 | if rotAngle != 0.0: 126 | self.renderer.copyEx(image.getTexture(), addr actualSrcRect, dstRect, rotAngle, rotCenter) 127 | else: 128 | self.renderer.copy(image.getTexture(), addr actualSrcRect, dstRect) 129 | 130 | else: 131 | if rotAngle != 0.0: 132 | self.renderer.copyEx(image.getTexture(), srcRect, dstRect, rotAngle, rotCenter) 133 | else: 134 | self.renderer.copy(image.getTexture(), srcRect, dstRect) 135 | 136 | proc drawImage*[N: SomeNumber](self: Painter, image: Image; x, y: N; spriteNum: int = 0; alpha: uint8 = 255; origin: OriginKind = OriginKind.NW; fixRatio: bool = false; rotAngle: float = 0.0) = 137 | ## Draw image. 138 | ## The unit of ``rotAngle`` is degree. 139 | let originPos: tuple[x, y: float] = origin.getOrigin(x, y, image.width, image.height) 140 | var dstRect: Rect = (x: originPos.x.toInt.cint, y: originPos.y.toInt.cint, w: image.width.cint, h: image.height.cint) 141 | 142 | self.drawImage0(image = image, dstRect = addr dstRect, spriteNum = spriteNum, alpha = alpha, rotAngle = rotAngle) 143 | 144 | proc drawImage*[N0, N1: SomeNumber](self: Painter, image: Image; x, y: N0; width, height: N1; spriteNum: int = 0; alpha: uint8 = 255; origin: OriginKind = OriginKind.NW; fixRatio: bool = false; rotAngle: float = 0.0) = 145 | ## Draw image. 146 | ## The unit of ``rotAngle`` is degree. 147 | var dstRect: Rect 148 | 149 | if fixRatio: 150 | let 151 | zoom: float = min(width / image.width, height / image.height) 152 | actualWidth: float = zoom * image.width.float 153 | actualHeight: float = zoom * image.height.float 154 | originPos: tuple[x, y: float] = origin.getOrigin(x, y, actualWidth, actualHeight) 155 | dstRect = (x: originPos.x.toInt.cint, y: originPos.y.toInt.cint, w: actualWidth.toInt.cint, h: actualHeight.toInt.cint) 156 | else: 157 | let originPos: tuple[x, y: float] = origin.getOrigin(x, y, width, height) 158 | dstRect = (x: originPos.x.toInt.cint, y: originPos.y.toInt.cint, w: width.toInt16.cint, h: height.toInt16.cint) 159 | 160 | self.drawImage0(image = image, dstRect = addr dstRect, spriteNum = spriteNum, alpha = alpha, rotAngle = rotAngle) 161 | 162 | proc drawImage*[N0, N1, N2, N3: SomeNumber](self: Painter, image: Image; srcX, srcY: N0; srcWidth, srcHeight: N1; x, y: N2; width, height: N3; spriteNum: int = 0; alpha: uint8 = 255; origin: OriginKind = OriginKind.NW; fixRatio: bool = false; rotAngle: float = 0.0) = 163 | ## Draw image. 164 | ## The unit of ``rotAngle`` is degree. 165 | let 166 | actualSrcWidth: float = min(srcWidth.float, image.width.float - srcX.float) 167 | actualSrcHeight: float = min(srcHeight.float, image.height.float - srcY.float) 168 | var 169 | srcRect: Rect = (x: srcX.toInt16.cint, y: srcY.toInt16.cint, w: actualSrcWidth.toInt.cint, h: actualSrcHeight.toInt.cint) 170 | dstRect: Rect 171 | 172 | if fixRatio: 173 | let 174 | zoom: float = min(width / srcWidth, height / srcHeight) 175 | actualWidth: float = zoom * actualSrcWidth 176 | actualHeight: float = zoom * actualSrcHeight 177 | originPos: tuple[x, y: float] = origin.getOrigin(x, y, zoom * srcWidth.float, zoom * srcHeight.float) 178 | dstRect = (x: originPos.x.toInt.cint, y: originPos.y.toInt.cint, w: actualWidth.toInt.cint, h: actualHeight.toInt.cint) 179 | else: 180 | let 181 | actualWidth: float = width.float * (actualSrcWidth / srcWidth.float) 182 | actualHeight: float = height.float * (actualSrcHeight / srcHeight.float) 183 | originPos: tuple[x, y: float] = origin.getOrigin(x, y, width, height) 184 | dstRect = (x: originPos.x.toInt.cint, y: originPos.y.toInt.cint, w: actualWidth.toInt.cint, h: actualHeight.toInt.cint) 185 | 186 | self.drawImage0(image = image, srcRect = addr srcRect, dstRect = addr dstRect, spriteNum = spriteNum, alpha = alpha, rotAngle = rotAngle) 187 | 188 | proc rect*[N0, N1: SomeNumber](self: Painter; x, y: N0; w, h: N1): PaintableRect = 189 | ## Create paintable rectangle. 190 | new result 191 | result.renderer = self.renderer 192 | result.x = x.toInt16 193 | result.y = y.toInt16 194 | result.w = w.toInt16 195 | result.h = h.toInt16 196 | 197 | proc roundRect*[N0, N1, N2: SomeNumber](self: Painter; x, y: N0; w, h: N1; radius: N2): PaintableRoundRect = 198 | ## Create paintable round rectangle. 199 | new result 200 | result.renderer = self.renderer 201 | result.x = x.toInt16 202 | result.y = y.toInt16 203 | result.w = w.toInt16 204 | result.h = h.toInt16 205 | result.radius = radius.toInt16 206 | 207 | proc line*[N: SomeNumber](self: Painter; x0, y0, x1, y1: N): PaintableLine = 208 | ## Create paintable line. 209 | new result 210 | result.renderer = self.renderer 211 | result.x0 = x0.toInt16 212 | result.y0 = y0.toInt16 213 | result.x1 = x1.toInt16 214 | result.y1 = y1.toInt16 215 | 216 | proc thickLine*[N: SomeNumber](self: Painter; x0, y0, x1, y1: N; width: uint8): PaintableThickLine = 217 | ## Create paintable thick line. 218 | new result 219 | result.renderer = self.renderer 220 | result.x0 = x0.toInt16 221 | result.y0 = y0.toInt16 222 | result.x1 = x1.toInt16 223 | result.y1 = y1.toInt16 224 | result.width = width 225 | 226 | proc circle*[N0, N1: SomeNumber](self: Painter; x, y: N0; radius: N1): PaintableCircle = 227 | ## Create paintable circle. 228 | new result 229 | result.renderer = self.renderer 230 | result.x = x.toInt16 231 | result.y = y.toInt16 232 | result.radius = radius.toInt16 233 | 234 | proc arc*[N0, N1, N2: SomeNumber](self: Painter; x, y: N0; radius: N1; start, finish: N2): PaintableArc = 235 | ## Create paintable arc. 236 | new result 237 | result.renderer = self.renderer 238 | result.x = x.toInt16 239 | result.y = y.toInt16 240 | result.radius = radius.toInt16 241 | result.start = start.toInt16 242 | result.finish = finish.toInt16 243 | 244 | proc ellipse*[N: SomeNumber](self: Painter; x, y, rx, ry: N): PaintableEllipse = 245 | ## Create paintable ellipse. 246 | new result 247 | result.renderer = self.renderer 248 | result.x = x.toInt16 249 | result.y = y.toInt16 250 | result.rx = rx.toInt16 251 | result.ry = ry.toInt16 252 | 253 | proc pie*[N0, N1, N2: SomeNumber](self: Painter; x, y: N0; radius: N1; start, finish: N2): PaintablePie = 254 | ## Create paintable pie. 255 | new result 256 | result.renderer = self.renderer 257 | result.x = x.toInt16 258 | result.y = y.toInt16 259 | result.radius = radius.toInt16 260 | result.start = start.toInt16 261 | result.finish = finish.toInt16 262 | 263 | proc polygon*[N: static[int], T: SomeNumber](self: Painter; xs, ys: array[N, T]): PaintablePolygon[N] = 264 | ## Create paintable polygon. 265 | new result 266 | result.renderer = self.renderer 267 | for i in 0..= 0.18.0" 14 | requires "sdl2 >= 1.1" 15 | 16 | # tasks 17 | 18 | task release, "do release build": 19 | exec "nimble build -d:release --opt:speed --app:gui && strip ./bin/tinamou" 20 | 21 | task cleanup, "clean up files": 22 | exec "rm -f bin/* && rm -rf src/nimcache" 23 | exec "find tests -type f ! -name \"*.*\" -delete && rm -rf tests/nimcache" 24 | 25 | task docgen, "generate documentation": 26 | exec "nimble doc2 src/tinamou.nim --project -o:docs" 27 | 28 | task test, "test codes": 29 | withDir "tests": 30 | exec "nim c -r test" 31 | exec "rm -f test" 32 | --------------------------------------------------------------------------------