├── .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 |
1230 | Section
1231 | Type
1232 |
1233 |
1234 |
1235 |
1236 | Imports
1237 |
1240 |
1241 |
1242 |
1243 |
1244 |
1245 |
1246 |
1247 |
Tinamou Game Library
1248 |
1249 |
1250 |
1251 | tinamou/core , tinamou/scene , tinamou/transition , tinamou/tools , tinamou/actions , tinamou/sharedinfo , tinamou/scenemanager , tinamou/imagemanager , tinamou/soundmanager , tinamou/windowmanager , tinamou/fontmanager , tinamou/painter , tinamou/keyboard , tinamou/mouse , tinamou/tween , tinamou/exception
1252 |
1253 |
1254 |
1255 |
1256 |
1257 |
1258 |
1259 |
1260 |
1261 | Made with Nim. Generated: 2018-10-01 13:52:04 UTC
1262 |
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 |
1230 | Section
1231 | Type
1232 |
1233 |
1234 |
1235 |
1236 | Imports
1237 |
1240 |
1241 |
1242 | Types
1243 |
1248 |
1249 |
1250 | Methods
1251 |
1260 |
1261 |
1262 |
1263 |
1264 |
1265 |
1266 |
1267 |
Scene
1268 |
1273 |
1274 |
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 |
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 |
1312 |
1313 |
1314 | Made with Nim. Generated: 2018-10-01 13:52:04 UTC
1315 |
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 |
1230 | Section
1231 | Type
1232 |
1233 |
1234 |
1235 |
1236 | Imports
1237 |
1240 |
1241 |
1242 | Types
1243 |
1249 |
1250 |
1251 | Procs
1252 |
1261 |
1262 |
1263 |
1264 |
1265 |
1266 |
1267 |
1268 |
Manager for Multiple Scenes
1269 |
1274 |
1275 |
1276 |
1277 | SceneManager = ref object of RootObj
1278 | table : TableRef [ SceneId , BaseScene ]
1279 |
1280 |
1281 |
1282 |
1283 |
1284 |
1285 |
1286 |
1287 |
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 |
1313 |
1314 |
1315 | Made with Nim. Generated: 2018-10-01 13:52:04 UTC
1316 |
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 |
1230 | Section
1231 | Type
1232 |
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 |
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 |
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 |
1285 |
1286 |
1287 | Made with Nim. Generated: 2018-10-01 13:52:04 UTC
1288 |
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 |
--------------------------------------------------------------------------------