├── .gitignore ├── LICENSE ├── README.md ├── audio ├── audio.go ├── openal │ ├── al │ │ ├── LICENSE │ │ ├── al.go │ │ ├── al_android.go │ │ ├── al_pc.go │ │ ├── alc.go │ │ ├── alc_android.go │ │ ├── alc_pc.go │ │ └── const.go │ ├── decoding │ │ ├── decoder.go │ │ ├── flac.go │ │ ├── mp3.go │ │ ├── vorbis.go │ │ └── wav.go │ ├── pool.go │ └── source.go └── wrap.go ├── cmd ├── app.go └── moony │ └── main.go ├── file └── file.go ├── gfx ├── canvas.go ├── const.go ├── display_state.go ├── font.go ├── font │ ├── bitmap_face.go │ ├── char_sets.go │ ├── doc.go │ └── ttf_face.go ├── font_rasterizers.go ├── graphics.go ├── image.go ├── opengl.go ├── polyline.go ├── quad.go ├── quad_indicies.go ├── shader.go ├── shader_templates.go ├── sprite_batch.go ├── text.go ├── text_line.go ├── texture.go ├── uniform.go ├── vertex_buffer.go ├── volatile.go └── wrap │ ├── font.go │ ├── graphics.go │ ├── quad.go │ ├── shader.go │ ├── sprite_batch.go │ ├── text.go │ ├── texture.go │ ├── utils.go │ └── wrap.go ├── go.mod ├── go.sum ├── input ├── input.go └── input_const.go ├── runtime ├── runtime.go ├── time.go ├── web │ └── main.go └── window.go └── test ├── icon.png ├── index.html └── main.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | vendor 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tim Anema 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## REDO 2 | IDEAS FOR BETTER WORLD 3 | - Make more portable with goxjs toolkit. Get games on web 4 | - Simplify and get rid of extra items like particle generator that isn't needed 5 | - scripting interface 6 | - combine keyboard, joystick and touch into a single input, mapping package 7 | - Somehow enable cross platform deploy easily 8 | 9 | TODO: 10 | - [x] minimize everything, this is for myself I don't need all this extra 11 | - [x] remove sdl and use [goxjs glfw](https://github.com/goxjs/glfw) 12 | - [x] reimlement input with mapping key to event 13 | - [x] goxjs file loading glfw.Open() 14 | - [ ] use [beep](https://github.com/faiface/beep) to have cross platform audio 15 | - [x] use [goxjs](https://github.com/goxjs/gl) for better cross platform 16 | - [ ] default antialiasing? 17 | - [ ] fix font rendering/sprite batching in web runtime 18 | - [x] use [glua](https://github.com/yuin/gopher-lua) for scripting 19 | - [ ] [cross compile](https://github.com/karalabe/xgo) on a singple platform 20 | 21 | These changes will reduce functionality but make it more portable. 22 | -------------------------------------------------------------------------------- /audio/audio.go: -------------------------------------------------------------------------------- 1 | // Package audio is use for creating audio sources, managing/pooling resources, 2 | // and playback of those audio sources. 3 | package audio 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/tanema/amore/audio/openal" 9 | ) 10 | 11 | // Source is an playable audio source 12 | type Source interface { 13 | IsFinished() bool 14 | GetDuration() time.Duration 15 | GetPitch() float32 16 | GetVolume() float32 17 | GetState() string 18 | IsLooping() bool 19 | IsPaused() bool 20 | IsPlaying() bool 21 | IsStatic() bool 22 | IsStopped() bool 23 | SetLooping(loop bool) 24 | SetPitch(p float32) 25 | SetVolume(v float32) 26 | Play() bool 27 | Pause() 28 | Resume() 29 | Rewind() 30 | Seek(time.Duration) 31 | Stop() 32 | Tell() time.Duration 33 | } 34 | 35 | // NewSource creates a new Source from a file at the path provided. If you 36 | // specify a static source it will all be buffered into a single buffer. If 37 | // false then it will create many buffers a cycle through them with data chunks. 38 | // This allows a smaller memory footprint while playing bigger music files. You 39 | // may want a static file if the sound is less than 2 seconds. It allows for faster 40 | // cleaning playing of shorter sounds like footsteps. 41 | func NewSource(filepath string, static bool) (Source, error) { 42 | return openal.NewSource(filepath, static) 43 | } 44 | 45 | // GetVolume returns the master volume. 46 | func GetVolume() float32 { return openal.GetVolume() } 47 | 48 | // SetVolume sets the master volume 49 | func SetVolume(gain float32) { openal.SetVolume(gain) } 50 | 51 | // PauseAll will pause all sources 52 | func PauseAll() { openal.PauseAll() } 53 | 54 | // PlayAll will play all sources 55 | func PlayAll() { openal.PlayAll() } 56 | 57 | // RewindAll will rewind all sources 58 | func RewindAll() { openal.RewindAll() } 59 | 60 | // StopAll stop all sources 61 | func StopAll() { openal.StopAll() } 62 | -------------------------------------------------------------------------------- /audio/openal/al/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /audio/openal/al/al.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin linux 6 | 7 | // Package al provides OpenAL Soft bindings for Go. 8 | // 9 | // Calls are not safe for concurrent use. 10 | // 11 | // More information about OpenAL Soft is available at 12 | // http://www.openal.org/documentation/openal-1.1-specification.pdf. 13 | // 14 | // In order to use this package on Linux desktop distros, 15 | // you will need OpenAL library as an external dependency. 16 | // On Ubuntu 14.04 'Trusty', you may have to install this library 17 | // by running the command below. 18 | // 19 | // sudo apt-get install libopenal-dev 20 | // 21 | // When compiled for Android, this package uses OpenAL Soft. Please add its 22 | // license file to the open source notices of your application. 23 | // OpenAL Soft's license file could be found at 24 | // http://repo.or.cz/w/openal-soft.git/blob/HEAD:/COPYING. 25 | // 26 | // Vendored and extended from github.com/golang/mobile for extending functionality 27 | package al 28 | 29 | // Capability represents OpenAL extension capabilities. 30 | type Capability int32 31 | 32 | // Enable enables a capability. 33 | func Enable(c Capability) { 34 | alEnable(int32(c)) 35 | } 36 | 37 | // Disable disables a capability. 38 | func Disable(c Capability) { 39 | alDisable(int32(c)) 40 | } 41 | 42 | // Enabled returns true if the specified capability is enabled. 43 | func Enabled(c Capability) bool { 44 | return alIsEnabled(int32(c)) 45 | } 46 | 47 | // Vector represents an vector in a Cartesian coordinate system. 48 | type Vector [3]float32 49 | 50 | // Cone is the audio cone 51 | type Cone struct { 52 | InnerAngle int32 53 | OuterAngle int32 54 | OuterVolume float32 55 | } 56 | 57 | // Orientation represents the angular position of an object in a 58 | // right-handed Cartesian coordinate system. 59 | // A cross product between the forward and up vector returns a vector 60 | // that points to the right. 61 | type Orientation struct { 62 | // Forward vector is the direction that the object is looking at. 63 | Forward Vector 64 | // Up vector represents the rotation of the object. 65 | Up Vector 66 | } 67 | 68 | func orientationFromSlice(v []float32) Orientation { 69 | return Orientation{ 70 | Forward: Vector{v[0], v[1], v[2]}, 71 | Up: Vector{v[3], v[4], v[5]}, 72 | } 73 | } 74 | 75 | func (v Orientation) slice() []float32 { 76 | return []float32{v.Forward[0], v.Forward[1], v.Forward[2], v.Up[0], v.Up[1], v.Up[2]} 77 | } 78 | 79 | func geti(param int) int32 { 80 | return alGetInteger(param) 81 | } 82 | 83 | func getf(param int) float32 { 84 | return alGetFloat(param) 85 | } 86 | 87 | func getString(param int) string { 88 | return alGetString(param) 89 | } 90 | 91 | // DistanceModel returns the distance model. 92 | func DistanceModel() int32 { 93 | return geti(paramDistanceModel) 94 | } 95 | 96 | // SetDistanceModel sets the distance model. 97 | func SetDistanceModel(v int32) { 98 | alDistanceModel(v) 99 | } 100 | 101 | // DopplerFactor returns the doppler factor. 102 | func DopplerFactor() float32 { 103 | return getf(paramDopplerFactor) 104 | } 105 | 106 | // SetDopplerFactor sets the doppler factor. 107 | func SetDopplerFactor(v float32) { 108 | alDopplerFactor(v) 109 | } 110 | 111 | // DopplerVelocity returns the doppler velocity. 112 | func DopplerVelocity() float32 { 113 | return getf(paramDopplerVelocity) 114 | } 115 | 116 | // SetDopplerVelocity sets the doppler velocity. 117 | func SetDopplerVelocity(v float32) { 118 | alDopplerVelocity(v) 119 | } 120 | 121 | // SpeedOfSound is the speed of sound in meters per second (m/s). 122 | func SpeedOfSound() float32 { 123 | return getf(paramSpeedOfSound) 124 | } 125 | 126 | // SetSpeedOfSound sets the speed of sound, its unit should be meters per second (m/s). 127 | func SetSpeedOfSound(v float32) { 128 | alSpeedOfSound(v) 129 | } 130 | 131 | // Vendor returns the vendor. 132 | func Vendor() string { 133 | return getString(paramVendor) 134 | } 135 | 136 | // Version returns the version string. 137 | func Version() string { 138 | return getString(paramVersion) 139 | } 140 | 141 | // Renderer returns the renderer information. 142 | func Renderer() string { 143 | return getString(paramRenderer) 144 | } 145 | 146 | // Extensions returns the enabled extensions. 147 | func Extensions() string { 148 | return getString(paramExtensions) 149 | } 150 | 151 | // Error returns the most recently generated error. 152 | func Error() int32 { 153 | return alGetError() 154 | } 155 | 156 | // Source represents an individual sound source in 3D-space. 157 | // They take PCM data, apply modifications and then submit them to 158 | // be mixed according to their spatial location. 159 | type Source uint32 160 | 161 | // GenSources generates n new sources. These sources should be deleted 162 | // once they are not in use. 163 | func GenSources(n int) []Source { 164 | return alGenSources(n) 165 | } 166 | 167 | // PlaySources plays the sources. 168 | func PlaySources(source ...Source) { 169 | alSourcePlayv(source) 170 | } 171 | 172 | // PauseSources pauses the sources. 173 | func PauseSources(source ...Source) { 174 | alSourcePausev(source) 175 | } 176 | 177 | // StopSources stops the sources. 178 | func StopSources(source ...Source) { 179 | alSourceStopv(source) 180 | } 181 | 182 | // RewindSources rewinds the sources to their beginning positions. 183 | func RewindSources(source ...Source) { 184 | alSourceRewindv(source) 185 | } 186 | 187 | // DeleteSources deletes the sources. 188 | func DeleteSources(source ...Source) { 189 | alDeleteSources(source) 190 | } 191 | 192 | // Gain returns the source gain. 193 | func (s Source) Gain() float32 { 194 | return getSourcef(s, paramGain) 195 | } 196 | 197 | // SetGain sets the source gain. 198 | func (s Source) SetGain(v float32) { 199 | setSourcef(s, paramGain, v) 200 | } 201 | 202 | // Pitch todo 203 | func (s Source) Pitch() float32 { 204 | return getSourcef(s, paramPitch) 205 | } 206 | 207 | // SetPitch todo 208 | func (s Source) SetPitch(p float32) { 209 | setSourcef(s, paramPitch, p) 210 | } 211 | 212 | // Rolloff todo 213 | func (s Source) Rolloff() float32 { 214 | return getSourcef(s, paramRolloffFactor) 215 | } 216 | 217 | // SetRolloff todo 218 | func (s Source) SetRolloff(rolloff float32) { 219 | setSourcef(s, paramRolloffFactor, rolloff) 220 | } 221 | 222 | // ReferenceDistance todo 223 | func (s Source) ReferenceDistance() float32 { 224 | return getSourcef(s, paramReferenceDistance) 225 | } 226 | 227 | // SetReferenceDistance todo 228 | func (s Source) SetReferenceDistance(dis float32) { 229 | setSourcef(s, paramReferenceDistance, dis) 230 | } 231 | 232 | // MaxDistance todo 233 | func (s Source) MaxDistance() float32 { 234 | return getSourcef(s, paramMaxDistance) 235 | } 236 | 237 | // SetMaxDistance todo 238 | func (s Source) SetMaxDistance(dis float32) { 239 | setSourcef(s, paramMaxDistance, dis) 240 | } 241 | 242 | // Looping returns the source looping. 243 | func (s Source) Looping() bool { 244 | return getSourcei(s, paramLooping) == 1 245 | } 246 | 247 | // SetLooping sets the source looping 248 | func (s Source) SetLooping(shouldloop bool) { 249 | if shouldloop { 250 | setSourcei(s, paramLooping, 1) 251 | } else { 252 | setSourcei(s, paramLooping, 0) 253 | } 254 | } 255 | 256 | // Relative todo 257 | func (s Source) Relative() bool { 258 | return getSourcei(s, paramSourceRelative) == 1 259 | } 260 | 261 | // SetRelative todo 262 | func (s Source) SetRelative(isRelative bool) { 263 | if isRelative { 264 | setSourcei(s, paramSourceRelative, 1) 265 | } else { 266 | setSourcei(s, paramSourceRelative, 0) 267 | } 268 | } 269 | 270 | // MinGain returns the source's minimum gain setting. 271 | func (s Source) MinGain() float32 { 272 | return getSourcef(s, paramMinGain) 273 | } 274 | 275 | // SetMinGain sets the source's minimum gain setting. 276 | func (s Source) SetMinGain(v float32) { 277 | setSourcef(s, paramMinGain, v) 278 | } 279 | 280 | // MaxGain returns the source's maximum gain setting. 281 | func (s Source) MaxGain() float32 { 282 | return getSourcef(s, paramMaxGain) 283 | } 284 | 285 | // SetMaxGain sets the source's maximum gain setting. 286 | func (s Source) SetMaxGain(v float32) { 287 | setSourcef(s, paramMaxGain, v) 288 | } 289 | 290 | // Position returns the position of the source. 291 | func (s Source) Position() Vector { 292 | v := Vector{} 293 | getSourcefv(s, paramPosition, v[:]) 294 | return v 295 | } 296 | 297 | // SetPosition sets the position of the source. 298 | func (s Source) SetPosition(v Vector) { 299 | setSourcefv(s, paramPosition, v[:]) 300 | } 301 | 302 | // Direction returns the direction of the source. 303 | func (s Source) Direction() Vector { 304 | v := Vector{} 305 | getSourcefv(s, paramDirection, v[:]) 306 | return v 307 | } 308 | 309 | // SetDirection sets the direction of the source. 310 | func (s Source) SetDirection(v Vector) { 311 | setSourcefv(s, paramDirection, v[:]) 312 | } 313 | 314 | // Cone returns the audio cone 315 | func (s Source) Cone() Cone { 316 | return Cone{ 317 | InnerAngle: getSourcei(s, paramConeInnerAngle), 318 | OuterAngle: getSourcei(s, paramConeOuterAngle), 319 | OuterVolume: getSourcef(s, paramConeOuterGain), 320 | } 321 | } 322 | 323 | // SetCone sets the audio cone 324 | func (s Source) SetCone(c Cone) { 325 | setSourcei(s, paramConeInnerAngle, c.InnerAngle) 326 | setSourcei(s, paramConeOuterAngle, c.OuterAngle) 327 | setSourcef(s, paramConeOuterGain, c.OuterVolume) 328 | } 329 | 330 | // Velocity returns the source's velocity. 331 | func (s Source) Velocity() Vector { 332 | v := Vector{} 333 | getSourcefv(s, paramVelocity, v[:]) 334 | return v 335 | } 336 | 337 | // SetVelocity sets the source's velocity. 338 | func (s Source) SetVelocity(v Vector) { 339 | setSourcefv(s, paramVelocity, v[:]) 340 | } 341 | 342 | // Orientation returns the orientation of the source. 343 | func (s Source) Orientation() Orientation { 344 | v := make([]float32, 6) 345 | getSourcefv(s, paramOrientation, v) 346 | return orientationFromSlice(v) 347 | } 348 | 349 | // SetOrientation sets the orientation of the source. 350 | func (s Source) SetOrientation(o Orientation) { 351 | setSourcefv(s, paramOrientation, o.slice()) 352 | } 353 | 354 | // State returns the playing state of the source. 355 | func (s Source) State() int32 { 356 | return getSourcei(s, paramSourceState) 357 | } 358 | 359 | // SetBuffer returns the number of the queued buffers. 360 | func (s Source) SetBuffer(b Buffer) { 361 | setSourcei(s, paramBuffer, int32(b)) 362 | } 363 | 364 | // Buffer returns the number of the queued buffers. 365 | func (s Source) Buffer() Buffer { 366 | return Buffer(getSourcei(s, paramBuffer)) 367 | } 368 | 369 | // ClearBuffers returns the number of the queued buffers. 370 | func (s Source) ClearBuffers() { 371 | setSourcei(s, paramBuffer, 0) 372 | } 373 | 374 | // BuffersQueued returns the number of the queued buffers. 375 | func (s Source) BuffersQueued() int32 { 376 | return getSourcei(s, paramBuffersQueued) 377 | } 378 | 379 | // BuffersProcessed returns the number of the processed buffers. 380 | func (s Source) BuffersProcessed() int32 { 381 | return getSourcei(s, paramBuffersProcessed) 382 | } 383 | 384 | // OffsetSeconds returns the current playback position of the source in seconds. 385 | func (s Source) OffsetSeconds() float32 { 386 | return getSourcef(s, paramSecOffset) 387 | } 388 | 389 | // SetOffsetSeconds returns the current playback position of the source in seconds. 390 | func (s Source) SetOffsetSeconds(seconds float32) { 391 | setSourcef(s, paramSecOffset, seconds) 392 | } 393 | 394 | // OffsetSample returns the sample offset of the current playback position. 395 | func (s Source) OffsetSample() float32 { 396 | return getSourcef(s, paramSampleOffset) 397 | } 398 | 399 | // SetOffsetSample returns the sample offset of the current playback position. 400 | func (s Source) SetOffsetSample(samples float32) { 401 | setSourcef(s, paramSampleOffset, samples) 402 | } 403 | 404 | // OffsetByte returns the byte offset of the current playback position. 405 | func (s Source) OffsetByte() int32 { 406 | return getSourcei(s, paramByteOffset) 407 | } 408 | 409 | // SetOffsetBytes returns the sample offset of the current playback position. 410 | func (s Source) SetOffsetBytes(bytes int32) { 411 | setSourcei(s, paramByteOffset, bytes) 412 | } 413 | 414 | func getSourcei(s Source, param int) int32 { 415 | return alGetSourcei(s, param) 416 | } 417 | 418 | func getSourcef(s Source, param int) float32 { 419 | return alGetSourcef(s, param) 420 | } 421 | 422 | func getSourcefv(s Source, param int, v []float32) { 423 | alGetSourcefv(s, param, v) 424 | } 425 | 426 | func setSourcei(s Source, param int, v int32) { 427 | alSourcei(s, param, v) 428 | } 429 | 430 | func setSourcef(s Source, param int, v float32) { 431 | alSourcef(s, param, v) 432 | } 433 | 434 | func setSourcefv(s Source, param int, v []float32) { 435 | alSourcefv(s, param, v) 436 | } 437 | 438 | // QueueBuffers adds the buffers to the buffer queue. 439 | func (s Source) QueueBuffers(buffer ...Buffer) { 440 | alSourceQueueBuffers(s, buffer) 441 | } 442 | 443 | // UnqueueBuffer removes the specified buffers from the buffer queue. 444 | func (s Source) UnqueueBuffer() Buffer { 445 | buffers := make([]Buffer, 1) 446 | alSourceUnqueueBuffers(s, buffers) 447 | return buffers[0] 448 | } 449 | 450 | // ListenerGain returns the total gain applied to the final mix. 451 | func ListenerGain() float32 { 452 | return getListenerf(paramGain) 453 | } 454 | 455 | // ListenerPosition returns the position of the listener. 456 | func ListenerPosition() Vector { 457 | v := Vector{} 458 | getListenerfv(paramPosition, v[:]) 459 | return v 460 | } 461 | 462 | // ListenerVelocity returns the velocity of the listener. 463 | func ListenerVelocity() Vector { 464 | v := Vector{} 465 | getListenerfv(paramVelocity, v[:]) 466 | return v 467 | } 468 | 469 | // ListenerOrientation returns the orientation of the listener. 470 | func ListenerOrientation() Orientation { 471 | v := make([]float32, 6) 472 | getListenerfv(paramOrientation, v) 473 | return orientationFromSlice(v) 474 | } 475 | 476 | // SetListenerGain sets the total gain that will be applied to the final mix. 477 | func SetListenerGain(v float32) { 478 | setListenerf(paramGain, v) 479 | } 480 | 481 | // SetListenerPosition sets the position of the listener. 482 | func SetListenerPosition(v Vector) { 483 | setListenerfv(paramPosition, v[:]) 484 | } 485 | 486 | // SetListenerVelocity sets the velocity of the listener. 487 | func SetListenerVelocity(v Vector) { 488 | setListenerfv(paramVelocity, v[:]) 489 | } 490 | 491 | // SetListenerOrientation sets the orientation of the listener. 492 | func SetListenerOrientation(v Orientation) { 493 | setListenerfv(paramOrientation, v.slice()) 494 | } 495 | 496 | func getListenerf(param int) float32 { 497 | return alGetListenerf(param) 498 | } 499 | 500 | func getListenerfv(param int, v []float32) { 501 | alGetListenerfv(param, v) 502 | } 503 | 504 | func setListenerf(param int, v float32) { 505 | alListenerf(param, v) 506 | } 507 | 508 | func setListenerfv(param int, v []float32) { 509 | alListenerfv(param, v) 510 | } 511 | 512 | // Buffer represents a chunk of PCM audio data that could be buffered to an audio 513 | // source. A single buffer could be shared between multiple sources. 514 | type Buffer uint32 515 | 516 | // GenBuffers generates n new buffers. The generated buffers should be deleted 517 | // once they are no longer in use. 518 | func GenBuffers(n int) []Buffer { 519 | return alGenBuffers(n) 520 | } 521 | 522 | // DeleteBuffers deletes the buffers. 523 | func DeleteBuffers(buffer ...Buffer) { 524 | alDeleteBuffers(buffer) 525 | } 526 | 527 | func getBufferi(b Buffer, param int) int32 { 528 | return alGetBufferi(b, param) 529 | } 530 | 531 | // Frequency returns the frequency of the buffer data in Hertz (Hz). 532 | func (b Buffer) Frequency() int32 { 533 | return getBufferi(b, paramFreq) 534 | } 535 | 536 | // Bits return the number of bits used to represent a sample. 537 | func (b Buffer) Bits() int32 { 538 | return getBufferi(b, paramBits) 539 | } 540 | 541 | // Channels return the number of the audio channels. 542 | func (b Buffer) Channels() int32 { 543 | return getBufferi(b, paramChannels) 544 | } 545 | 546 | // Size returns the size of the data. 547 | func (b Buffer) Size() int32 { 548 | return getBufferi(b, paramSize) 549 | } 550 | 551 | // BufferData buffers PCM data to the current buffer. 552 | func (b Buffer) BufferData(format uint32, data []byte, freq int32) { 553 | alBufferData(b, format, data, freq) 554 | } 555 | 556 | // Valid returns true if the buffer exists and is valid. 557 | func (b Buffer) Valid() bool { 558 | return alIsBuffer(b) 559 | } 560 | -------------------------------------------------------------------------------- /audio/openal/al/al_pc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin linux,!android 6 | 7 | package al 8 | 9 | /* 10 | #cgo darwin CFLAGS: -DGOOS_darwin 11 | #cgo linux CFLAGS: -DGOOS_linux 12 | #cgo darwin LDFLAGS: -framework OpenAL 13 | #cgo linux LDFLAGS: -lopenal 14 | 15 | #ifdef GOOS_darwin 16 | #include 17 | #include 18 | #endif 19 | 20 | #ifdef GOOS_linux 21 | #include 22 | #include 23 | #endif 24 | 25 | #ifdef GOOS_windows 26 | #include 27 | #include 28 | #endif 29 | */ 30 | import "C" 31 | import "unsafe" 32 | 33 | func alEnable(capability int32) { 34 | C.alEnable(C.ALenum(capability)) 35 | } 36 | 37 | func alDisable(capability int32) { 38 | C.alDisable(C.ALenum(capability)) 39 | } 40 | 41 | func alIsEnabled(capability int32) bool { 42 | return C.alIsEnabled(C.ALenum(capability)) == C.AL_TRUE 43 | } 44 | 45 | func alGetInteger(k int) int32 { 46 | return int32(C.alGetInteger(C.ALenum(k))) 47 | } 48 | 49 | func alGetFloat(k int) float32 { 50 | return float32(C.alGetFloat(C.ALenum(k))) 51 | } 52 | 53 | func alGetString(v int) string { 54 | value := C.alGetString(C.ALenum(v)) 55 | return C.GoString((*C.char)(value)) 56 | } 57 | 58 | func alDistanceModel(v int32) { 59 | C.alDistanceModel(C.ALenum(v)) 60 | } 61 | 62 | func alDopplerFactor(v float32) { 63 | C.alDopplerFactor(C.ALfloat(v)) 64 | } 65 | 66 | func alDopplerVelocity(v float32) { 67 | C.alDopplerVelocity(C.ALfloat(v)) 68 | } 69 | 70 | func alSpeedOfSound(v float32) { 71 | C.alSpeedOfSound(C.ALfloat(v)) 72 | } 73 | 74 | func alGetError() int32 { 75 | return int32(C.alGetError()) 76 | } 77 | 78 | func alGenSources(n int) []Source { 79 | s := make([]Source, n) 80 | C.alGenSources(C.ALsizei(n), (*C.ALuint)(unsafe.Pointer(&s[0]))) 81 | return s 82 | } 83 | 84 | func alSourcePlayv(s []Source) { 85 | C.alSourcePlayv(C.ALsizei(len(s)), (*C.ALuint)(unsafe.Pointer(&s[0]))) 86 | } 87 | 88 | func alSourcePausev(s []Source) { 89 | C.alSourcePausev(C.ALsizei(len(s)), (*C.ALuint)(unsafe.Pointer(&s[0]))) 90 | 91 | } 92 | 93 | func alSourceStopv(s []Source) { 94 | C.alSourceStopv(C.ALsizei(len(s)), (*C.ALuint)(unsafe.Pointer(&s[0]))) 95 | } 96 | 97 | func alSourceRewindv(s []Source) { 98 | C.alSourceRewindv(C.ALsizei(len(s)), (*C.ALuint)(unsafe.Pointer(&s[0]))) 99 | } 100 | 101 | func alDeleteSources(s []Source) { 102 | C.alDeleteSources(C.ALsizei(len(s)), (*C.ALuint)(unsafe.Pointer(&s[0]))) 103 | } 104 | 105 | func alGetSourcei(s Source, k int) int32 { 106 | var v C.ALint 107 | C.alGetSourcei(C.ALuint(s), C.ALenum(k), &v) 108 | return int32(v) 109 | } 110 | 111 | func alGetSourcef(s Source, k int) float32 { 112 | var v C.ALfloat 113 | C.alGetSourcef(C.ALuint(s), C.ALenum(k), &v) 114 | return float32(v) 115 | } 116 | 117 | func alGetSourcefv(s Source, k int, v []float32) { 118 | C.alGetSourcefv(C.ALuint(s), C.ALenum(k), (*C.ALfloat)(unsafe.Pointer(&v[0]))) 119 | } 120 | 121 | func alSourcei(s Source, k int, v int32) { 122 | C.alSourcei(C.ALuint(s), C.ALenum(k), C.ALint(v)) 123 | } 124 | 125 | func alSourcef(s Source, k int, v float32) { 126 | C.alSourcef(C.ALuint(s), C.ALenum(k), C.ALfloat(v)) 127 | } 128 | 129 | func alSourcefv(s Source, k int, v []float32) { 130 | C.alSourcefv(C.ALuint(s), C.ALenum(k), (*C.ALfloat)(unsafe.Pointer(&v[0]))) 131 | } 132 | 133 | func alSourceQueueBuffers(s Source, b []Buffer) { 134 | C.alSourceQueueBuffers(C.ALuint(s), C.ALsizei(len(b)), (*C.ALuint)(unsafe.Pointer(&b[0]))) 135 | } 136 | 137 | func alSourceUnqueueBuffers(s Source, b []Buffer) { 138 | C.alSourceUnqueueBuffers(C.ALuint(s), C.ALsizei(1), (*C.ALuint)(unsafe.Pointer(&b[0]))) 139 | } 140 | 141 | func alGetListenerf(k int) float32 { 142 | var v C.ALfloat 143 | C.alGetListenerf(C.ALenum(k), &v) 144 | return float32(v) 145 | } 146 | 147 | func alGetListenerfv(k int, v []float32) { 148 | C.alGetListenerfv(C.ALenum(k), (*C.ALfloat)(unsafe.Pointer(&v[0]))) 149 | } 150 | 151 | func alListenerf(k int, v float32) { 152 | C.alListenerf(C.ALenum(k), C.ALfloat(v)) 153 | } 154 | 155 | func alListenerfv(k int, v []float32) { 156 | C.alListenerfv(C.ALenum(k), (*C.ALfloat)(unsafe.Pointer(&v[0]))) 157 | } 158 | 159 | func alGenBuffers(n int) []Buffer { 160 | s := make([]Buffer, n) 161 | C.alGenBuffers(C.ALsizei(n), (*C.ALuint)(unsafe.Pointer(&s[0]))) 162 | return s 163 | } 164 | 165 | func alDeleteBuffers(b []Buffer) { 166 | C.alDeleteBuffers(C.ALsizei(len(b)), (*C.ALuint)(unsafe.Pointer(&b[0]))) 167 | } 168 | 169 | func alGetBufferi(b Buffer, k int) int32 { 170 | var v C.ALint 171 | C.alGetBufferi(C.ALuint(b), C.ALenum(k), &v) 172 | return int32(v) 173 | } 174 | 175 | func alBufferData(b Buffer, format uint32, data []byte, freq int32) { 176 | C.alBufferData(C.ALuint(b), C.ALenum(format), unsafe.Pointer(&data[0]), C.ALsizei(len(data)), C.ALsizei(freq)) 177 | } 178 | 179 | func alIsBuffer(b Buffer) bool { 180 | return C.alIsBuffer(C.ALuint(b)) == C.AL_TRUE 181 | } 182 | -------------------------------------------------------------------------------- /audio/openal/al/alc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin linux 6 | 7 | package al 8 | 9 | import ( 10 | "errors" 11 | "sync" 12 | "unsafe" 13 | ) 14 | 15 | var ( 16 | mu sync.Mutex 17 | device unsafe.Pointer 18 | context unsafe.Pointer 19 | ) 20 | 21 | // DeviceError returns the last known error from the current device. 22 | func DeviceError() int32 { 23 | return alcGetError(device) 24 | } 25 | 26 | // TODO(jbd): Investigate the cases where multiple audio output 27 | // devices might be needed. 28 | 29 | // OpenDevice opens the default audio device. 30 | // Calls to OpenDevice are safe for concurrent use. 31 | func OpenDevice() error { 32 | mu.Lock() 33 | defer mu.Unlock() 34 | 35 | // already opened 36 | if device != nil { 37 | return nil 38 | } 39 | 40 | dev := alcOpenDevice("") 41 | if dev == nil { 42 | return errors.New("al: cannot open the default audio device") 43 | } 44 | ctx := alcCreateContext(dev, nil) 45 | if ctx == nil { 46 | alcCloseDevice(dev) 47 | return errors.New("al: cannot create a new context") 48 | } 49 | if !alcMakeContextCurrent(ctx) { 50 | alcCloseDevice(dev) 51 | return errors.New("al: cannot make context current") 52 | } 53 | device = dev 54 | context = ctx 55 | return nil 56 | } 57 | 58 | // CloseDevice closes the device and frees related resources. 59 | // Calls to CloseDevice are safe for concurrent use. 60 | func CloseDevice() { 61 | mu.Lock() 62 | defer mu.Unlock() 63 | 64 | if device == nil { 65 | return 66 | } 67 | 68 | alcCloseDevice(device) 69 | if context != nil { 70 | alcDestroyContext(context) 71 | } 72 | device = nil 73 | context = nil 74 | } 75 | -------------------------------------------------------------------------------- /audio/openal/al/alc_android.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package al 6 | 7 | /* 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | ALCint call_alcGetError(LPALCGETERROR fn, ALCdevice* d) { 14 | return fn(d); 15 | } 16 | 17 | ALCdevice* call_alcOpenDevice(LPALCOPENDEVICE fn, const ALCchar* name) { 18 | return fn(name); 19 | } 20 | 21 | ALCboolean call_alcCloseDevice(LPALCCLOSEDEVICE fn, ALCdevice* d) { 22 | return fn(d); 23 | } 24 | 25 | ALCcontext* call_alcCreateContext(LPALCCREATECONTEXT fn, ALCdevice* d, const ALCint* attrs) { 26 | return fn(d, attrs); 27 | } 28 | 29 | ALCboolean call_alcMakeContextCurrent(LPALCMAKECONTEXTCURRENT fn, ALCcontext* c) { 30 | return fn(c); 31 | } 32 | 33 | void call_alcDestroyContext(LPALCDESTROYCONTEXT fn, ALCcontext* c) { 34 | return fn(c); 35 | } 36 | */ 37 | import "C" 38 | import ( 39 | "sync" 40 | "unsafe" 41 | ) 42 | 43 | var once sync.Once 44 | 45 | func alcGetError(d unsafe.Pointer) int32 { 46 | dev := (*C.ALCdevice)(d) 47 | return int32(C.call_alcGetError(alcGetErrorFunc, dev)) 48 | } 49 | 50 | func alcOpenDevice(name string) unsafe.Pointer { 51 | once.Do(initAL) 52 | n := C.CString(name) 53 | defer C.free(unsafe.Pointer(n)) 54 | 55 | return (unsafe.Pointer)(C.call_alcOpenDevice(alcOpenDeviceFunc, (*C.ALCchar)(unsafe.Pointer(n)))) 56 | } 57 | 58 | func alcCloseDevice(d unsafe.Pointer) bool { 59 | dev := (*C.ALCdevice)(d) 60 | return C.call_alcCloseDevice(alcCloseDeviceFunc, dev) == C.AL_TRUE 61 | } 62 | 63 | func alcCreateContext(d unsafe.Pointer, attrs []int32) unsafe.Pointer { 64 | dev := (*C.ALCdevice)(d) 65 | // TODO(jbd): Handle attrs. 66 | return (unsafe.Pointer)(C.call_alcCreateContext(alcCreateContextFunc, dev, nil)) 67 | } 68 | 69 | func alcMakeContextCurrent(c unsafe.Pointer) bool { 70 | ctx := (*C.ALCcontext)(c) 71 | return C.call_alcMakeContextCurrent(alcMakeContextCurrentFunc, ctx) == C.AL_TRUE 72 | } 73 | 74 | func alcDestroyContext(c unsafe.Pointer) { 75 | C.call_alcDestroyContext(alcDestroyContextFunc, (*C.ALCcontext)(c)) 76 | } 77 | -------------------------------------------------------------------------------- /audio/openal/al/alc_pc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin linux,!android 6 | 7 | package al 8 | 9 | /* 10 | #cgo darwin CFLAGS: -DGOOS_darwin 11 | #cgo linux CFLAGS: -DGOOS_linux 12 | #cgo darwin LDFLAGS: -framework OpenAL 13 | #cgo linux LDFLAGS: -lopenal 14 | 15 | #ifdef GOOS_darwin 16 | #include 17 | #include 18 | #endif 19 | 20 | #ifdef GOOS_linux 21 | #include 22 | #include 23 | #endif 24 | */ 25 | import "C" 26 | import "unsafe" 27 | 28 | /* 29 | On Ubuntu 14.04 'Trusty', you may have to install these libraries: 30 | sudo apt-get install libopenal-dev 31 | */ 32 | 33 | func alcGetError(d unsafe.Pointer) int32 { 34 | dev := (*C.ALCdevice)(d) 35 | return int32(C.alcGetError(dev)) 36 | } 37 | 38 | func alcOpenDevice(name string) unsafe.Pointer { 39 | if name == "" { 40 | return (unsafe.Pointer)(C.alcOpenDevice((*C.ALCchar)(nil))) 41 | } 42 | n := C.CString(name) 43 | defer C.free(unsafe.Pointer(n)) 44 | return (unsafe.Pointer)(C.alcOpenDevice((*C.ALCchar)(unsafe.Pointer(n)))) 45 | } 46 | 47 | func alcCloseDevice(d unsafe.Pointer) bool { 48 | dev := (*C.ALCdevice)(d) 49 | return C.alcCloseDevice(dev) == C.ALC_TRUE 50 | } 51 | 52 | func alcCreateContext(d unsafe.Pointer, attrs []int32) unsafe.Pointer { 53 | dev := (*C.ALCdevice)(d) 54 | return (unsafe.Pointer)(C.alcCreateContext(dev, nil)) 55 | } 56 | 57 | func alcMakeContextCurrent(c unsafe.Pointer) bool { 58 | ctx := (*C.ALCcontext)(c) 59 | return C.alcMakeContextCurrent(ctx) == C.ALC_TRUE 60 | } 61 | 62 | func alcDestroyContext(c unsafe.Pointer) { 63 | C.alcDestroyContext((*C.ALCcontext)(c)) 64 | } 65 | -------------------------------------------------------------------------------- /audio/openal/al/const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin linux 6 | 7 | package al 8 | 9 | // Error returns one of these error codes. 10 | const ( 11 | NoError = 0x0000 12 | InvalidName = 0xA001 13 | InvalidEnum = 0xA002 14 | InvalidValue = 0xA003 15 | InvalidOperation = 0xA004 16 | OutOfMemory = 0xA005 17 | ) 18 | 19 | // Distance models. 20 | const ( 21 | InverseDistance = 0xD001 22 | InverseDistanceClamped = 0xD002 23 | LinearDistance = 0xD003 24 | LinearDistanceClamped = 0xD004 25 | ExponentDistance = 0xD005 26 | ExponentDistanceClamped = 0xD006 27 | ) 28 | 29 | // Global parameters. 30 | const ( 31 | paramDistanceModel = 0xD000 32 | paramDopplerFactor = 0xC000 33 | paramDopplerVelocity = 0xC001 34 | paramSpeedOfSound = 0xC003 35 | paramVendor = 0xB001 36 | paramVersion = 0xB002 37 | paramRenderer = 0xB003 38 | paramExtensions = 0xB004 39 | ) 40 | 41 | // Source and listener parameters. 42 | const ( 43 | paramPosition = 0x1004 44 | paramVelocity = 0x1006 45 | paramGain = 0x100A 46 | 47 | paramOrientation = 0x100F 48 | 49 | paramSourceRelative = 0x0202 50 | paramSourceType = 0x1027 51 | paramLooping = 0x1007 52 | paramBuffer = 0x1009 53 | paramBuffersQueued = 0x1015 54 | paramBuffersProcessed = 0x1016 55 | paramMinGain = 0x100D 56 | paramMaxGain = 0x100E 57 | paramReferenceDistance = 0x1020 58 | paramRolloffFactor = 0x1021 59 | paramMaxDistance = 0x1023 60 | paramPitch = 0x1003 61 | paramDirection = 0x1005 62 | paramConeInnerAngle = 0x1001 63 | paramConeOuterAngle = 0x1002 64 | paramConeOuterGain = 0x1022 65 | paramSecOffset = 0x1024 66 | paramSampleOffset = 0x1025 67 | paramByteOffset = 0x1026 68 | paramSourceState = 0x1010 69 | ) 70 | 71 | // A source could be in the state of initial, playing, paused or stopped. 72 | const ( 73 | Initial = 0x1011 74 | Playing = 0x1012 75 | Paused = 0x1013 76 | Stopped = 0x1014 77 | ) 78 | 79 | // Buffer parameters. 80 | const ( 81 | paramFreq = 0x2001 82 | paramBits = 0x2002 83 | paramChannels = 0x2003 84 | paramSize = 0x2004 85 | ) 86 | 87 | // Audio formats. Buffer.BufferData accepts one of these formats as the data format. 88 | const ( 89 | FormatMono8 = 0x1100 90 | FormatMono16 = 0x1101 91 | FormatStereo8 = 0x1102 92 | FormatStereo16 = 0x1103 93 | ) 94 | 95 | // CapabilityDistanceModel represents the capability of specifying a different distance 96 | // model for each source. 97 | const CapabilityDistanceModel = Capability(0x200) 98 | -------------------------------------------------------------------------------- /audio/openal/decoding/decoder.go: -------------------------------------------------------------------------------- 1 | // Package decoding is used for converting familiar file types to data usable by 2 | // OpenAL. 3 | package decoding 4 | 5 | import ( 6 | "errors" 7 | "io" 8 | "os" 9 | "time" 10 | 11 | "github.com/tanema/amore/audio/openal/al" 12 | "github.com/tanema/amore/file" 13 | ) 14 | 15 | type ( 16 | // Decoder is a base implementation of a few methods to keep tryings DRY 17 | Decoder struct { 18 | src io.ReadCloser 19 | codec io.ReadSeeker 20 | bitDepth int16 21 | eof bool 22 | dataSize int32 23 | currentPos int64 24 | byteRate int32 25 | 26 | Channels int16 27 | SampleRate int32 28 | Buffer []byte 29 | Format uint32 30 | } 31 | ) 32 | 33 | var extHandlers = map[string]func(io.ReadCloser) (*Decoder, error){ 34 | ".wav": newWaveDecoder, 35 | ".ogg": newVorbisDecoder, 36 | ".flac": newFlacDecoder, 37 | ".mp3": newMp3Decoder, 38 | } 39 | 40 | // arbitrary buffer size, could be tuned in the future 41 | const bufferSize = 128 * 1024 42 | 43 | // Decode will get the file at the path provided. It will then send it to the decoder 44 | // that will handle its file type by the extention on the path. Supported formats 45 | // are wav, ogg, and flac. If there is an error retrieving the file or decoding it, 46 | // will return that error. 47 | func Decode(filepath string) (*Decoder, error) { 48 | src, err := file.Open(filepath) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | handler, hasHandler := extHandlers[file.Ext(filepath)] 54 | if !hasHandler { 55 | src.Close() 56 | return nil, errors.New("unsupported audio file extention") 57 | } 58 | 59 | decoder, err := handler(src) 60 | if err != nil { 61 | src.Close() 62 | return nil, err 63 | } 64 | 65 | return decoder, nil 66 | } 67 | 68 | func newDecoder(src io.ReadCloser, codec io.ReadSeeker, channels int16, sampleRate int32, bitDepth int16, dataSize int32) *Decoder { 69 | decoder := &Decoder{ 70 | src: src, 71 | codec: codec, 72 | Channels: channels, 73 | SampleRate: sampleRate, 74 | bitDepth: bitDepth, 75 | dataSize: dataSize, 76 | currentPos: 0, 77 | byteRate: (sampleRate * int32(channels) * int32(bitDepth)) / 8, 78 | } 79 | 80 | switch { 81 | case channels == 1 && bitDepth == 8: 82 | decoder.Format = al.FormatMono8 83 | case channels == 1 && bitDepth == 16: 84 | decoder.Format = al.FormatMono16 85 | case channels == 2 && bitDepth == 8: 86 | decoder.Format = al.FormatStereo8 87 | case channels == 2 && bitDepth == 16: 88 | decoder.Format = al.FormatStereo16 89 | } 90 | 91 | return decoder 92 | } 93 | 94 | // IsFinished returns is the decoder is finished decoding 95 | func (decoder *Decoder) IsFinished() bool { 96 | return decoder.eof 97 | } 98 | 99 | // Duration will return the total time duration of this clip 100 | func (decoder *Decoder) Duration() time.Duration { 101 | return decoder.ByteOffsetToDur(decoder.dataSize) 102 | } 103 | 104 | // GetData returns the complete set of data 105 | func (decoder *Decoder) GetData() []byte { 106 | data := make([]byte, decoder.dataSize) 107 | decoder.Seek(0) 108 | decoder.codec.Read(data) 109 | return data 110 | } 111 | 112 | // ByteOffsetToDur will translate byte count to time duration 113 | func (decoder *Decoder) ByteOffsetToDur(offset int32) time.Duration { 114 | return time.Duration(offset/decoder.byteRate) * time.Second 115 | } 116 | 117 | // DurToByteOffset will translate time duration to a byte count 118 | func (decoder *Decoder) DurToByteOffset(dur time.Duration) int32 { 119 | return int32(dur/time.Second) * decoder.byteRate 120 | } 121 | 122 | // Decode will read the next chunk into the buffer and return the amount of bytes read 123 | func (decoder *Decoder) Decode() int { 124 | buffer := make([]byte, bufferSize) 125 | n, err := decoder.codec.Read(buffer) 126 | decoder.Buffer = buffer[:n] 127 | decoder.eof = (err == io.EOF) 128 | return n 129 | } 130 | 131 | // Seek will seek in the source data by count of bytes 132 | func (decoder *Decoder) Seek(s int64) bool { 133 | decoder.currentPos = s 134 | _, err := decoder.codec.Seek(decoder.currentPos, os.SEEK_SET) 135 | decoder.eof = (err == io.EOF) 136 | return err == nil || decoder.eof 137 | } 138 | -------------------------------------------------------------------------------- /audio/openal/decoding/flac.go: -------------------------------------------------------------------------------- 1 | package decoding 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/eaburns/flac" 7 | ) 8 | 9 | func newFlacDecoder(src io.ReadCloser) (*Decoder, error) { 10 | r, err := flac.NewDecoder(src) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | totalBytes := int32(r.TotalSamples * int64(r.NChannels) * int64(r.BitsPerSample/8)) 16 | d := &flacReader{ 17 | data: make([]byte, totalBytes), 18 | totalBytes: int(totalBytes), 19 | source: src, 20 | decoder: r, 21 | } 22 | 23 | return newDecoder( 24 | src, 25 | d, 26 | int16(r.NChannels), 27 | int32(r.SampleRate), 28 | int16(r.BitsPerSample), 29 | totalBytes, 30 | ), nil 31 | } 32 | 33 | type flacReader struct { 34 | data []byte 35 | totalBytes int 36 | readBytes int 37 | pos int 38 | source io.Closer 39 | decoder *flac.Decoder 40 | } 41 | 42 | func (d *flacReader) readUntil(pos int) error { 43 | for d.readBytes < pos { 44 | data, err := d.decoder.Next() 45 | if len(data) > 0 { 46 | p := d.readBytes 47 | for i := 0; i < len(data); i++ { 48 | d.data[p+i] = data[i] 49 | } 50 | d.readBytes += len(data) 51 | } 52 | if err == io.EOF { 53 | if err := d.source.Close(); err != nil { 54 | return err 55 | } 56 | break 57 | } 58 | if err != nil { 59 | return err 60 | } 61 | } 62 | return nil 63 | } 64 | 65 | func (d *flacReader) Read(b []byte) (int, error) { 66 | left := d.totalBytes - d.pos 67 | if left > len(b) { 68 | left = len(b) 69 | } 70 | if left <= 0 { 71 | return 0, io.EOF 72 | } 73 | if err := d.readUntil(d.pos + left); err != nil { 74 | return 0, err 75 | } 76 | copy(b, d.data[d.pos:d.pos+left]) 77 | d.pos += left 78 | if d.pos == d.totalBytes { 79 | return left, io.EOF 80 | } 81 | return left, nil 82 | } 83 | 84 | func (d *flacReader) Seek(offset int64, whence int) (int64, error) { 85 | next := int64(0) 86 | switch whence { 87 | case io.SeekStart: 88 | next = offset 89 | case io.SeekCurrent: 90 | next = int64(d.pos) + offset 91 | case io.SeekEnd: 92 | next = int64(d.totalBytes) + offset 93 | } 94 | d.pos = int(next) 95 | if err := d.readUntil(d.pos); err != nil { 96 | return 0, err 97 | } 98 | return next, nil 99 | } 100 | -------------------------------------------------------------------------------- /audio/openal/decoding/mp3.go: -------------------------------------------------------------------------------- 1 | package decoding 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/hajimehoshi/go-mp3" 7 | ) 8 | 9 | func newMp3Decoder(src io.ReadCloser) (*Decoder, error) { 10 | r, err := mp3.NewDecoder(src) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | d := &mp3Reader{ 16 | data: []byte{}, 17 | source: src, 18 | decoder: r, 19 | } 20 | 21 | return newDecoder( 22 | src, 23 | d, 24 | 2, 25 | int32(r.SampleRate()), 26 | 16, 27 | int32(r.Length()), 28 | ), nil 29 | } 30 | 31 | type mp3Reader struct { 32 | data []byte 33 | readBytes int 34 | pos int 35 | source io.Closer 36 | decoder *mp3.Decoder 37 | } 38 | 39 | func (d *mp3Reader) readUntil(pos int) error { 40 | for len(d.data) <= pos { 41 | buf := make([]uint8, 8192) 42 | n, err := d.decoder.Read(buf) 43 | d.data = append(d.data, buf[:n]...) 44 | if err != nil { 45 | if err == io.EOF { 46 | return io.EOF 47 | } 48 | return err 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | func (d *mp3Reader) Read(b []byte) (int, error) { 55 | left := int(d.decoder.Length()) - d.pos 56 | if left > len(b) { 57 | left = len(b) 58 | } 59 | if left <= 0 { 60 | return 0, io.EOF 61 | } 62 | if err := d.readUntil(d.pos + left); err != nil { 63 | return 0, err 64 | } 65 | copy(b, d.data[d.pos:d.pos+left-1]) 66 | d.pos += left 67 | if d.pos == int(d.decoder.Length()) { 68 | return left, io.EOF 69 | } 70 | return left, nil 71 | } 72 | 73 | func (d *mp3Reader) Seek(offset int64, whence int) (int64, error) { 74 | next := int64(0) 75 | switch whence { 76 | case io.SeekStart: 77 | next = offset 78 | case io.SeekCurrent: 79 | next = int64(d.pos) + offset 80 | case io.SeekEnd: 81 | next = int64(d.decoder.Length()) + offset 82 | } 83 | d.pos = int(next) 84 | if err := d.readUntil(d.pos); err != nil { 85 | return 0, err 86 | } 87 | return next, nil 88 | } 89 | -------------------------------------------------------------------------------- /audio/openal/decoding/vorbis.go: -------------------------------------------------------------------------------- 1 | package decoding 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jfreymuth/oggvorbis" 7 | ) 8 | 9 | func newVorbisDecoder(src io.ReadCloser) (*Decoder, error) { 10 | r, err := oggvorbis.NewReader(src) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | d := &vorbisReader{ 16 | data: make([]float32, r.Length()*2), 17 | totalBytes: int(r.Length()) * 4, 18 | source: src, 19 | decoder: r, 20 | } 21 | 22 | return newDecoder( 23 | src, 24 | d, 25 | int16(r.Channels()), 26 | int32(r.SampleRate()), 27 | 16, 28 | int32(d.totalBytes), 29 | ), nil 30 | } 31 | 32 | type vorbisReader struct { 33 | data []float32 34 | totalBytes int 35 | readBytes int 36 | pos int 37 | source io.Closer 38 | decoder *oggvorbis.Reader 39 | } 40 | 41 | func (d *vorbisReader) readUntil(pos int) error { 42 | buffer := make([]float32, 8192) 43 | for d.readBytes < pos { 44 | n, err := d.decoder.Read(buffer) 45 | if n > 0 { 46 | if d.readBytes+n*2 > d.totalBytes { 47 | n = (d.totalBytes - d.readBytes) / 2 48 | } 49 | p := d.readBytes / 2 50 | for i := 0; i < n; i++ { 51 | d.data[p+i] = buffer[i] 52 | } 53 | d.readBytes += n * 2 54 | } 55 | if err == io.EOF { 56 | if err := d.source.Close(); err != nil { 57 | return err 58 | } 59 | break 60 | } 61 | if err != nil { 62 | return err 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | func (d *vorbisReader) Read(b []byte) (int, error) { 69 | left := d.totalBytes - d.pos 70 | if left > len(b) { 71 | left = len(b) 72 | } 73 | if left <= 0 { 74 | return 0, io.EOF 75 | } 76 | left = left / 2 * 2 77 | if err := d.readUntil(d.pos + left); err != nil { 78 | return 0, err 79 | } 80 | for i := 0; i < left/2; i++ { 81 | f := d.data[d.pos/2+i] 82 | s := int16(f * (1<<15 - 1)) 83 | b[2*i] = uint8(s) 84 | b[2*i+1] = uint8(s >> 8) 85 | } 86 | d.pos += left 87 | if d.pos == d.totalBytes { 88 | return left, io.EOF 89 | } 90 | return left, nil 91 | } 92 | 93 | func (d *vorbisReader) Seek(offset int64, whence int) (int64, error) { 94 | next := int64(0) 95 | switch whence { 96 | case io.SeekStart: 97 | next = offset 98 | case io.SeekCurrent: 99 | next = int64(d.pos) + offset 100 | case io.SeekEnd: 101 | next = int64(d.totalBytes) + offset 102 | } 103 | // pos should be always even 104 | next = next / 2 * 2 105 | d.pos = int(next) 106 | if err := d.readUntil(d.pos); err != nil { 107 | return 0, err 108 | } 109 | return next, nil 110 | } 111 | -------------------------------------------------------------------------------- /audio/openal/decoding/wav.go: -------------------------------------------------------------------------------- 1 | package decoding 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | ) 11 | 12 | type waveDecoder struct { 13 | io.Reader 14 | src io.ReadCloser 15 | header *riffHeader 16 | info *riffChunkFmt 17 | firstSamplePos uint32 18 | dataSize int32 19 | } 20 | 21 | type riffHeader struct { 22 | Ftype [4]byte 23 | ChunkSize uint32 24 | ChunkFormat [4]byte 25 | } 26 | 27 | type riffChunkFmt struct { 28 | LengthOfHeader uint32 29 | AudioFormat uint16 // 1 = PCM not compressed 30 | NumChannels uint16 31 | SampleRate uint32 32 | BytesPerSec uint32 33 | BytesPerBloc uint16 34 | BitsPerSample uint16 35 | } 36 | 37 | func newWaveDecoder(src io.ReadCloser) (*Decoder, error) { 38 | decoder := &waveDecoder{ 39 | src: src, 40 | Reader: src, 41 | info: &riffChunkFmt{}, 42 | header: &riffHeader{}, 43 | } 44 | 45 | if err := decoder.decode(); err != nil { 46 | return nil, err 47 | } 48 | 49 | return newDecoder( 50 | src, 51 | decoder, 52 | int16(decoder.info.NumChannels), 53 | int32(decoder.info.SampleRate), 54 | int16(decoder.info.BitsPerSample), 55 | decoder.dataSize, 56 | ), nil 57 | } 58 | 59 | func (decoder *waveDecoder) decode() error { 60 | if err := binary.Read(decoder.src, binary.LittleEndian, decoder.header); err != nil { 61 | return err 62 | } 63 | 64 | if !bytes.Equal(decoder.header.Ftype[:], []byte("RIFF")) || 65 | !bytes.Equal(decoder.header.ChunkFormat[:], []byte("WAVE")) { 66 | return errors.New("not a RIFF/WAVE file") 67 | } 68 | 69 | var chunk [4]byte 70 | var chunkSize uint32 71 | for { 72 | // Read next chunkID 73 | err := binary.Read(decoder.src, binary.BigEndian, &chunk) 74 | if err == io.EOF { 75 | return io.ErrUnexpectedEOF 76 | } else if err != nil { 77 | return err 78 | } 79 | 80 | // and it's size in bytes 81 | err = binary.Read(decoder.src, binary.LittleEndian, &chunkSize) 82 | if err == io.EOF { 83 | return io.ErrUnexpectedEOF 84 | } else if err != nil { 85 | return err 86 | } 87 | 88 | seeker := decoder.src.(io.Seeker) 89 | if bytes.Equal(chunk[:], []byte("fmt ")) { 90 | // seek 4 bytes back because riffChunkFmt reads the chunkSize again 91 | if _, err = seeker.Seek(-4, os.SEEK_CUR); err != nil { 92 | return err 93 | } 94 | 95 | if err = binary.Read(decoder.src, binary.LittleEndian, decoder.info); err != nil { 96 | return err 97 | } 98 | if decoder.info.LengthOfHeader > 16 { // canonical format if chunklen == 16 99 | // Skip extra params 100 | if _, err = seeker.Seek(int64(decoder.info.LengthOfHeader-16), os.SEEK_CUR); err != nil { 101 | return err 102 | } 103 | } 104 | 105 | // Is audio supported ? 106 | if decoder.info.AudioFormat != 1 { 107 | return fmt.Errorf("Audio Format not supported") 108 | } 109 | } else if bytes.Equal(chunk[:], []byte("data")) { 110 | size, _ := seeker.Seek(0, os.SEEK_CUR) 111 | decoder.firstSamplePos = uint32(size) 112 | decoder.dataSize = int32(chunkSize) 113 | break 114 | } else { 115 | if _, err = seeker.Seek(int64(chunkSize), os.SEEK_CUR); err != nil { 116 | return err 117 | } 118 | } 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (decoder *waveDecoder) Seek(offset int64, whence int) (int64, error) { 125 | seeker := decoder.src.(io.Seeker) 126 | switch whence { 127 | case io.SeekStart: 128 | offset += int64(decoder.firstSamplePos) 129 | case io.SeekCurrent: 130 | case io.SeekEnd: 131 | offset += int64(decoder.firstSamplePos) + int64(decoder.dataSize) 132 | whence = io.SeekStart 133 | } 134 | return seeker.Seek(offset, whence) 135 | } 136 | -------------------------------------------------------------------------------- /audio/openal/pool.go: -------------------------------------------------------------------------------- 1 | package openal 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/tanema/amore/audio/openal/al" 8 | ) 9 | 10 | const maxSources = 64 11 | 12 | var pool *audioPool 13 | 14 | // audioPool manages all openAL generated sources. 15 | type audioPool struct { 16 | mutex sync.Mutex 17 | totalSources int 18 | sources [maxSources]al.Source 19 | available []al.Source 20 | playing map[al.Source]*Source 21 | } 22 | 23 | // init will open the audio interface. 24 | func init() { 25 | if err := al.OpenDevice(); err != nil { 26 | panic(err) 27 | } 28 | } 29 | 30 | // GetVolume returns the master volume. 31 | func GetVolume() float32 { return al.ListenerGain() } 32 | 33 | // SetVolume sets the master volume 34 | func SetVolume(gain float32) { 35 | al.SetListenerGain(gain) 36 | } 37 | 38 | // PauseAll will pause all sources 39 | func PauseAll() { 40 | for _, source := range pool.playing { 41 | source.Pause() 42 | } 43 | } 44 | 45 | // PlayAll will play all sources 46 | func PlayAll() { 47 | for _, source := range pool.playing { 48 | source.Play() 49 | } 50 | } 51 | 52 | // RewindAll will rewind all sources 53 | func RewindAll() { 54 | for _, source := range pool.playing { 55 | source.Rewind() 56 | } 57 | } 58 | 59 | // StopAll stop all sources 60 | func StopAll() { 61 | for _, source := range pool.playing { 62 | source.Stop() 63 | } 64 | pool.playing = make(map[al.Source]*Source) 65 | } 66 | 67 | // createPool generates a new pool and gets the max sources. 68 | func createPool() { 69 | pool = &audioPool{ 70 | sources: [maxSources]al.Source{}, 71 | available: []al.Source{}, 72 | playing: make(map[al.Source]*Source), 73 | } 74 | 75 | // Generate sources. 76 | for i := 0; i < maxSources; i++ { 77 | pool.sources[i] = al.GenSources(1)[0] 78 | 79 | // We might hit an implementation-dependent limit on the total number 80 | // of sources before reaching maxSources. 81 | if al.Error() != 0 { 82 | break 83 | } 84 | 85 | pool.totalSources++ 86 | } 87 | 88 | if pool.totalSources < 4 { 89 | panic("Could not generate audio sources.") 90 | } 91 | 92 | // Make all sources available initially. 93 | for i := 0; i < pool.totalSources; i++ { 94 | pool.available = append(pool.available, pool.sources[i]) 95 | } 96 | 97 | go func() { 98 | ticker := time.NewTicker(1 * time.Second) 99 | go func() { 100 | for { 101 | select { 102 | case <-ticker.C: 103 | pool.update() 104 | } 105 | } 106 | }() 107 | }() 108 | } 109 | 110 | // update will cycle through al the playing sources and updates the buffers 111 | func (p *audioPool) update() { 112 | for _, source := range p.playing { 113 | if !source.update() { 114 | source.Stop() 115 | } 116 | } 117 | } 118 | 119 | // getSourceCount will get the count of playing sources 120 | func (p *audioPool) getSourceCount() int { 121 | return len(p.playing) 122 | } 123 | 124 | // claim will take an openAL source for playing with an amore source 125 | func (p *audioPool) claim(source *Source) bool { 126 | if len(p.available) > 0 { 127 | source.source, p.available = p.available[len(p.available)-1], p.available[:len(p.available)-1] 128 | p.playing[source.source] = source 129 | return true 130 | } 131 | return false 132 | } 133 | 134 | // release will put an openAL source back in to the available queue 135 | func (p *audioPool) release(source *Source) { 136 | p.available = append(p.available, source.source) 137 | delete(p.playing, source.source) 138 | source.source = 0 139 | } 140 | -------------------------------------------------------------------------------- /audio/openal/source.go: -------------------------------------------------------------------------------- 1 | package openal 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/tanema/amore/audio/openal/al" 7 | "github.com/tanema/amore/audio/openal/decoding" 8 | ) 9 | 10 | const ( 11 | maxAttenuationDistance = 1000000.0 // upper limit of sound attentuation time. 12 | maxBuffers = 8 //arbitrary limit of umber of buffers a source can use to stream 13 | ) 14 | 15 | // Source manages decoding sound data, creates an openal sound and manages the 16 | // data associated with the source. 17 | type Source struct { 18 | decoder *decoding.Decoder 19 | source al.Source 20 | isStatic bool 21 | pitch float32 22 | volume float32 23 | looping bool 24 | paused bool 25 | staticBuffer al.Buffer 26 | offsetBytes int32 27 | } 28 | 29 | // NewSource creates a new Source from a file at the path provided. If you 30 | // specify a static source it will all be buffered into a single buffer. If 31 | // false then it will create many buffers a cycle through them with data chunks. 32 | // This allows a smaller memory footprint while playing bigger music files. You 33 | // may want a static file if the sound is less than 2 seconds. It allows for faster 34 | // cleaning playing of shorter sounds like footsteps. 35 | func NewSource(filepath string, static bool) (*Source, error) { 36 | if pool == nil { 37 | createPool() 38 | } 39 | 40 | decoder, err := decoding.Decode(filepath) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | newSource := &Source{ 46 | decoder: decoder, 47 | isStatic: static, 48 | pitch: 1, 49 | volume: 1, 50 | } 51 | 52 | if static { 53 | newSource.staticBuffer = al.GenBuffers(1)[0] 54 | newSource.staticBuffer.BufferData(decoder.Format, decoder.GetData(), decoder.SampleRate) 55 | } 56 | 57 | return newSource, nil 58 | } 59 | 60 | // isValid will return true if the source is associated with an openal source anymore. 61 | // if not it will return false and will disable most funtionality 62 | func (s *Source) isValid() bool { 63 | return s.source != 0 64 | } 65 | 66 | // IsFinished will return true if the source is at the end of its duration and 67 | // it is not a looping source. 68 | func (s *Source) IsFinished() bool { 69 | if s.isStatic { 70 | return s.IsStopped() 71 | } 72 | return s.IsStopped() && !s.IsLooping() && s.decoder.IsFinished() 73 | } 74 | 75 | // update will return true if successfully updated the source. If the source is 76 | // static it will return if the item is still playing. If the item is a streamed 77 | // source it will return true if it is still playing but after updating it's buffers. 78 | func (s *Source) update() bool { 79 | if !s.isValid() { 80 | return false 81 | } 82 | 83 | if s.isStatic { 84 | return !s.IsStopped() 85 | } else if s.IsLooping() || !s.IsFinished() { 86 | pool.mutex.Lock() 87 | defer pool.mutex.Unlock() 88 | for i := s.source.BuffersProcessed(); i > 0; i-- { 89 | curOffsetBytes := s.source.OffsetByte() 90 | buffer := s.source.UnqueueBuffer() 91 | newOffsetBytes := s.source.OffsetByte() 92 | s.offsetBytes += (curOffsetBytes - newOffsetBytes) 93 | if s.stream(buffer) > 0 { 94 | s.source.QueueBuffers(buffer) 95 | } 96 | } 97 | return true 98 | } 99 | 100 | return false 101 | } 102 | 103 | // reset sets all the source's values in openal to the preset values. 104 | func (s *Source) reset() { 105 | if !s.isValid() { 106 | return 107 | } 108 | s.source.SetGain(s.volume) 109 | s.source.SetPitch(s.pitch) 110 | if s.isStatic { 111 | s.source.SetLooping(s.looping) 112 | } 113 | } 114 | 115 | // GetDuration returns the total duration of the source. 116 | func (s *Source) GetDuration() time.Duration { 117 | return s.decoder.Duration() 118 | } 119 | 120 | // GetPitch returns the current pitch of the Source in the range 0.0, 1.0 121 | func (s *Source) GetPitch() float32 { 122 | if s.isValid() { 123 | return s.source.Pitch() 124 | } 125 | return s.pitch 126 | } 127 | 128 | // GetVolume returns the current volume of the Source. 129 | func (s *Source) GetVolume() float32 { 130 | if s.isValid() { 131 | return s.source.Gain() 132 | } 133 | return s.volume 134 | } 135 | 136 | // GetState returns the playing state of the source. 137 | func (s *Source) GetState() string { 138 | switch s.source.State() { 139 | case al.Initial: 140 | return "initial" 141 | case al.Playing: 142 | return "playing" 143 | case al.Paused: 144 | return "paused" 145 | case al.Stopped: 146 | return "stopped" 147 | default: 148 | return "unknown" 149 | } 150 | } 151 | 152 | // IsLooping returns whether the Source will loop. 153 | func (s *Source) IsLooping() bool { 154 | return s.looping 155 | } 156 | 157 | // IsPaused returns whether the Source is paused. 158 | func (s *Source) IsPaused() bool { 159 | if s.isValid() { 160 | return s.GetState() == "paused" 161 | } 162 | return false 163 | } 164 | 165 | // IsPlaying returns whether the Source is playing. 166 | func (s *Source) IsPlaying() bool { 167 | if s.isValid() { 168 | return s.GetState() == "playing" 169 | } 170 | return false 171 | } 172 | 173 | // IsStatic returns whether the Source is static or stream. 174 | func (s *Source) IsStatic() bool { 175 | return s.isStatic 176 | } 177 | 178 | // IsStopped returns whether the Source is stopped. 179 | func (s *Source) IsStopped() bool { 180 | if s.isValid() { 181 | return s.GetState() == "stopped" 182 | } 183 | return true 184 | } 185 | 186 | // SetLooping sets whether the Source should loop when the source is complete. 187 | func (s *Source) SetLooping(loop bool) { 188 | s.looping = loop 189 | s.reset() 190 | } 191 | 192 | // SetPitch sets the pitch of the Source, the value should be between 0.0, 1.0 193 | func (s *Source) SetPitch(p float32) { 194 | s.pitch = p 195 | s.reset() 196 | } 197 | 198 | // SetVolume sets the current volume of the Source. 199 | func (s *Source) SetVolume(v float32) { 200 | s.volume = v 201 | s.reset() 202 | } 203 | 204 | // Play starts playing the source. 205 | func (s *Source) Play() bool { 206 | if s.IsPlaying() { 207 | return true 208 | } 209 | 210 | if s.IsPaused() { 211 | s.Resume() 212 | return true 213 | } 214 | 215 | //claim a source for ourselves and make sure it worked 216 | if !pool.claim(s) || !s.isValid() { 217 | return false 218 | } 219 | 220 | pool.mutex.Lock() 221 | defer pool.mutex.Unlock() 222 | 223 | if s.isStatic { 224 | s.source.SetBuffer(s.staticBuffer) 225 | } else { 226 | buffers := []al.Buffer{} 227 | for i := 0; i < maxBuffers; i++ { 228 | buffer := al.GenBuffers(1)[0] 229 | if s.stream(buffer) > 0 { 230 | buffers = append(buffers, buffer) 231 | } 232 | if s.decoder.IsFinished() { 233 | break 234 | } 235 | } 236 | if len(buffers) > 0 { 237 | s.source.QueueBuffers(buffers...) 238 | } 239 | } 240 | 241 | // This Source may now be associated with an OpenAL source that still has 242 | // the properties of another Source. Let's reset it to the settings 243 | // of the new one. 244 | s.reset() 245 | 246 | // Clear errors. 247 | al.Error() 248 | 249 | al.PlaySources(s.source) 250 | 251 | // alSourcePlay may fail if the system has reached its limit of simultaneous 252 | // playing sources. 253 | return al.Error() == al.NoError 254 | } 255 | 256 | // stream fills a buffer with the next chunk of data 257 | func (s *Source) stream(buffer al.Buffer) int { 258 | decoded := s.decoder.Decode() //get more data 259 | if decoded > 0 { 260 | buffer.BufferData(s.decoder.Format, s.decoder.Buffer, s.decoder.SampleRate) 261 | } 262 | if s.decoder.IsFinished() && s.IsLooping() { 263 | s.Rewind() 264 | } 265 | return decoded 266 | } 267 | 268 | // Pause pauses the source. 269 | func (s *Source) Pause() { 270 | if s.isValid() { 271 | pool.mutex.Lock() 272 | defer pool.mutex.Unlock() 273 | al.PauseSources(s.source) 274 | s.paused = true 275 | } 276 | } 277 | 278 | // Resume resumes a paused source. 279 | func (s *Source) Resume() { 280 | if s.isValid() && s.paused { 281 | pool.mutex.Lock() 282 | defer pool.mutex.Unlock() 283 | al.PlaySources(s.source) 284 | s.paused = false 285 | } 286 | } 287 | 288 | // Rewind rewinds the source source to its start time. 289 | func (s *Source) Rewind() { s.Seek(0) } 290 | 291 | // Seek sets the currently playing position of the Source. 292 | func (s *Source) Seek(offset time.Duration) { 293 | if !s.isValid() { 294 | return 295 | } 296 | s.offsetBytes = s.decoder.DurToByteOffset(offset) 297 | if !s.isStatic { 298 | al.StopSources(s.source) 299 | s.decoder.Seek(int64(s.offsetBytes)) 300 | for i := s.source.BuffersQueued(); i > 0; i-- { 301 | buffer := s.source.UnqueueBuffer() 302 | if s.stream(buffer) > 0 { 303 | s.source.QueueBuffers(buffer) 304 | } else { 305 | al.DeleteBuffers(buffer) 306 | } 307 | } 308 | if !s.paused { 309 | al.PlaySources(s.source) 310 | } 311 | } else { 312 | pool.mutex.Lock() 313 | defer pool.mutex.Unlock() 314 | s.source.SetOffsetBytes(s.offsetBytes) 315 | } 316 | } 317 | 318 | // Stop stops a playing source. 319 | func (s *Source) Stop() { 320 | if s.isValid() { 321 | pool.mutex.Lock() 322 | defer pool.mutex.Unlock() 323 | al.StopSources(s.source) 324 | s.offsetBytes = 0 325 | if !s.isStatic { 326 | queued := s.source.BuffersQueued() 327 | for i := queued; i > 0; i-- { 328 | buffer := s.source.UnqueueBuffer() 329 | al.DeleteBuffers(buffer) 330 | } 331 | s.decoder.Seek(0) 332 | } else { 333 | s.source.SetOffsetBytes(0) 334 | } 335 | s.source.ClearBuffers() 336 | pool.release(s) 337 | } 338 | } 339 | 340 | // Tell returns the currently playing position of the Source. 341 | func (s *Source) Tell() time.Duration { 342 | if s.isValid() { 343 | pool.mutex.Lock() 344 | defer pool.mutex.Unlock() 345 | if s.isStatic { 346 | return s.decoder.ByteOffsetToDur(s.source.OffsetByte()) 347 | } 348 | return s.decoder.ByteOffsetToDur(s.offsetBytes + s.source.OffsetByte()) 349 | } 350 | return time.Duration(0.0) 351 | } 352 | -------------------------------------------------------------------------------- /audio/wrap.go: -------------------------------------------------------------------------------- 1 | package audio 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/yuin/gopher-lua" 7 | 8 | "github.com/tanema/amore/runtime" 9 | ) 10 | 11 | var audioFunctions = runtime.LuaFuncs{ 12 | "new": audioNewSource, 13 | "getvolume": audioGetVolume, 14 | "setvolume": audioSetVolume, 15 | "pause": audioPauseAll, 16 | "play": audioPlayAll, 17 | "rewind": audioRewindAll, 18 | "stop": audioStopAll, 19 | } 20 | 21 | var audioMetaTables = runtime.LuaMetaTable{ 22 | "Source": { 23 | "finished": audioSourceIsFinished, 24 | "looping": audioSourceIsLooping, 25 | "paused": audioSourceIsPaused, 26 | "playing": audioSourceIsPlaying, 27 | "static": audioSourceIsStatic, 28 | "stopped": audioSourceIsStopped, 29 | "duration": audioSourceGetDuration, 30 | "pitch": audioSourceGetPitch, 31 | "volume": audioSourceGetVolume, 32 | "state": audioSourceGetState, 33 | "setlooping": audioSourceSetLooping, 34 | "setpitch": audioSourceSetPitch, 35 | "setvolume": audioSourceSetVolume, 36 | "play": audioSourcePlay, 37 | "pause": audioSourcePause, 38 | "resume": audioSourceResume, 39 | "rewind": audioSourceRewind, 40 | "seek": audioSourceSeek, 41 | "stop": audioSourceStop, 42 | "tell": audioSourceTell, 43 | }, 44 | } 45 | 46 | func init() { 47 | runtime.RegisterModule("audio", audioFunctions, audioMetaTables) 48 | } 49 | 50 | func audioNewSource(ls *lua.LState) int { 51 | source, err := NewSource(ls.ToString(1), ls.ToBool(2)) 52 | if err != nil { 53 | return 0 54 | } 55 | return returnSource(ls, source) 56 | } 57 | 58 | func audioGetVolume(ls *lua.LState) int { 59 | ls.Push(lua.LNumber(GetVolume())) 60 | return 1 61 | } 62 | 63 | func audioSetVolume(ls *lua.LState) int { 64 | SetVolume(float32(ls.ToNumber(1))) 65 | return 0 66 | } 67 | 68 | func audioPlayAll(ls *lua.LState) int { 69 | PlayAll() 70 | return 0 71 | } 72 | 73 | func audioPauseAll(ls *lua.LState) int { 74 | PlayAll() 75 | return 0 76 | } 77 | 78 | func audioRewindAll(ls *lua.LState) int { 79 | RewindAll() 80 | return 0 81 | } 82 | 83 | func audioStopAll(ls *lua.LState) int { 84 | StopAll() 85 | return 0 86 | } 87 | 88 | func audioSourceIsFinished(ls *lua.LState) int { 89 | ls.Push(lua.LBool(toSource(ls, 1).IsFinished())) 90 | return 1 91 | } 92 | 93 | func audioSourceIsLooping(ls *lua.LState) int { 94 | ls.Push(lua.LBool(toSource(ls, 1).IsLooping())) 95 | return 1 96 | } 97 | 98 | func audioSourceIsPaused(ls *lua.LState) int { 99 | ls.Push(lua.LBool(toSource(ls, 1).IsPaused())) 100 | return 1 101 | } 102 | 103 | func audioSourceIsPlaying(ls *lua.LState) int { 104 | ls.Push(lua.LBool(toSource(ls, 1).IsPlaying())) 105 | return 1 106 | } 107 | 108 | func audioSourceIsStatic(ls *lua.LState) int { 109 | ls.Push(lua.LBool(toSource(ls, 1).IsStatic())) 110 | return 1 111 | } 112 | 113 | func audioSourceIsStopped(ls *lua.LState) int { 114 | ls.Push(lua.LBool(toSource(ls, 1).IsStopped())) 115 | return 1 116 | } 117 | 118 | func audioSourceGetDuration(ls *lua.LState) int { 119 | ls.Push(lua.LNumber(toSource(ls, 1).GetDuration().Seconds())) 120 | return 1 121 | } 122 | 123 | func audioSourceTell(ls *lua.LState) int { 124 | ls.Push(lua.LNumber(toSource(ls, 1).Tell().Seconds())) 125 | return 1 126 | } 127 | 128 | func audioSourceGetPitch(ls *lua.LState) int { 129 | ls.Push(lua.LNumber(toSource(ls, 1).GetPitch())) 130 | return 1 131 | } 132 | 133 | func audioSourceGetVolume(ls *lua.LState) int { 134 | ls.Push(lua.LNumber(toSource(ls, 1).GetVolume())) 135 | return 1 136 | } 137 | 138 | func audioSourceGetState(ls *lua.LState) int { 139 | ls.Push(lua.LString(toSource(ls, 1).GetState())) 140 | return 1 141 | } 142 | 143 | func audioSourceSetLooping(ls *lua.LState) int { 144 | toSource(ls, 1).SetLooping(ls.ToBool(2)) 145 | return 0 146 | } 147 | 148 | func audioSourceSetPitch(ls *lua.LState) int { 149 | toSource(ls, 1).SetPitch(float32(ls.ToNumber(2))) 150 | return 0 151 | } 152 | 153 | func audioSourceSetVolume(ls *lua.LState) int { 154 | toSource(ls, 1).SetVolume(float32(ls.ToNumber(2))) 155 | return 0 156 | } 157 | 158 | func audioSourceSeek(ls *lua.LState) int { 159 | toSource(ls, 1).Seek(time.Duration(float32(ls.ToNumber(2)) * 1e09)) 160 | return 0 161 | } 162 | 163 | func audioSourcePlay(ls *lua.LState) int { 164 | toSource(ls, 1).Play() 165 | return 0 166 | } 167 | 168 | func audioSourcePause(ls *lua.LState) int { 169 | toSource(ls, 1).Pause() 170 | return 0 171 | } 172 | 173 | func audioSourceResume(ls *lua.LState) int { 174 | toSource(ls, 1).Resume() 175 | return 0 176 | } 177 | 178 | func audioSourceRewind(ls *lua.LState) int { 179 | toSource(ls, 1).Rewind() 180 | return 0 181 | } 182 | 183 | func audioSourceStop(ls *lua.LState) int { 184 | toSource(ls, 1).Stop() 185 | return 0 186 | } 187 | 188 | func toSource(ls *lua.LState, offset int) Source { 189 | img := ls.CheckUserData(offset) 190 | if v, ok := img.Value.(Source); ok { 191 | return v 192 | } 193 | ls.ArgError(offset, "audio source expected") 194 | return nil 195 | } 196 | 197 | func returnSource(ls *lua.LState, source Source) int { 198 | f := ls.NewUserData() 199 | f.Value = source 200 | ls.SetMetatable(f, ls.GetTypeMetatable("Source")) 201 | ls.Push(f) 202 | return 1 203 | } 204 | -------------------------------------------------------------------------------- /cmd/app.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/mitchellh/cli" 8 | 9 | // These are lua wrapped code that will be made accessible to lua 10 | _ "github.com/tanema/amore/audio" 11 | _ "github.com/tanema/amore/gfx/wrap" 12 | _ "github.com/tanema/amore/input" 13 | 14 | "github.com/tanema/amore/runtime" 15 | ) 16 | 17 | var App = &cli.CLI{ 18 | Name: "moony", 19 | Args: os.Args[1:], 20 | Commands: commands, 21 | Autocomplete: true, 22 | AutocompleteNoDefaultFlags: true, 23 | } 24 | 25 | var ui = &cli.BasicUi{ 26 | Reader: os.Stdin, 27 | Writer: os.Stdout, 28 | ErrorWriter: os.Stderr, 29 | } 30 | 31 | var commands = map[string]cli.CommandFactory{ 32 | "": func() (cli.Command, error) { return &runCommand{ui: ui}, nil }, 33 | "run": func() (cli.Command, error) { return &runCommand{ui: ui}, nil }, 34 | } 35 | 36 | type runCommand struct { 37 | ui cli.Ui 38 | } 39 | 40 | func (run *runCommand) Run(args []string) int { 41 | if err := runtime.Run("main.lua"); err != nil { 42 | run.ui.Error(err.Error()) 43 | return 1 44 | } 45 | return 0 46 | } 47 | 48 | func (run *runCommand) Synopsis() string { 49 | return "" 50 | } 51 | 52 | func (run *runCommand) Help() string { 53 | helpText := ` 54 | Usage: moony 55 | Run your program yo 56 | Options: 57 | -h, --help show this help 58 | ` 59 | return strings.TrimSpace(helpText) 60 | } 61 | -------------------------------------------------------------------------------- /cmd/moony/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/tanema/amore/cmd" 8 | ) 9 | 10 | func main() { 11 | exitStatus, err := cmd.App.Run() 12 | if err != nil { 13 | log.Println(err) 14 | } 15 | os.Exit(exitStatus) 16 | } 17 | -------------------------------------------------------------------------------- /file/file.go: -------------------------------------------------------------------------------- 1 | // Package file is meant to take care of all asset file opening so that file 2 | // access is safe if trying to access a bundled file or a file from the disk 3 | package file 4 | 5 | import ( 6 | "archive/zip" 7 | "io" 8 | "io/ioutil" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/goxjs/glfw" 13 | ) 14 | 15 | var ( 16 | // map of zip file data related to thier real file path for consistent access 17 | zipFiles = make(map[string]*zip.File) 18 | ) 19 | 20 | // Register will be called by bundled assets to register the bundled files into the 21 | // zip file data to be used by the program. 22 | func Register(data string) { 23 | zipReader, err := zip.NewReader(strings.NewReader(data), int64(len(data))) 24 | if err != nil { 25 | panic(err) 26 | } 27 | for _, file := range zipReader.File { 28 | zipFiles[file.Name] = file 29 | } 30 | } 31 | 32 | // Read will read a file at the path specified in total and return a byte 33 | // array of the file contents 34 | func Read(path string) ([]byte, error) { 35 | path = normalizePath(path) 36 | var file io.ReadCloser 37 | var err error 38 | if zipfile, ok := zipFiles[path]; ok { 39 | file, err = zipfile.Open() 40 | } else { 41 | file, err = Open(path) 42 | } 43 | if err != nil { 44 | return nil, err 45 | } 46 | defer file.Close() 47 | return ioutil.ReadAll(file) 48 | } 49 | 50 | // ReadString acts like Read but instead return a string. This is useful in certain 51 | // circumstances. 52 | func ReadString(filename string) string { 53 | s, err := Read(filename) 54 | if err != nil { 55 | return "" 56 | } 57 | return string(s[:]) 58 | } 59 | 60 | // Open will return the file if its bundled or on disk and return a File interface 61 | // for the file and an error if it does not exist. The File interface allows for 62 | // consitent access to disk files and zip files. 63 | func Open(path string) (io.ReadCloser, error) { 64 | path = normalizePath(path) 65 | zipFile, ok := zipFiles[path] 66 | if !ok { 67 | return glfw.Open(path) 68 | } 69 | return zipFile.Open() 70 | } 71 | 72 | // Ext will return the extention of the file 73 | func Ext(filename string) string { 74 | return filepath.Ext(normalizePath(filename)) 75 | } 76 | 77 | // normalizePath will prefix a path with assets/ if it comes from that directory 78 | // or if it is bundled in such a way. 79 | func normalizePath(filename string) string { 80 | p := strings.Replace(filename, "//", "/", -1) 81 | //bundle assets from asset folder 82 | if _, ok := zipFiles["assets/"+p]; ok { 83 | return "assets/" + p 84 | } 85 | return p 86 | } 87 | -------------------------------------------------------------------------------- /gfx/canvas.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | 7 | "github.com/go-gl/mathgl/mgl32" 8 | 9 | "github.com/goxjs/gl" 10 | ) 11 | 12 | // Canvas is an off-screen render target. 13 | type Canvas struct { 14 | *Texture 15 | fbo gl.Framebuffer 16 | depthStencil gl.Renderbuffer 17 | status uint32 18 | width, height int32 19 | systemViewport []int32 20 | } 21 | 22 | // NewCanvas creates a pointer to a new canvas with the privided width and height 23 | func NewCanvas(width, height int32) *Canvas { 24 | newCanvas := &Canvas{ 25 | width: width, 26 | height: height, 27 | } 28 | registerVolatile(newCanvas) 29 | return newCanvas 30 | } 31 | 32 | // loadVolatile will create the framebuffer and return true if successful 33 | func (canvas *Canvas) loadVolatile() bool { 34 | canvas.status = gl.FRAMEBUFFER_COMPLETE 35 | 36 | // glTexImage2D is guaranteed to error in this case. 37 | if canvas.width > maxTextureSize || canvas.height > maxTextureSize { 38 | canvas.status = gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT 39 | return false 40 | } 41 | 42 | canvas.Texture = newTexture(canvas.width, canvas.height, false) 43 | //NULL means reserve texture memory, but texels are undefined 44 | gl.TexImage2D(gl.TEXTURE_2D, 0, int(canvas.width), int(canvas.height), gl.RGBA, gl.UNSIGNED_BYTE, nil) 45 | if gl.GetError() != gl.NO_ERROR { 46 | canvas.status = gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT 47 | return false 48 | } 49 | 50 | canvas.fbo, canvas.status = newFBO(canvas.getHandle()) 51 | 52 | if canvas.status != gl.FRAMEBUFFER_COMPLETE { 53 | if canvas.fbo.Valid() { 54 | gl.DeleteFramebuffer(canvas.fbo) 55 | canvas.fbo = gl.Framebuffer{} 56 | } 57 | return false 58 | } 59 | 60 | return true 61 | } 62 | 63 | // unLoadVolatile will release the texture, framebuffer and depth buffer 64 | func (canvas *Canvas) unLoadVolatile() { 65 | if glState.currentCanvas == canvas { 66 | canvas.stopGrab(false) 67 | } 68 | gl.DeleteFramebuffer(canvas.fbo) 69 | gl.DeleteRenderbuffer(canvas.depthStencil) 70 | 71 | canvas.fbo = gl.Framebuffer{} 72 | canvas.depthStencil = gl.Renderbuffer{} 73 | } 74 | 75 | // startGrab will bind this canvas to grab all drawing operations 76 | func (canvas *Canvas) startGrab() error { 77 | if glState.currentCanvas == canvas { 78 | return nil // already grabbing 79 | } 80 | 81 | // cleanup after previous Canvas 82 | if glState.currentCanvas != nil { 83 | canvas.systemViewport = glState.currentCanvas.systemViewport 84 | glState.currentCanvas.stopGrab(true) 85 | } else { 86 | canvas.systemViewport = GetViewport() 87 | } 88 | 89 | // indicate we are using this Canvas. 90 | glState.currentCanvas = canvas 91 | // bind the framebuffer object. 92 | gl.BindFramebuffer(gl.FRAMEBUFFER, canvas.fbo) 93 | SetViewport(0, 0, canvas.width, canvas.height) 94 | // Set up the projection matrix 95 | glState.projectionStack.Push() 96 | glState.projectionStack.Load(mgl32.Ortho(0.0, float32(screenWidth), 0.0, float32(screenHeight), -1, 1)) 97 | 98 | return nil 99 | } 100 | 101 | // stopGrab will bind the context back to the default framebuffer and set back 102 | // all the settings 103 | func (canvas *Canvas) stopGrab(switchingToOtherCanvas bool) error { 104 | // i am not grabbing. leave me alone 105 | if glState.currentCanvas != canvas { 106 | return nil 107 | } 108 | glState.projectionStack.Pop() 109 | if !switchingToOtherCanvas { 110 | // bind system framebuffer. 111 | glState.currentCanvas = nil 112 | gl.BindFramebuffer(gl.FRAMEBUFFER, getDefaultFBO()) 113 | SetViewport(canvas.systemViewport[0], canvas.systemViewport[1], canvas.systemViewport[2], canvas.systemViewport[3]) 114 | } 115 | return nil 116 | } 117 | 118 | // NewImageData will create an image from the canvas data. It will return an error 119 | // only if the dimensions given are invalid 120 | func (canvas *Canvas) NewImageData(x, y, w, h int32) (*Image, error) { 121 | if x < 0 || y < 0 || w <= 0 || h <= 0 || (x+w) > canvas.width || (y+h) > canvas.height { 122 | return nil, fmt.Errorf("invalid ImageData rectangle dimensions") 123 | } 124 | prevCanvas := GetCanvas() 125 | SetCanvas(canvas) 126 | screenshot := image.NewRGBA(image.Rect(int(x), int(y), int(w), int(h))) 127 | stride := int32(screenshot.Stride) 128 | pixels := make([]byte, len(screenshot.Pix)) 129 | gl.ReadPixels(pixels, int(x), int(y), int(w), int(h), gl.RGBA, gl.UNSIGNED_BYTE) 130 | for y := int32(0); y < h; y++ { 131 | i := (h - 1 - y) * stride 132 | copy(screenshot.Pix[y*stride:], pixels[i:i+w*4]) 133 | } 134 | SetCanvas(prevCanvas) 135 | newImage := &Image{Texture: newImageTexture(screenshot, false)} 136 | registerVolatile(newImage) 137 | return newImage, nil 138 | } 139 | 140 | // checkCreateStencil if a stencil is set on a canvas then we need to create 141 | // some buffers to handle this. 142 | func (canvas *Canvas) checkCreateStencil() bool { 143 | // Do nothing if we've already created the stencil buffer. 144 | if canvas.depthStencil.Valid() { 145 | return true 146 | } 147 | 148 | if glState.currentCanvas != canvas { 149 | gl.BindFramebuffer(gl.FRAMEBUFFER, canvas.fbo) 150 | } 151 | 152 | format := gl.STENCIL_INDEX8 153 | attachment := gl.STENCIL_ATTACHMENT 154 | 155 | canvas.depthStencil = gl.CreateRenderbuffer() 156 | gl.BindRenderbuffer(gl.RENDERBUFFER, canvas.depthStencil) 157 | gl.RenderbufferStorage(gl.RENDERBUFFER, gl.Enum(format), int(canvas.width), int(canvas.height)) 158 | 159 | // Attach the stencil buffer to the framebuffer object. 160 | gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.Enum(attachment), gl.RENDERBUFFER, canvas.depthStencil) 161 | gl.BindRenderbuffer(gl.RENDERBUFFER, gl.Renderbuffer{}) 162 | 163 | success := (gl.CheckFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE) 164 | 165 | // We don't want the stencil buffer filled with garbage. 166 | if success { 167 | gl.Clear(gl.STENCIL_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 168 | } else { 169 | gl.DeleteRenderbuffer(canvas.depthStencil) 170 | canvas.depthStencil = gl.Renderbuffer{} 171 | } 172 | 173 | if glState.currentCanvas != nil && glState.currentCanvas != canvas { 174 | gl.BindFramebuffer(gl.FRAMEBUFFER, glState.currentCanvas.fbo) 175 | } else if glState.currentCanvas == nil { 176 | gl.BindFramebuffer(gl.FRAMEBUFFER, getDefaultFBO()) 177 | } 178 | 179 | return success 180 | } 181 | 182 | // newFBO will generate a new Frame Buffer Object for use with the canvas 183 | func newFBO(texture gl.Texture) (gl.Framebuffer, uint32) { 184 | // get currently bound fbo to reset to it later 185 | currentFBO := gl.GetBoundFramebuffer() 186 | 187 | framebuffer := gl.CreateFramebuffer() 188 | gl.BindFramebuffer(gl.FRAMEBUFFER, framebuffer) 189 | if texture.Valid() { 190 | gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0) 191 | // Initialize the texture to transparent black. 192 | gl.ClearColor(0.0, 0.0, 0.0, 0.0) 193 | gl.Clear(gl.COLOR_BUFFER_BIT) 194 | } 195 | status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER) 196 | 197 | // unbind framebuffer 198 | gl.BindFramebuffer(gl.FRAMEBUFFER, currentFBO) 199 | 200 | return framebuffer, uint32(status) 201 | } 202 | -------------------------------------------------------------------------------- /gfx/const.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "github.com/goxjs/gl" 4 | 5 | type ( 6 | // WrapMode is used for setting texture/image/canvas wrap 7 | WrapMode int 8 | // FilterMode is used for setting texture/image/canvas filters 9 | FilterMode int 10 | // StencilAction is how a stencil function modifies the stencil values of pixels it touches. 11 | StencilAction uint32 12 | // CompareMode defines different types of per-pixel stencil test comparisons. 13 | // The pixels of an object will be drawn if the comparison succeeds, for each 14 | // pixel that the object touches. 15 | CompareMode uint32 16 | // Usage is used for sprite batch usage, and specifies if it is static, dynamic, or stream 17 | Usage uint32 18 | ) 19 | 20 | // ColorMask contains an rgba color mask 21 | type ColorMask struct { 22 | r, g, b, a bool 23 | } 24 | 25 | var ( //opengl attribute variables 26 | shaderPos = gl.Attrib{Value: 0} 27 | shaderTexCoord = gl.Attrib{Value: 1} 28 | shaderColor = gl.Attrib{Value: 2} 29 | shaderConstantColor = gl.Attrib{Value: 3} 30 | ) 31 | 32 | //texture wrap 33 | const ( 34 | WrapClamp WrapMode = 0x812F 35 | WrapRepeat WrapMode = 0x2901 36 | WrapMirroredRepeat WrapMode = 0x8370 37 | ) 38 | 39 | //texture filter 40 | const ( 41 | FilterNone FilterMode = 0 42 | FilterNearest FilterMode = 0x2600 43 | FilterLinear FilterMode = 0x2601 44 | ) 45 | 46 | //stencil actions 47 | const ( 48 | StencilReplace StencilAction = 0x1E01 49 | StencilIncrement StencilAction = 0x1E02 50 | StencilDecrement StencilAction = 0x1E03 51 | StencilIncrementWrap StencilAction = 0x8507 52 | StencilDecrementWrap StencilAction = 0x8508 53 | StencilInvert StencilAction = 0x150A 54 | ) 55 | 56 | // stenicl test modes 57 | const ( 58 | CompareGreater CompareMode = 0x0201 59 | CompareEqual CompareMode = 0x0202 60 | CompareGequal CompareMode = 0x0203 61 | CompareLess CompareMode = 0x0204 62 | CompareNotequal CompareMode = 0x0205 63 | CompareLequal CompareMode = 0x0206 64 | CompareAlways CompareMode = 0x0207 65 | ) 66 | 67 | // spritebatch usage 68 | const ( 69 | UsageStream Usage = 0x88E0 70 | UsageStatic Usage = 0x88E4 71 | UsageDynamic Usage = 0x88E8 72 | ) 73 | -------------------------------------------------------------------------------- /gfx/display_state.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "github.com/go-gl/mathgl/mgl32/matstack" 5 | 6 | "github.com/goxjs/gl" 7 | ) 8 | 9 | // displayState track a certain point in transformations 10 | type displayState struct { 11 | color []float32 12 | backgroundColor []float32 13 | blendMode string 14 | lineWidth float32 15 | lineJoin string 16 | pointSize float32 17 | scissor bool 18 | scissorBox []int32 19 | stencilCompare CompareMode 20 | stencilTestValue int32 21 | font *Font 22 | shader *Shader 23 | colorMask ColorMask 24 | canvas *Canvas 25 | defaultFilter Filter 26 | } 27 | 28 | // glState keeps track of the context attributes 29 | type openglState struct { 30 | initialized bool 31 | boundTextures []gl.Texture 32 | curTextureUnit int 33 | viewport []int32 34 | framebufferSRGBEnabled bool 35 | defaultTexture gl.Texture 36 | defaultFBO gl.Framebuffer 37 | projectionStack *matstack.MatStack 38 | viewStack *matstack.MatStack 39 | currentCanvas *Canvas 40 | currentShader *Shader 41 | textureCounters []int 42 | writingToStencil bool 43 | } 44 | 45 | // newDisplayState initializes a display states default values 46 | func newDisplayState() displayState { 47 | return displayState{ 48 | blendMode: "alpha", 49 | pointSize: 5, 50 | stencilCompare: CompareAlways, 51 | lineWidth: 1, 52 | lineJoin: "miter", 53 | shader: defaultShader, 54 | font: defaultFont, 55 | defaultFilter: newFilter(), 56 | color: []float32{1, 1, 1, 1}, 57 | colorMask: ColorMask{r: true, g: true, b: true, a: true}, 58 | scissorBox: make([]int32, 4), 59 | } 60 | } 61 | 62 | // displayStateStack is a simple stack for keeping track of display state. 63 | type displayStateStack struct { 64 | stack []displayState 65 | } 66 | 67 | // push a new element onto the top of the stack 68 | func (s *displayStateStack) push(state displayState) { 69 | s.stack = append(s.stack, state) 70 | } 71 | 72 | // take the top element off the stack 73 | func (s *displayStateStack) pop() displayState { 74 | var state displayState 75 | state, s.stack = s.stack[len(s.stack)-1], s.stack[:len(s.stack)-1] 76 | return state 77 | } 78 | 79 | // get the top element in the stack 80 | func (s *displayStateStack) back() *displayState { 81 | return &s.stack[len(s.stack)-1] 82 | } 83 | -------------------------------------------------------------------------------- /gfx/font.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "github.com/tanema/amore/gfx/font" 5 | ) 6 | 7 | // Font is a rasterized font data 8 | type Font struct { 9 | rasterizers []*rasterizer 10 | lineHeight float32 11 | } 12 | 13 | // NewFont rasterizes a ttf font and returns a pointer to a new Font 14 | func NewFont(filename string, fontSize float32) (*Font, error) { 15 | face, err := font.NewTTFFace(filename, fontSize) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return newFont(face, font.ASCII, font.Latin), nil 20 | } 21 | 22 | // NewImageFont rasterizes an image using the glyphHints. The glyphHints should 23 | // list all characters in the image. The characters should all have equal width 24 | // and height. Using the glyphHints, the image is split up into equal rectangles 25 | // for rasterization. The function will return a pointer to a new Font 26 | func NewImageFont(filename, glyphHints string) (*Font, error) { 27 | face, err := font.NewBitmapFace(filename, glyphHints) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return newFont(face, []rune(glyphHints)), nil 32 | } 33 | 34 | func newFont(face font.Face, runeSets ...[]rune) *Font { 35 | if runeSets == nil || len(runeSets) == 0 { 36 | runeSets = append(runeSets, font.ASCII, font.Latin) 37 | } 38 | return &Font{rasterizers: []*rasterizer{newRasterizer(face, runeSets...)}} 39 | } 40 | 41 | // SetLineHeight sets the height between lines 42 | func (font *Font) SetLineHeight(height float32) { 43 | font.lineHeight = height 44 | } 45 | 46 | // GetLineHeight will return the current line height of the font 47 | func (font *Font) GetLineHeight() float32 { 48 | if font.lineHeight <= 0 { 49 | return float32(font.rasterizers[0].lineHeight) 50 | } 51 | return font.lineHeight 52 | } 53 | 54 | // SetFilter sets the filtering on the font. 55 | func (font *Font) SetFilter(min, mag FilterMode) error { 56 | for _, rasterizer := range font.rasterizers { 57 | if err := rasterizer.texture.SetFilter(min, mag); err != nil { 58 | return err 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | // GetFilter will return the filter of the font 65 | func (font *Font) GetFilter() Filter { 66 | return font.rasterizers[0].texture.GetFilter() 67 | } 68 | 69 | // GetAscent gets the height of the font from the baseline 70 | func (font *Font) GetAscent() float32 { 71 | return font.rasterizers[0].ascent 72 | } 73 | 74 | // GetDescent gets the height of the font below the base line 75 | func (font *Font) GetDescent() float32 { 76 | return font.rasterizers[0].descent 77 | } 78 | 79 | // GetBaseline returns the position of the base line. 80 | func (font *Font) GetBaseline() float32 { 81 | return font.rasterizers[0].lineHeight 82 | } 83 | 84 | // HasGlyph checks if this font has a character for the given rune 85 | func (font *Font) HasGlyph(g rune) bool { 86 | _, _, ok := font.findGlyph(g) 87 | return ok 88 | } 89 | 90 | // findGlyph will fetch the glyphData for the given rune 91 | func (font *Font) findGlyph(r rune) (glyphData, *rasterizer, bool) { 92 | for _, rasterizer := range font.rasterizers { 93 | if g, ok := rasterizer.mapping[r]; ok { 94 | return g, rasterizer, ok 95 | } 96 | } 97 | rasterizer := font.rasterizers[0] 98 | return rasterizer.mapping[r], rasterizer, false 99 | } 100 | 101 | // Kern will return the space between two characters 102 | func (font *Font) Kern(first, second rune) float32 { 103 | for _, r := range font.rasterizers { 104 | _, hasFirst := r.mapping[first] 105 | _, hasSecond := r.mapping[second] 106 | if hasFirst && hasSecond { 107 | return float32(r.face.Kern(first, second)) 108 | } 109 | } 110 | 111 | return float32(font.rasterizers[0].face.Kern(first, second)) 112 | } 113 | 114 | // SetFallbacks will add extra fonts in case some characters are not available 115 | // in this font. If the character is not available it will be rendered with one 116 | // of the fallback characters 117 | func (font *Font) SetFallbacks(fallbacks ...*Font) { 118 | if fallbacks == nil || len(fallbacks) == 0 { 119 | return 120 | } 121 | for _, fallback := range fallbacks { 122 | font.rasterizers = append(font.rasterizers, fallback.rasterizers...) 123 | } 124 | } 125 | 126 | // GetHeight will get the height of the font. 127 | func (font *Font) GetHeight() float32 { 128 | return font.rasterizers[0].lineHeight 129 | } 130 | 131 | // GetWidth will get the width of a given string after rendering. 132 | func (font *Font) GetWidth(text string) float32 { 133 | _, width, _ := generateLines(font, []string{text}, [][]float32{GetColor()}, -1) 134 | return width 135 | } 136 | 137 | // GetWrap will split a string given a wrap limit. It will return the max width 138 | // of the longest string and it will return the string split into the strings that 139 | // are smaller than the wrap limit. 140 | func (font *Font) GetWrap(text string, wrapLimit float32) (float32, []string) { 141 | lines, width, _ := generateLines(font, []string{text}, [][]float32{GetColor()}, wrapLimit) 142 | stringLines := make([]string, len(lines)) 143 | for i, l := range lines { 144 | stringLines[i] = string(l.chars) 145 | } 146 | return width, stringLines 147 | } 148 | -------------------------------------------------------------------------------- /gfx/font/bitmap_face.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | "image" 5 | 6 | "golang.org/x/image/font" 7 | "golang.org/x/image/math/fixed" 8 | 9 | "github.com/tanema/amore/file" 10 | ) 11 | 12 | // BitmapFace holds data for a bitmap font face to satisfy the font.Face interface 13 | type BitmapFace struct { 14 | img image.Image 15 | glyphs map[rune]glyphData 16 | advance fixed.Int26_6 17 | metrics font.Metrics 18 | } 19 | 20 | type glyphData struct { 21 | rect image.Rectangle 22 | pt image.Point 23 | } 24 | 25 | // NewBitmapFace will load up an image font face for creating a font in graphics 26 | func NewBitmapFace(filepath, glyphHints string) (font.Face, error) { 27 | imgFile, err := file.Open(filepath) 28 | if err != nil { 29 | return nil, err 30 | } 31 | defer imgFile.Close() 32 | 33 | img, _, err := image.Decode(imgFile) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | glyphRuneHints := []rune(glyphHints) 39 | advance := img.Bounds().Dx() / len(glyphRuneHints) 40 | newFace := BitmapFace{ 41 | img: img, 42 | glyphs: make(map[rune]glyphData), 43 | advance: fixed.I(advance), 44 | metrics: font.Metrics{ 45 | Height: fixed.I(img.Bounds().Dy()), 46 | Ascent: fixed.I(img.Bounds().Dy()), 47 | Descent: 0, 48 | }, 49 | } 50 | 51 | var gx int 52 | for i, r := range glyphRuneHints { 53 | newFace.glyphs[r] = glyphData{ 54 | rect: image.Rect(gx, 0, gx+advance, newFace.metrics.Height.Ceil()), 55 | pt: image.Pt(i*advance, 0), 56 | } 57 | gx += advance 58 | } 59 | 60 | return newFace, err 61 | } 62 | 63 | // Glyph returns the draw.DrawMask parameters (dr, mask, maskp) to draw r's 64 | // glyph at the sub-pixel destination location dot, and that glyph's 65 | // advance width. 66 | // 67 | // It returns !ok if the face does not contain a glyph for r. 68 | // 69 | // The contents of the mask image returned by one Glyph call may change 70 | // after the next Glyph call. Callers that want to cache the mask must make 71 | // a copy. 72 | func (face BitmapFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) { 73 | var glyph glyphData 74 | glyph, ok = face.glyphs[r] 75 | if !ok { 76 | return 77 | } 78 | dr.Min = image.Point{ 79 | X: dot.X.Floor(), 80 | Y: dot.Y.Floor() - face.metrics.Height.Floor(), 81 | } 82 | dr.Max = image.Point{ 83 | X: dr.Min.X + face.advance.Floor(), 84 | Y: dr.Min.Y + face.metrics.Height.Floor(), 85 | } 86 | return dr, face.img, glyph.pt, face.advance, ok 87 | } 88 | 89 | // GlyphBounds returns the bounding box of r's glyph, drawn at a dot equal 90 | // to the origin, and that glyph's advance width. 91 | // 92 | // It returns !ok if the face does not contain a glyph for r. 93 | // 94 | // The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y. A 95 | // visual depiction of what these metrics are is at 96 | // https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png 97 | func (face BitmapFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) { 98 | _, ok = face.glyphs[r] 99 | return fixed.R(0, 0, face.advance.Ceil(), face.metrics.Height.Ceil()), face.advance, ok 100 | } 101 | 102 | // GlyphAdvance returns the advance width of r's glyph. 103 | // 104 | // It returns !ok if the face does not contain a glyph for r. 105 | func (face BitmapFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) { 106 | return face.advance, true 107 | } 108 | 109 | // Kern returns the horizontal adjustment for the kerning pair (r0, r1). A 110 | // positive kern means to move the glyphs further apart. 111 | func (face BitmapFace) Kern(r0, r1 rune) fixed.Int26_6 { 112 | return 0.0 113 | } 114 | 115 | // Metrics returns the metrics for this Face. 116 | func (face BitmapFace) Metrics() font.Metrics { 117 | return face.metrics 118 | } 119 | 120 | // Close to satisfy face interface 121 | func (face BitmapFace) Close() error { 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /gfx/font/char_sets.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import "unicode" 4 | 5 | var ( 6 | // ASCII is a set of all ASCII runes. These runes are codepoints from 32 to 127 inclusive. 7 | ASCII []rune 8 | // Latin is a set of all the Latin runes 9 | Latin []rune 10 | ) 11 | 12 | func init() { 13 | ASCII = make([]rune, unicode.MaxASCII-32) 14 | for i := range ASCII { 15 | ASCII[i] = rune(32 + i) 16 | } 17 | Latin = rangeTable(unicode.Latin) 18 | } 19 | 20 | func rangeTable(table *unicode.RangeTable) []rune { 21 | var runes []rune 22 | for _, rng := range table.R16 { 23 | for r := rng.Lo; r <= rng.Hi; r += rng.Stride { 24 | runes = append(runes, rune(r)) 25 | } 26 | } 27 | for _, rng := range table.R32 { 28 | for r := rng.Lo; r <= rng.Hi; r += rng.Stride { 29 | runes = append(runes, rune(r)) 30 | } 31 | } 32 | return runes 33 | } 34 | -------------------------------------------------------------------------------- /gfx/font/doc.go: -------------------------------------------------------------------------------- 1 | // Package font provides an easy interface for creating font faces to provide to 2 | // the gfx package. It may include more in the future. 3 | package font 4 | -------------------------------------------------------------------------------- /gfx/font/ttf_face.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | "github.com/golang/freetype/truetype" 5 | "golang.org/x/image/font" 6 | "golang.org/x/image/font/gofont/gobold" 7 | "golang.org/x/image/font/gofont/goitalic" 8 | "golang.org/x/image/font/gofont/goregular" 9 | 10 | "github.com/tanema/amore/file" 11 | ) 12 | 13 | // Face just an alias so you don't ahve to import multople packages named font 14 | type Face font.Face 15 | 16 | // NewTTFFace will load up a ttf font face for creating a font in graphics 17 | func NewTTFFace(filepath string, size float32) (font.Face, error) { 18 | fontBytes, err := file.Read(filepath) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return ttfFromBytes(fontBytes, size) 24 | } 25 | 26 | // Bold will return an bold font face with the request font size 27 | func Bold(fontSize float32) (font.Face, error) { 28 | return ttfFromBytes(gobold.TTF, fontSize) 29 | } 30 | 31 | // Default will return an regular font face with the request font size 32 | func Default(fontSize float32) (font.Face, error) { 33 | return ttfFromBytes(goregular.TTF, fontSize) 34 | } 35 | 36 | // Italic will return an italic font face with the request font size 37 | func Italic(fontSize float32) (font.Face, error) { 38 | return ttfFromBytes(goitalic.TTF, fontSize) 39 | } 40 | 41 | func ttfFromBytes(fontBytes []byte, size float32) (font.Face, error) { 42 | ttf, err := truetype.Parse(fontBytes) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return truetype.NewFace(ttf, &truetype.Options{ 47 | Size: float64(size), 48 | GlyphCacheEntries: 1, 49 | }), nil 50 | } 51 | -------------------------------------------------------------------------------- /gfx/font_rasterizers.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "math" 7 | "unicode" 8 | 9 | "golang.org/x/image/font" 10 | "golang.org/x/image/math/fixed" 11 | ) 12 | 13 | type ( 14 | rasterizer struct { 15 | face font.Face 16 | atlasImg *image.RGBA 17 | texture *Texture 18 | mapping map[rune]glyphData 19 | lineHeight float32 20 | ascent float32 21 | descent float32 22 | advance float32 23 | } 24 | glyphData struct { 25 | quad *Quad 26 | advance float32 27 | descent float32 28 | lsb float32 29 | rsb float32 30 | } 31 | ) 32 | 33 | const glyphPadding int = 2 34 | 35 | func newRasterizer(face font.Face, runeSets ...[]rune) *rasterizer { 36 | runes := uniqRunesForSets(runeSets...) 37 | imageWidth, imageHeight, advance, height := calcSquareMapping(face, runes) 38 | atlasImg := image.NewRGBA(image.Rect(0, 0, imageWidth, imageHeight)) 39 | mapping := make(map[rune]glyphData) 40 | var gx, gy int 41 | for _, r := range runes { 42 | rect, srcImg, srcPoint, adv, ok := face.Glyph(fixed.P(gx, gy+height), r) 43 | if !ok { 44 | continue 45 | } 46 | draw.Draw(atlasImg, rect, srcImg, srcPoint, draw.Src) 47 | mapping[r] = glyphData{ 48 | descent: float32(rect.Min.Y - gy), 49 | lsb: float32(rect.Min.X - gx), 50 | rsb: i2f(adv) - float32((rect.Max.X - gx)), 51 | quad: NewQuad( 52 | int32(rect.Min.X), int32(rect.Min.Y), 53 | int32(rect.Dx()), int32(rect.Dy()), 54 | int32(imageWidth), int32(imageHeight), 55 | ), 56 | advance: i2f(adv), 57 | } 58 | 59 | gx += advance + glyphPadding 60 | if gx+advance >= imageWidth { 61 | gx = 0 62 | gy += height + glyphPadding 63 | } 64 | } 65 | 66 | newRast := &rasterizer{ 67 | face: face, 68 | atlasImg: atlasImg, 69 | mapping: mapping, 70 | ascent: i2f(face.Metrics().Ascent), 71 | descent: i2f(face.Metrics().Descent), 72 | lineHeight: i2f(face.Metrics().Height) * 1.25, 73 | advance: float32(advance), 74 | } 75 | 76 | registerVolatile(newRast) 77 | return newRast 78 | } 79 | 80 | func (rast *rasterizer) loadVolatile() bool { 81 | rast.texture = newImageTexture(rast.atlasImg, false) 82 | return true 83 | } 84 | 85 | func (rast *rasterizer) unloadVolatile() {} 86 | 87 | func uniqRunesForSets(runeSets ...[]rune) []rune { 88 | seen := make(map[rune]bool) 89 | runes := []rune{unicode.ReplacementChar} 90 | for _, set := range runeSets { 91 | for _, r := range set { 92 | if !seen[r] { 93 | runes = append(runes, r) 94 | seen[r] = true 95 | } 96 | } 97 | } 98 | return runes 99 | } 100 | 101 | func calcSquareMapping(face font.Face, runes []rune) (imageWidth, imageHeight, advance, height int) { 102 | var maxAdvance fixed.Int26_6 103 | for _, r := range runes { 104 | _, adv, ok := face.GlyphBounds(r) 105 | if !ok { 106 | continue 107 | } 108 | if adv > maxAdvance { 109 | maxAdvance = adv 110 | } 111 | } 112 | a := i2f(maxAdvance) 113 | h := i2f(face.Metrics().Ascent + face.Metrics().Descent) 114 | squareWidth := float32(math.Ceil(math.Sqrt(float64(len(runes))))) 115 | imageWidth = int(pow2(uint32(a * squareWidth))) 116 | imageHeight = int(pow2(uint32(h * squareWidth))) 117 | return imageWidth, imageHeight, ceil(a), ceil(h) 118 | } 119 | 120 | func i2f(i fixed.Int26_6) float32 { 121 | return float32(i) / (1 << 6) 122 | } 123 | 124 | func f2i(f float64) fixed.Int26_6 { 125 | return fixed.Int26_6(f * (1 << 6)) 126 | } 127 | 128 | func pow2(x uint32) uint32 { 129 | x-- 130 | x |= x >> 1 131 | x |= x >> 2 132 | x |= x >> 4 133 | x |= x >> 8 134 | x |= x >> 16 135 | return x + 1 136 | } 137 | 138 | func ceil(x float32) int { 139 | return int(math.Ceil(float64(x))) 140 | } 141 | -------------------------------------------------------------------------------- /gfx/graphics.go: -------------------------------------------------------------------------------- 1 | // Package gfx is used largly to simplify OpenGL calls and to manage state 2 | // of transformations. Anything meant to be drawn to screen will come from this 3 | // pacakge. 4 | package gfx 5 | 6 | import ( 7 | "image" 8 | "math" 9 | 10 | "github.com/go-gl/mathgl/mgl32" 11 | 12 | "github.com/goxjs/gl" 13 | ) 14 | 15 | // this is the default amount of points to allow a circle or arc to use when 16 | // generating points 17 | //const defaultPointCount = 30 18 | 19 | // Circle will draw a circle at x, y with a radius as specified. 20 | // points specifies how many points should be generated in the arc. 21 | // If it is lower it will look jagged. If it is higher it will hit performace. 22 | // The drawmode specifies either a fill or line draw 23 | func Circle(mode string, x, y, radius float32, points int) { 24 | Ellipse(mode, x, y, radius, radius, points) 25 | } 26 | 27 | // Arc is like Arc except that you can define how many points you want to generate 28 | // the arc. 29 | // If it is lower it will look jagged. If it is higher it will hit performace. 30 | // The drawmode specifies either a fill or line draw 31 | func Arc(mode string, x, y, radius, angle1, angle2 float32, points int) { 32 | // Nothing to display with no points or equal angles. (Or is there with line mode?) 33 | if points <= 0 || angle1 == angle2 { 34 | return 35 | } 36 | 37 | // Oh, you want to draw a circle? 38 | if math.Abs(float64(angle1-angle2)) >= (2.0 * math.Pi) { 39 | Circle(mode, x, y, radius, points) 40 | return 41 | } 42 | 43 | angleShift := (angle2 - angle1) / float32(points) 44 | // Bail on precision issues. 45 | if angleShift == 0.0 { 46 | return 47 | } 48 | 49 | phi := angle1 50 | numCoords := (points + 3) * 2 51 | coords := make([]float32, numCoords) 52 | coords[0] = x 53 | coords[numCoords-2] = x 54 | coords[1] = y 55 | coords[numCoords-1] = y 56 | 57 | for i := 0; i <= points; i++ { 58 | phi = phi + angleShift 59 | coords[2*(i+1)] = x + radius*float32(math.Cos(float64(phi))) 60 | coords[2*(i+1)+1] = y + radius*float32(math.Sin(float64(phi))) 61 | } 62 | 63 | if mode == "line" { 64 | PolyLine(coords) 65 | } else { 66 | prepareDraw(nil) 67 | bindTexture(glState.defaultTexture) 68 | useVertexAttribArrays(shaderPos) 69 | 70 | buffer := newVertexBuffer(len(coords), coords, UsageStatic) 71 | buffer.bind() 72 | defer buffer.unbind() 73 | 74 | gl.VertexAttribPointer(shaderPos, 2, gl.FLOAT, false, 0, 0) 75 | gl.DrawArrays(gl.TRIANGLE_FAN, 0, len(coords)/2-1) 76 | } 77 | } 78 | 79 | // Ellipse will draw a circle at x, y with a radius as specified. 80 | // radiusx and radiusy will specify how much the width will be along those axis 81 | // points specifies how many points should be generated in the arc. 82 | // If it is lower it will look jagged. If it is higher it will hit performace. 83 | // The drawmode specifies either a fill or line draw 84 | func Ellipse(mode string, x, y, radiusx, radiusy float32, points int) { 85 | twoPi := math.Pi * 2.0 86 | if points <= 0 { 87 | points = 1 88 | } 89 | 90 | angleShift := float32(twoPi) / float32(points) 91 | phi := float32(0.0) 92 | 93 | coords := make([]float32, 2*(points+1)) 94 | for i := 0; i < points; i++ { 95 | phi += angleShift 96 | coords[2*i+0] = x + radiusx*float32(math.Cos(float64(phi))) 97 | coords[2*i+1] = y + radiusy*float32(math.Sin(float64(phi))) 98 | } 99 | 100 | coords[2*points+0] = coords[0] 101 | coords[2*points+1] = coords[1] 102 | 103 | if mode == "line" { 104 | PolyLine(coords) 105 | } else { 106 | prepareDraw(nil) 107 | bindTexture(glState.defaultTexture) 108 | useVertexAttribArrays(shaderPos) 109 | 110 | buffer := newVertexBuffer(len(coords), coords, UsageStatic) 111 | buffer.bind() 112 | defer buffer.unbind() 113 | 114 | gl.VertexAttribPointer(shaderPos, 2, gl.FLOAT, false, 0, 0) 115 | gl.DrawArrays(gl.TRIANGLE_FAN, 0, len(coords)/2-1) 116 | } 117 | } 118 | 119 | // Points will draw a point on the screen at x, y position. The size of the point 120 | // is dependant on the point size set with SetPointSize. 121 | func Points(coords []float32) { 122 | prepareDraw(nil) 123 | bindTexture(glState.defaultTexture) 124 | useVertexAttribArrays(shaderPos) 125 | 126 | buffer := newVertexBuffer(len(coords), coords, UsageStatic) 127 | buffer.bind() 128 | defer buffer.unbind() 129 | 130 | gl.VertexAttribPointer(shaderPos, 2, gl.FLOAT, false, 0, 0) 131 | gl.DrawArrays(gl.POINTS, 0, len(coords)/2) 132 | } 133 | 134 | // PolyLine will draw a line with an array in the form of x1, y1, x2, y2, x3, y3, ..... xn, yn 135 | func PolyLine(coords []float32) { 136 | polyline := newPolyLine(states.back().lineJoin, states.back().lineWidth) 137 | polyline.render(coords) 138 | } 139 | 140 | // Rect draws a rectangle with the top left corner at x, y with the specified width 141 | // and height 142 | // The drawmode specifies either a fill or line draw 143 | func Rect(mode string, x, y, width, height float32) { 144 | Polygon(mode, []float32{x, y, x, y + height, x + width, y + height, x + width, y}) 145 | } 146 | 147 | // Polygon will draw a closed polygon with an array in the form of x1, y1, x2, y2, x3, y3, ..... xn, yn 148 | // The drawmode specifies either a fill or line draw 149 | func Polygon(mode string, coords []float32) { 150 | coords = append(coords, coords[0], coords[1]) 151 | if mode == "line" { 152 | PolyLine(coords) 153 | } else { 154 | prepareDraw(nil) 155 | bindTexture(glState.defaultTexture) 156 | useVertexAttribArrays(shaderPos) 157 | 158 | buffer := newVertexBuffer(len(coords), coords, UsageStatic) 159 | buffer.bind() 160 | defer buffer.unbind() 161 | 162 | gl.VertexAttribPointer(shaderPos, 2, gl.FLOAT, false, 0, 0) 163 | gl.DrawArrays(gl.TRIANGLE_FAN, 0, len(coords)/2-1) 164 | } 165 | } 166 | 167 | // NewScreenshot will take a screenshot of the screen and convert it to an image.Image 168 | func NewScreenshot() *Image { 169 | // Temporarily unbind the currently active canvas (glReadPixels reads the active framebuffer, not the main one.) 170 | canvas := GetCanvas() 171 | SetCanvas(nil) 172 | 173 | w, h := int32(screenWidth), int32(screenHeight) 174 | screenshot := image.NewRGBA(image.Rect(0, 0, int(w), int(h))) 175 | stride := int32(screenshot.Stride) 176 | pixels := make([]byte, len(screenshot.Pix)) 177 | gl.ReadPixels(pixels, 0, 0, int(w), int(h), gl.RGBA, gl.UNSIGNED_BYTE) 178 | 179 | // OpenGL sucks and reads pixels from the lower-left. Let's fix that. 180 | for y := int32(0); y < h; y++ { 181 | i := (h - 1 - y) * stride 182 | copy(screenshot.Pix[y*stride:], pixels[i:i+w*4]) 183 | } 184 | 185 | // Re-bind the active canvas, if necessary. 186 | SetCanvas(canvas) 187 | newImage := &Image{Texture: newImageTexture(screenshot, false)} 188 | registerVolatile(newImage) 189 | return newImage 190 | } 191 | 192 | // Normalized an array of floats into these params if they exist 193 | // if they are not present then thier default values are returned 194 | // x The position of the object along the x-axis. 195 | // y The position of the object along the y-axis. 196 | // angle The angle of the object (in radians). 197 | // sx The scale factor along the x-axis. 198 | // sy The scale factor along the y-axis. 199 | // ox The origin offset along the x-axis. 200 | // oy The origin offset along the y-axis. 201 | // kx Shear along the x-axis. 202 | // ky Shear along the y-axis. 203 | func normalizeDrawCallArgs(args []float32) (float32, float32, float32, float32, float32, float32, float32, float32, float32) { 204 | var x, y, angle, sx, sy, ox, oy, kx, ky float32 205 | sx = 1 206 | sy = 1 207 | 208 | if args == nil || len(args) < 2 { 209 | return x, y, angle, sx, sy, ox, oy, kx, ky 210 | } 211 | 212 | argsLength := len(args) 213 | 214 | switch argsLength { 215 | case 9: 216 | ky = args[8] 217 | fallthrough 218 | case 8: 219 | kx = args[7] 220 | if argsLength == 8 { 221 | ky = kx 222 | } 223 | fallthrough 224 | case 7: 225 | oy = args[6] 226 | fallthrough 227 | case 6: 228 | ox = args[5] 229 | if argsLength == 6 { 230 | oy = ox 231 | } 232 | fallthrough 233 | case 5: 234 | sy = args[4] 235 | fallthrough 236 | case 4: 237 | sx = args[3] 238 | if argsLength == 4 { 239 | sy = sx 240 | } 241 | fallthrough 242 | case 3: 243 | angle = args[2] 244 | fallthrough 245 | case 2: 246 | x = args[0] 247 | y = args[1] 248 | } 249 | 250 | return x, y, angle, sx, sy, ox, oy, kx, ky 251 | } 252 | 253 | // generateModelMatFromArgs will take in the arguments 254 | // x, y, r, sx, sy, ox, oy, kx, ky 255 | // and generate a matrix to be applied to the model transformation. 256 | func generateModelMatFromArgs(args []float32) *mgl32.Mat4 { 257 | x, y, angle, sx, sy, ox, oy, kx, ky := normalizeDrawCallArgs(args) 258 | mat := mgl32.Ident4() 259 | c := float32(math.Cos(float64(angle))) 260 | s := float32(math.Sin(float64(angle))) 261 | // matrix multiplication carried out on paper: 262 | // |1 x| |c -s | |sx | | 1 ky | |1 -ox| 263 | // | 1 y| |s c | | sy | |kx 1 | | 1 -oy| 264 | // | 1 | | 1 | | 1 | | 1 | | 1 | 265 | // | 1| | 1| | 1| | 1| | 1 | 266 | // move rotate scale skew origin 267 | mat[10] = 1 268 | mat[15] = 1 269 | mat[0] = c*sx - ky*s*sy // = a 270 | mat[1] = s*sx + ky*c*sy // = b 271 | mat[4] = kx*c*sx - s*sy // = c 272 | mat[5] = kx*s*sx + c*sy // = d 273 | mat[12] = x - ox*mat[0] - oy*mat[4] 274 | mat[13] = y - ox*mat[1] - oy*mat[5] 275 | 276 | return &mat 277 | } 278 | 279 | func f32Bytes(values []float32) []byte { 280 | b := make([]byte, 4*len(values)) 281 | for i, v := range values { 282 | u := math.Float32bits(v) 283 | b[4*i+0] = byte(u >> 0) 284 | b[4*i+1] = byte(u >> 8) 285 | b[4*i+2] = byte(u >> 16) 286 | b[4*i+3] = byte(u >> 24) 287 | } 288 | return b 289 | } 290 | 291 | func ui32Bytes(values []uint32) []byte { 292 | b := make([]byte, 4*len(values)) 293 | for i, v := range values { 294 | b[4*i+0] = byte(v) 295 | b[4*i+1] = byte(v >> 8) 296 | b[4*i+2] = byte(v >> 16) 297 | b[4*i+3] = byte(v >> 24) 298 | } 299 | return b 300 | } 301 | -------------------------------------------------------------------------------- /gfx/image.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | // All image types have been imported for loading them 6 | _ "image/gif" 7 | _ "image/jpeg" 8 | _ "image/png" 9 | 10 | "github.com/tanema/amore/file" 11 | ) 12 | 13 | // Image is an image that is drawable to the screen 14 | type Image struct { 15 | *Texture 16 | filePath string 17 | mipmaps bool 18 | } 19 | 20 | // NewImage will create a new texture for this image and return the *Image. If the 21 | // file does not exist or cannot be decoded it will return an error. 22 | func NewImage(path string, mipmapped bool) *Image { 23 | newImage := &Image{filePath: path, mipmaps: mipmapped} 24 | registerVolatile(newImage) 25 | return newImage 26 | } 27 | 28 | // loadVolatile will create the volatile objects 29 | func (img *Image) loadVolatile() bool { 30 | if img.filePath == "" { 31 | return false 32 | } 33 | 34 | imgFile, err := file.Open(img.filePath) 35 | if err != nil { 36 | return false 37 | } 38 | defer imgFile.Close() 39 | 40 | decodedImg, _, err := image.Decode(imgFile) 41 | if err != nil || decodedImg == nil { 42 | return false 43 | } 44 | 45 | img.Texture = newImageTexture(decodedImg, img.mipmaps) 46 | return true 47 | } 48 | -------------------------------------------------------------------------------- /gfx/polyline.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/goxjs/gl" 7 | ) 8 | 9 | // treat adjacent segments with angles between their directions <5 degree as straight 10 | const linesParallelEPS float32 = 0.05 11 | 12 | type polyLine struct { 13 | join string 14 | halfwidth float32 15 | } 16 | 17 | func determinant(vec1, vec2 []float32) float32 { 18 | return vec1[0]*vec2[1] - vec1[1]*vec2[0] 19 | } 20 | 21 | func getNormal(v1 []float32, scale float32) []float32 { 22 | return []float32{-v1[1] * scale, v1[0] * scale} 23 | } 24 | 25 | func normalize(v1 []float32, length float32) []float32 { 26 | lengthCurrent := vecLen(v1) 27 | if lengthCurrent > 0 { 28 | scale := length / lengthCurrent 29 | return []float32{v1[0] * scale, v1[1] * scale} 30 | } 31 | return v1 32 | } 33 | 34 | func vecLen(v1 []float32) float32 { 35 | return float32(math.Hypot(float64(v1[0]), float64(v1[1]))) 36 | } 37 | 38 | func abs(a float32) float32 { 39 | if a < 0 { 40 | return -a 41 | } else if a == 0 { 42 | return 0 43 | } 44 | return a 45 | } 46 | 47 | func newPolyLine(join string, lineWidth float32) polyLine { 48 | newPolyline := polyLine{ 49 | join: join, 50 | halfwidth: lineWidth * 0.5, 51 | } 52 | return newPolyline 53 | } 54 | 55 | func (polyline *polyLine) render(coords []float32) { 56 | var sleeve, current, next []float32 57 | vertices := []float32{} 58 | 59 | coordsCount := len(coords) 60 | isLooping := (coords[0] == coords[coordsCount-2]) && (coords[1] == coords[coordsCount-1]) 61 | if !isLooping { // virtual starting point at second point mirrored on first point 62 | sleeve = []float32{coords[2] - coords[0], coords[3] - coords[1]} 63 | } else { // virtual starting point at last vertex 64 | sleeve = []float32{coords[0] - coords[coordsCount-4], coords[1] - coords[coordsCount-3]} 65 | } 66 | 67 | for i := 0; i+3 < coordsCount; i += 2 { 68 | current = []float32{coords[i], coords[i+1]} 69 | next = []float32{coords[i+2], coords[i+3]} 70 | vertices = append(vertices, polyline.renderEdge(sleeve, current, next)...) 71 | sleeve = []float32{next[0] - current[0], next[1] - current[1]} 72 | } 73 | 74 | if isLooping { 75 | vertices = append(vertices, polyline.renderEdge(sleeve, next, []float32{coords[2], coords[3]})...) 76 | } else { 77 | vertices = append(vertices, polyline.renderEdge(sleeve, next, []float32{next[0] + sleeve[0], next[1] + sleeve[1]})...) 78 | } 79 | 80 | prepareDraw(nil) 81 | bindTexture(glState.defaultTexture) 82 | useVertexAttribArrays(shaderPos) 83 | 84 | buffer := newVertexBuffer(len(vertices), vertices, UsageStatic) 85 | buffer.bind() 86 | defer buffer.unbind() 87 | 88 | gl.VertexAttribPointer(gl.Attrib{Value: 0}, 2, gl.FLOAT, false, 0, 0) 89 | gl.DrawArrays(gl.TRIANGLE_STRIP, 0, len(vertices)/2) 90 | } 91 | 92 | func (polyline *polyLine) renderEdge(sleeve, current, next []float32) []float32 { 93 | if polyline.join == "bevel" { 94 | return polyline.renderBevelEdge(sleeve, current, next) 95 | } 96 | return polyline.renderMiterEdge(sleeve, current, next) 97 | } 98 | 99 | func (polyline *polyLine) generateEdges(current []float32, normals ...float32) []float32 { 100 | verts := make([]float32, len(normals)) 101 | for i := 0; i < len(normals); i += 2 { 102 | verts[i] = current[0] + normals[i] 103 | verts[i+1] = current[1] + normals[i+1] 104 | } 105 | return verts 106 | } 107 | 108 | /** Calculate line boundary points. 109 | * 110 | * Sketch: 111 | * 112 | * u1 113 | * -------------+---...___ 114 | * | ```'''-- --- 115 | * p- - - - - - q- - . _ _ | w/2 116 | * | ` ' ' r + 117 | * -------------+---...___ | w/2 118 | * u2 ```'''-- --- 119 | * 120 | * u1 and u2 depend on four things: 121 | * - the half line width w/2 122 | * - the previous line vertex p 123 | * - the current line vertex q 124 | * - the next line vertex r 125 | * 126 | * u1/u2 are the intersection points of the parallel lines to p-q and q-r, 127 | * i.e. the point where 128 | * 129 | * (q + w/2 * ns) + lambda * (q - p) = (q + w/2 * nt) + mu * (r - q) (u1) 130 | * (q - w/2 * ns) + lambda * (q - p) = (q - w/2 * nt) + mu * (r - q) (u2) 131 | * 132 | * with nt,nt being the normals on the segments s = p-q and t = q-r, 133 | * 134 | * ns = perp(s) / |s| 135 | * nt = perp(t) / |t|. 136 | * 137 | * Using the linear equation system (similar for u2) 138 | * 139 | * q + w/2 * ns + lambda * s - (q + w/2 * nt + mu * t) = 0 (u1) 140 | * <=> q-q + lambda * s - mu * t = (nt - ns) * w/2 141 | * <=> lambda * s - mu * t = (nt - ns) * w/2 142 | * 143 | * the intersection points can be efficiently calculated using Cramer's rule. 144 | */ 145 | func (polyline *polyLine) renderMiterEdge(sleeve, current, next []float32) []float32 { 146 | sleeveNormal := getNormal(sleeve, polyline.halfwidth/vecLen(sleeve)) 147 | t := []float32{next[0] - current[0], next[1] - current[1]} 148 | lenT := vecLen(t) 149 | 150 | det := determinant(sleeve, t) 151 | // lines parallel, compute as u1 = q + ns * w/2, u2 = q - ns * w/2 152 | if abs(det)/(vecLen(sleeve)*lenT) < linesParallelEPS && (sleeve[0]*t[0]+sleeve[1]*t[1]) > 0 { 153 | return polyline.generateEdges(current, sleeveNormal[0], sleeveNormal[1], sleeveNormal[0]*-1, sleeveNormal[1]*-1) 154 | } 155 | // cramers rule 156 | nt := getNormal(t, polyline.halfwidth/lenT) 157 | lambda := determinant([]float32{nt[0] - sleeveNormal[0], nt[1] - sleeveNormal[1]}, t) / det 158 | sleeveChange := []float32{sleeve[0] * lambda, sleeve[1] * lambda} 159 | d := []float32{sleeveNormal[0] + sleeveChange[0], sleeveNormal[1] + sleeveChange[1]} 160 | return polyline.generateEdges(current, d[0], d[1], d[0]*-1, d[1]*-1) 161 | } 162 | 163 | /** Calculate line boundary points. 164 | * 165 | * Sketch: 166 | * 167 | * uh1___uh2 168 | * .' '. 169 | * .' q '. 170 | * .' ' ' '. 171 | *.' ' .'. ' '. 172 | * ' .' ul'. ' 173 | * p .' '. r 174 | * 175 | * 176 | * ul can be found as above, uh1 and uh2 are much simpler: 177 | * 178 | * uh1 = q + ns * w/2, uh2 = q + nt * w/2 179 | */ 180 | func (polyline *polyLine) renderBevelEdge(sleeve, current, next []float32) []float32 { 181 | t := []float32{next[0] - current[0], next[1] - current[1]} 182 | lenT := vecLen(t) 183 | 184 | det := determinant(sleeve, t) 185 | if abs(det)/(vecLen(sleeve)*lenT) < linesParallelEPS && (sleeve[0]*t[0]+sleeve[1]*t[1]) > 0 { 186 | // lines parallel, compute as u1 = q + ns * w/2, u2 = q - ns * w/2 187 | n := getNormal(t, polyline.halfwidth/lenT) 188 | return polyline.generateEdges(current, n[0], n[1], n[0]*-1, n[1]*-1) 189 | } 190 | 191 | // cramers rule 192 | sleeveNormal := getNormal(sleeve, polyline.halfwidth/vecLen(sleeve)) 193 | nt := getNormal(t, polyline.halfwidth/lenT) 194 | lambda := determinant([]float32{nt[0] - sleeveNormal[0], nt[1] - sleeveNormal[1]}, t) / det 195 | sleeveChange := []float32{sleeve[0] * lambda, sleeve[1] * lambda} 196 | d := []float32{sleeveNormal[0] + sleeveChange[0], sleeveNormal[1] + sleeveChange[1]} 197 | 198 | if det > 0 { // 'left' turn -> intersection on the top 199 | return polyline.generateEdges(current, 200 | d[0], d[1], 201 | sleeveNormal[0]*-1, sleeveNormal[1]*-1, 202 | d[0], d[1], 203 | nt[0]*-1, nt[1]*-1, 204 | ) 205 | } 206 | 207 | return polyline.generateEdges(current, 208 | sleeveNormal[0], sleeveNormal[1], 209 | d[0]*-1, d[1]*-1, 210 | nt[0], nt[1], 211 | d[0]*-1, d[1]*-1, 212 | ) 213 | } 214 | -------------------------------------------------------------------------------- /gfx/quad.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | type ( 4 | // Quad is essentially a crop of an image/texture 5 | Quad struct { 6 | vertices []float32 7 | x float32 8 | y float32 9 | w float32 10 | h float32 11 | sw float32 12 | sh float32 13 | } 14 | ) 15 | 16 | // NewQuad will generate a new *Quad with the dimensions given 17 | // x, y are position on the texture 18 | // w, h are the size of the quad 19 | // sw, sh are references on how large the texture is. image.GetWidth(), image.GetHeight() 20 | func NewQuad(x, y, w, h, sw, sh int32) *Quad { 21 | newQuad := &Quad{ 22 | x: float32(x), 23 | y: float32(y), 24 | w: float32(w), 25 | h: float32(h), 26 | sw: float32(sw), 27 | sh: float32(sh), 28 | } 29 | newQuad.generateVertices() 30 | return newQuad 31 | } 32 | 33 | // generateVertices generates an array of data for drawing the quad. 34 | func (quad *Quad) generateVertices() { 35 | quad.vertices = []float32{ 36 | 0, 0, quad.x / quad.sw, quad.y / quad.sh, 37 | 0, quad.h, quad.x / quad.sw, (quad.y + quad.h) / quad.sh, 38 | quad.w, 0, (quad.x + quad.w) / quad.sw, quad.y / quad.sh, 39 | quad.w, quad.h, (quad.x + quad.w) / quad.sw, (quad.y + quad.h) / quad.sh, 40 | } 41 | } 42 | 43 | // getVertices will return the generated verticies 44 | func (quad *Quad) getVertices() []float32 { 45 | return quad.vertices 46 | } 47 | 48 | // SetViewport sets the texture coordinates according to a viewport. 49 | func (quad *Quad) SetViewport(x, y, w, h int32) { 50 | quad.x = float32(x) 51 | quad.y = float32(y) 52 | quad.w = float32(w) 53 | quad.h = float32(h) 54 | quad.generateVertices() 55 | } 56 | 57 | // GetWidth gets the width of the quad 58 | func (quad *Quad) GetWidth() float32 { 59 | return quad.w 60 | } 61 | 62 | // GetHeight gets the height of the quad 63 | func (quad *Quad) GetHeight() float32 { 64 | return quad.h 65 | } 66 | 67 | // GetViewport gets the current viewport of this Quad. 68 | func (quad *Quad) GetViewport() (x, y, w, h int32) { 69 | return int32(quad.x), int32(quad.y), int32(quad.w), int32(quad.h) 70 | } 71 | -------------------------------------------------------------------------------- /gfx/quad_indicies.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "github.com/goxjs/gl" 5 | ) 6 | 7 | type indexBuffer struct { 8 | isBound bool // Whether the buffer is currently bound. 9 | ibo gl.Buffer // The IBO identifier. Assigned by OpenGL. 10 | data []uint32 // A pointer to mapped memory. 11 | } 12 | 13 | func newIndexBuffer(data []uint32) *indexBuffer { 14 | newBuffer := &indexBuffer{data: data} 15 | registerVolatile(newBuffer) 16 | return newBuffer 17 | } 18 | 19 | func (buffer *indexBuffer) bind() { 20 | gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer.ibo) 21 | buffer.isBound = true 22 | } 23 | 24 | func (buffer *indexBuffer) unbind() { 25 | if buffer.isBound { 26 | gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.Buffer{}) 27 | } 28 | buffer.isBound = false 29 | } 30 | 31 | func (buffer *indexBuffer) drawElements(mode uint32, offset, size int) { 32 | buffer.bind() 33 | defer buffer.unbind() 34 | gl.DrawElements(gl.Enum(mode), size, gl.UNSIGNED_INT, offset*4) 35 | } 36 | 37 | func (buffer *indexBuffer) loadVolatile() bool { 38 | buffer.ibo = gl.CreateBuffer() 39 | buffer.bind() 40 | defer buffer.unbind() 41 | gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, ui32Bytes(buffer.data), gl.STATIC_DRAW) 42 | return true 43 | } 44 | 45 | func (buffer *indexBuffer) unloadVolatile() { 46 | gl.DeleteBuffer(buffer.ibo) 47 | } 48 | 49 | /** 50 | * QuadIndices manages one shared buffer that stores the indices for an 51 | * element array. Vertex arrays using the vertex structure (or anything else 52 | * that can use the pattern below) can request a size and use it for the 53 | * drawElements call. 54 | * 55 | * 0----2 56 | * | / | 57 | * | / | 58 | * 1----3 59 | * 60 | * indices[i*6 + 0] = i*4 + 0; 61 | * indices[i*6 + 1] = i*4 + 1; 62 | * indices[i*6 + 2] = i*4 + 2; 63 | * 64 | * indices[i*6 + 3] = i*4 + 2; 65 | * indices[i*6 + 4] = i*4 + 1; 66 | * indices[i*6 + 5] = i*4 + 3; 67 | * 68 | * There will always be a large enough buffer around until all 69 | * QuadIndices instances have been deleted. 70 | * 71 | * Q: Why have something like QuadIndices? 72 | * A: The indices for the SpriteBatch do not change, only the array size 73 | * varies. Using one buffer for all element arrays removes this 74 | * duplicated data and saves some memory. 75 | */ 76 | type quadIndices struct { 77 | *indexBuffer 78 | } 79 | 80 | func newQuadIndices(size int) *quadIndices { 81 | indices := make([]uint32, size*6) 82 | for i := 0; i < size; i++ { 83 | indices[i*6+0] = uint32(i*4 + 0) 84 | indices[i*6+1] = uint32(i*4 + 1) 85 | indices[i*6+2] = uint32(i*4 + 2) 86 | 87 | indices[i*6+3] = uint32(i*4 + 2) 88 | indices[i*6+4] = uint32(i*4 + 1) 89 | indices[i*6+5] = uint32(i*4 + 3) 90 | } 91 | 92 | return &quadIndices{ 93 | indexBuffer: newIndexBuffer(indices), 94 | } 95 | } 96 | 97 | func newAltQuadIndices(size int) *quadIndices { 98 | indices := make([]uint32, size*6) 99 | for i := 0; i < size; i++ { 100 | indices[i*6+0] = uint32(i*4 + 0) 101 | indices[i*6+1] = uint32(i*4 + 1) 102 | indices[i*6+2] = uint32(i*4 + 2) 103 | 104 | indices[i*6+3] = uint32(i*4 + 2) 105 | indices[i*6+4] = uint32(i*4 + 3) 106 | indices[i*6+5] = uint32(i*4 + 1) 107 | } 108 | 109 | return &quadIndices{ 110 | indexBuffer: newIndexBuffer(indices), 111 | } 112 | } 113 | 114 | func (qi *quadIndices) drawElements(mode uint32, offset, size int) { 115 | qi.indexBuffer.drawElements(mode, offset*6, size*6) 116 | } 117 | -------------------------------------------------------------------------------- /gfx/shader.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/go-gl/mathgl/mgl32" 11 | "github.com/goxjs/gl" 12 | 13 | "github.com/tanema/amore/file" 14 | ) 15 | 16 | // Shader is a glsl program that can be applied while drawing. 17 | type Shader struct { 18 | vertexCode string 19 | fragmentCode string 20 | program gl.Program 21 | uniforms map[string]uniform // uniform location buffer map 22 | texUnitPool map[string]int 23 | activeTexUnits []gl.Texture 24 | } 25 | 26 | // NewShader will create a new shader program. It takes in either paths to glsl 27 | // files or shader code directly 28 | func NewShader(paths ...string) *Shader { 29 | newShader := &Shader{} 30 | code := pathsToCode(paths...) 31 | newShader.vertexCode, newShader.fragmentCode = shaderCodeToGLSL(code...) 32 | registerVolatile(newShader) 33 | return newShader 34 | } 35 | 36 | func (shader *Shader) loadVolatile() bool { 37 | vert := compileCode(gl.VERTEX_SHADER, shader.vertexCode) 38 | frag := compileCode(gl.FRAGMENT_SHADER, shader.fragmentCode) 39 | shader.program = gl.CreateProgram() 40 | shader.texUnitPool = make(map[string]int) 41 | shader.activeTexUnits = make([]gl.Texture, maxTextureUnits) 42 | 43 | gl.AttachShader(shader.program, vert) 44 | gl.AttachShader(shader.program, frag) 45 | 46 | gl.BindAttribLocation(shader.program, shaderPos, "VertexPosition") 47 | gl.BindAttribLocation(shader.program, shaderTexCoord, "VertexTexCoord") 48 | gl.BindAttribLocation(shader.program, shaderColor, "VertexColor") 49 | gl.BindAttribLocation(shader.program, shaderConstantColor, "ConstantColor") 50 | 51 | gl.LinkProgram(shader.program) 52 | gl.DeleteShader(vert) 53 | gl.DeleteShader(frag) 54 | 55 | if gl.GetProgrami(shader.program, gl.LINK_STATUS) == 0 { 56 | gl.DeleteProgram(shader.program) 57 | panic(fmt.Errorf("shader link error: %s", gl.GetProgramInfoLog(shader.program))) 58 | } 59 | 60 | shader.mapUniforms() 61 | 62 | return true 63 | } 64 | 65 | func (shader *Shader) unloadVolatile() { 66 | // active texture list is probably invalid, clear it 67 | gl.DeleteProgram(shader.program) 68 | 69 | // decrement global texture id counters for texture units which had textures bound from this shader 70 | for i := 0; i < len(shader.activeTexUnits); i++ { 71 | if shader.activeTexUnits[i].Valid() { 72 | glState.textureCounters[i] = glState.textureCounters[i] - 1 73 | } 74 | } 75 | } 76 | 77 | func (shader *Shader) mapUniforms() { 78 | // Built-in uniform locations default to -1 (nonexistent.) 79 | shader.uniforms = map[string]uniform{} 80 | 81 | for i := 0; i < gl.GetProgrami(shader.program, gl.ACTIVE_UNIFORMS); i++ { 82 | u := uniform{} 83 | 84 | u.Name, u.Count, u.Type = gl.GetActiveUniform(shader.program, uint32(i)) 85 | u.Location = gl.GetUniformLocation(shader.program, u.Name) 86 | u.CalculateTypeInfo() 87 | 88 | // glGetActiveUniform appends "[0]" to the end of array uniform names... 89 | if len(u.Name) > 3 { 90 | if strings.Contains(u.Name, "[0]") { 91 | u.Name = u.Name[:len(u.Name)-3] 92 | } 93 | } 94 | 95 | shader.uniforms[u.Name] = u 96 | } 97 | } 98 | 99 | func (shader *Shader) attach(temporary bool) { 100 | if glState.currentShader != shader { 101 | gl.UseProgram(shader.program) 102 | glState.currentShader = shader 103 | } 104 | if !temporary { 105 | // make sure all sent textures are properly bound to their respective texture units 106 | // note: list potentially contains texture ids of deleted/invalid textures! 107 | for i := 0; i < len(shader.activeTexUnits); i++ { 108 | if shader.activeTexUnits[i].Valid() { 109 | bindTextureToUnit(shader.activeTexUnits[i], i+1, false) 110 | } 111 | } 112 | 113 | // We always want to use texture unit 0 for everyhing else. 114 | setTextureUnit(0) 115 | } 116 | } 117 | 118 | // GetUniformType will return the type and count if it exists and false if it doesn't 119 | func (shader *Shader) GetUniformType(name string) (UniformType, bool) { 120 | u, ok := shader.uniforms[name] 121 | if ok { 122 | return u.BaseType, ok 123 | } 124 | return UniformType(-1), false 125 | } 126 | 127 | func (shader *Shader) getUniformAndCheck(name string, expected UniformType, count int) (uniform, error) { 128 | u, ok := shader.uniforms[name] 129 | if !ok { 130 | return u, fmt.Errorf("no uniform with the name %v", name) 131 | } 132 | if u.BaseType != expected { 133 | return u, errors.New("Invalid type for uniform " + name + ". expected " + translateUniformBaseType(u.BaseType) + " and got " + translateUniformBaseType(expected)) 134 | } 135 | if count != u.Count*u.TypeSize { 136 | return u, fmt.Errorf("invalid number of arguments for uniform %v expected %v and got %v", name, (u.Count * u.TypeSize), count) 137 | } 138 | return u, nil 139 | } 140 | 141 | // SendInt allows you to pass in integer values into your shader, by the name of 142 | // the variable 143 | func (shader *Shader) SendInt(name string, values ...int32) error { 144 | shader.attach(true) 145 | defer states.back().shader.attach(false) 146 | 147 | u, err := shader.getUniformAndCheck(name, UniformInt, len(values)) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | switch u.TypeSize { 153 | case 4: 154 | gl.Uniform4iv(u.Location, values) 155 | return nil 156 | case 3: 157 | gl.Uniform3iv(u.Location, values) 158 | return nil 159 | case 2: 160 | gl.Uniform2iv(u.Location, values) 161 | return nil 162 | case 1: 163 | gl.Uniform1iv(u.Location, values) 164 | return nil 165 | } 166 | return errors.New("Invalid type size for uniform: " + name) 167 | } 168 | 169 | // SendFloat allows you to pass in float32 values into your shader, by the name of 170 | // the variable 171 | func (shader *Shader) SendFloat(name string, values ...float32) error { 172 | shader.attach(true) 173 | defer states.back().shader.attach(false) 174 | 175 | u, err := shader.getUniformAndCheck(name, UniformFloat, len(values)) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | switch u.TypeSize { 181 | case 4: 182 | gl.Uniform4fv(u.Location, values) 183 | return nil 184 | case 3: 185 | gl.Uniform3fv(u.Location, values) 186 | return nil 187 | case 2: 188 | gl.Uniform2fv(u.Location, values) 189 | return nil 190 | case 1: 191 | gl.Uniform1fv(u.Location, values) 192 | return nil 193 | } 194 | return errors.New("Invalid type size for uniform: " + name) 195 | } 196 | 197 | // SendMat4 allows you to pass in a 4x4 matrix value into your shader, by the name of 198 | // the variable 199 | func (shader *Shader) SendMat4(name string, mat mgl32.Mat4) error { 200 | shader.attach(true) 201 | defer states.back().shader.attach(false) 202 | 203 | u, err := shader.getUniformAndCheck(name, UniformFloat, 4) 204 | if err != nil { 205 | return err 206 | } 207 | gl.UniformMatrix4fv(u.Location, []float32{ 208 | mat[0], mat[1], mat[2], mat[3], 209 | mat[4], mat[5], mat[6], mat[7], 210 | mat[8], mat[9], mat[10], mat[11], 211 | mat[12], mat[13], mat[14], mat[15], 212 | }) 213 | return nil 214 | } 215 | 216 | // SendMat3 allows you to pass in a 3x3 matrix value into your shader, by the name of 217 | // the variable 218 | func (shader *Shader) SendMat3(name string, mat mgl32.Mat3) error { 219 | shader.attach(true) 220 | defer states.back().shader.attach(false) 221 | 222 | u, err := shader.getUniformAndCheck(name, UniformFloat, 3) 223 | if err != nil { 224 | return err 225 | } 226 | gl.UniformMatrix3fv(u.Location, []float32{ 227 | mat[0], mat[1], mat[2], 228 | mat[3], mat[4], mat[5], 229 | mat[6], mat[7], mat[8], 230 | }) 231 | return nil 232 | } 233 | 234 | // SendMat2 allows you to pass in a 2x2 matrix value into your shader, by the name of 235 | // the variable 236 | func (shader *Shader) SendMat2(name string, mat mgl32.Mat2) error { 237 | shader.attach(true) 238 | defer states.back().shader.attach(false) 239 | 240 | u, err := shader.getUniformAndCheck(name, UniformFloat, 3) 241 | if err != nil { 242 | return err 243 | } 244 | gl.UniformMatrix2fv(u.Location, []float32{ 245 | mat[0], mat[1], 246 | mat[2], mat[3], 247 | }) 248 | return nil 249 | } 250 | 251 | // SendTexture allows you to pass in a ITexture to your shader as a sampler, by the name of 252 | // the variable. This means you can pass in an image but also a canvas. 253 | func (shader *Shader) SendTexture(name string, texture ITexture) error { 254 | shader.attach(true) 255 | defer states.back().shader.attach(false) 256 | 257 | gltex := texture.getHandle() 258 | texunit := shader.getTextureUnit(name) 259 | 260 | u, err := shader.getUniformAndCheck(name, UniformSampler, 1) 261 | if err != nil { 262 | return err 263 | } 264 | 265 | bindTextureToUnit(gltex, texunit, true) 266 | 267 | gl.Uniform1i(u.Location, int(texunit)) 268 | 269 | // increment global shader texture id counter for this texture unit, if we haven't already 270 | if !shader.activeTexUnits[texunit-1].Valid() { 271 | glState.textureCounters[texunit-1]++ 272 | } 273 | 274 | // store texture id so it can be re-bound to the proper texture unit later 275 | shader.activeTexUnits[texunit-1] = gltex 276 | 277 | return nil 278 | } 279 | 280 | func (shader *Shader) getTextureUnit(name string) int { 281 | unit, found := shader.texUnitPool[name] 282 | if found { 283 | return unit 284 | } 285 | 286 | texunit := -1 287 | // prefer texture units which are unused by all other shaders 288 | for i := 0; i < len(glState.textureCounters); i++ { 289 | if glState.textureCounters[i] == 0 { 290 | texunit = i + 1 291 | break 292 | } 293 | } 294 | 295 | if texunit == -1 { 296 | // no completely unused texture units exist, try to use next free slot in our own list 297 | for i := 0; i < len(shader.activeTexUnits); i++ { 298 | if !shader.activeTexUnits[i].Valid() { 299 | texunit = i + 1 300 | break 301 | } 302 | } 303 | 304 | if texunit == -1 { 305 | panic("No more texture units available for shader.") 306 | } 307 | } 308 | 309 | shader.texUnitPool[name] = texunit 310 | return shader.texUnitPool[name] 311 | } 312 | 313 | func createCode(header, code, footer string) string { 314 | var templateWriter bytes.Buffer 315 | if err := shaderTemplate.Execute(&templateWriter, struct { 316 | Header, Code, Footer string 317 | }{Header: header, Code: code, Footer: footer}); err != nil { 318 | panic(err) 319 | } 320 | return templateWriter.String() 321 | } 322 | 323 | func isVertexCode(code string) bool { 324 | match, _ := regexp.MatchString(`vec4\s+position\s*\(`, code) 325 | return match 326 | } 327 | 328 | func isFragmentCode(code string) bool { 329 | match, _ := regexp.MatchString(`vec4\s+effect\s*\(`, code) 330 | return match 331 | } 332 | 333 | //convert paths to strings of code 334 | //if string is already code just pass it along 335 | func pathsToCode(paths ...string) []string { 336 | code := []string{} 337 | if paths != nil { 338 | for _, path := range paths { 339 | if path == "" { 340 | continue 341 | } 342 | //if this is not code it must be a path 343 | if !isVertexCode(path) && !isFragmentCode(path) { 344 | code = append(code, file.ReadString(path)) 345 | } else { //it is code! 346 | code = append(code, path) 347 | } 348 | } 349 | } 350 | return code 351 | } 352 | 353 | func shaderCodeToGLSL(code ...string) (string, string) { 354 | vertexcode := defaultVertexShaderCode 355 | fragmentCode := defaultFragmentShaderCode 356 | for _, shaderCode := range code { 357 | if isVertexCode(shaderCode) { 358 | vertexcode = shaderCode 359 | } 360 | if isFragmentCode(shaderCode) { 361 | fragmentCode = shaderCode 362 | } 363 | } 364 | return createCode(vertexHeader, vertexcode, vertexFooter), createCode(fragmentHeader, fragmentCode, fragmentFooter) 365 | } 366 | 367 | func compileCode(shaderType gl.Enum, src string) gl.Shader { 368 | shader := gl.CreateShader(shaderType) 369 | if !shader.Valid() { 370 | panic(fmt.Errorf("could not create shader (type %v)", shaderType)) 371 | } 372 | gl.ShaderSource(shader, src) 373 | gl.CompileShader(shader) 374 | if gl.GetShaderi(shader, gl.COMPILE_STATUS) == 0 { 375 | defer gl.DeleteShader(shader) 376 | panic(fmt.Errorf("shader compile: %s", gl.GetShaderInfoLog(shader))) 377 | } 378 | return shader 379 | } 380 | -------------------------------------------------------------------------------- /gfx/shader_templates.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "text/template" 5 | ) 6 | 7 | var ( 8 | shaderTemplate, _ = template.New("shader").Parse(` 9 | #ifdef GL_ES 10 | precision highp float; 11 | #endif 12 | uniform mat4 TransformMat; 13 | uniform vec4 ScreenSize; 14 | {{.Header}} 15 | #line 1 16 | {{.Code}} 17 | {{.Footer}} 18 | `) 19 | ) 20 | 21 | const ( 22 | vertexHeader = ` 23 | attribute vec4 VertexPosition; 24 | attribute vec4 VertexTexCoord; 25 | attribute vec4 VertexColor; 26 | attribute vec4 ConstantColor; 27 | varying vec4 VaryingTexCoord; 28 | varying vec4 VaryingColor; 29 | uniform float PointSize; 30 | ` 31 | 32 | defaultVertexShaderCode = ` 33 | vec4 position(mat4 transformMatrix, vec4 vertexPosition) { 34 | return transformMatrix * vertexPosition; 35 | }` 36 | 37 | vertexFooter = ` 38 | void main() { 39 | VaryingTexCoord = VertexTexCoord; 40 | VaryingColor = VertexColor * ConstantColor; 41 | gl_PointSize = PointSize; 42 | gl_Position = position(TransformMat, VertexPosition); 43 | }` 44 | 45 | fragmentHeader = ` 46 | varying vec4 VaryingTexCoord; 47 | varying vec4 VaryingColor; 48 | uniform sampler2D Texture0; 49 | ` 50 | 51 | defaultFragmentShaderCode = ` 52 | vec4 effect(vec4 color, sampler2D texture, vec2 textureCoordinate, vec2 pixcoord) { 53 | return texture2D(texture, textureCoordinate) * color; 54 | }` 55 | 56 | fragmentFooter = ` 57 | void main() { 58 | vec2 pixelcoord = vec2(gl_FragCoord.x, (gl_FragCoord.y * ScreenSize.z) + ScreenSize.w); 59 | gl_FragColor = effect(VaryingColor, Texture0, VaryingTexCoord.st, pixelcoord); 60 | }` 61 | ) 62 | -------------------------------------------------------------------------------- /gfx/sprite_batch.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "github.com/go-gl/mathgl/mgl32" 8 | 9 | "github.com/goxjs/gl" 10 | ) 11 | 12 | // SpriteBatch is a collection of images/quads/textures all drawn with a single draw call 13 | type SpriteBatch struct { 14 | size int 15 | count int 16 | color []float32 // Current color. This color, if present, will be applied to the next added sprite. 17 | arrayBuf *vertexBuffer 18 | quadIndices *quadIndices 19 | usage Usage 20 | texture ITexture 21 | rangeMin int 22 | rangeMax int 23 | } 24 | 25 | // NewSpriteBatch is like NewSpriteBatch but allows you to set the usage. 26 | func NewSpriteBatch(texture ITexture, size int, usage Usage) *SpriteBatch { 27 | return &SpriteBatch{ 28 | size: size, 29 | texture: texture, 30 | usage: usage, 31 | color: []float32{1, 1, 1, 1}, 32 | arrayBuf: newVertexBuffer(size*4*8, []float32{}, usage), 33 | quadIndices: newQuadIndices(size), 34 | rangeMin: -1, 35 | rangeMax: -1, 36 | } 37 | } 38 | 39 | // Add adds a sprite to the batch. Sprites are drawn in the order they are added. 40 | // x, y The position to draw the object 41 | // r rotation of the object 42 | // sx, sy scale of the object 43 | // ox, oy offset of the object 44 | // kx, ky shear of the object 45 | func (spriteBatch *SpriteBatch) Add(args ...float32) error { 46 | return spriteBatch.addv(spriteBatch.texture.getVerticies(), generateModelMatFromArgs(args), -1) 47 | } 48 | 49 | // Addq adds a Quad to the batch. This is very useful for something like a tilemap. 50 | func (spriteBatch *SpriteBatch) Addq(quad *Quad, args ...float32) error { 51 | return spriteBatch.addv(quad.getVertices(), generateModelMatFromArgs(args), -1) 52 | } 53 | 54 | // Set changes a sprite in the batch with the same arguments as add 55 | func (spriteBatch *SpriteBatch) Set(index int, args ...float32) error { 56 | return spriteBatch.addv(spriteBatch.texture.getVerticies(), generateModelMatFromArgs(args), index) 57 | } 58 | 59 | // Setq changes a sprite in the batch with the same arguments as addq 60 | func (spriteBatch *SpriteBatch) Setq(index int, quad *Quad, args ...float32) error { 61 | return spriteBatch.addv(quad.getVertices(), generateModelMatFromArgs(args), index) 62 | } 63 | 64 | // Clear will remove all the sprites from the batch 65 | func (spriteBatch *SpriteBatch) Clear() { 66 | spriteBatch.arrayBuf = newVertexBuffer(spriteBatch.size*4*8, []float32{}, spriteBatch.usage) 67 | spriteBatch.count = 0 68 | } 69 | 70 | // flush will ensure the data is uploaded to the buffer 71 | func (spriteBatch *SpriteBatch) flush() { 72 | spriteBatch.arrayBuf.bufferData() 73 | } 74 | 75 | // SetTexture will change the texture of the batch to a new one 76 | func (spriteBatch *SpriteBatch) SetTexture(newtexture ITexture) { 77 | spriteBatch.texture = newtexture 78 | } 79 | 80 | // GetTexture will return the currently bound texture of this sprite batch. 81 | func (spriteBatch *SpriteBatch) GetTexture() ITexture { 82 | return spriteBatch.texture 83 | } 84 | 85 | // SetColor will set the color that will be used for the next add or set operations. 86 | func (spriteBatch *SpriteBatch) SetColor(vals ...float32) { 87 | spriteBatch.color = vals 88 | } 89 | 90 | // ClearColor will reset the color back to white 91 | func (spriteBatch *SpriteBatch) ClearColor() { 92 | spriteBatch.color = []float32{1, 1, 1, 1} 93 | } 94 | 95 | // GetColor will return the currently used color. 96 | func (spriteBatch *SpriteBatch) GetColor() []float32 { 97 | return spriteBatch.color 98 | } 99 | 100 | // GetCount will return the amount of sprites already added to the batch 101 | func (spriteBatch *SpriteBatch) GetCount() int { 102 | return spriteBatch.count 103 | } 104 | 105 | // SetBufferSize will resize the buffer, change the limit of sprites you can add 106 | // to this batch. 107 | func (spriteBatch *SpriteBatch) SetBufferSize(newsize int) error { 108 | if newsize <= 0 { 109 | return fmt.Errorf("invalid SpriteBatch size") 110 | } else if newsize == spriteBatch.size { 111 | return nil 112 | } 113 | spriteBatch.arrayBuf = newVertexBuffer(newsize*4*8, spriteBatch.arrayBuf.data, spriteBatch.usage) 114 | spriteBatch.quadIndices = newQuadIndices(newsize) 115 | spriteBatch.size = newsize 116 | return nil 117 | } 118 | 119 | // GetBufferSize will return the limit of sprites you can add to this batch. 120 | func (spriteBatch *SpriteBatch) GetBufferSize() int { 121 | return spriteBatch.size 122 | } 123 | 124 | // addv will add a sprite to the batch using the verts, a transform and an index to 125 | // place it 126 | func (spriteBatch *SpriteBatch) addv(verts []float32, mat *mgl32.Mat4, index int) error { 127 | if index == -1 && spriteBatch.count >= spriteBatch.size { 128 | return fmt.Errorf("Sprite Batch Buffer Full") 129 | } 130 | 131 | sprite := make([]float32, 8*4) 132 | for i := 0; i < 32; i += 8 { 133 | j := (i / 2) 134 | sprite[i+0] = (mat[0] * verts[j+0]) + (mat[4] * verts[j+1]) + mat[12] 135 | sprite[i+1] = (mat[1] * verts[j+0]) + (mat[5] * verts[j+1]) + mat[13] 136 | sprite[i+2] = verts[j+2] 137 | sprite[i+3] = verts[j+3] 138 | sprite[i+4] = spriteBatch.color[0] 139 | sprite[i+5] = spriteBatch.color[1] 140 | sprite[i+6] = spriteBatch.color[2] 141 | sprite[i+7] = spriteBatch.color[3] 142 | } 143 | 144 | if index == -1 { 145 | spriteBatch.arrayBuf.fill(spriteBatch.count*4*8, sprite) 146 | spriteBatch.count++ 147 | } else { 148 | spriteBatch.arrayBuf.fill(index*4*8, sprite) 149 | } 150 | 151 | return nil 152 | } 153 | 154 | // SetDrawRange will set a range in the points to draw. This is useful if you only 155 | // need to render a portion of the batch. 156 | func (spriteBatch *SpriteBatch) SetDrawRange(min, max int) error { 157 | if min < 0 || max < 0 || min > max { 158 | return fmt.Errorf("invalid draw range") 159 | } 160 | spriteBatch.rangeMin = min 161 | spriteBatch.rangeMax = max 162 | return nil 163 | } 164 | 165 | // ClearDrawRange will reset the draw range if you want to draw the whole batch again. 166 | func (spriteBatch *SpriteBatch) ClearDrawRange() { 167 | spriteBatch.rangeMin = -1 168 | spriteBatch.rangeMax = -1 169 | } 170 | 171 | // GetDrawRange will return the min, max range set on the batch. If no range is set 172 | // the range will return -1, -1 173 | func (spriteBatch *SpriteBatch) GetDrawRange() (int, int) { 174 | min := 0 175 | max := spriteBatch.count - 1 176 | if spriteBatch.rangeMax >= 0 { 177 | max = int(math.Min(float64(spriteBatch.rangeMax), float64(max))) 178 | } 179 | if spriteBatch.rangeMin >= 0 { 180 | min = int(math.Min(float64(spriteBatch.rangeMin), float64(max))) 181 | } 182 | return min, max 183 | } 184 | 185 | // Draw satisfies the Drawable interface. Inputs are as follows 186 | // x, y, r, sx, sy, ox, oy, kx, ky 187 | // x, y are position 188 | // r is rotation 189 | // sx, sy is the scale, if sy is not given sy will equal sx 190 | // ox, oy are offset 191 | // kx, ky are the shear. If ky is not given ky will equal kx 192 | func (spriteBatch *SpriteBatch) Draw(args ...float32) { 193 | if spriteBatch.count == 0 { 194 | return 195 | } 196 | 197 | prepareDraw(generateModelMatFromArgs(args)) 198 | bindTexture(spriteBatch.texture.getHandle()) 199 | useVertexAttribArrays(shaderPos, shaderTexCoord, shaderColor) 200 | 201 | spriteBatch.arrayBuf.bind() 202 | defer spriteBatch.arrayBuf.unbind() 203 | 204 | gl.VertexAttribPointer(gl.Attrib{Value: 0}, 2, gl.FLOAT, false, 8*4, 0) 205 | gl.VertexAttribPointer(gl.Attrib{Value: 1}, 2, gl.FLOAT, false, 8*4, 2*4) 206 | gl.VertexAttribPointer(gl.Attrib{Value: 2}, 4, gl.FLOAT, false, 8*4, 4*4) 207 | 208 | min, max := spriteBatch.GetDrawRange() 209 | spriteBatch.quadIndices.drawElements(gl.TRIANGLES, min, max-min+1) 210 | } 211 | -------------------------------------------------------------------------------- /gfx/text.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type ( 8 | // Text is a container of text, color and text formatting. 9 | Text struct { 10 | font *Font 11 | strings []string 12 | colors [][]float32 13 | wrapLimit float32 14 | align string 15 | batches map[*rasterizer]*SpriteBatch 16 | width float32 17 | height float32 18 | } 19 | ) 20 | 21 | // Print will print out a colored string. It accepts the normal drawable arguments 22 | func Print(strs []string, colors [][]float32, argv ...float32) { 23 | NewText(GetFont(), strs, colors, -1, "left").Draw(argv...) 24 | } 25 | 26 | // Printf will print out a string with a wrap limit and alignment. It accepts the 27 | // normal drawable arguments 28 | func Printf(strs []string, colors [][]float32, wrapLimit float32, align string, argv ...float32) { 29 | NewText(GetFont(), strs, colors, wrapLimit, align).Draw(argv...) 30 | } 31 | 32 | // NewText will create a colored text object with the provided font and 33 | // text. A wrap and alignment can be provided as well. If wrapLimit is < 0 it will 34 | // not wrap 35 | func NewText(font *Font, strs []string, colors [][]float32, wrapLimit float32, align string) *Text { 36 | newText := &Text{ 37 | font: font, 38 | strings: strs, 39 | colors: colors, 40 | wrapLimit: wrapLimit, 41 | align: align, 42 | batches: make(map[*rasterizer]*SpriteBatch), 43 | } 44 | registerVolatile(newText) 45 | return newText 46 | } 47 | 48 | func (text *Text) loadVolatile() bool { 49 | length := len(strings.Join(text.strings, "")) 50 | for _, rast := range text.font.rasterizers { 51 | text.batches[rast] = NewSpriteBatch(rast.texture, length, UsageDynamic) 52 | } 53 | text.generate() 54 | return true 55 | } 56 | 57 | func (text *Text) unloadVolatile() {} 58 | 59 | func (text *Text) generate() { 60 | for _, batch := range text.batches { 61 | batch.Clear() 62 | } 63 | 64 | var lines []*textLine 65 | lines, text.width, text.height = generateLines(text.font, text.strings, text.colors, text.wrapLimit) 66 | 67 | for _, l := range lines { 68 | var gx, spacing float32 69 | 70 | if spaceGlyph, _, ok := text.font.findGlyph(' '); ok { 71 | spacing = spaceGlyph.advance 72 | } else { 73 | spacing = text.font.rasterizers[0].advance 74 | } 75 | 76 | switch text.align { 77 | case "left": 78 | case "right": 79 | gx = text.wrapLimit - l.width 80 | case "center": 81 | gx = (text.wrapLimit - l.width) / 2.0 82 | case "justify": 83 | amountOfSpace := float32(l.spaceCount-1) * spacing 84 | widthWithoutSpace := l.width - amountOfSpace 85 | spacing = (text.wrapLimit - widthWithoutSpace) / float32(l.spaceCount) 86 | } 87 | 88 | for i := 0; i < l.size; i++ { 89 | ch := l.chars[i] 90 | if ch == ' ' { 91 | gx += spacing 92 | } else { 93 | glyph := l.glyphs[i] 94 | rast := l.rasts[i] 95 | text.batches[rast].SetColor(l.colors[i]...) 96 | gx += l.kern[i] 97 | text.batches[rast].Addq(glyph.quad, gx, l.y+glyph.descent) 98 | gx += glyph.advance 99 | } 100 | } 101 | } 102 | 103 | for _, batch := range text.batches { 104 | if batch.GetCount() > 0 { 105 | batch.SetBufferSize(batch.GetCount()) 106 | } 107 | } 108 | } 109 | 110 | // GetWidth will return the text obejcts set width which will be <= wrapLimit 111 | func (text *Text) GetWidth() float32 { 112 | return text.width 113 | } 114 | 115 | // GetHeight will return the height of the text object after text wrap. 116 | func (text *Text) GetHeight() float32 { 117 | return text.height 118 | } 119 | 120 | // GetDimensions will return the width and height of the text object 121 | func (text *Text) GetDimensions() (float32, float32) { 122 | return text.width, text.height 123 | } 124 | 125 | // GetFont will return the font that this text object has been created with 126 | func (text *Text) GetFont() *Font { 127 | return text.font 128 | } 129 | 130 | // SetFont will set the font in which this text object will use to render the 131 | // string 132 | func (text *Text) SetFont(f *Font) { 133 | text.font = f 134 | text.loadVolatile() 135 | } 136 | 137 | // Set will set the string and colors for this text object to be rendered. 138 | func (text *Text) Set(strs []string, colors [][]float32) { 139 | text.strings = strs 140 | text.colors = colors 141 | text.generate() 142 | } 143 | 144 | // Draw satisfies the Drawable interface. Inputs are as follows 145 | // x, y, r, sx, sy, ox, oy, kx, ky 146 | // x, y are position 147 | // r is rotation 148 | // sx, sy is the scale, if sy is not given sy will equal sx 149 | // ox, oy are offset 150 | // kx, ky are the shear. If ky is not given ky will equal kx 151 | func (text *Text) Draw(args ...float32) { 152 | for _, batch := range text.batches { 153 | if batch.GetCount() > 0 { 154 | batch.Draw(args...) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /gfx/text_line.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | type textLine struct { 8 | chars []rune 9 | glyphs []glyphData 10 | colors [][]float32 11 | rasts []*rasterizer 12 | kern []float32 13 | size int 14 | lastBreak int 15 | spaceCount int 16 | width float32 17 | y float32 18 | } 19 | 20 | func generateLines(font *Font, text []string, color [][]float32, wrapLimit float32) ([]*textLine, float32, float32) { 21 | var lines []*textLine 22 | var prevChar rune 23 | var width, gy float32 24 | currentLine := &textLine{} 25 | 26 | breakOff := func(y float32, immediate bool) { 27 | newLine := currentLine.breakOff(immediate) 28 | newLine.y = y 29 | width = float32(math.Max(float64(width), float64(currentLine.width))) 30 | lines = append(lines, currentLine) 31 | currentLine = newLine 32 | prevChar = 0 33 | } 34 | 35 | for i, st := range text { 36 | for _, char := range st { 37 | if char == '\n' { 38 | gy += font.GetLineHeight() 39 | breakOff(gy, true) 40 | continue 41 | } else if char == '\r' { 42 | breakOff(gy, true) 43 | continue 44 | } else if char == '\t' { 45 | if glyph, rast, ok := font.findGlyph(' '); ok { 46 | currentLine.add(' ', glyph, color[i], rast, font.Kern(prevChar, char)) 47 | currentLine.add(' ', glyph, color[i], rast, font.Kern(' ', char)) 48 | currentLine.add(' ', glyph, color[i], rast, font.Kern(' ', char)) 49 | currentLine.add(' ', glyph, color[i], rast, font.Kern(' ', char)) 50 | } 51 | continue 52 | } 53 | if glyph, rast, ok := font.findGlyph(char); ok { 54 | currentLine.add(char, glyph, color[i], rast, font.Kern(prevChar, char)) 55 | } 56 | if wrapLimit > 0 && currentLine.width >= wrapLimit { 57 | gy += font.GetLineHeight() 58 | breakOff(gy, false) 59 | } else { 60 | prevChar = char 61 | } 62 | } 63 | } 64 | 65 | lines = append(lines, currentLine) 66 | width = float32(math.Max(float64(width), float64(currentLine.width))) 67 | 68 | return lines, width, gy + font.GetLineHeight() 69 | } 70 | 71 | func (l *textLine) add(char rune, g glyphData, color []float32, rast *rasterizer, kern float32) { 72 | if char == ' ' { 73 | l.lastBreak = l.size 74 | l.spaceCount++ 75 | l.width += g.advance 76 | } else { 77 | l.width += g.advance + kern + g.lsb 78 | } 79 | l.chars = append(l.chars, char) 80 | l.glyphs = append(l.glyphs, g) 81 | l.colors = append(l.colors, color) 82 | l.rasts = append(l.rasts, rast) 83 | l.kern = append(l.kern, kern+g.lsb) 84 | l.size++ 85 | } 86 | 87 | func (l *textLine) breakOff(immediate bool) *textLine { 88 | breakPoint := l.lastBreak 89 | if l.lastBreak == -1 || immediate { 90 | breakPoint = l.size - 1 91 | } 92 | 93 | newLine := &textLine{} 94 | 95 | for i := l.size - 1; i > breakPoint; i-- { 96 | ch, g, cl, r, k := l.trimLastChar() 97 | newLine.chars = append([]rune{ch}, newLine.chars...) 98 | newLine.glyphs = append([]glyphData{g}, newLine.glyphs...) 99 | newLine.colors = append([][]float32{cl}, newLine.colors...) 100 | newLine.rasts = append([]*rasterizer{r}, newLine.rasts...) 101 | newLine.kern = append([]float32{k}, newLine.kern...) 102 | newLine.size++ 103 | newLine.width += float32(g.advance) + k 104 | } 105 | 106 | for i := l.size - 1; i >= 0; i-- { 107 | if l.chars[i] == ' ' { 108 | l.trimLastChar() 109 | continue 110 | } 111 | break 112 | } 113 | 114 | return newLine 115 | } 116 | 117 | func (l *textLine) trimLastChar() (rune, glyphData, []float32, *rasterizer, float32) { 118 | i := l.size - 1 119 | ch, g, cl, r, k := l.chars[i], l.glyphs[i], l.colors[i], l.rasts[i], l.kern[i] 120 | l.chars = l.chars[:i] 121 | l.glyphs = l.glyphs[:i] 122 | l.colors = l.colors[:i] 123 | l.rasts = l.rasts[:i] 124 | l.kern = l.kern[:i] 125 | l.size-- 126 | l.width -= float32(g.advance) + k 127 | if ch == ' ' { 128 | l.spaceCount-- 129 | } 130 | return ch, g, cl, r, k 131 | } 132 | -------------------------------------------------------------------------------- /gfx/texture.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/draw" 7 | // import all image packages to support them all 8 | _ "image/gif" 9 | _ "image/jpeg" 10 | _ "image/png" 11 | "runtime" 12 | 13 | "github.com/go-gl/mathgl/mgl32" 14 | 15 | "github.com/goxjs/gl" 16 | ) 17 | 18 | type ( 19 | // Filter is a representation of texture filtering that contains both min and 20 | // mag filter modes 21 | Filter struct { 22 | min, mag, mipmap FilterMode 23 | anisotropy float32 24 | } 25 | // Wrap is a representation of texture rapping containing both s and t wrap 26 | Wrap struct { 27 | s, t WrapMode 28 | } 29 | // Texture is a struct to wrap the opengl texture object 30 | Texture struct { 31 | textureID gl.Texture 32 | Width, Height int32 33 | vertices []float32 34 | filter Filter 35 | wrap Wrap 36 | mipmaps bool 37 | } 38 | // ITexture is an interface for any object that can be used like a texture. 39 | ITexture interface { 40 | getHandle() gl.Texture 41 | GetWidth() int32 42 | GetHeight() int32 43 | getVerticies() []float32 44 | } 45 | ) 46 | 47 | // newFilter will create a Filter with default values 48 | func newFilter() Filter { 49 | return Filter{ 50 | min: FilterLinear, 51 | mag: FilterLinear, 52 | mipmap: FilterNone, 53 | anisotropy: 1.0, 54 | } 55 | } 56 | 57 | // newTexture will return a new generated texture will not data uploaded to it. 58 | func newTexture(width, height int32, mipmaps bool) *Texture { 59 | newTexture := &Texture{ 60 | textureID: gl.CreateTexture(), 61 | Width: width, 62 | Height: height, 63 | wrap: Wrap{s: WrapClamp, t: WrapClamp}, 64 | filter: newFilter(), 65 | mipmaps: mipmaps, 66 | } 67 | 68 | newTexture.SetFilter(FilterNearest, FilterNearest) 69 | newTexture.SetWrap(WrapClamp, WrapClamp) 70 | 71 | if newTexture.mipmaps { 72 | newTexture.filter.mipmap = FilterNearest 73 | } 74 | 75 | newTexture.generateVerticies() 76 | 77 | return newTexture 78 | } 79 | 80 | func newImageTexture(img image.Image, mipmaps bool) *Texture { 81 | bounds := img.Bounds() 82 | newTexture := newTexture(int32(bounds.Dx()), int32(bounds.Dy()), mipmaps) 83 | rgba := image.NewRGBA(img.Bounds()) //generate a uniform image and upload to vram 84 | draw.Draw(rgba, bounds, img, image.Point{0, 0}, draw.Src) 85 | bindTexture(newTexture.getHandle()) 86 | gl.TexImage2D(gl.TEXTURE_2D, 0, bounds.Dx(), bounds.Dy(), gl.RGBA, gl.UNSIGNED_BYTE, rgba.Pix) 87 | if newTexture.mipmaps { 88 | newTexture.generateMipmaps() 89 | } 90 | return newTexture 91 | } 92 | 93 | // getHandle will return the gl texutre handle 94 | func (texture *Texture) getHandle() gl.Texture { 95 | return texture.textureID 96 | } 97 | 98 | // generate both the x, y coords at origin and the uv coords. 99 | func (texture *Texture) generateVerticies() { 100 | w := float32(texture.Width) 101 | h := float32(texture.Height) 102 | texture.vertices = []float32{ 103 | 0, 0, 0, 0, 104 | 0, h, 0, 1, 105 | w, 0, 1, 0, 106 | w, h, 1, 1, 107 | } 108 | } 109 | 110 | // GetWidth will return the height of the texture. 111 | func (texture *Texture) GetWidth() int32 { 112 | return texture.Width 113 | } 114 | 115 | // GetHeight will return the height of the texture. 116 | func (texture *Texture) GetHeight() int32 { 117 | return texture.Height 118 | } 119 | 120 | // GetDimensions will return the width and height of the texture. 121 | func (texture *Texture) GetDimensions() (int32, int32) { 122 | return texture.Width, texture.Height 123 | } 124 | 125 | // getVerticies will return the verticies generated when this texture was created. 126 | func (texture *Texture) getVerticies() []float32 { 127 | return texture.vertices 128 | } 129 | 130 | // generateMipmaps will generate mipmaps for the gl texture 131 | func (texture *Texture) generateMipmaps() { 132 | // The GL_GENERATE_MIPMAP texparameter is set in loadVolatile if we don't 133 | // have support for glGenerateMipmap. 134 | if texture.mipmaps { 135 | // Driver bug: http://www.opengl.org/wiki/Common_Mistakes#Automatic_mipmap_generation 136 | if runtime.GOOS == "windows" || runtime.GOOS == "linux" { 137 | gl.Enable(gl.TEXTURE_2D) 138 | } 139 | 140 | gl.GenerateMipmap(gl.TEXTURE_2D) 141 | } 142 | } 143 | 144 | // SetWrap will set how the texture behaves when applies to a plane that is larger 145 | // than itself. 146 | func (texture *Texture) SetWrap(wrapS, wrapT WrapMode) { 147 | texture.wrap.s = wrapS 148 | texture.wrap.t = wrapT 149 | bindTexture(texture.getHandle()) 150 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, int(wrapS)) 151 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, int(wrapT)) 152 | } 153 | 154 | // GetWrap will return the wrapping for how the texture behaves on a plane that 155 | // is larger than itself 156 | func (texture *Texture) GetWrap() Wrap { 157 | return texture.wrap 158 | } 159 | 160 | // SetFilter will set the min, mag filters for the texture filtering. 161 | func (texture *Texture) SetFilter(min, mag FilterMode) error { 162 | if !texture.validateFilter() { 163 | if texture.filter.mipmap != FilterNone && !texture.mipmaps { 164 | return fmt.Errorf("non-mipmapped image cannot have mipmap filtering") 165 | } 166 | return fmt.Errorf("invalid texture filter") 167 | } 168 | texture.filter.min = min 169 | texture.filter.mag = mag 170 | texture.setTextureFilter() 171 | return nil 172 | } 173 | 174 | // setTextureFilter will set the texture filter on the actual gl texture. It will 175 | // not reach this state if the filter is not valid. 176 | func (texture *Texture) setTextureFilter() { 177 | var gmin, gmag uint32 178 | 179 | bindTexture(texture.getHandle()) 180 | 181 | if texture.filter.mipmap == FilterNone { 182 | if texture.filter.min == FilterNearest { 183 | gmin = gl.NEAREST 184 | } else { // f.min == FilterLinear 185 | gmin = gl.LINEAR 186 | } 187 | } else { 188 | if texture.filter.min == FilterNearest && texture.filter.mipmap == FilterNearest { 189 | gmin = gl.NEAREST_MIPMAP_NEAREST 190 | } else if texture.filter.min == FilterNearest && texture.filter.mipmap == FilterLinear { 191 | gmin = gl.NEAREST_MIPMAP_LINEAR 192 | } else if texture.filter.min == FilterLinear && texture.filter.mipmap == FilterNearest { 193 | gmin = gl.LINEAR_MIPMAP_NEAREST 194 | } else if texture.filter.min == FilterLinear && texture.filter.mipmap == FilterLinear { 195 | gmin = gl.LINEAR_MIPMAP_LINEAR 196 | } else { 197 | gmin = gl.LINEAR 198 | } 199 | } 200 | 201 | switch texture.filter.mag { 202 | case FilterNearest: 203 | gmag = gl.NEAREST 204 | case FilterLinear: 205 | fallthrough 206 | default: 207 | gmag = gl.LINEAR 208 | } 209 | 210 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, int(gmin)) 211 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, int(gmag)) 212 | //gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAX_ANISOTROPY_EXT, texture.filter.anisotropy) 213 | } 214 | 215 | // GetFilter will return the filter set on this texture. 216 | func (texture *Texture) GetFilter() Filter { 217 | return texture.filter 218 | } 219 | 220 | // validateFilter will the the near and far filters and makes sure that it is possible 221 | func (texture *Texture) validateFilter() bool { 222 | if !texture.mipmaps && texture.filter.mipmap != FilterNone { 223 | return false 224 | } 225 | 226 | if texture.filter.mag != FilterLinear && texture.filter.mag != FilterNearest { 227 | return false 228 | } 229 | 230 | if texture.filter.min != FilterLinear && texture.filter.min != FilterNearest { 231 | return false 232 | } 233 | 234 | if texture.filter.mipmap != FilterLinear && texture.filter.mipmap != FilterNearest && texture.filter.mipmap != FilterNone { 235 | return false 236 | } 237 | 238 | return true 239 | } 240 | 241 | // loadVolatile satisfies the volatile interface, so that it can be unloaded 242 | func (texture *Texture) loadVolatile() bool { 243 | return false 244 | } 245 | 246 | // unloadVolatile release the texture data 247 | func (texture *Texture) unloadVolatile() { 248 | if texture != nil { 249 | return 250 | } 251 | deleteTexture(texture.textureID) 252 | texture = nil 253 | } 254 | 255 | // drawv will take in verticies from the public draw calls and draw the texture 256 | // with the verticies and the model matrix 257 | func (texture *Texture) drawv(model *mgl32.Mat4, vertices []float32) { 258 | prepareDraw(model) 259 | bindTexture(texture.getHandle()) 260 | useVertexAttribArrays(shaderPos, shaderTexCoord) 261 | 262 | buffer := newVertexBuffer(len(vertices), vertices, UsageStatic) 263 | buffer.bind() 264 | defer buffer.unbind() 265 | 266 | gl.VertexAttribPointer(shaderPos, 2, gl.FLOAT, false, 4*4, 0) 267 | gl.VertexAttribPointer(shaderTexCoord, 2, gl.FLOAT, false, 4*4, 2*4) 268 | 269 | gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) 270 | } 271 | 272 | // Draw satisfies the Drawable interface. Inputs are as follows 273 | // x, y, r, sx, sy, ox, oy, kx, ky 274 | // x, y are position 275 | // r is rotation 276 | // sx, sy is the scale, if sy is not given sy will equal sx 277 | // ox, oy are offset 278 | // kx, ky are the shear. If ky is not given ky will equal kx 279 | func (texture *Texture) Draw(args ...float32) { 280 | texture.drawv(generateModelMatFromArgs(args), texture.vertices) 281 | } 282 | 283 | // Drawq satisfies the QuadDrawable interface. 284 | // Inputs are as follows 285 | // quad is the quad to crop the texture 286 | // x, y, r, sx, sy, ox, oy, kx, ky 287 | // x, y are position 288 | // r is rotation 289 | // sx, sy is the scale, if sy is not given sy will equal sx 290 | // ox, oy are offset 291 | // kx, ky are the shear. If ky is not given ky will equal kx 292 | func (texture *Texture) Drawq(quad *Quad, args ...float32) { 293 | texture.drawv(generateModelMatFromArgs(args), quad.getVertices()) 294 | } 295 | -------------------------------------------------------------------------------- /gfx/uniform.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "github.com/goxjs/gl" 4 | 5 | // UniformType is the data type of a uniform 6 | type UniformType int 7 | 8 | //uniform types for shaders 9 | const ( 10 | UniformFloat UniformType = iota 11 | UniformInt 12 | UniformBool 13 | UniformSampler 14 | UniformUnknown 15 | UniformBase UniformType = iota 16 | UniformVec 17 | UniformMat 18 | ) 19 | 20 | // uniform represents a uniform in the shaders 21 | type uniform struct { 22 | Location gl.Uniform 23 | Type gl.Enum 24 | BaseType UniformType 25 | SecondType UniformType 26 | Count int 27 | TypeSize int 28 | Name string 29 | } 30 | 31 | func (u *uniform) CalculateTypeInfo() { 32 | u.BaseType = u.getBaseType() 33 | u.SecondType = u.getSecondType() 34 | u.TypeSize = u.getTypeSize() 35 | } 36 | 37 | func (u *uniform) getTypeSize() int { 38 | switch u.Type { 39 | case gl.INT, gl.FLOAT, gl.BOOL, gl.SAMPLER_2D, gl.SAMPLER_CUBE: 40 | return 1 41 | case gl.INT_VEC2, gl.FLOAT_VEC2, gl.FLOAT_MAT2, gl.BOOL_VEC2: 42 | return 2 43 | case gl.INT_VEC3, gl.FLOAT_VEC3, gl.FLOAT_MAT3, gl.BOOL_VEC3: 44 | return 3 45 | case gl.INT_VEC4, gl.FLOAT_VEC4, gl.FLOAT_MAT4, gl.BOOL_VEC4: 46 | return 4 47 | } 48 | return 1 49 | } 50 | 51 | func (u *uniform) getBaseType() UniformType { 52 | switch u.Type { 53 | case gl.INT, gl.INT_VEC2, gl.INT_VEC3, gl.INT_VEC4: 54 | return UniformInt 55 | case gl.FLOAT, gl.FLOAT_VEC2, gl.FLOAT_VEC3, 56 | gl.FLOAT_VEC4, gl.FLOAT_MAT2, gl.FLOAT_MAT3, gl.FLOAT_MAT4: 57 | return UniformFloat 58 | case gl.BOOL, gl.BOOL_VEC2, gl.BOOL_VEC3, gl.BOOL_VEC4: 59 | return UniformBool 60 | case gl.SAMPLER_2D, gl.SAMPLER_CUBE: 61 | return UniformSampler 62 | } 63 | return UniformUnknown 64 | } 65 | 66 | func (u uniform) getSecondType() UniformType { 67 | switch u.Type { 68 | case gl.INT_VEC2, gl.INT_VEC3, gl.INT_VEC4, gl.FLOAT_VEC2, 69 | gl.FLOAT_VEC3, gl.FLOAT_VEC4, gl.BOOL_VEC2, gl.BOOL_VEC3, gl.BOOL_VEC4: 70 | return UniformVec 71 | case gl.FLOAT_MAT2, gl.FLOAT_MAT3, gl.FLOAT_MAT4: 72 | return UniformMat 73 | } 74 | return UniformBase 75 | } 76 | 77 | func translateUniformBaseType(t UniformType) string { 78 | switch t { 79 | case UniformFloat: 80 | return "float" 81 | case UniformInt: 82 | return "int" 83 | case UniformBool: 84 | return "bool" 85 | case UniformSampler: 86 | return "sampler" 87 | } 88 | return "unknown" 89 | } 90 | -------------------------------------------------------------------------------- /gfx/vertex_buffer.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/goxjs/gl" 7 | ) 8 | 9 | type vertexBuffer struct { 10 | isBound bool // Whether the buffer is currently bound. 11 | usage Usage // Usage hint. GL_[DYNAMIC, STATIC, STREAM]_DRAW. 12 | vbo gl.Buffer // The VBO identifier. Assigned by OpenGL. 13 | data []float32 // A pointer to mapped memory. 14 | modifiedOffset int 15 | modifiedSize int 16 | } 17 | 18 | func newVertexBuffer(size int, data []float32, usage Usage) *vertexBuffer { 19 | newBuffer := &vertexBuffer{ 20 | usage: usage, 21 | data: make([]float32, size), 22 | } 23 | if len(data) > 0 { 24 | copy(newBuffer.data, data[:size]) 25 | } 26 | registerVolatile(newBuffer) 27 | return newBuffer 28 | } 29 | 30 | func (buffer *vertexBuffer) bufferStatic() { 31 | if buffer.modifiedSize == 0 { 32 | return 33 | } 34 | // Upload the mapped data to the buffer. 35 | gl.BufferSubData(gl.ARRAY_BUFFER, buffer.modifiedOffset*4, f32Bytes(buffer.data)) 36 | } 37 | 38 | func (buffer *vertexBuffer) bufferStream() { 39 | gl.BufferData(gl.ARRAY_BUFFER, f32Bytes(buffer.data), gl.Enum(buffer.usage)) 40 | } 41 | 42 | func (buffer *vertexBuffer) bufferData() { 43 | if buffer.modifiedSize != 0 { //if there is no modified size might as well do the whole buffer 44 | buffer.modifiedOffset = int(math.Min(float64(buffer.modifiedOffset), float64(len(buffer.data)-1))) 45 | buffer.modifiedSize = int(math.Min(float64(buffer.modifiedSize), float64(len(buffer.data)-buffer.modifiedOffset))) 46 | } else { 47 | buffer.modifiedOffset = 0 48 | buffer.modifiedSize = len(buffer.data) 49 | } 50 | 51 | buffer.bind() 52 | if buffer.modifiedSize > 0 { 53 | switch buffer.usage { 54 | case UsageStatic: 55 | buffer.bufferStatic() 56 | case UsageStream: 57 | buffer.bufferStream() 58 | case UsageDynamic: 59 | // It's probably more efficient to treat it like a streaming buffer if 60 | // at least a third of its contents have been modified during the map(). 61 | if buffer.modifiedSize >= len(buffer.data)/3 { 62 | buffer.bufferStream() 63 | } else { 64 | buffer.bufferStatic() 65 | } 66 | } 67 | } 68 | buffer.modifiedOffset = 0 69 | buffer.modifiedSize = 0 70 | } 71 | 72 | func (buffer *vertexBuffer) bind() { 73 | gl.BindBuffer(gl.ARRAY_BUFFER, buffer.vbo) 74 | buffer.isBound = true 75 | } 76 | 77 | func (buffer *vertexBuffer) unbind() { 78 | if buffer.isBound { 79 | gl.BindBuffer(gl.ARRAY_BUFFER, gl.Buffer{}) 80 | } 81 | buffer.isBound = false 82 | } 83 | 84 | func (buffer *vertexBuffer) fill(offset int, data []float32) { 85 | copy(buffer.data[offset:], data[:]) 86 | if !buffer.vbo.Valid() { 87 | return 88 | } 89 | // We're being conservative right now by internally marking the whole range 90 | // from the start of section a to the end of section b as modified if both 91 | // a and b are marked as modified. 92 | oldRangeEnd := buffer.modifiedOffset + buffer.modifiedSize 93 | buffer.modifiedOffset = int(math.Min(float64(buffer.modifiedOffset), float64(offset))) 94 | newRangeEnd := int(math.Max(float64(offset+len(data)), float64(oldRangeEnd))) 95 | buffer.modifiedSize = newRangeEnd - buffer.modifiedOffset 96 | buffer.bufferData() 97 | } 98 | 99 | func (buffer *vertexBuffer) loadVolatile() bool { 100 | buffer.vbo = gl.CreateBuffer() 101 | buffer.bind() 102 | defer buffer.unbind() 103 | gl.BufferData(gl.ARRAY_BUFFER, f32Bytes(buffer.data), gl.Enum(buffer.usage)) 104 | return true 105 | } 106 | 107 | func (buffer *vertexBuffer) unloadVolatile() { 108 | gl.DeleteBuffer(buffer.vbo) 109 | } 110 | -------------------------------------------------------------------------------- /gfx/volatile.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "runtime" 4 | 5 | // volatile is an interface for all items that are loaded into the gl context. 6 | // The volatile wrapper allows you to instantiate any gl items before the context 7 | // exist, and they will be completed after the context exists. 8 | type volatile interface { 9 | loadVolatile() bool 10 | unloadVolatile() 11 | } 12 | 13 | var ( 14 | allVolatile = []volatile{} 15 | unloadcallQueue = make(chan func(), 20) 16 | ) 17 | 18 | // registerVolatile will put the volatile in the current object group and call 19 | // loadVolatile if the gl context is initialized. 20 | func registerVolatile(newVolatile volatile) { 21 | if !glState.initialized { 22 | allVolatile = append(allVolatile, newVolatile) 23 | return 24 | } 25 | loadVolatile(newVolatile) 26 | } 27 | 28 | func loadAllVolatile() { 29 | for _, vol := range allVolatile { 30 | loadVolatile(vol) 31 | } 32 | allVolatile = []volatile{} 33 | } 34 | 35 | func loadVolatile(newVolatile volatile) { 36 | newVolatile.loadVolatile() 37 | runtime.SetFinalizer(newVolatile, func(vol volatile) { 38 | unloadcallQueue <- vol.unloadVolatile 39 | }) 40 | } 41 | 42 | // used to make sure that the unload functions are called on the main thread 43 | // and are called after each game loop. This is a bit more overhead but it 44 | // make sure that the users don't need to release resources explicitly like 45 | // some old ass technology 46 | func cleanupVolatile() { 47 | for { 48 | select { 49 | case f := <-unloadcallQueue: 50 | f() 51 | default: 52 | return 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /gfx/wrap/font.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "github.com/yuin/gopher-lua" 5 | 6 | "github.com/tanema/amore/gfx" 7 | ) 8 | 9 | func toFont(ls *lua.LState, offset int) *gfx.Font { 10 | img := ls.CheckUserData(offset) 11 | if v, ok := img.Value.(*gfx.Font); ok { 12 | return v 13 | } 14 | ls.ArgError(offset, "font expected") 15 | return nil 16 | } 17 | 18 | func gfxNewFont(ls *lua.LState) int { 19 | newFont, err := gfx.NewFont(toString(ls, 1), toFloat(ls, 2)) 20 | if err == nil { 21 | return returnUD(ls, "Font", newFont) 22 | } 23 | ls.Push(lua.LNil) 24 | return 1 25 | } 26 | 27 | func gfxFontGetWidth(ls *lua.LState) int { 28 | font := toFont(ls, 1) 29 | ls.Push(lua.LNumber(font.GetWidth(toString(ls, 2)))) 30 | return 1 31 | } 32 | 33 | func gfxFontGetHeight(ls *lua.LState) int { 34 | font := toFont(ls, 1) 35 | ls.Push(lua.LNumber(font.GetHeight())) 36 | return 1 37 | } 38 | 39 | func gfxFontSetFallback(ls *lua.LState) int { 40 | font := toFont(ls, 1) 41 | start := 2 42 | fallbacks := []*gfx.Font{} 43 | for x := ls.Get(start); x != nil; start++ { 44 | img := ls.CheckUserData(start) 45 | if v, ok := img.Value.(*gfx.Font); ok { 46 | fallbacks = append(fallbacks, v) 47 | } 48 | ls.ArgError(start, "font expected") 49 | } 50 | font.SetFallbacks(fallbacks...) 51 | return 0 52 | } 53 | 54 | func gfxFontGetWrap(ls *lua.LState) int { 55 | font := toFont(ls, 1) 56 | wrap, strs := font.GetWrap(toString(ls, 1), toFloat(ls, 2)) 57 | ls.Push(lua.LNumber(wrap)) 58 | table := ls.NewTable() 59 | for _, str := range strs { 60 | table.Append(lua.LString(str)) 61 | } 62 | ls.Push(table) 63 | return 2 64 | } 65 | -------------------------------------------------------------------------------- /gfx/wrap/graphics.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "github.com/yuin/gopher-lua" 5 | 6 | "github.com/tanema/amore/gfx" 7 | ) 8 | 9 | func gfxCirle(ls *lua.LState) int { 10 | gfx.Circle(extractMode(ls, 1), toFloat(ls, 2), toFloat(ls, 3), toFloat(ls, 4), toIntD(ls, 5, 30)) 11 | return 0 12 | } 13 | 14 | func gfxArc(ls *lua.LState) int { 15 | gfx.Arc(extractMode(ls, 1), toFloat(ls, 2), toFloat(ls, 3), toFloat(ls, 4), toFloat(ls, 5), toFloat(ls, 6), toIntD(ls, 6, 30)) 16 | return 0 17 | } 18 | 19 | func gfxEllipse(ls *lua.LState) int { 20 | gfx.Ellipse(extractMode(ls, 1), toFloat(ls, 2), toFloat(ls, 3), toFloat(ls, 4), toFloat(ls, 5), toIntD(ls, 6, 30)) 21 | return 0 22 | } 23 | 24 | func gfxPoints(ls *lua.LState) int { 25 | gfx.Points(extractCoords(ls, 1)) 26 | return 0 27 | } 28 | 29 | func gfxLine(ls *lua.LState) int { 30 | gfx.PolyLine(extractCoords(ls, 1)) 31 | return 0 32 | } 33 | 34 | func gfxRectangle(ls *lua.LState) int { 35 | gfx.Rect(extractMode(ls, 1), toFloat(ls, 2), toFloat(ls, 3), toFloat(ls, 4), toFloat(ls, 5)) 36 | return 0 37 | } 38 | 39 | func gfxPolygon(ls *lua.LState) int { 40 | gfx.Polygon(extractMode(ls, 1), extractCoords(ls, 2)) 41 | return 0 42 | } 43 | 44 | func gfxScreenShot(ls *lua.LState) int { 45 | return returnUD(ls, "Image", gfx.NewScreenshot()) 46 | } 47 | 48 | func gfxGetViewport(ls *lua.LState) int { 49 | for _, x := range gfx.GetViewport() { 50 | ls.Push(lua.LNumber(x)) 51 | } 52 | return 4 53 | } 54 | 55 | func gfxSetViewport(ls *lua.LState) int { 56 | viewport := extractCoords(ls, 1) 57 | if len(viewport) == 0 { 58 | w, h := gfx.GetDimensions() 59 | gfx.SetViewportSize(int32(w), int32(h)) 60 | } else if len(viewport) == 2 { 61 | gfx.SetViewportSize(int32(viewport[0]), int32(viewport[1])) 62 | } else if len(viewport) == 4 { 63 | gfx.SetViewport(int32(viewport[0]), int32(viewport[1]), int32(viewport[2]), int32(viewport[3])) 64 | } else { 65 | ls.ArgError(1, "either provide (x, y, w, h) or (w, h) nothing at all to reset the viewport") 66 | } 67 | return 0 68 | } 69 | 70 | func gfxGetWidth(ls *lua.LState) int { 71 | ls.Push(lua.LNumber(gfx.GetWidth())) 72 | return 1 73 | } 74 | 75 | func gfxGetHeight(ls *lua.LState) int { 76 | ls.Push(lua.LNumber(gfx.GetHeight())) 77 | return 1 78 | } 79 | 80 | func gfxGetDimensions(ls *lua.LState) int { 81 | w, h := gfx.GetDimensions() 82 | ls.Push(lua.LNumber(w)) 83 | ls.Push(lua.LNumber(h)) 84 | return 2 85 | } 86 | 87 | func gfxOrigin(ls *lua.LState) int { 88 | gfx.Origin() 89 | return 0 90 | } 91 | 92 | func gfxTranslate(ls *lua.LState) int { 93 | x, y := toFloat(ls, 1), toFloat(ls, 2) 94 | gfx.Translate(x, y) 95 | return 0 96 | } 97 | 98 | func gfxRotate(ls *lua.LState) int { 99 | gfx.Rotate(toFloat(ls, 1)) 100 | return 0 101 | } 102 | 103 | func gfxScale(ls *lua.LState) int { 104 | sx := toFloat(ls, 1) 105 | sy := toFloatD(ls, 2, sx) 106 | gfx.Scale(sx, sy) 107 | return 0 108 | } 109 | 110 | func gfxShear(ls *lua.LState) int { 111 | kx := toFloat(ls, 1) 112 | ky := toFloatD(ls, 2, kx) 113 | gfx.Shear(kx, ky) 114 | return 0 115 | } 116 | 117 | func gfxPush(ls *lua.LState) int { 118 | gfx.Push() 119 | return 0 120 | } 121 | 122 | func gfxPop(ls *lua.LState) int { 123 | gfx.Pop() 124 | return 0 125 | } 126 | 127 | func gfxClear(ls *lua.LState) int { 128 | gfx.Clear(extractColor(ls, 1)) 129 | return 0 130 | } 131 | 132 | func gfxSetScissor(ls *lua.LState) int { 133 | args := extractFloatArray(ls, 1) 134 | if len(args) == 2 { 135 | gfx.SetScissor(0, 0, int32(args[0]), int32(args[1])) 136 | } else if len(args) == 4 { 137 | gfx.SetScissor(int32(args[0]), int32(args[1]), int32(args[2]), int32(args[3])) 138 | } else if len(args) == 0 { 139 | gfx.ClearScissor() 140 | } else { 141 | ls.ArgError(1, "either pass 2, 4 or no arguments") 142 | } 143 | return 0 144 | } 145 | 146 | func gfxGetScissor(ls *lua.LState) int { 147 | x, y, w, h := gfx.GetScissor() 148 | ls.Push(lua.LNumber(x)) 149 | ls.Push(lua.LNumber(y)) 150 | ls.Push(lua.LNumber(w)) 151 | ls.Push(lua.LNumber(h)) 152 | return 4 153 | } 154 | 155 | func gfxSetLineWidth(ls *lua.LState) int { 156 | gfx.SetLineWidth(toFloat(ls, 1)) 157 | return 0 158 | } 159 | 160 | func gfxSetLineJoin(ls *lua.LState) int { 161 | gfx.SetLineJoin(extractLineJoin(ls, 1)) 162 | return 0 163 | } 164 | 165 | func gfxGetLineWidth(ls *lua.LState) int { 166 | ls.Push(lua.LNumber(gfx.GetLineWidth())) 167 | return 1 168 | } 169 | 170 | func gfxGetLineJoin(ls *lua.LState) int { 171 | ls.Push(lua.LString(gfx.GetLineJoin())) 172 | return 1 173 | } 174 | 175 | func gfxSetPointSize(ls *lua.LState) int { 176 | gfx.SetPointSize(toFloat(ls, 1)) 177 | return 0 178 | } 179 | 180 | func gfxGetPointSize(ls *lua.LState) int { 181 | ls.Push(lua.LNumber(gfx.GetPointSize())) 182 | return 1 183 | } 184 | 185 | func gfxSetColor(ls *lua.LState) int { 186 | gfx.SetColor(extractColor(ls, 1)) 187 | return 0 188 | } 189 | 190 | func gfxSetBackgroundColor(ls *lua.LState) int { 191 | gfx.SetBackgroundColor(extractColor(ls, 1)) 192 | return 0 193 | } 194 | 195 | func gfxGetColor(ls *lua.LState) int { 196 | for _, x := range gfx.GetColor() { 197 | ls.Push(lua.LNumber(x)) 198 | } 199 | return 4 200 | } 201 | 202 | func gfxGetBackgroundColor(ls *lua.LState) int { 203 | for _, x := range gfx.GetBackgroundColor() { 204 | ls.Push(lua.LNumber(x)) 205 | } 206 | return 4 207 | } 208 | 209 | func gfxGetColorMask(ls *lua.LState) int { 210 | r, g, b, a := gfx.GetColorMask() 211 | ls.Push(lua.LBool(r)) 212 | ls.Push(lua.LBool(g)) 213 | ls.Push(lua.LBool(b)) 214 | ls.Push(lua.LBool(a)) 215 | return 4 216 | } 217 | 218 | func gfxSetColorMask(ls *lua.LState) int { 219 | args := []bool{} 220 | offset := 1 221 | for x := ls.Get(offset); x != nil; offset++ { 222 | val := ls.Get(offset) 223 | if lv, ok := val.(lua.LBool); ok { 224 | args = append(args, bool(lv)) 225 | } else if val.Type() == lua.LTNil { 226 | break 227 | } else { 228 | ls.ArgError(offset, "argument wrong type, should be boolean") 229 | } 230 | } 231 | 232 | if len(args) == 4 { 233 | gfx.SetColorMask(args[0], args[1], args[2], args[3]) 234 | } else if len(args) == 3 { 235 | gfx.SetColorMask(args[0], args[1], args[2], true) 236 | } else if len(args) == 1 { 237 | gfx.SetColorMask(args[0], args[0], args[0], args[0]) 238 | } else if len(args) == 0 { 239 | gfx.ClearStencilTest() 240 | } else { 241 | ls.ArgError(offset, "invalid argument count") 242 | } 243 | 244 | return 0 245 | } 246 | 247 | func gfxSetFont(ls *lua.LState) int { 248 | gfx.SetFont(toFont(ls, 1)) 249 | return 0 250 | } 251 | 252 | func gfxGetFont(ls *lua.LState) int { 253 | return returnUD(ls, "Font", gfx.GetFont()) 254 | } 255 | 256 | func gfxSetBlendMode(ls *lua.LState) int { 257 | gfx.SetBlendMode(extractBlendmode(ls, 1)) 258 | return 0 259 | } 260 | 261 | func gfxGetCanvas(ls *lua.LState) int { 262 | return returnUD(ls, "Canvas", gfx.GetCanvas()) 263 | } 264 | 265 | func gfxSetCanvas(ls *lua.LState) int { 266 | gfx.SetCanvas(toCanvas(ls, 1)) 267 | return 0 268 | } 269 | 270 | func gfxGetStencilTest(ls *lua.LState) int { 271 | mode, value := gfx.GetStencilTest() 272 | ls.Push(lua.LString(fromCompareMode(mode))) 273 | ls.Push(lua.LNumber(value)) 274 | return 2 275 | } 276 | 277 | func gfxSetStencilTest(ls *lua.LState) int { 278 | wrapStr := toStringD(ls, 1, "") 279 | if wrapStr == "" { 280 | gfx.ClearStencilTest() 281 | return 0 282 | } 283 | gfx.SetStencilTest(toCompareMode(wrapStr, 1), int32(toInt(ls, 2))) 284 | return 0 285 | } 286 | 287 | func gfxStencil(ls *lua.LState) int { 288 | fn := ls.ToFunction(1) 289 | if fn == lua.LNil { 290 | ls.ArgError(1, "a function is required for a stencil") 291 | } 292 | fnWrap := func() { 293 | ls.CallByParam(lua.P{Fn: fn, Protect: true}) 294 | } 295 | gfx.Stencil(fnWrap, toStencilAction(toStringD(ls, 2, "replace"), 2), int32(toIntD(ls, 3, 1)), ls.ToBool(4)) 296 | return 0 297 | } 298 | 299 | func gfxSetShader(ls *lua.LState) int { 300 | gfx.SetShader(toShader(ls, 1)) 301 | return 0 302 | } 303 | -------------------------------------------------------------------------------- /gfx/wrap/quad.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "github.com/yuin/gopher-lua" 5 | 6 | "github.com/tanema/amore/gfx" 7 | ) 8 | 9 | func toQuad(ls *lua.LState, offset int) *gfx.Quad { 10 | img := ls.CheckUserData(offset) 11 | if v, ok := img.Value.(*gfx.Quad); ok { 12 | return v 13 | } 14 | ls.ArgError(offset, "quad expected") 15 | return nil 16 | } 17 | 18 | func gfxNewQuad(ls *lua.LState) int { 19 | offset := 1 20 | args := []int32{} 21 | for x := ls.Get(offset); x != nil; offset++ { 22 | val := ls.Get(offset) 23 | if lv, ok := val.(lua.LNumber); ok { 24 | args = append(args, int32(lv)) 25 | } else if val.Type() == lua.LTNil { 26 | break 27 | } else { 28 | ls.ArgError(offset, "argument wrong type, should be number") 29 | } 30 | } 31 | if offset < 6 { 32 | ls.ArgError(len(args)-1, "not enough arguments") 33 | } 34 | return returnUD(ls, "Quad", gfx.NewQuad(args[0], args[1], args[2], args[3], args[4], args[5])) 35 | } 36 | 37 | func gfxQuadGetWidth(ls *lua.LState) int { 38 | ls.Push(lua.LNumber(toQuad(ls, 1).GetWidth())) 39 | return 1 40 | } 41 | 42 | func gfxQuadGetHeight(ls *lua.LState) int { 43 | ls.Push(lua.LNumber(toQuad(ls, 1).GetHeight())) 44 | return 1 45 | } 46 | 47 | func gfxQuadGetViewport(ls *lua.LState) int { 48 | x, y, w, h := toQuad(ls, 1).GetViewport() 49 | ls.Push(lua.LNumber(x)) 50 | ls.Push(lua.LNumber(y)) 51 | ls.Push(lua.LNumber(w)) 52 | ls.Push(lua.LNumber(h)) 53 | return 4 54 | } 55 | 56 | func gfxQuadSetViewport(ls *lua.LState) int { 57 | toQuad(ls, 1).SetViewport(int32(toInt(ls, 2)), int32(toInt(ls, 3)), int32(toInt(ls, 4)), int32(toInt(ls, 5))) 58 | return 0 59 | } 60 | -------------------------------------------------------------------------------- /gfx/wrap/shader.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yuin/gopher-lua" 7 | 8 | "github.com/tanema/amore/gfx" 9 | ) 10 | 11 | func toShader(ls *lua.LState, offset int) *gfx.Shader { 12 | img := ls.CheckUserData(offset) 13 | if v, ok := img.Value.(*gfx.Shader); ok { 14 | return v 15 | } 16 | ls.ArgError(offset, "shader expected") 17 | return nil 18 | } 19 | 20 | func gfxNewShader(ls *lua.LState) int { 21 | return returnUD(ls, "Shader", gfx.NewShader(toString(ls, 1), toStringD(ls, 2, ""))) 22 | } 23 | 24 | func gfxShaderSend(ls *lua.LState) int { 25 | program := toShader(ls, 1) 26 | name := toString(ls, 2) 27 | uniformType, found := program.GetUniformType(name) 28 | if !found { 29 | ls.ArgError(2, fmt.Sprintf("unknown uniform with name [%s]", name)) 30 | } 31 | switch uniformType { 32 | case gfx.UniformFloat: 33 | program.SendFloat(name, extractFloatArray(ls, 3)...) 34 | case gfx.UniformInt: 35 | program.SendInt(name, extractIntArray(ls, 3)...) 36 | case gfx.UniformSampler: 37 | program.SendTexture(name, toTexture(ls, 3)) 38 | } 39 | return 0 40 | } 41 | -------------------------------------------------------------------------------- /gfx/wrap/sprite_batch.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "github.com/yuin/gopher-lua" 5 | 6 | "github.com/tanema/amore/gfx" 7 | ) 8 | 9 | func toSpriteBatch(ls *lua.LState, offset int) *gfx.SpriteBatch { 10 | img := ls.CheckUserData(offset) 11 | if v, ok := img.Value.(*gfx.SpriteBatch); ok { 12 | return v 13 | } 14 | ls.ArgError(offset, "sprite batch expected") 15 | return nil 16 | } 17 | 18 | func gfxNewSpriteBatch(ls *lua.LState) int { 19 | return returnUD( 20 | ls, 21 | "SpriteBatch", 22 | gfx.NewSpriteBatch(toTexture(ls, 1), toIntD(ls, 2, 1000), toUsage(ls, 3)), 23 | ) 24 | } 25 | 26 | func gfxSpriteBatchAdd(ls *lua.LState) int { 27 | toSpriteBatch(ls, 1).Add(extractFloatArray(ls, 2)...) 28 | return 0 29 | } 30 | 31 | func gfxSpriteBatchAddq(ls *lua.LState) int { 32 | toSpriteBatch(ls, 1).Addq(toQuad(ls, 2), extractFloatArray(ls, 3)...) 33 | return 0 34 | } 35 | 36 | func gfxSpriteBatchSet(ls *lua.LState) int { 37 | toSpriteBatch(ls, 1).Set(toInt(ls, 2), extractFloatArray(ls, 3)...) 38 | return 0 39 | } 40 | 41 | func gfxSpriteBatchSetq(ls *lua.LState) int { 42 | toSpriteBatch(ls, 1).Setq(toInt(ls, 2), toQuad(ls, 3), extractFloatArray(ls, 4)...) 43 | return 0 44 | } 45 | 46 | func gfxSpriteBatchClear(ls *lua.LState) int { 47 | toSpriteBatch(ls, 1).Clear() 48 | return 0 49 | } 50 | 51 | func gfxSpriteBatchSetTexture(ls *lua.LState) int { 52 | toSpriteBatch(ls, 1).SetTexture(toTexture(ls, 2)) 53 | return 0 54 | } 55 | 56 | func gfxSpriteBatchGetTexture(ls *lua.LState) int { 57 | return returnUD(ls, "Image", toSpriteBatch(ls, 1).GetTexture()) 58 | } 59 | 60 | func gfxSpriteBatchSetColor(ls *lua.LState) int { 61 | batch := toSpriteBatch(ls, 1) 62 | if len(extractFloatArray(ls, 2)) == 0 { 63 | batch.ClearColor() 64 | } else { 65 | r, g, b, a := extractColor(ls, 2) 66 | batch.SetColor(r, g, b, a) 67 | } 68 | return 0 69 | } 70 | 71 | func gfxSpriteBatchGetColor(ls *lua.LState) int { 72 | batch := toSpriteBatch(ls, 1) 73 | for _, x := range batch.GetColor() { 74 | ls.Push(lua.LNumber(x)) 75 | } 76 | return 4 77 | } 78 | 79 | func gfxSpriteBatchGetCount(ls *lua.LState) int { 80 | ls.Push(lua.LNumber(toSpriteBatch(ls, 1).GetCount())) 81 | return 1 82 | } 83 | 84 | func gfxSpriteBatchSetBufferSize(ls *lua.LState) int { 85 | toSpriteBatch(ls, 1).SetBufferSize(toInt(ls, 2)) 86 | return 0 87 | } 88 | 89 | func gfxSpriteBatchGetBufferSize(ls *lua.LState) int { 90 | ls.Push(lua.LNumber(toSpriteBatch(ls, 1).GetBufferSize())) 91 | return 1 92 | } 93 | 94 | func gfxSpriteBatchSetDrawRange(ls *lua.LState) int { 95 | toSpriteBatch(ls, 1).SetDrawRange(toIntD(ls, 2, -1), toIntD(ls, 3, -1)) 96 | return 0 97 | } 98 | 99 | func gfxSpriteBatchGetDrawRange(ls *lua.LState) int { 100 | min, max := toSpriteBatch(ls, 1).GetDrawRange() 101 | ls.Push(lua.LNumber(min)) 102 | ls.Push(lua.LNumber(max)) 103 | return 2 104 | } 105 | 106 | func gfxSpriteBatchDraw(ls *lua.LState) int { 107 | toSpriteBatch(ls, 1).Draw(extractFloatArray(ls, 2)...) 108 | return 0 109 | } 110 | -------------------------------------------------------------------------------- /gfx/wrap/text.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "github.com/yuin/gopher-lua" 5 | 6 | "github.com/tanema/amore/gfx" 7 | ) 8 | 9 | func extractPrintable(ls *lua.LState, offset int) ([]string, [][]float32) { 10 | strs := []string{} 11 | colors := [][]float32{} 12 | 13 | val1 := ls.Get(offset) 14 | if val1.Type() == lua.LTTable { 15 | table := val1.(*lua.LTable) 16 | rawstrs, strok := (table.RawGetInt(1)).(*lua.LTable) 17 | rawclrs, clrok := table.RawGetInt(2).(*lua.LTable) 18 | if !strok || !clrok { 19 | ls.ArgError(offset, "unexpected argument type") 20 | } 21 | 22 | rawstrs.ForEach(func(index lua.LValue, str lua.LValue) { 23 | strs = append(strs, str.String()) 24 | }) 25 | 26 | rawclrs.ForEach(func(index lua.LValue, color lua.LValue) { 27 | setColor := []float32{} 28 | (color.(*lua.LTable)).ForEach(func(index lua.LValue, color lua.LValue) { 29 | setColor = append(setColor, float32(color.(lua.LNumber))) 30 | }) 31 | if len(setColor) == 3 { 32 | setColor = append(setColor, 1) 33 | } else if len(setColor) < 4 { 34 | ls.ArgError(offset, "not enough values for a color") 35 | } 36 | colors = append(colors, setColor) 37 | }) 38 | } else if val1.Type() == lua.LTString { 39 | return []string{val1.String()}, [][]float32{gfx.GetColor()} 40 | } else { 41 | ls.ArgError(offset, "unexpected argument type") 42 | } 43 | 44 | return strs, colors 45 | } 46 | 47 | func toText(ls *lua.LState, offset int) *gfx.Text { 48 | img := ls.CheckUserData(offset) 49 | if v, ok := img.Value.(*gfx.Text); ok { 50 | return v 51 | } 52 | ls.ArgError(offset, "text expected") 53 | return nil 54 | } 55 | 56 | func gfxPrint(ls *lua.LState) int { 57 | str, clrs := extractPrintable(ls, 1) 58 | args := extractFloatArray(ls, 2) 59 | gfx.Print(str, clrs, args...) 60 | return 0 61 | } 62 | 63 | func gfxPrintf(ls *lua.LState) int { 64 | str, clrs := extractPrintable(ls, 1) 65 | wrap := toFloat(ls, 2) 66 | align := toString(ls, 3) 67 | args := extractFloatArray(ls, 4) 68 | gfx.Printf(str, clrs, wrap, align, args...) 69 | return 0 70 | } 71 | 72 | func gfxNewText(ls *lua.LState) int { 73 | str, clrs := extractPrintable(ls, 2) 74 | text := gfx.NewText(toFont(ls, 1), str, clrs, toFloatD(ls, 3, -1), toStringD(ls, 4, "left")) 75 | return returnUD(ls, "Text", text) 76 | } 77 | 78 | func gfxTextDraw(ls *lua.LState) int { 79 | txt := toText(ls, 1) 80 | txt.Draw(extractFloatArray(ls, 2)...) 81 | return 0 82 | } 83 | 84 | func gfxTextSet(ls *lua.LState) int { 85 | txt := toText(ls, 1) 86 | str, clrs := extractPrintable(ls, 2) 87 | txt.Set(str, clrs) 88 | return 0 89 | } 90 | 91 | func gfxTextGetWidth(ls *lua.LState) int { 92 | txt := toText(ls, 1) 93 | ls.Push(lua.LNumber(txt.GetWidth())) 94 | return 1 95 | } 96 | 97 | func gfxTextGetHeight(ls *lua.LState) int { 98 | txt := toText(ls, 1) 99 | ls.Push(lua.LNumber(txt.GetHeight())) 100 | return 1 101 | } 102 | 103 | func gfxTextGetDimensions(ls *lua.LState) int { 104 | txt := toText(ls, 1) 105 | w, h := txt.GetDimensions() 106 | ls.Push(lua.LNumber(w)) 107 | ls.Push(lua.LNumber(h)) 108 | return 2 109 | } 110 | 111 | func gfxTextGetFont(ls *lua.LState) int { 112 | return returnUD(ls, "Font", toText(ls, 1).GetFont()) 113 | } 114 | 115 | func gfxTextSetFont(ls *lua.LState) int { 116 | txt := toText(ls, 1) 117 | txt.SetFont(toFont(ls, 2)) 118 | return 0 119 | } 120 | -------------------------------------------------------------------------------- /gfx/wrap/texture.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "github.com/yuin/gopher-lua" 5 | 6 | "github.com/tanema/amore/gfx" 7 | ) 8 | 9 | const luaImageType = "Image" 10 | 11 | func toImage(ls *lua.LState, offset int) *gfx.Image { 12 | img := ls.CheckUserData(offset) 13 | if v, ok := img.Value.(*gfx.Image); ok { 14 | if v.Texture == nil { 15 | ls.ArgError(offset, "image not loaded") 16 | } 17 | return v 18 | } 19 | ls.ArgError(offset, "image expected") 20 | return nil 21 | } 22 | 23 | func toCanvas(ls *lua.LState, offset int) *gfx.Canvas { 24 | canvas := ls.CheckUserData(offset) 25 | if v, ok := canvas.Value.(*gfx.Canvas); ok { 26 | return v 27 | } 28 | ls.ArgError(offset, "canvas expected") 29 | return nil 30 | } 31 | 32 | func toTexture(ls *lua.LState, offset int) *gfx.Texture { 33 | text := ls.CheckUserData(offset) 34 | if v, ok := text.Value.(*gfx.Canvas); ok { 35 | return v.Texture 36 | } else if v, ok := text.Value.(*gfx.Image); ok { 37 | return v.Texture 38 | } 39 | ls.ArgError(offset, "texture expected") 40 | return nil 41 | } 42 | 43 | func gfxNewImage(ls *lua.LState) int { 44 | return returnUD(ls, "Image", gfx.NewImage(toString(ls, 1), ls.ToBool(2))) 45 | } 46 | 47 | func gfxNewCanvas(ls *lua.LState) int { 48 | w, h := gfx.GetDimensions() 49 | cw, ch := toIntD(ls, 1, int(w)), toIntD(ls, 2, int(h)) 50 | return returnUD(ls, "Canvas", gfx.NewCanvas(int32(cw), int32(ch))) 51 | } 52 | 53 | func gfxCanvasNewImage(ls *lua.LState) int { 54 | canvas := toCanvas(ls, 1) 55 | cw, ch := canvas.GetDimensions() 56 | x, y, w, h := toIntD(ls, 2, 0), toIntD(ls, 3, 0), toIntD(ls, 4, int(cw)), toIntD(ls, 5, int(ch)) 57 | img, err := canvas.NewImageData(int32(x), int32(y), int32(w), int32(h)) 58 | if err == nil { 59 | return returnUD(ls, "Image", img) 60 | } 61 | ls.Push(lua.LNil) 62 | return 1 63 | } 64 | 65 | func gfxTextureDraw(ls *lua.LState) int { 66 | toTexture(ls, 1).Draw(extractFloatArray(ls, 2)...) 67 | return 0 68 | } 69 | 70 | func gfxTextureDrawq(ls *lua.LState) int { 71 | toTexture(ls, 1).Drawq(toQuad(ls, 2), extractFloatArray(ls, 3)...) 72 | return 0 73 | } 74 | 75 | func gfxTextureSetWrap(ls *lua.LState) int { 76 | toTexture(ls, 1).SetWrap(toWrap(ls, 2), toWrap(ls, 3)) 77 | return 0 78 | } 79 | 80 | func gfxTextureSetFilter(ls *lua.LState) int { 81 | toTexture(ls, 1).SetFilter(toFilter(ls, 2), toFilter(ls, 3)) 82 | return 0 83 | } 84 | 85 | func gfxTextureGetWidth(ls *lua.LState) int { 86 | ls.Push(lua.LNumber(int(toTexture(ls, 1).Width))) 87 | return 1 88 | } 89 | 90 | func gfxTextureGetHeight(ls *lua.LState) int { 91 | ls.Push(lua.LNumber(int(toTexture(ls, 1).Height))) 92 | return 1 93 | } 94 | 95 | func gfxTextureGetDimensions(ls *lua.LState) int { 96 | text := toTexture(ls, 1) 97 | ls.Push(lua.LNumber(int(text.Width))) 98 | ls.Push(lua.LNumber(int(text.Height))) 99 | return 2 100 | } 101 | -------------------------------------------------------------------------------- /gfx/wrap/utils.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import ( 4 | "github.com/yuin/gopher-lua" 5 | 6 | "github.com/tanema/amore/gfx" 7 | ) 8 | 9 | func extractCoords(ls *lua.LState, offset int) []float32 { 10 | coords := extractFloatArray(ls, offset) 11 | if len(coords)%2 != 0 { 12 | ls.ArgError(0, "coordinates need to be given in pairs, arguments are not even") 13 | } 14 | return coords 15 | } 16 | 17 | func toFloat(ls *lua.LState, offset int) float32 { 18 | val := ls.Get(offset) 19 | lv, ok := val.(lua.LNumber) 20 | if !ok { 21 | ls.ArgError(offset, "invalid required argument") 22 | return 0 23 | } 24 | return float32(lv) 25 | } 26 | 27 | func toFloatD(ls *lua.LState, offset int, fallback float32) float32 { 28 | val := ls.Get(offset) 29 | if lv, ok := val.(lua.LNumber); ok { 30 | return float32(lv) 31 | } 32 | return fallback 33 | } 34 | 35 | func toInt(ls *lua.LState, offset int) int { 36 | val := ls.Get(offset) 37 | lv, ok := val.(lua.LNumber) 38 | if !ok { 39 | ls.ArgError(offset, "invalid required argument") 40 | return 0 41 | } 42 | return int(lv) 43 | } 44 | 45 | func toIntD(ls *lua.LState, offset int, fallback int) int { 46 | val := ls.Get(offset) 47 | if lv, ok := val.(lua.LNumber); ok { 48 | return int(lv) 49 | } 50 | return fallback 51 | } 52 | 53 | func toString(ls *lua.LState, offset int) string { 54 | val := ls.Get(offset) 55 | lv, ok := val.(lua.LString) 56 | if !ok { 57 | ls.ArgError(offset, "invalid required argument") 58 | return "" 59 | } 60 | return string(lv) 61 | } 62 | 63 | func toStringD(ls *lua.LState, offset int, fallback string) string { 64 | val := ls.Get(offset) 65 | if lv, ok := val.(lua.LString); ok { 66 | return string(lv) 67 | } 68 | return fallback 69 | } 70 | 71 | func extractMode(ls *lua.LState, offset int) string { 72 | mode := ls.ToString(offset) 73 | if mode == "" || (mode != "fill" && mode != "line") { 74 | ls.ArgError(offset, "invalid drawmode") 75 | } 76 | return mode 77 | } 78 | 79 | func extractBlendmode(ls *lua.LState, offset int) string { 80 | mode := ls.ToString(offset) 81 | if mode == "" || (mode != "multiplicative" && mode != "premultiplied" && 82 | mode != "subtractive" && mode != "additive" && mode != "screen" && mode != "replace" && mode != "alpha") { 83 | ls.ArgError(offset, "invalid blendmode") 84 | } 85 | return mode 86 | } 87 | 88 | func extractLineJoin(ls *lua.LState, offset int) string { 89 | join := ls.ToString(offset) 90 | if join == "" || (join != "bevel" && join != "miter") { 91 | ls.ArgError(offset, "invalid drawmode") 92 | } 93 | return join 94 | } 95 | 96 | func extractFloatArray(ls *lua.LState, offset int) []float32 { 97 | args := []float32{} 98 | for x := ls.Get(offset); x != nil; offset++ { 99 | val := ls.Get(offset) 100 | if lv, ok := val.(lua.LNumber); ok { 101 | args = append(args, float32(lv)) 102 | } else if val.Type() == lua.LTNil { 103 | break 104 | } else { 105 | ls.ArgError(offset, "argument wrong type, should be number") 106 | } 107 | } 108 | 109 | return args 110 | } 111 | 112 | func extractIntArray(ls *lua.LState, offset int) []int32 { 113 | args := []int32{} 114 | for x := ls.Get(offset); x != nil; offset++ { 115 | val := ls.Get(offset) 116 | if lv, ok := val.(lua.LNumber); ok { 117 | args = append(args, int32(lv)) 118 | } else if val.Type() == lua.LTNil { 119 | break 120 | } else { 121 | ls.ArgError(offset, "argument wrong type, should be number") 122 | } 123 | } 124 | 125 | return args 126 | } 127 | 128 | func returnUD(ls *lua.LState, metatable string, item interface{}) int { 129 | f := ls.NewUserData() 130 | f.Value = item 131 | ls.SetMetatable(f, ls.GetTypeMetatable(metatable)) 132 | ls.Push(f) 133 | return 1 134 | } 135 | 136 | func extractColor(ls *lua.LState, offset int) (r, g, b, a float32) { 137 | args := extractFloatArray(ls, offset) 138 | if len(args) == 0 { 139 | return 1, 1, 1, 1 140 | } 141 | argsLength := len(args) 142 | switch argsLength { 143 | case 4: 144 | return args[0], args[1], args[2], args[3] 145 | case 3: 146 | return args[0], args[1], args[2], 1 147 | case 2: 148 | ls.ArgError(offset, "argument wrong type, should be number") 149 | case 1: 150 | return args[0], args[0], args[0], 1 151 | } 152 | return 1, 1, 1, 1 153 | } 154 | 155 | func toWrap(ls *lua.LState, offset int) gfx.WrapMode { 156 | wrapStr := toStringD(ls, offset, "clamp") 157 | switch wrapStr { 158 | case "clamp": 159 | return gfx.WrapClamp 160 | case "repeat": 161 | return gfx.WrapRepeat 162 | case "mirror": 163 | return gfx.WrapMirroredRepeat 164 | default: 165 | ls.ArgError(offset, "invalid wrap mode") 166 | } 167 | return gfx.WrapClamp 168 | } 169 | 170 | func fromWrap(mode gfx.WrapMode) string { 171 | switch mode { 172 | case gfx.WrapRepeat: 173 | return "repeat" 174 | case gfx.WrapMirroredRepeat: 175 | return "mirror" 176 | case gfx.WrapClamp: 177 | fallthrough 178 | default: 179 | return "clamp" 180 | } 181 | } 182 | 183 | func toFilter(ls *lua.LState, offset int) gfx.FilterMode { 184 | wrapStr := toStringD(ls, offset, "near") 185 | switch wrapStr { 186 | case "none": 187 | return gfx.FilterNone 188 | case "near": 189 | return gfx.FilterNearest 190 | case "linear": 191 | return gfx.FilterLinear 192 | default: 193 | ls.ArgError(offset, "invalid filter mode") 194 | } 195 | return gfx.FilterNearest 196 | } 197 | 198 | func fromFilter(mode gfx.FilterMode) string { 199 | switch mode { 200 | case gfx.FilterLinear: 201 | return "linear" 202 | case gfx.FilterNone: 203 | return "none" 204 | case gfx.FilterNearest: 205 | fallthrough 206 | default: 207 | return "near" 208 | } 209 | } 210 | 211 | func toUsage(ls *lua.LState, offset int) gfx.Usage { 212 | wrapStr := toStringD(ls, offset, "dynamic") 213 | switch wrapStr { 214 | case "dynamic": 215 | return gfx.UsageDynamic 216 | case "static": 217 | return gfx.UsageStatic 218 | case "stream": 219 | return gfx.UsageStream 220 | default: 221 | ls.ArgError(offset, "invalid usage mode") 222 | } 223 | return gfx.UsageDynamic 224 | } 225 | 226 | func fromUsage(mode gfx.Usage) string { 227 | switch mode { 228 | case gfx.UsageStatic: 229 | return "static" 230 | case gfx.UsageStream: 231 | return "stream" 232 | case gfx.UsageDynamic: 233 | fallthrough 234 | default: 235 | return "dynamic" 236 | } 237 | } 238 | 239 | func toCompareMode(wrapStr string, offset int) gfx.CompareMode { 240 | switch wrapStr { 241 | case "always": 242 | return gfx.CompareAlways 243 | case "greater": 244 | return gfx.CompareGreater 245 | case "equal": 246 | return gfx.CompareEqual 247 | case "gequal": 248 | return gfx.CompareGequal 249 | case "less": 250 | return gfx.CompareLess 251 | case "nequal": 252 | return gfx.CompareNotequal 253 | case "lequal": 254 | return gfx.CompareLequal 255 | } 256 | return gfx.CompareAlways 257 | } 258 | 259 | func fromCompareMode(mode gfx.CompareMode) string { 260 | switch mode { 261 | case gfx.CompareLequal: 262 | return "lequal" 263 | case gfx.CompareNotequal: 264 | return "nequal" 265 | case gfx.CompareLess: 266 | return "less" 267 | case gfx.CompareGequal: 268 | return "gequal" 269 | case gfx.CompareEqual: 270 | return "equal" 271 | case gfx.CompareGreater: 272 | return "greater" 273 | case gfx.CompareAlways: 274 | fallthrough 275 | default: 276 | return "always" 277 | } 278 | } 279 | 280 | func toStencilAction(wrapStr string, offset int) gfx.StencilAction { 281 | switch wrapStr { 282 | case "replace": 283 | return gfx.StencilReplace 284 | case "increment": 285 | return gfx.StencilIncrement 286 | case "decrement": 287 | return gfx.StencilDecrement 288 | case "incrementwrap": 289 | return gfx.StencilIncrementWrap 290 | case "decrementwrap": 291 | return gfx.StencilDecrementWrap 292 | case "invert": 293 | return gfx.StencilInvert 294 | } 295 | return gfx.StencilReplace 296 | } 297 | -------------------------------------------------------------------------------- /gfx/wrap/wrap.go: -------------------------------------------------------------------------------- 1 | package wrap 2 | 3 | import "github.com/tanema/amore/runtime" 4 | 5 | var graphicsFunctions = runtime.LuaFuncs{ 6 | "circle": gfxCirle, 7 | "arc": gfxArc, 8 | "ellipse": gfxEllipse, 9 | "points": gfxPoints, 10 | "line": gfxLine, 11 | "rectangle": gfxRectangle, 12 | "polygon": gfxPolygon, 13 | "getviewport": gfxGetViewport, 14 | "setviewport": gfxSetViewport, 15 | "getwidth": gfxGetWidth, 16 | "getheight": gfxGetHeight, 17 | "getdimensions": gfxGetDimensions, 18 | "origin": gfxOrigin, 19 | "translate": gfxTranslate, 20 | "rotate": gfxRotate, 21 | "scale": gfxScale, 22 | "shear": gfxShear, 23 | "push": gfxPush, 24 | "pop": gfxPop, 25 | "clear": gfxClear, 26 | "setscissor": gfxSetScissor, 27 | "getscissor": gfxGetScissor, 28 | "setlinewidth": gfxSetLineWidth, 29 | "setlinejoin": gfxSetLineJoin, 30 | "getlinewidth": gfxGetLineWidth, 31 | "getlinejoin": gfxGetLineJoin, 32 | "setpointsize": gfxSetPointSize, 33 | "getpointsize": gfxGetPointSize, 34 | "setcolor": gfxSetColor, 35 | "setbackgroundcolor": gfxSetBackgroundColor, 36 | "getcolor": gfxGetColor, 37 | "getbackgroundcolor": gfxGetBackgroundColor, 38 | "getcolormask": gfxGetColorMask, 39 | "setcolormask": gfxSetColorMask, 40 | "print": gfxPrint, 41 | "printf": gfxPrintf, 42 | "getfont": gfxGetFont, 43 | "setfont": gfxSetFont, 44 | "setblendmode": gfxSetBlendMode, 45 | "getstenciltest": gfxGetStencilTest, 46 | "setstenciltest": gfxSetStencilTest, 47 | "stencil": gfxStencil, 48 | "setshader": gfxSetShader, 49 | 50 | // metatable entries 51 | "newimage": gfxNewImage, 52 | "newtext": gfxNewText, 53 | "newfont": gfxNewFont, 54 | "newquad": gfxNewQuad, 55 | "newcanvas": gfxNewCanvas, 56 | "newspritebatch": gfxNewSpriteBatch, 57 | "newshader": gfxNewShader, 58 | } 59 | 60 | var graphicsMetaTables = runtime.LuaMetaTable{ 61 | "Image": { 62 | "draw": gfxTextureDraw, 63 | "drawq": gfxTextureDrawq, 64 | "getwidth": gfxTextureGetWidth, 65 | "getheigth": gfxTextureGetHeight, 66 | "getDimensions": gfxTextureGetDimensions, 67 | "setwrap": gfxTextureSetWrap, 68 | "setfilter": gfxTextureSetFilter, 69 | }, 70 | "Text": { 71 | "set": gfxTextSet, 72 | "draw": gfxTextDraw, 73 | "getwidth": gfxTextGetWidth, 74 | "getheight": gfxTextGetHeight, 75 | "getdimensions": gfxTextGetDimensions, 76 | "getfont": gfxTextGetFont, 77 | "setfont": gfxTextSetFont, 78 | }, 79 | "Font": { 80 | "getwidth": gfxFontGetWidth, 81 | "getheight": gfxFontGetHeight, 82 | "setfallback": gfxFontSetFallback, 83 | "getwrap": gfxFontGetWrap, 84 | }, 85 | "Quad": { 86 | "getwidth": gfxQuadGetWidth, 87 | "geteheight": gfxQuadGetHeight, 88 | "getviewport": gfxQuadGetViewport, 89 | "setviewport": gfxQuadSetViewport, 90 | }, 91 | "Canvas": { 92 | "newimage": gfxCanvasNewImage, 93 | "draw": gfxTextureDraw, 94 | "drawq": gfxTextureDrawq, 95 | "getwidth": gfxTextureGetWidth, 96 | "getheigth": gfxTextureGetHeight, 97 | "getDimensions": gfxTextureGetDimensions, 98 | "setwrap": gfxTextureSetWrap, 99 | "setfilter": gfxTextureSetFilter, 100 | }, 101 | "SpriteBatch": { 102 | "add": gfxSpriteBatchAdd, 103 | "addq": gfxSpriteBatchAddq, 104 | "set": gfxSpriteBatchSet, 105 | "setq": gfxSpriteBatchSetq, 106 | "clear": gfxSpriteBatchClear, 107 | "settexture": gfxSpriteBatchSetTexture, 108 | "gettexture": gfxSpriteBatchGetTexture, 109 | "setcolor": gfxSpriteBatchSetColor, 110 | "getcolor": gfxSpriteBatchGetColor, 111 | "getcount": gfxSpriteBatchGetCount, 112 | "setbuffersize": gfxSpriteBatchSetBufferSize, 113 | "getbuffersize": gfxSpriteBatchGetBufferSize, 114 | "setdrawrange": gfxSpriteBatchSetDrawRange, 115 | "getdrawrange": gfxSpriteBatchGetDrawRange, 116 | "draw": gfxSpriteBatchDraw, 117 | }, 118 | "Shader": { 119 | "send": gfxShaderSend, 120 | }, 121 | } 122 | 123 | func init() { 124 | runtime.RegisterModule("gfx", graphicsFunctions, graphicsMetaTables) 125 | } 126 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tanema/amore 2 | 3 | require ( 4 | github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d // indirect 5 | github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1 6 | github.com/go-gl/gl v0.0.0-20180407155706-68e253793080 // indirect 7 | github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f // indirect 8 | github.com/go-gl/mathgl v0.0.0-20180319210751-5ab0e04e1f55 9 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 10 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect 11 | github.com/goxjs/gl v0.0.0-20171128034433-dc8f4a9a3c9c 12 | github.com/goxjs/glfw v0.0.0-20171018044755-7dec05603e06 13 | github.com/hajimehoshi/go-mp3 v0.1.0 14 | github.com/jfreymuth/oggvorbis v1.0.0 15 | github.com/jfreymuth/vorbis v1.0.0 // indirect 16 | golang.org/x/image v0.0.0-20180403161127-f315e4403028 17 | golang.org/x/mobile v0.0.0-20180501173530-c909788f9903 18 | honnef.co/go/js/dom v0.0.0-20181202134054-9dbdcd412bde // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= 2 | github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d h1:HB5J9+f1xpkYLgWQ/RqEcbp3SEufyOIMYLoyKNKiG7E= 4 | github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= 5 | github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1 h1:wl/ggSfTHqAy46hyzw1IlrUYwjqmXYvbJyPdH3rT7YE= 6 | github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= 7 | github.com/go-gl/gl v0.0.0-20180407155706-68e253793080 h1:pNxZva3052YM+z2p1aP08FgaTE2NzrRJZ5BHJCmKLzE= 8 | github.com/go-gl/gl v0.0.0-20180407155706-68e253793080/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= 9 | github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f h1:7MsFMbSn8Lcw0blK4+NEOf8DuHoOBDhJsHz04yh13pM= 10 | github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 11 | github.com/go-gl/mathgl v0.0.0-20180319210751-5ab0e04e1f55 h1:XK0Hs1nb11ih2vOSaLRnsjeqDJsd0QZAy5aVE2RUGZk= 12 | github.com/go-gl/mathgl v0.0.0-20180319210751-5ab0e04e1f55/go.mod h1:dvrdneKbyWbK2skTda0nM4B9zSlS2GZSnjX7itr/skQ= 13 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 14 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 15 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= 16 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 17 | github.com/goxjs/gl v0.0.0-20171128034433-dc8f4a9a3c9c h1:naEGFd9BJuTM/1m2lP07Bl9O7O6Jqe+bUplBfAwdygg= 18 | github.com/goxjs/gl v0.0.0-20171128034433-dc8f4a9a3c9c/go.mod h1:dy/f2gjY09hwVfIyATps4G2ai7/hLwLkc5TrPqONuXY= 19 | github.com/goxjs/glfw v0.0.0-20171018044755-7dec05603e06 h1:6jKIS84Kj8pXaqte+73xLvaT+OGt0+fs+TbFczltZP0= 20 | github.com/goxjs/glfw v0.0.0-20171018044755-7dec05603e06/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY= 21 | github.com/hajimehoshi/go-mp3 v0.1.0 h1:nYqjO6rub69tJFI1JJSItb5xFi1z0jBED3INDwyKKlc= 22 | github.com/hajimehoshi/go-mp3 v0.1.0/go.mod h1:SUuVIxBoRGgFS+RYeGIJoiD2wUWy7EajN5tmFlh2ncc= 23 | github.com/jfreymuth/oggvorbis v1.0.0 h1:aOpiihGrFLXpsh2osOlEvTcg5/aluzGQeC7m3uYWOZ0= 24 | github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM= 25 | github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7U= 26 | github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= 27 | golang.org/x/image v0.0.0-20180403161127-f315e4403028 h1:jFFHllCVd24xe3zq5lxsTaMRYoTWSrdWOIA5PEltZ8I= 28 | golang.org/x/image v0.0.0-20180403161127-f315e4403028/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 29 | golang.org/x/mobile v0.0.0-20180501173530-c909788f9903 h1:GxVK7c/X4uagszrFQaHrcQ1DPlEnODQ0Zh/yFmDMgDU= 30 | golang.org/x/mobile v0.0.0-20180501173530-c909788f9903/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 31 | honnef.co/go/js/dom v0.0.0-20181202134054-9dbdcd412bde h1:a/zxkB+dOtFR2DO19YIQtrpHfuZVprzJsA19Og0p53A= 32 | honnef.co/go/js/dom v0.0.0-20181202134054-9dbdcd412bde/go.mod h1:sUMDUKNB2ZcVjt92UnLy3cdGs+wDAcrPdV3JP6sVgA4= 33 | -------------------------------------------------------------------------------- /input/input.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/goxjs/glfw" 5 | "github.com/yuin/gopher-lua" 6 | 7 | "github.com/tanema/amore/runtime" 8 | ) 9 | 10 | type inputCapture struct { 11 | ls *lua.LState 12 | isInside bool 13 | mousex, mousey float64 14 | scrollx float64 15 | scrolly float64 16 | mouseButtons map[string]bool 17 | keys map[string]bool 18 | } 19 | 20 | var currentCapture inputCapture 21 | 22 | func init() { 23 | runtime.RegisterHook(func(ls *lua.LState, window *glfw.Window) { 24 | currentCapture = inputCapture{ 25 | ls: ls, 26 | mouseButtons: map[string]bool{}, 27 | keys: map[string]bool{}, 28 | } 29 | window.SetCursorEnterCallback(currentCapture.mouseEnter) 30 | window.SetMouseMovementCallback(currentCapture.mouseMove) 31 | window.SetScrollCallback(currentCapture.mouseScroll) 32 | window.SetMouseButtonCallback(currentCapture.mouseButton) 33 | window.SetKeyCallback(currentCapture.key) 34 | }) 35 | } 36 | 37 | func (input *inputCapture) dispatch(device, button, action string, modifiers []string) { 38 | callback := input.ls.GetGlobal("oninput") 39 | if callback == nil { 40 | return 41 | } 42 | 43 | luaModifiers := input.ls.NewTable() 44 | for _, mod := range modifiers { 45 | luaModifiers.Append(lua.LString(mod)) 46 | } 47 | 48 | input.ls.CallByParam( 49 | lua.P{Fn: callback, Protect: true}, 50 | lua.LString(device), 51 | lua.LString(button), 52 | lua.LString(action), 53 | luaModifiers, 54 | ) 55 | } 56 | 57 | func (input *inputCapture) mouseEnter(w *glfw.Window, entered bool) { input.isInside = entered } 58 | 59 | func (input *inputCapture) mouseScroll(w *glfw.Window, xoff, yoff float64) { 60 | input.scrollx, input.scrolly = xoff, yoff 61 | } 62 | 63 | func (input *inputCapture) mouseMove(w *glfw.Window, xpos, ypos, xdelta, ydelta float64) { 64 | input.mousex, input.mousey = xpos, ypos 65 | } 66 | 67 | func (input *inputCapture) mouseButton(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) { 68 | buttonName := mouseButtons[button] 69 | if action == glfw.Press { 70 | input.mouseButtons[buttonName] = true 71 | } else if action == glfw.Release { 72 | input.mouseButtons[buttonName] = false 73 | } 74 | input.dispatch("mouse", buttonName, actions[action], expandModifiers(mods)) 75 | } 76 | 77 | func (input *inputCapture) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { 78 | buttonName := keyboardMap[key] 79 | if action == glfw.Press { 80 | input.keys[buttonName] = true 81 | } else if action == glfw.Release { 82 | input.keys[buttonName] = false 83 | } 84 | input.dispatch("keyboard", buttonName, actions[action], expandModifiers(mods)) 85 | } 86 | 87 | func expandModifiers(keys glfw.ModifierKey) []string { 88 | mods := []string{} 89 | if keys&glfw.ModShift == glfw.ModShift { 90 | mods = append(mods, "shift") 91 | } 92 | if keys&glfw.ModControl == glfw.ModControl { 93 | mods = append(mods, "control") 94 | } 95 | if keys&glfw.ModAlt == glfw.ModAlt { 96 | mods = append(mods, "alt") 97 | } 98 | if keys&glfw.ModSuper == glfw.ModSuper { 99 | mods = append(mods, "super") 100 | } 101 | return mods 102 | } 103 | -------------------------------------------------------------------------------- /input/input_const.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/goxjs/glfw" 5 | ) 6 | 7 | var actions = map[glfw.Action]string{ 8 | glfw.Release: "release", 9 | glfw.Press: "press", 10 | glfw.Repeat: "repeat", 11 | } 12 | 13 | var mouseButtons = map[glfw.MouseButton]string{ 14 | glfw.MouseButtonLeft: "left", 15 | glfw.MouseButtonRight: "right", 16 | glfw.MouseButtonMiddle: "middle", 17 | } 18 | 19 | var keyboardMap = map[glfw.Key]string{ 20 | glfw.KeySpace: "space", 21 | glfw.KeyApostrophe: "apostrophe", 22 | glfw.KeyComma: "comma", 23 | glfw.KeyMinus: "minus", 24 | glfw.KeyPeriod: "period", 25 | glfw.KeySlash: "slash", 26 | glfw.Key0: "0", 27 | glfw.Key1: "1", 28 | glfw.Key2: "2", 29 | glfw.Key3: "3", 30 | glfw.Key4: "4", 31 | glfw.Key5: "5", 32 | glfw.Key6: "6", 33 | glfw.Key7: "7", 34 | glfw.Key8: "8", 35 | glfw.Key9: "9", 36 | glfw.KeySemicolon: "semicolon", 37 | glfw.KeyEqual: "equal", 38 | glfw.KeyA: "a", 39 | glfw.KeyB: "b", 40 | glfw.KeyC: "c", 41 | glfw.KeyD: "d", 42 | glfw.KeyE: "e", 43 | glfw.KeyF: "f", 44 | glfw.KeyG: "g", 45 | glfw.KeyH: "h", 46 | glfw.KeyI: "i", 47 | glfw.KeyJ: "j", 48 | glfw.KeyK: "k", 49 | glfw.KeyL: "l", 50 | glfw.KeyM: "m", 51 | glfw.KeyN: "n", 52 | glfw.KeyO: "o", 53 | glfw.KeyP: "p", 54 | glfw.KeyQ: "q", 55 | glfw.KeyR: "r", 56 | glfw.KeyS: "s", 57 | glfw.KeyT: "t", 58 | glfw.KeyU: "u", 59 | glfw.KeyV: "v", 60 | glfw.KeyW: "w", 61 | glfw.KeyX: "x", 62 | glfw.KeyY: "y", 63 | glfw.KeyZ: "z", 64 | glfw.KeyLeftBracket: "leftbracket", 65 | glfw.KeyBackslash: "backslash", 66 | glfw.KeyRightBracket: "rightbracket", 67 | glfw.KeyGraveAccent: "graveaccent", 68 | glfw.KeyWorld1: "world1", 69 | glfw.KeyWorld2: "world2", 70 | glfw.KeyEscape: "escape", 71 | glfw.KeyEnter: "enter", 72 | glfw.KeyTab: "tab", 73 | glfw.KeyBackspace: "backspace", 74 | glfw.KeyInsert: "insert", 75 | glfw.KeyDelete: "delete", 76 | glfw.KeyRight: "right", 77 | glfw.KeyLeft: "left", 78 | glfw.KeyDown: "down", 79 | glfw.KeyUp: "up", 80 | glfw.KeyPageUp: "pageup", 81 | glfw.KeyPageDown: "pagedown", 82 | glfw.KeyHome: "home", 83 | glfw.KeyEnd: "end", 84 | glfw.KeyCapsLock: "capslock", 85 | glfw.KeyScrollLock: "scrolllock", 86 | glfw.KeyNumLock: "numlock", 87 | glfw.KeyPrintScreen: "printscreen", 88 | glfw.KeyPause: "pause", 89 | glfw.KeyF1: "f1", 90 | glfw.KeyF2: "f2", 91 | glfw.KeyF3: "f3", 92 | glfw.KeyF4: "f4", 93 | glfw.KeyF5: "f5", 94 | glfw.KeyF6: "f6", 95 | glfw.KeyF7: "f7", 96 | glfw.KeyF8: "f8", 97 | glfw.KeyF9: "f9", 98 | glfw.KeyF10: "f10", 99 | glfw.KeyF11: "f11", 100 | glfw.KeyF12: "f12", 101 | glfw.KeyF13: "f13", 102 | glfw.KeyF14: "f14", 103 | glfw.KeyF15: "f15", 104 | glfw.KeyF16: "f16", 105 | glfw.KeyF17: "f17", 106 | glfw.KeyF18: "f18", 107 | glfw.KeyF19: "f19", 108 | glfw.KeyF20: "f20", 109 | glfw.KeyF21: "f21", 110 | glfw.KeyF22: "f22", 111 | glfw.KeyF23: "f23", 112 | glfw.KeyF24: "f24", 113 | glfw.KeyF25: "f25", 114 | glfw.KeyKP0: "kp0", 115 | glfw.KeyKP1: "kp1", 116 | glfw.KeyKP2: "kp2", 117 | glfw.KeyKP3: "kp3", 118 | glfw.KeyKP4: "kp4", 119 | glfw.KeyKP5: "kp5", 120 | glfw.KeyKP6: "kp6", 121 | glfw.KeyKP7: "kp7", 122 | glfw.KeyKP8: "kp8", 123 | glfw.KeyKP9: "kp9", 124 | glfw.KeyKPDecimal: "kpdecimal", 125 | glfw.KeyKPDivide: "kpdivide", 126 | glfw.KeyKPMultiply: "kpmultiply", 127 | glfw.KeyKPSubtract: "kpsubtract", 128 | glfw.KeyKPAdd: "kpadd", 129 | glfw.KeyKPEnter: "kpenter", 130 | glfw.KeyKPEqual: "kpequal", 131 | glfw.KeyLeftShift: "leftshift", 132 | glfw.KeyLeftControl: "leftcontrol", 133 | glfw.KeyLeftAlt: "leftalt", 134 | glfw.KeyLeftSuper: "leftsuper", 135 | glfw.KeyRightShift: "rightshift", 136 | glfw.KeyRightControl: "rightcontrol", 137 | glfw.KeyRightAlt: "rightalt", 138 | glfw.KeyRightSuper: "rightsuper", 139 | glfw.KeyMenu: "menu", 140 | } 141 | -------------------------------------------------------------------------------- /runtime/runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/goxjs/gl" 7 | "github.com/goxjs/glfw" 8 | "github.com/yuin/gopher-lua" 9 | 10 | "github.com/tanema/amore/file" 11 | "github.com/tanema/amore/gfx" 12 | ) 13 | 14 | // LuaFuncs is the declarations of functions within a module 15 | type LuaFuncs map[string]lua.LGFunction 16 | 17 | // LuaMetaTable is the declarations of metatables within a module 18 | type LuaMetaTable map[string]LuaFuncs 19 | 20 | // LuaLoadHook is a function that will be called before the gameloop starts with 21 | // the state. This is good for fetching global callbacks to call later. 22 | type LuaLoadHook func(*lua.LState, *glfw.Window) 23 | 24 | type luaModule struct { 25 | name string 26 | functions LuaFuncs 27 | metatables map[string]LuaFuncs 28 | } 29 | 30 | var ( 31 | registeredModules = []luaModule{} 32 | registeredHooks = []LuaLoadHook{} 33 | ) 34 | 35 | func init() { 36 | runtime.GOMAXPROCS(runtime.NumCPU()) 37 | runtime.LockOSThread() //important OpenGl Demand it and stamp thier feet if you dont 38 | } 39 | 40 | // RegisterModule registers a lua module within the global namespace for easy access 41 | func RegisterModule(name string, funcs LuaFuncs, metatables LuaMetaTable) { 42 | registeredModules = append(registeredModules, luaModule{name: name, functions: funcs, metatables: metatables}) 43 | } 44 | 45 | // RegisterHook will add a lua state load hook 46 | func RegisterHook(fn LuaLoadHook) { 47 | registeredHooks = append(registeredHooks, fn) 48 | } 49 | 50 | // Run starts the lua program 51 | func Run(entrypoint string) error { 52 | if err := glfw.Init(gl.ContextWatcher); err != nil { 53 | return err 54 | } 55 | defer glfw.Terminate() 56 | win, err := createWindow(conf) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | ls := lua.NewState() 62 | defer ls.Close() 63 | 64 | gfx.InitContext(win.Window) 65 | importGlobals(ls, win.Window) 66 | importModules(ls) 67 | runHooks(ls, win.Window) 68 | 69 | entryfile, err := file.Open(entrypoint) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | if fn, err := ls.Load(entryfile, entrypoint); err != nil { 75 | return err 76 | } else { 77 | ls.Push(fn) 78 | if err := ls.PCall(0, lua.MultRet, nil); err != nil { 79 | return err 80 | } 81 | } 82 | 83 | if load := ls.GetGlobal("onload"); load != lua.LNil { 84 | if err := ls.CallByParam(lua.P{Fn: load, Protect: true}); err != nil { 85 | return err 86 | } 87 | } 88 | 89 | return gameloop(ls, win) 90 | } 91 | 92 | func importGlobals(ls *lua.LState, win *glfw.Window) { 93 | ls.SetGlobal("getfps", ls.NewFunction(func(L *lua.LState) int { 94 | L.Push(lua.LNumber(fps)) 95 | return 1 96 | })) 97 | ls.SetGlobal("quit", ls.NewFunction(func(L *lua.LState) int { 98 | win.SetShouldClose(true) 99 | return 0 100 | })) 101 | } 102 | 103 | func importModules(ls *lua.LState) { 104 | for _, mod := range registeredModules { 105 | newMod := ls.NewTable() 106 | ls.SetFuncs(newMod, mod.functions) 107 | for tablename, metatable := range mod.metatables { 108 | mt := ls.NewTypeMetatable(tablename) 109 | newMod.RawSetString(tablename, mt) 110 | ls.SetField(mt, "__index", ls.SetFuncs(ls.NewTable(), metatable)) 111 | } 112 | ls.SetGlobal(mod.name, newMod) 113 | } 114 | } 115 | 116 | func runHooks(ls *lua.LState, win *glfw.Window) { 117 | for _, fn := range registeredHooks { 118 | fn(ls, win) 119 | } 120 | } 121 | 122 | // Start creates a window and context for the game to run on and runs the game 123 | // loop. As such this function should be put as the last call in your main function. 124 | // update and draw will be called synchronously because calls to OpenGL that are 125 | // not on the main thread will crash your program. 126 | func gameloop(luaState *lua.LState, win window) error { 127 | for !win.ShouldClose() { 128 | if update := luaState.GetGlobal("update"); update != lua.LNil { 129 | dt := lua.LNumber(step()) 130 | if err := luaState.CallByParam(lua.P{Fn: update, Protect: true}, dt); err != nil { 131 | return err 132 | } 133 | } 134 | if win.active { 135 | color := gfx.GetBackgroundColor() 136 | gfx.Clear(color[0], color[1], color[2], color[3]) 137 | gfx.Origin() 138 | if draw := luaState.GetGlobal("draw"); draw != lua.LNil { 139 | if err := luaState.CallByParam(lua.P{Fn: draw, Protect: true}); err != nil { 140 | return err 141 | } 142 | } 143 | gfx.Present() 144 | win.SwapBuffers() 145 | } 146 | glfw.PollEvents() 147 | } 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /runtime/time.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var ( 8 | fps int // frames per second 9 | frames int // frames since last update freq 10 | previousTime time.Time // last frame time 11 | previousFPSUpdate time.Time // last time fps was updated 12 | ) 13 | 14 | func step() float32 { 15 | frames++ 16 | dt := float32(time.Since(previousTime).Seconds()) 17 | previousTime = time.Now() 18 | timeSinceLast := float32(time.Since(previousFPSUpdate).Seconds()) 19 | if timeSinceLast > 1 { //update 1 per second 20 | fps = int((float32(frames) / timeSinceLast) + 0.5) 21 | previousFPSUpdate = previousTime 22 | frames = 0 23 | } 24 | return dt 25 | } 26 | -------------------------------------------------------------------------------- /runtime/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | // These are lua wrapped code that will be made accessible to lua 7 | _ "github.com/tanema/amore/gfx/wrap" 8 | _ "github.com/tanema/amore/input" 9 | 10 | "github.com/tanema/amore/runtime" 11 | ) 12 | 13 | func main() { 14 | if err := runtime.Run("main.lua"); err != nil { 15 | fmt.Println(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /runtime/window.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "github.com/goxjs/glfw" 5 | ) 6 | 7 | type config struct { 8 | Title string 9 | Width int 10 | Height int 11 | Resizable bool 12 | Fullscreen bool 13 | MouseShown bool 14 | } 15 | 16 | var ( 17 | conf = config{ 18 | Width: 800, 19 | Height: 600, 20 | } 21 | ) 22 | 23 | type window struct { 24 | *glfw.Window 25 | active bool 26 | } 27 | 28 | func createWindow(conf config) (window, error) { 29 | newWin := window{active: true} 30 | 31 | var err error 32 | newWin.Window, err = glfw.CreateWindow(conf.Width, conf.Height, conf.Title, nil, nil) 33 | if err != nil { 34 | return window{}, err 35 | } 36 | newWin.MakeContextCurrent() 37 | if conf.Resizable { 38 | glfw.WindowHint(glfw.Resizable, 1) 39 | } else { 40 | glfw.WindowHint(glfw.Resizable, 0) 41 | } 42 | glfw.WindowHint(glfw.Samples, 4.0) 43 | newWin.SetFocusCallback(newWin.focus) 44 | newWin.SetIconifyCallback(newWin.iconify) 45 | return newWin, nil 46 | } 47 | 48 | func (win *window) focus(w *glfw.Window, focused bool) { win.active = focused } 49 | func (win *window) iconify(w *glfw.Window, iconified bool) { win.active = !iconified } 50 | -------------------------------------------------------------------------------- /test/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanema/amore/4c1a19391c79fb98a35c7d2f00b8be0e213fab70/test/icon.png -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/main.lua: -------------------------------------------------------------------------------- 1 | local img, txt 2 | 3 | function onload() 4 | img = gfx.newimage("icon.png") 5 | txt = gfx.newtext(gfx.getfont(), "This is text") 6 | end 7 | 8 | function oninput(device, button, action, modifiers) 9 | if device == "keyboard" and button == "escape" and action == "release" then 10 | quit() 11 | end 12 | end 13 | 14 | function update(dt) 15 | end 16 | 17 | function draw() 18 | gfx.setcolor(1, 1, 1, 1) 19 | gfx.print({{"fps: ", getfps()}, {{1, 1, 0}, {0, 1, 1}}}, 0, 0) 20 | gfx.rectangle("fill", 300, 300, 480, 440) 21 | img:draw(300, 300) 22 | txt:draw(100, 100) 23 | gfx.setlinewidth(2) 24 | gfx.setlinejoin("bevel") 25 | gfx.ellipse("line", 300, 300, 100, 200) 26 | gfx.setcolor(1, 0, 0, 1) 27 | gfx.rectangle("line", 50, 50, 100, 100) 28 | gfx.setcolor(1, 1, 1, 1) 29 | gfx.line(0, 0, 100, 100, 200, 100) 30 | 31 | gfx.stencil(function() gfx.rectangle("fill", 225, 200, 350, 300) end, "replace", 1) 32 | gfx.setstenciltest("greater", 0) 33 | gfx.setcolor(1, 0, 0, 0.45) 34 | gfx.circle("fill", 300, 300, 150, 50) 35 | gfx.setcolor(0, 1, 0, 0.45) 36 | gfx.circle("fill", 500, 300, 150, 50) 37 | gfx.setcolor(0, 0, 1, 0.45) 38 | gfx.circle("fill", 400, 400, 150, 50) 39 | gfx.setstenciltest() 40 | end 41 | --------------------------------------------------------------------------------