├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── config └── config.exs ├── data.xml ├── lib ├── breakout.ex └── breakout │ ├── application.ex │ ├── audio.ex │ ├── ball_object.ex │ ├── game.ex │ ├── game_level.ex │ ├── game_object.ex │ ├── image_parser.ex │ ├── input.ex │ ├── logger.ex │ ├── math │ ├── mat2.ex │ ├── mat3.ex │ ├── mat4.ex │ ├── quat.ex │ ├── vec2.ex │ ├── vec3.ex │ └── vec4.ex │ ├── particle.ex │ ├── particle_generator.ex │ ├── player.ex │ ├── power_up.ex │ ├── renderer │ ├── open_gl.ex │ ├── post_processor.ex │ ├── renderer.ex │ ├── shader.ex │ ├── sprite.ex │ ├── text.ex │ ├── texture2d.ex │ └── window.ex │ ├── resource_manager.ex │ ├── state.ex │ ├── util.ex │ └── wx_records.ex ├── mix.exs ├── mix.lock ├── priv ├── audio │ ├── block.mp3 │ ├── breakout.mp3 │ ├── paddle.mp3 │ ├── powerup.mp3 │ └── solid.mp3 ├── levels │ ├── four.lvl │ ├── one.lvl │ ├── three.lvl │ └── two.lvl ├── shaders │ ├── particle │ │ ├── fragment.fs │ │ └── vertex.vs │ ├── post_processor │ │ ├── fragment.fs │ │ └── vertex.vs │ └── sprite │ │ ├── fragment.fs │ │ └── vertex.vs ├── sprites │ └── test.png └── textures │ ├── ascii.png │ ├── ascii_rgb.png │ ├── awesomeface.png │ ├── background.jpg │ ├── background.png │ ├── block.png │ ├── block_solid.png │ ├── paddle.png │ ├── particle.png │ ├── powerup_chaos.png │ ├── powerup_confuse.png │ ├── powerup_increase.png │ ├── powerup_passthrough.png │ ├── powerup_speed.png │ └── powerup_sticky.png ├── src ├── gl_const.erl └── wx_const.erl └── test ├── breakout_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | breakout-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ian Harris 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 | # Breakout 2 | 3 | Breakout in Elixir using OpenGL. Largely an incomplete and poor port of 4 | https://learnopengl.com/In-Practice/2D-Game/Breakout. 5 | 6 | ## Pre-emptive notes 7 | 8 | The code is very bad. I'm working on improving it. Please feel free to suggest 9 | updates/changes, ideally with an eye towards how games can be implemented in the 10 | future (i.e., not just improvements to this breakout implementation, but game 11 | development in Elixir as a whole). 12 | 13 | ## Launching 14 | 15 | This implementation has no dependencies. The following should work (tested on 16 | modern macOS, Ubuntu, and Windows): 17 | 18 | ```sh 19 | $ git clone https://github.com/harrisi/elixir_breakout 20 | $ iex -S mix # or mix run --no-halt 21 | ``` 22 | 23 | ## Gameplay 24 | 25 | `A`/`D`, `H`/`L` (hello, fellow vim users), or left/right arrows to move 26 | left/right, space to launch the ball, `N` to switch between levels, `P` to 27 | profile with tprof for ten seconds (if you're in to that). 28 | 29 | There is no win condition. -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, 4 | :default_formatter, 5 | format: {Breakout.Logger, :format}, 6 | handle_otp_reports: true, 7 | handle_sasl_reports: true, 8 | # format: "$time [$level] $message\n\t$metadata\n", 9 | metadata: [ 10 | :error_code, 11 | :mfa, 12 | :line, 13 | :pid, 14 | :registered_name, 15 | :process_label, 16 | :crash_reason, 17 | :msg 18 | ] 19 | -------------------------------------------------------------------------------- /data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | Adobe Photoshop CS6 (Windows) 7 | 2018-06-19T20:29:54+02:00 8 | 2020-04-23T12:12:01+02:00 9 | 2020-04-23T12:12:01+02:00 10 | 11 | 13 | image/png 14 | 15 | 17 | 3 18 | sRGB IEC61966-2.1 19 | 20 | 23 | xmp.iid:133477AC4A85EA11B6AEC9C5B8B52EE9 24 | xmp.did:133477AC4A85EA11B6AEC9C5B8B52EE9 25 | xmp.did:133477AC4A85EA11B6AEC9C5B8B52EE9 26 | 27 | 28 | 29 | created 30 | xmp.iid:133477AC4A85EA11B6AEC9C5B8B52EE9 31 | 2018-06-19T20:29:54+02:00 32 | Adobe Photoshop CS6 (Windows) 33 | 34 | 35 | 36 | 37 | 39 | 1 40 | 720000/10000 41 | 720000/10000 42 | 2 43 | 44 | 46 | 1 47 | 512 48 | 512 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /lib/breakout.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout do 2 | end 3 | -------------------------------------------------------------------------------- /lib/breakout/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Application do 2 | require Logger 3 | 4 | # See https://hexdocs.pm/elixir/Application.html 5 | # for more information on OTP Applications 6 | @moduledoc false 7 | 8 | use Application 9 | 10 | @impl Application 11 | def start(_type, _args) do 12 | children = [ 13 | # having ResourceManager as a separate app makes things difficult because 14 | # that means I need to share the context. I should probably see what it 15 | # would look like to move all opengl stuff to it's own application 16 | # (Renderer), but I just realized that gets kinda complicated - what about 17 | # the window? should the renderer also handle input? hm. 18 | 19 | # for now, I'm actually going to just have a single app. it makes handling 20 | # opengl stuff easier. 21 | 22 | # Breakout.ResourceManager, 23 | {Breakout.Audio, [Breakout.Util.to_priv("audio/breakout.mp3")]}, 24 | Breakout.Game, 25 | ] 26 | 27 | # See https://hexdocs.pm/elixir/Supervisor.html 28 | # for other strategies and supported options 29 | opts = [strategy: :one_for_one, name: Breakout.Supervisor, auto_shutdown: :any_significant] 30 | Supervisor.start_link(children, opts) 31 | end 32 | 33 | @impl Application 34 | def stop(state) do 35 | IO.inspect(state, label: "in application stop") 36 | 37 | :init.stop() 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/breakout/audio.ex: -------------------------------------------------------------------------------- 1 | # This is taken from one of the Membrane maintainers (Mateusz Front, I believe). 2 | # slight modification to make it loop infinitely. 3 | defmodule Breakout.Audio.LoopFilter do 4 | use Membrane.Filter 5 | 6 | def_input_pad :input, accepted_format: _any 7 | def_output_pad :output, accepted_format: _any 8 | 9 | def_options loops: [spec: integer() | :infinity] 10 | 11 | @impl true 12 | def handle_init(_ctx, opts) do 13 | {[], %{loops: opts.loops}} 14 | end 15 | 16 | @impl true 17 | def handle_playing(_ctx, state) do 18 | seek(state) 19 | end 20 | 21 | @impl true 22 | def handle_event(:input, %Membrane.File.EndOfSeekEvent{}, _ctx, state) do 23 | seek(state) 24 | end 25 | 26 | @impl true 27 | def handle_event(pad, event, ctx, state) do 28 | super(pad, event, ctx, state) 29 | end 30 | 31 | @impl true 32 | def handle_buffer(:input, buffer, _ctx, state) do 33 | {[buffer: {:output, buffer}], state} 34 | end 35 | 36 | defp seek(state) do 37 | %{loops: loops} = state 38 | 39 | event = case loops do 40 | :infinity -> 41 | %Membrane.File.SeekSourceEvent{start: :bof, size_to_read: :infinity} 42 | 43 | 1 -> 44 | %Membrane.File.SeekSourceEvent{start: :bof, size_to_read: :infinity, last?: true} 45 | 46 | _ -> nil 47 | end 48 | 49 | actions = if event, do: [event: {:input, event}], else: [] 50 | state = unless state.loops == :infinity do 51 | update_in(state.loops, &(&1 - 1)) 52 | else 53 | state 54 | end 55 | 56 | {actions, state} 57 | end 58 | end 59 | 60 | defmodule Breakout.Audio do 61 | use Membrane.Pipeline 62 | 63 | def start_link(args) do 64 | Membrane.Pipeline.start_link(__MODULE__, args, name: __MODULE__) 65 | end 66 | 67 | # This is taken from membraneframework/membrane_demo/simple_pipeline 68 | 69 | @impl Membrane.Pipeline 70 | def handle_init(_ctx, path_to_mp3) do 71 | spec = 72 | child(:file, %Membrane.File.Source{location: path_to_mp3, seekable?: true}) 73 | |> child(:decoder, Membrane.MP3.MAD.Decoder) 74 | |> child(:converter, %Membrane.FFmpeg.SWResample.Converter{ 75 | output_stream_format: %Membrane.RawAudio{ 76 | sample_format: :s16le, 77 | sample_rate: 44_100, 78 | channels: 2, 79 | } 80 | }) 81 | |> child(%Breakout.Audio.LoopFilter{loops: :infinity}) 82 | |> child(:portaudio, Membrane.PortAudio.Sink) 83 | 84 | {[spec: spec], %{}} 85 | end 86 | 87 | @impl Membrane.Pipeline 88 | def handle_element_end_of_stream(:sink, :input, _ctx, state) do 89 | {[terminate: :normal], state} 90 | end 91 | 92 | @impl Membrane.Pipeline 93 | def handle_element_end_of_stream(_element, _pad, _ctx, state) do 94 | {[], state} 95 | end 96 | 97 | # @impl Membrane.Pipeline 98 | # def handle_element_end_of_stream(child, pad, context, state) do 99 | # Membrane.Pipeline.terminate(__MODULE__) 100 | # end 101 | end 102 | 103 | defmodule Breakout.Audio.SoundEffect do 104 | use Membrane.Pipeline 105 | 106 | def play(which) do 107 | case which do 108 | :block -> 109 | {:ok, _sup, _pipe} = 110 | Membrane.Pipeline.start(__MODULE__, Breakout.Util.to_priv("audio/block.mp3")) 111 | :solid -> 112 | {:ok, _sup, _pipe} = 113 | Membrane.Pipeline.start(__MODULE__, Breakout.Util.to_priv("audio/solid.mp3")) 114 | :paddle -> 115 | {:ok, _sup, _pipe} = 116 | Membrane.Pipeline.start(__MODULE__, Breakout.Util.to_priv("audio/paddle.mp3")) 117 | :powerup -> 118 | {:ok, _sup, _pipe} = 119 | Membrane.Pipeline.start(__MODULE__, Breakout.Util.to_priv("audio/powerup.mp3")) 120 | _ -> nil 121 | end 122 | end 123 | 124 | def start(args) do 125 | Membrane.Pipeline.start(__MODULE__, args, name: __MODULE__) 126 | end 127 | 128 | # This is taken from membraneframework/membrane_demo/simple_pipeline 129 | 130 | @impl Membrane.Pipeline 131 | def handle_init(_ctx, path_to_mp3) do 132 | spec = 133 | child(:file, %Membrane.File.Source{location: path_to_mp3}) 134 | |> child(:decoder, Membrane.MP3.MAD.Decoder) 135 | |> child(:converter, %Membrane.FFmpeg.SWResample.Converter{ 136 | output_stream_format: %Membrane.RawAudio{ 137 | sample_format: :s16le, 138 | sample_rate: 48_000, 139 | channels: 2, 140 | } 141 | }) 142 | # |> child(%Breakout.Audio.LoopFilter{loops: :infinity}) 143 | |> child(:portaudio, Membrane.PortAudio.Sink) 144 | 145 | {[spec: spec], %{}} 146 | end 147 | 148 | @impl Membrane.Pipeline 149 | def handle_element_end_of_stream(:sink, :input, _ctx, state) do 150 | {[terminate: :normal], state} 151 | end 152 | 153 | @impl Membrane.Pipeline 154 | def handle_element_end_of_stream(_element, _pad, _ctx, state) do 155 | {[], state} 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/breakout/ball_object.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.BallObject do 2 | alias Breakout.Math.Vec2 3 | alias Breakout.Math.Vec3 4 | alias Breakout.GameObject 5 | 6 | @type t :: %__MODULE__{ 7 | game_object: GameObject.t(), 8 | radius: number(), 9 | stuck: boolean(), 10 | sticky: boolean(), 11 | passthrough: boolean() 12 | } 13 | 14 | defstruct game_object: GameObject.new(), 15 | radius: 1, 16 | stuck: true, 17 | sticky: false, 18 | passthrough: false 19 | 20 | def new() do 21 | %__MODULE__{} 22 | end 23 | 24 | def new(position, radius, velocity, sprite) do 25 | %__MODULE__{ 26 | radius: radius, 27 | game_object: 28 | GameObject.new( 29 | position, 30 | Vec2.new(radius * 2, radius * 2), 31 | sprite, 32 | Vec3.new(1, 1, 1), 33 | velocity 34 | ) 35 | } 36 | end 37 | 38 | @spec reset(ball :: t(), position :: Vec2.t(), velocity :: Vec2.t()) :: t() 39 | def reset(ball, position, velocity) do 40 | new(position, ball.radius, velocity, ball.game_object.sprite) 41 | end 42 | 43 | @spec move(ball :: t(), dt :: number(), window_width :: number()) :: t() 44 | def move(ball, dt, window_width) do 45 | unless ball.stuck do 46 | {this_velocity_x, this_velocity_y} = this_velocity = ball.game_object.velocity 47 | {this_size_x, _} = ball.game_object.size 48 | 49 | {new_x, new_y} = 50 | pos = 51 | ball.game_object.position 52 | |> Vec2.add(Vec2.scale(this_velocity, dt)) 53 | 54 | {{new_velocity_x, new_velocity_y} = new_velocity, 55 | {new_position_x, new_position_y} = new_position} = 56 | cond do 57 | new_x <= 0 -> 58 | {{-this_velocity_x, this_velocity_y}, {0.0, new_y}} 59 | 60 | new_x + (ball.game_object.size |> elem(0)) >= window_width -> 61 | {{-this_velocity_x, this_velocity_y}, {window_width - this_size_x, new_y}} 62 | 63 | true -> 64 | {this_velocity, pos} 65 | end 66 | 67 | {new_velocity, new_position} = 68 | if new_position_y <= 0 do 69 | {{new_velocity_x, -new_velocity_y}, {new_position_x, 0.0}} 70 | else 71 | {new_velocity, new_position} 72 | end 73 | 74 | b = new(new_position, ball.radius, new_velocity, ball.game_object.sprite) 75 | 76 | # , radius: ball.radius, stuck: ball.stuck} 77 | %__MODULE__{ball | game_object: b.game_object} 78 | else 79 | ball 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/breakout/game.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Game do 2 | require Logger 3 | 4 | import Breakout.WxRecords 5 | 6 | alias Breakout.State 7 | alias Breakout.PowerUp 8 | alias Breakout.Renderer.PostProcessor 9 | alias Breakout.ParticleGenerator 10 | alias Breakout.{BallObject, GameObject, GameLevel} 11 | alias Breakout.Renderer 12 | alias Renderer.{Texture2D, Sprite, Shader, Window, OpenGL} 13 | alias Breakout.Math 14 | alias Math.{Vec2, Vec3, Mat4} 15 | alias Breakout.Audio.SoundEffect 16 | 17 | @behaviour :wx_object 18 | 19 | @screen_width 1200 20 | @screen_height 800 21 | 22 | @initial_ball_velocity_x -0.25 23 | @initial_ball_velocity_y -0.25 24 | @initial_ball_velocity {@initial_ball_velocity_x, @initial_ball_velocity_y} 25 | 26 | @ball_radius 12.5 27 | 28 | @player_position_x @screen_width / 2 - 50 29 | @player_position_y @screen_height - 60 30 | @player_position {@player_position_x, @player_position_y} 31 | 32 | def child_spec(arg) do 33 | %{ 34 | id: __MODULE__, 35 | start: {__MODULE__, :start_link, [arg]}, 36 | significant: true, 37 | restart: :temporary 38 | } 39 | end 40 | 41 | @impl :wx_object 42 | def init(_arg) do 43 | window = Window.init(@screen_width, @screen_height) 44 | 45 | OpenGL.init() 46 | 47 | font = :wxFont.new(32, :wx_const.wx_fontfamily_teletype, :wx_const.wx_fontstyle_normal, :wx_const.wx_fontweight_normal) 48 | brush = :wxBrush.new({0, 0, 0}) 49 | 50 | state = %State{ 51 | t: :erlang.monotonic_time(:millisecond), 52 | start: :erlang.monotonic_time(), 53 | dt: 1, 54 | window: window, 55 | width: @screen_width, 56 | height: @screen_height, 57 | power_ups: [], 58 | ball: BallObject.new(), 59 | font: font, 60 | brush: brush, 61 | } 62 | 63 | menu_text = """ 64 | press enter to start 65 | press w or s to select level 66 | """ 67 | 68 | menu_font = :wxFont.new(60, :wx_const.wx_fontfamily_teletype, :wx_const.wx_fontstyle_normal, :wx_const.wx_fontweight_bold) 69 | 70 | {menu_string_texture, menu_string_w, menu_string_h} = load_texture_by_string(menu_font, brush, {222, 222, 222}, menu_text, false) 71 | 72 | state = put_in(state.menu_string_size, {menu_string_w, menu_string_h}) 73 | 74 | state = put_in(state.resources.textures[:menu_string], menu_string_texture) 75 | 76 | projection = Mat4.ortho(0.0, state.width + 0.0, state.height + 0.0, 0.0, -1.0, 1.0) 77 | 78 | sprite_shader = 79 | Shader.init(Breakout.Util.to_priv("shaders/sprite/vertex.vs"), Breakout.Util.to_priv("shaders/sprite/fragment.fs")) 80 | |> Shader.use_shader() 81 | |> Shader.set(~c"image", 0) 82 | |> Shader.set(~c"projection", [projection |> Mat4.flatten()]) 83 | 84 | # |> ResourceManager.put_shader(:sprite) 85 | # state = %Breakout.State{state | resources} 86 | state = put_in(state.resources.shaders[:sprite], sprite_shader) 87 | sprite_renderer = Sprite.new(sprite_shader) 88 | 89 | particle_shader = 90 | Shader.init(Breakout.Util.to_priv("shaders/particle/vertex.vs"), Breakout.Util.to_priv("shaders/particle/fragment.fs")) 91 | |> Shader.set(~c"projection", [projection |> Mat4.flatten()], true) 92 | 93 | state = put_in(state.resources.shaders[:particle], particle_shader) 94 | 95 | state = 96 | put_in( 97 | state.resources.textures[:particle], 98 | Texture2D.load(Breakout.Util.to_priv("textures/particle.png"), true) 99 | ) 100 | 101 | state = %{ 102 | state 103 | | particle_generator: 104 | ParticleGenerator.new( 105 | particle_shader, 106 | state.resources.textures[:particle], 107 | 500 108 | ) 109 | } 110 | 111 | state = %Breakout.State{state | sprite_renderer: sprite_renderer} 112 | 113 | state = 114 | put_in( 115 | state.resources.textures[:face], 116 | Texture2D.load(Breakout.Util.to_priv("textures/awesomeface.png"), true) 117 | ) 118 | 119 | state = put_in(state.resources.textures[:ascii], 120 | Texture2D.load(Breakout.Util.to_priv("textures/ascii_rgb.png"), false) 121 | ) 122 | 123 | # |> ResourceManager.put_texture(:face) 124 | 125 | state = 126 | put_in( 127 | state.resources.textures[:background], 128 | Texture2D.load(Breakout.Util.to_priv("textures/background.png"), false) 129 | ) 130 | 131 | state = 132 | put_in(state.resources.textures[:block], Texture2D.load(Breakout.Util.to_priv("textures/block.png"), false)) 133 | 134 | power_up_textures = 135 | [ 136 | :chaos, 137 | :confuse, 138 | :increase, 139 | :passthrough, 140 | :speed, 141 | :sticky 142 | ] 143 | |> Enum.reduce(state.resources.textures, fn el, acc -> 144 | put_in(acc[el], Texture2D.load(Breakout.Util.to_priv("textures/powerup_#{el}.png"), true)) 145 | end) 146 | 147 | state = %Breakout.State{ 148 | state 149 | | resources: %{ 150 | state.resources 151 | | textures: Map.merge(state.resources.textures, power_up_textures) 152 | } 153 | } 154 | 155 | state = 156 | put_in( 157 | state.resources.textures[:block_solid], 158 | Texture2D.load(Breakout.Util.to_priv("textures/block_solid.png"), false) 159 | ) 160 | 161 | # |> ResourceManager.put_texture(:block_solid) 162 | 163 | state = %Breakout.State{ 164 | state 165 | | levels: { 166 | GameLevel.load(Breakout.Util.to_priv("levels/one.lvl"), @screen_width, @screen_height / 2), 167 | GameLevel.load(Breakout.Util.to_priv("levels/two.lvl"), @screen_width, @screen_height / 2), 168 | GameLevel.load(Breakout.Util.to_priv("levels/three.lvl"), @screen_width, @screen_height / 2), 169 | GameLevel.load(Breakout.Util.to_priv("levels/four.lvl"), @screen_width, @screen_height / 2) 170 | }, 171 | level: 0 172 | } 173 | 174 | state = 175 | put_in(state.resources.textures[:paddle], Texture2D.load(Breakout.Util.to_priv("textures/paddle.png"), true)) 176 | 177 | # |> ResourceManager.put_texture(:paddle) 178 | 179 | # {:ok, player_texture} = ResourceManager.get_texture(:paddle) 180 | player_texture = state.resources.textures[:paddle] 181 | 182 | player = 183 | GameObject.new( 184 | @player_position, 185 | Vec2.new(100, 20), 186 | player_texture, 187 | Vec3.new(1, 1, 1), 188 | 500.0 189 | ) 190 | 191 | state = %Breakout.State{state | player: player} 192 | 193 | ball_pos = 194 | player.position 195 | |> Vec2.add(Vec2.new(@player_position_x / 2 - @ball_radius, -@ball_radius * 2)) 196 | 197 | # {:ok, ball_tex} = ResourceManager.get_texture(:face) 198 | ball_tex = state.resources.textures[:face] 199 | ball = BallObject.new(ball_pos, @ball_radius, @initial_ball_velocity, ball_tex) 200 | 201 | state = 202 | %Breakout.State{state | ball: ball} 203 | |> reset_ball() 204 | 205 | # {:ok, background} = ResourceManager.get_texture(:background) 206 | background = state.resources.textures[:background] 207 | 208 | state = %Breakout.State{state | background_texture: background} 209 | 210 | pp_shader = 211 | Shader.init( 212 | Breakout.Util.to_priv("shaders/post_processor/vertex.vs"), 213 | Breakout.Util.to_priv("shaders/post_processor/fragment.fs") 214 | ) 215 | 216 | {scaled_width, scaled_height} = 217 | Vec2.new(@screen_width, @screen_height) 218 | |> Vec2.scale(:wxWindow.getDPIScaleFactor(window.frame)) 219 | 220 | post_processor = PostProcessor.new(pp_shader, trunc(scaled_width), trunc(scaled_height)) 221 | 222 | state = %Breakout.State{state | post_processor: post_processor} 223 | 224 | send(self(), :loop) 225 | 226 | {window.frame, state} 227 | end 228 | 229 | @spec do_collisions(State.t()) :: State.t() 230 | def do_collisions(%State{} = state) do 231 | cur_level = state.levels |> elem(state.level) 232 | 233 | {updated_bricks, new_state} = 234 | Enum.map_reduce(cur_level.bricks, state, fn %GameObject{} = box, acc -> 235 | unless box.destroyed do 236 | {collided, dir, diff} = GameObject.check_collision(acc.ball, box) 237 | 238 | if collided do 239 | {box, new_state} = 240 | if not box.is_solid do 241 | new_box = %GameObject{box | destroyed: true} 242 | new_state = spawn_power_ups(acc, new_box) 243 | SoundEffect.play(:block) 244 | 245 | {new_box, new_state} 246 | else 247 | new_state = %State{ 248 | acc 249 | | shake_time: 0.05, 250 | post_processor: %PostProcessor{acc.post_processor | shake: true} 251 | } 252 | SoundEffect.play(:solid) 253 | 254 | {box, new_state} 255 | end 256 | 257 | # TODO: this allows passing through solid blocks, which is kinda weird. 258 | updated_ball = 259 | unless new_state.ball.passthrough do 260 | resolve_collision(new_state.ball, dir, diff) 261 | else 262 | new_state.ball 263 | end 264 | 265 | new_state = %State{new_state | ball: updated_ball} 266 | {box, new_state} 267 | else 268 | {box, acc} 269 | end 270 | else 271 | {box, acc} 272 | end 273 | end) 274 | 275 | level = %{cur_level | bricks: updated_bricks} 276 | 277 | new_state = 278 | Enum.with_index(new_state.power_ups) 279 | |> Enum.reduce(new_state, fn {%PowerUp{game_object: %GameObject{}} = power_up, index}, 280 | %State{} = acc -> 281 | unless power_up.game_object.destroyed do 282 | power_up = 283 | put_in( 284 | power_up.game_object.destroyed, 285 | power_up.game_object.position |> elem(1) >= @screen_height 286 | ) 287 | 288 | if GameObject.check_collision(acc.player, power_up.game_object) do 289 | acc = activate_power_up(acc, power_up) 290 | power_up = put_in(power_up.game_object.destroyed, true) 291 | power_up = put_in(power_up.activated, true) 292 | power_ups = List.update_at(acc.power_ups, index, fn _ -> power_up end) 293 | SoundEffect.play(:powerup) 294 | put_in(acc.power_ups, power_ups) 295 | else 296 | power_ups = List.update_at(acc.power_ups, index, fn _ -> power_up end) 297 | put_in(acc.power_ups, power_ups) 298 | end 299 | else 300 | acc 301 | end 302 | end) 303 | 304 | {paddle_collision, _, _} = GameObject.check_collision(new_state.ball, state.player) 305 | 306 | ball = 307 | if not new_state.ball.stuck and paddle_collision do 308 | {player_x, _player_y} = new_state.player.position 309 | {player_w, _player_h} = new_state.player.size 310 | {ball_x, _} = new_state.ball.game_object.position 311 | 312 | center_board = player_x + player_w / 2 313 | distance = ball_x + new_state.ball.radius - center_board 314 | percentage = distance / (player_w / 2) 315 | 316 | strength = 2 317 | old_vel = new_state.ball.game_object.velocity 318 | 319 | ball = %{ 320 | new_state.ball 321 | | game_object: %{ 322 | new_state.ball.game_object 323 | | velocity: 324 | {@initial_ball_velocity_x * percentage * strength, @initial_ball_velocity_y} 325 | } 326 | } 327 | 328 | ball = %{ 329 | ball 330 | | game_object: %{ 331 | ball.game_object 332 | | velocity: 333 | Vec2.normalize(ball.game_object.velocity) |> Vec2.scale(Vec2.length(old_vel)) 334 | } 335 | } 336 | 337 | {ball_vel_x, ball_vel_y} = ball.game_object.velocity 338 | ball = put_in(ball.game_object.velocity, {ball_vel_x, -1 * abs(ball_vel_y)}) 339 | 340 | SoundEffect.play(:paddle) 341 | 342 | put_in(ball.stuck, ball.sticky) 343 | else 344 | new_state.ball 345 | end 346 | 347 | %State{new_state | levels: put_elem(state.levels, state.level, level), ball: ball} 348 | end 349 | 350 | defp resolve_collision(ball, dir, {diff_x, diff_y}) do 351 | {ball_vel_x, ball_vel_y} = ball.game_object.velocity 352 | {ball_pos_x, ball_pos_y} = ball.game_object.position 353 | 354 | case dir do 355 | :left -> 356 | ball = %{ball | game_object: %{ball.game_object | velocity: {-ball_vel_x, ball_vel_y}}} 357 | penetration = ball.radius - abs(diff_x) 358 | 359 | %{ 360 | ball 361 | | game_object: %{ball.game_object | position: {ball_pos_x + penetration, ball_pos_y}} 362 | } 363 | 364 | :right -> 365 | ball = %{ball | game_object: %{ball.game_object | velocity: {-ball_vel_x, ball_vel_y}}} 366 | penetration = ball.radius - abs(diff_x) 367 | 368 | %{ 369 | ball 370 | | game_object: %{ball.game_object | position: {ball_pos_x - penetration, ball_pos_y}} 371 | } 372 | 373 | :up -> 374 | ball = %{ball | game_object: %{ball.game_object | velocity: {ball_vel_x, -ball_vel_y}}} 375 | penetration = ball.radius - abs(diff_y) 376 | 377 | %{ 378 | ball 379 | | game_object: %{ball.game_object | position: {ball_pos_x, ball_pos_y - penetration}} 380 | } 381 | 382 | :down -> 383 | ball = %{ball | game_object: %{ball.game_object | velocity: {ball_vel_x, -ball_vel_y}}} 384 | penetration = ball.radius - abs(diff_y) 385 | 386 | %{ 387 | ball 388 | | game_object: %{ball.game_object | position: {ball_pos_x, ball_pos_y + penetration}} 389 | } 390 | end 391 | end 392 | 393 | def start do 394 | :wx_object.start_link(__MODULE__, [], []) 395 | end 396 | 397 | def start_link(arg) do 398 | :wx_object.start_link({:local, __MODULE__}, __MODULE__, arg, []) 399 | {:ok, self()} 400 | end 401 | 402 | @impl :wx_object 403 | def terminate(reason, state) do 404 | Logger.error(msg: reason) 405 | IO.inspect(reason, label: "terminate") 406 | Supervisor.stop(Breakout.Supervisor) 407 | 408 | {:shutdown, state} 409 | end 410 | 411 | @impl :wx_object 412 | def handle_event(wx(event: wxClose()), state) do 413 | IO.inspect(state, label: "closing") 414 | :wxWindow."Destroy"(state.window.frame) 415 | 416 | {:stop, :normal, state} 417 | end 418 | 419 | @impl :wx_object 420 | def handle_event(request, state) do 421 | IO.inspect(request, label: "handle_event") 422 | {:noreply, state} 423 | end 424 | 425 | @impl :wx_object 426 | def handle_call(request, _from, state) do 427 | IO.inspect(request, label: "handle_call") 428 | {:noreply, state} 429 | end 430 | 431 | @impl :wx_object 432 | def handle_cast({:key_down, key_code}, state) do 433 | state = %{ 434 | state 435 | | keys: MapSet.put(state.keys, key_code), 436 | level: 437 | if(key_code == ?N, 438 | do: rem(state.level + 1, tuple_size(state.levels)), 439 | else: state.level 440 | ) 441 | } 442 | 443 | if key_code == ?P do 444 | send(self(), :start_profiling) 445 | end 446 | 447 | {:noreply, state} 448 | end 449 | 450 | @impl :wx_object 451 | def handle_cast({:key_up, key_code}, state) do 452 | state = %{state | keys: MapSet.delete(state.keys, key_code)} 453 | state = %{state | keys_processed: MapSet.delete(state.keys_processed, key_code)} 454 | 455 | {:noreply, state} 456 | end 457 | 458 | @impl :wx_object 459 | def handle_cast(request, state) do 460 | IO.inspect(request, label: "handle_cast") 461 | {:noreply, state} 462 | end 463 | 464 | def handle_info(:start_profiling, state) do 465 | :tprof.start(%{type: :call_time}) 466 | :tprof.enable_trace(:all) 467 | :tprof.set_pattern(:_, :_, :_) 468 | # :eprof.start_profiling([self()]) 469 | # :eprof.log(~c'eprof') 470 | Process.send_after(self(), :stop_profiling, 10_000) 471 | {:noreply, state} 472 | end 473 | 474 | def handle_info(:stop_profiling, state) do 475 | :tprof.disable_trace(:all) 476 | sample = :tprof.collect() 477 | inspected = :tprof.inspect(sample, :process, :measurement) 478 | shell = :maps.get(self(), inspected) 479 | 480 | IO.puts(:tprof.format(shell)) 481 | 482 | # :eprof.stop_profiling() 483 | # :eprof.analyze() 484 | {:noreply, state} 485 | end 486 | 487 | @impl :wx_object 488 | def handle_info(:loop, %State{} = state) do 489 | t = :erlang.monotonic_time(:millisecond) 490 | dt = t - state.t 491 | now = :erlang.monotonic_time() 492 | elapsed = now - state.start 493 | state = put_in(state.elapsed, elapsed / :erlang.convert_time_unit(1, :second, :native)) 494 | 495 | send(self(), {:update, dt}) 496 | send(self(), {:process_input, dt}) 497 | send(self(), :render) 498 | send(self(), :loop) 499 | 500 | {:noreply, %State{state | t: t, dt: dt}} 501 | end 502 | 503 | @impl :wx_object 504 | def handle_info({:update, dt}, %State{} = state) do 505 | ball = BallObject.move(state.ball, dt, @screen_width) 506 | 507 | state = put_in(state.ball, ball) 508 | 509 | state = do_collisions(state) 510 | 511 | {_, ball_y} = state.ball.game_object.position 512 | 513 | state = 514 | if ball_y >= @screen_height do 515 | state = update_in(state.lives, &(&1 - 1)) 516 | if state.lives == 0 do 517 | state = state 518 | |> reset_level() 519 | put_in(state.game_state, :menu) 520 | else 521 | state 522 | end 523 | |> reset_player() 524 | |> reset_ball() 525 | else 526 | state 527 | end 528 | 529 | state = update_power_ups(state, dt) 530 | 531 | pg = 532 | ParticleGenerator.update( 533 | state.particle_generator, 534 | dt, 535 | state.ball.game_object, 536 | 2, 537 | Vec2.new(state.ball.radius / 2.0, state.ball.radius / 2.0) 538 | ) 539 | 540 | state = put_in(state.particle_generator, pg) 541 | 542 | {shake_time, pp} = 543 | if state.shake_time > 0 do 544 | st = state.shake_time - 0.005 545 | 546 | pp = 547 | if st <= 0 do 548 | %PostProcessor{state.post_processor | shake: false} 549 | else 550 | state.post_processor 551 | end 552 | 553 | {st, pp} 554 | else 555 | {state.shake_time, state.post_processor} 556 | end 557 | 558 | # t = :erlang.system_time() / 1_000_000_000 559 | # r = 127.5 * (1 + :math.sin(t)) 560 | # g = 127.5 * (1 + :math.sin(t + 2 * :math.pi / 3)) 561 | # b = 127.5 * (1 + :math.sin(t + 4 * :math.pi / 3)) 562 | 563 | state = put_in(state.resources.textures[:string], load_texture_by_string(state.font, state.brush, {222, 222, 222}, "Lives: #{state.lives}", false) |> elem(0)) 564 | 565 | {:noreply, %State{state | shake_time: shake_time, post_processor: pp}} 566 | end 567 | 568 | @impl :wx_object 569 | def handle_info({:process_input, dt}, state) do 570 | state = if state.game_state == :menu do 571 | state = if MapSet.member?(state.keys, 13) and not MapSet.member?(state.keys_processed, 13) do 572 | state = put_in(state.game_state, :active) 573 | put_in(state.keys_processed, MapSet.put(state.keys_processed, 13)) 574 | else 575 | state 576 | end 577 | state = if MapSet.member?(state.keys, ?W) and not MapSet.member?(state.keys_processed, ?W) do 578 | state = update_in(state.level, &(rem(&1 + 1, tuple_size(state.levels)))) 579 | put_in(state.keys_processed, MapSet.put(state.keys_processed, ?W)) 580 | else 581 | state 582 | end 583 | if MapSet.member?(state.keys, ?S) and not MapSet.member?(state.keys_processed, ?S) do 584 | state = if state.level > 0 do 585 | update_in(state.level, &(&1 - 1)) 586 | else 587 | put_in(state.level, 3) 588 | end 589 | put_in(state.keys_processed, MapSet.put(state.keys_processed, ?S)) 590 | else 591 | state 592 | end 593 | else 594 | state 595 | end 596 | 597 | state = 598 | if state.game_state == :active do 599 | velocity = state.player.velocity * dt / 1_000 600 | 601 | state = 602 | if MapSet.member?(state.keys, ?A) or 603 | MapSet.member?(state.keys, ?H) or 604 | MapSet.member?(state.keys, 314) do 605 | {_, new_state} = 606 | get_and_update_in(state.player.position, fn {x, y} = orig -> 607 | if x >= 0 do 608 | {orig, {x - velocity, y}} 609 | else 610 | {orig, orig} 611 | end 612 | end) 613 | 614 | {player_x, _} = state.player.position 615 | 616 | new_ball = 617 | if player_x >= 0 and state.ball.stuck do 618 | {ball_x, ball_y} = state.ball.game_object.position 619 | 620 | b = 621 | BallObject.new( 622 | Vec2.new(ball_x - velocity, ball_y), 623 | state.ball.radius, 624 | state.ball.game_object.velocity, 625 | state.ball.game_object.sprite 626 | ) 627 | 628 | %BallObject{ 629 | state.ball 630 | | game_object: b.game_object, 631 | stuck: state.ball.stuck, 632 | radius: state.ball.radius 633 | } 634 | else 635 | state.ball 636 | end 637 | 638 | %Breakout.State{new_state | ball: new_ball} 639 | else 640 | state 641 | end 642 | 643 | state = 644 | if MapSet.member?(state.keys, ?D) or 645 | MapSet.member?(state.keys, ?L) or 646 | MapSet.member?(state.keys, 316) do 647 | {_, new_state} = 648 | get_and_update_in(state.player.position, fn {x, y} = orig -> 649 | if x + 100 <= @screen_width do 650 | {orig, {x + velocity, y}} 651 | else 652 | {orig, orig} 653 | end 654 | end) 655 | 656 | {player_x, _} = state.player.position 657 | 658 | new_ball = 659 | if player_x + 100 <= @screen_width and state.ball.stuck do 660 | {ball_x, ball_y} = state.ball.game_object.position 661 | 662 | b = 663 | BallObject.new( 664 | Vec2.new(ball_x + velocity, ball_y), 665 | state.ball.radius, 666 | state.ball.game_object.velocity, 667 | state.ball.game_object.sprite 668 | ) 669 | 670 | %BallObject{ 671 | state.ball 672 | | game_object: b.game_object, 673 | stuck: state.ball.stuck, 674 | radius: state.ball.radius 675 | } 676 | else 677 | state.ball 678 | end 679 | 680 | %Breakout.State{new_state | ball: new_ball} 681 | else 682 | state 683 | end 684 | 685 | state 686 | else 687 | state 688 | end 689 | 690 | state = 691 | if MapSet.member?(state.keys, ~c" " |> hd) do 692 | %Breakout.State{ 693 | state 694 | | ball: %BallObject{ 695 | state.ball 696 | | game_object: state.ball.game_object, 697 | radius: state.ball.radius, 698 | stuck: false 699 | } 700 | } 701 | else 702 | state 703 | end 704 | 705 | {:noreply, state} 706 | end 707 | 708 | def handle_info(:render, %Breakout.State{} = state) do 709 | :wx.batch(fn -> 710 | :gl.clearColor(0.0, 0.0, 0.0, 1.0) 711 | :gl.clear(:gl_const.gl_color_buffer_bit()) 712 | 713 | if state.game_state in [:active, :menu] do 714 | PostProcessor.begin_render(state.post_processor) 715 | 716 | Sprite.draw( 717 | state, 718 | :background, 719 | Vec2.new(0, 0), 720 | Vec2.new(state.width, state.height), 721 | 0, 722 | Vec3.new(1, 1, 1) 723 | ) 724 | 725 | level = state.levels |> elem(state.level) 726 | GameLevel.draw(level, state.sprite_renderer, state) 727 | GameObject.draw(state.player, :paddle, state.sprite_renderer, state) 728 | ParticleGenerator.draw(state.particle_generator) 729 | GameObject.draw(state.ball.game_object, :face, state.sprite_renderer, state) 730 | 731 | Enum.each(state.power_ups, fn %PowerUp{game_object: %GameObject{}} = power_up -> 732 | unless power_up.game_object.destroyed do 733 | GameObject.draw(power_up.game_object, power_up.type, state.sprite_renderer, state) 734 | end 735 | end) 736 | 737 | PostProcessor.end_render(state.post_processor) 738 | PostProcessor.render(state.post_processor, state.elapsed) 739 | 740 | Sprite.draw( 741 | state, 742 | :string, 743 | Vec2.new(10, 0), 744 | Vec2.new(200, 100), 745 | 0, 746 | Vec3.new(1, 1, 1) 747 | ) 748 | end 749 | 750 | if state.game_state == :menu do 751 | {w, h} = state.menu_string_size 752 | Sprite.draw( 753 | state, 754 | :menu_string, 755 | # TODO: either I'm very tired, or this is.. weird. 756 | Vec2.new((state.width - w / 2) / 2, state.height / 2 - h), 757 | state.menu_string_size, 758 | 0, 759 | Vec3.new(1, 1, 1) 760 | ) 761 | end 762 | 763 | :wxGLCanvas.swapBuffers(state.window.canvas) 764 | end) 765 | 766 | {:noreply, state} 767 | end 768 | 769 | @impl :wx_object 770 | def handle_info(info, state) do 771 | Logger.debug(info: info, state: state) 772 | 773 | {:noreply, state} 774 | end 775 | 776 | defp reset_level(%State{} = state) do 777 | levels = 778 | put_elem( 779 | state.levels, 780 | state.level, 781 | GameLevel.load( 782 | Breakout.Util.to_priv("levels/#{level_name(state.level)}"), 783 | @screen_width, 784 | @screen_height / 2 785 | ) 786 | ) 787 | 788 | state = put_in(state.levels, levels) 789 | put_in(state.lives, 3) 790 | end 791 | 792 | defp level_name(0), do: "one.lvl" 793 | defp level_name(1), do: "two.lvl" 794 | defp level_name(2), do: "three.lvl" 795 | defp level_name(3), do: "four.lvl" 796 | 797 | defp reset_player(%State{} = state) do 798 | player = %{state.player | position: @player_position, size: Vec2.new(100, 20)} 799 | 800 | put_in(state.player, player) 801 | end 802 | 803 | defp reset_ball(state) do 804 | ball = 805 | BallObject.reset( 806 | state.ball, 807 | Vec2.add( 808 | state.player.position, 809 | Vec2.new(100 / 2 - @ball_radius, -@ball_radius * 2) 810 | ), 811 | @initial_ball_velocity 812 | ) 813 | 814 | put_in(state.ball, ball) 815 | end 816 | 817 | defp should_spawn(chance) do 818 | :rand.uniform(chance) == chance 819 | end 820 | 821 | @spec spawn_power_ups(state :: Breakout.State.t(), block :: GameObject.t()) :: 822 | Breakout.State.t() 823 | defp spawn_power_ups(state, block) do 824 | state 825 | |> maybe_spawn_power_up(:chaos, Vec3.new(0.9, 0.25, 0.25), 15, block, :chaos, 10) 826 | |> maybe_spawn_power_up(:confuse, Vec3.new(1, 0.3, 0.3), 15, block, :confuse, 10) 827 | |> maybe_spawn_power_up(:increase, Vec3.new(1, 0.6, 0.4), 10, block, :increase, 10) 828 | |> maybe_spawn_power_up(:passthrough, Vec3.new(0.5, 1, 0.5), 10, block, :passthrough, 10) 829 | |> maybe_spawn_power_up(:speed, Vec3.new(0.5, 0.5, 1), 10, block, :speed, 10) 830 | |> maybe_spawn_power_up(:sticky, Vec3.new(1, 0.5, 1), 10, block, :sticky, 10) 831 | end 832 | 833 | @spec maybe_spawn_power_up( 834 | state :: Breakout.State.t(), 835 | type :: PowerUp.power_up_types(), 836 | color :: Vec3.t(), 837 | duration :: number(), 838 | block :: GameObject.t(), 839 | texture :: atom(), 840 | chance :: non_neg_integer() 841 | ) :: Breakout.State.t() 842 | defp maybe_spawn_power_up(state, type, color, duration, %GameObject{} = block, texture, chance) do 843 | if should_spawn(chance) do 844 | power_up = 845 | PowerUp.new( 846 | type, 847 | color, 848 | duration + 0.0, 849 | block.position, 850 | state.resources.textures[texture] 851 | ) 852 | 853 | Map.update(state, :power_ups, [power_up], &[power_up | &1]) 854 | else 855 | state 856 | end 857 | end 858 | 859 | @spec activate_power_up(state :: State.t(), power_up :: PowerUp.t()) :: State.t() 860 | def activate_power_up(%State{} = state, %PowerUp{} = power_up) do 861 | case power_up.type do 862 | :speed -> 863 | update_in(state.ball.game_object.velocity, &Vec2.scale(&1, 1.2)) 864 | 865 | :sticky -> 866 | %State{ 867 | state 868 | | ball: %{state.ball | sticky: true}, 869 | player: %{state.player | color: Vec3.new(1, 0.5, 1)} 870 | } 871 | 872 | :passthrough -> 873 | %State{ 874 | state 875 | | ball: %{state.ball | passthrough: true} 876 | } 877 | 878 | :increase -> 879 | %State{ 880 | state 881 | | player: %{ 882 | state.player 883 | | size: {(state.player.size |> elem(0)) + 50, state.player.size |> elem(1)} 884 | } 885 | } 886 | 887 | :confuse -> 888 | unless state.post_processor.chaos do 889 | %State{ 890 | state 891 | | post_processor: %{state.post_processor | confuse: true} 892 | } 893 | else 894 | state 895 | end 896 | 897 | :chaos -> 898 | unless state.post_processor.confuse do 899 | %State{ 900 | state 901 | | post_processor: %{state.post_processor | chaos: true} 902 | } 903 | else 904 | state 905 | end 906 | _ -> 907 | state 908 | end 909 | end 910 | 911 | @spec update_power_ups(state :: State.t(), dt :: float()) :: State.t() 912 | defp update_power_ups(%State{} = state, dt) do 913 | state = 914 | Enum.with_index(state.power_ups) 915 | |> Enum.reduce(state, fn {%PowerUp{} = power_up, index}, %State{} = acc -> 916 | power_up = 917 | power_up.game_object.position 918 | |> update_in(&Vec2.add(&1, Vec2.scale(power_up.game_object.velocity, dt / 1_000))) 919 | 920 | if power_up.activated do 921 | # TODO: subtract some better amount of `dt` instead 922 | power_up = update_in(power_up.duration, &(&1 - dt / 500.0)) 923 | 924 | power_up = 925 | if power_up.duration <= 0 do 926 | put_in(power_up.activated, false) 927 | else 928 | power_up 929 | end 930 | 931 | updated = List.update_at(acc.power_ups, index, fn _ -> power_up end) 932 | 933 | acc = put_in(acc.power_ups, updated) 934 | 935 | unless power_up.activated do 936 | case power_up.type do 937 | :sticky -> 938 | unless is_other_power_up_active(acc.power_ups, :sticky) do 939 | acc = put_in(acc.ball.sticky, false) 940 | put_in(acc.player.color, Vec3.new(1, 1, 1)) 941 | else 942 | acc 943 | end 944 | 945 | :passthrough -> 946 | unless is_other_power_up_active(acc.power_ups, :passthrough) do 947 | acc = put_in(acc.ball.passthrough, false) 948 | put_in(acc.ball.game_object.color, Vec3.new(1, 1, 1)) 949 | else 950 | acc 951 | end 952 | 953 | :confuse -> 954 | unless is_other_power_up_active(acc.power_ups, :confuse) do 955 | put_in(acc.post_processor.confuse, false) 956 | else 957 | acc 958 | end 959 | 960 | :chaos -> 961 | unless is_other_power_up_active(acc.power_ups, :chaos) do 962 | put_in(acc.post_processor.chaos, false) 963 | else 964 | acc 965 | end 966 | 967 | _ -> 968 | acc 969 | end 970 | else 971 | acc 972 | end 973 | else 974 | updated = List.update_at(acc.power_ups, index, fn _ -> power_up end) 975 | 976 | put_in(acc.power_ups, updated) 977 | end 978 | end) 979 | 980 | update_in(state.power_ups, fn el -> 981 | Enum.reject(el, fn %PowerUp{game_object: %GameObject{}} = power_up -> 982 | power_up.game_object.destroyed and not power_up.activated 983 | end) 984 | end) 985 | end 986 | 987 | defp is_other_power_up_active(power_ups, type) do 988 | power_ups 989 | |> Enum.any?(&(&1.activated and &1.type == type)) 990 | end 991 | 992 | # This is taken from lib/wx/examples/demo/ex_gl.erl, with the following comment: 993 | # %% This algorithm (based on http://d0t.dbclan.de/snippets/gltext.html) 994 | # %% prints a string to a bitmap and loads that onto an opengl texture. 995 | # %% Comments for the createTexture function: 996 | # %% 997 | # %% "Creates a texture from the settings saved in TextElement, to be 998 | # %% able to use normal system fonts conviently a wx.MemoryDC is 999 | # %% used to draw on a wx.Bitmap. As wxwidgets device contexts don't 1000 | # %% support alpha at all it is necessary to apply a little hack to 1001 | # %% preserve antialiasing without sticking to a fixed background 1002 | # %% color: 1003 | # %% 1004 | # %% We draw the bmp in b/w mode so we can use its data as a alpha 1005 | # %% channel for a solid color bitmap which after GL_ALPHA_TEST and 1006 | # %% GL_BLEND will show a nicely antialiased text on any surface. 1007 | # %% 1008 | # %% To access the raw pixel data the bmp gets converted to a 1009 | # %% wx.Image. Now we just have to merge our foreground color with 1010 | # %% the alpha data we just created and push it all into a OpenGL 1011 | # %% texture and we are DONE *inhalesdelpy*" 1012 | 1013 | defp load_texture_by_string(font, brush, color, string, flip) do 1014 | tmp_bmp = :wxBitmap.new(200, 200) 1015 | tmp = :wxMemoryDC.new(tmp_bmp) 1016 | :wxMemoryDC.setFont(tmp, font) 1017 | {str_w, str_h} = :wxDC.getTextExtent(tmp, string) 1018 | :wxMemoryDC.destroy(tmp) 1019 | :wxBitmap.destroy(tmp_bmp) 1020 | 1021 | w = get_power_of_two_roof(str_w) 1022 | h = get_power_of_two_roof(str_h) 1023 | 1024 | bmp = :wxBitmap.new(w, h) 1025 | dc = :wxMemoryDC.new(bmp) 1026 | :wxMemoryDC.setFont(dc, font) 1027 | :wxMemoryDC.setBackground(dc, brush) 1028 | :wxMemoryDC.clear(dc) 1029 | :wxMemoryDC.setTextForeground(dc, {255, 255, 255}) 1030 | :wxMemoryDC.drawText(dc, string, {0, 0}) 1031 | 1032 | img_0 = :wxBitmap.convertToImage(bmp) 1033 | img = case flip do 1034 | true -> 1035 | img = :wxImage.mirror(img_0, horizontally: false) 1036 | :wxImage.destroy(img_0) 1037 | img 1038 | false -> 1039 | img_0 1040 | end 1041 | 1042 | alpha = :wxImage.getData(img) 1043 | data = colourize_image(alpha, color) 1044 | :wxImage.destroy(img) 1045 | :wxBitmap.destroy(bmp) 1046 | :wxMemoryDC.destroy(dc) 1047 | 1048 | [tid] = :gl.genTextures(1) 1049 | :gl.bindTexture(:gl_const.gl_texture_2d, tid) 1050 | :gl.texParameteri(:gl_const.gl_texture_2d, :gl_const.gl_texture_mag_filter, :gl_const.gl_linear) 1051 | :gl.texParameteri(:gl_const.gl_texture_2d, :gl_const.gl_texture_min_filter, :gl_const.gl_linear) 1052 | :gl.texEnvi(:gl_const.gl_texture_env, :gl_const.gl_texture_env_mode, :gl_const.gl_replace) 1053 | :gl.texImage2D(:gl_const.gl_texture_2d, 0, :gl_const.gl_rgba, w, h, 0, :gl_const.gl_rgba, :gl_const.gl_unsigned_byte, data) 1054 | 1055 | {%Texture2D{ 1056 | id: tid, 1057 | width: w, 1058 | height: h, 1059 | internal_format: :gl_const.gl_rgba, 1060 | image_format: :gl_const.gl_rgba, 1061 | wrap_s: :gl_const.gl_repeat, 1062 | wrap_t: :gl_const.gl_repeat, 1063 | filter_min: :gl_const.gl_linear, 1064 | filter_max: :gl_const.gl_linear 1065 | }, str_w, str_h} 1066 | end 1067 | 1068 | defp colourize_image(alpha, {r, g, b}) do 1069 | for <>, into: <<>> do 1070 | <> 1071 | end 1072 | end 1073 | 1074 | defp get_power_of_two_roof(x), do: get_power_of_two_roof_2(1, x) 1075 | defp get_power_of_two_roof_2(n, x) when n >= x, do: n 1076 | defp get_power_of_two_roof_2(n, x), do: get_power_of_two_roof_2(n * 2, x) 1077 | end 1078 | -------------------------------------------------------------------------------- /lib/breakout/game_level.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.GameLevel do 2 | # alias Breakout.ResourceManager 3 | alias Breakout.GameObject 4 | alias Breakout.Math.Vec3 5 | 6 | @type t :: %__MODULE__{ 7 | bricks: [GameObject.t()] 8 | } 9 | 10 | defstruct bricks: [] 11 | 12 | def new() do 13 | end 14 | 15 | def load(file, width, height) do 16 | data = 17 | File.stream!(file) 18 | |> Stream.map(&String.trim/1) 19 | |> Stream.map(&String.split/1) 20 | |> Stream.map(fn row -> 21 | Enum.map(row, &String.to_integer/1) 22 | end) 23 | |> Enum.into([]) 24 | 25 | init(data, width, height) 26 | end 27 | 28 | def draw(level, renderer, state) do 29 | Enum.each(level.bricks, fn tile -> 30 | unless tile.destroyed, do: GameObject.draw(tile, tile.sprite, renderer, state) 31 | end) 32 | end 33 | 34 | def is_completed(level) do 35 | Enum.any?(level, fn tile -> 36 | not tile.is_solid && not tile.destroyed 37 | end) 38 | end 39 | 40 | defp init(tile_data, level_width, level_height) do 41 | height = length(tile_data) 42 | width = length(tile_data |> hd) 43 | unit_width = level_width / width 44 | unit_height = level_height / height 45 | 46 | bricks = 47 | for {row, y} <- Enum.with_index(tile_data), 48 | {tile, x} <- Enum.with_index(row), 49 | reduce: [] do 50 | acc -> 51 | pos = {unit_width * x, unit_height * y} 52 | size = {unit_width, unit_height} 53 | 54 | case tile do 55 | 1 -> 56 | # {:ok, texture} = ResourceManager.get_texture(:block_solid) 57 | obj = %GameObject{ 58 | # TODO: I need to reorganize ResourceManager 59 | position: pos, 60 | size: size, 61 | sprite: :block_solid, 62 | color: Vec3.new(0.8, 0.8, 0.7), 63 | is_solid: true 64 | } 65 | 66 | [obj | acc] 67 | 68 | t when t > 1 -> 69 | color = 70 | case t do 71 | 2 -> Vec3.new(0.2, 0.6, 1.0) 72 | 3 -> Vec3.new(0, 0.7, 0) 73 | 4 -> Vec3.new(0.8, 0.8, 0.4) 74 | 5 -> Vec3.new(1, 0.5, 0) 75 | end 76 | 77 | # {:ok, texture} = ResourceManager.get_texture(:block) 78 | obj = %GameObject{ 79 | position: pos, 80 | size: size, 81 | sprite: :block, 82 | color: color, 83 | # this is the default, but just to be explicit 84 | is_solid: false 85 | } 86 | 87 | [obj | acc] 88 | 89 | _ -> 90 | acc 91 | end 92 | end 93 | 94 | %__MODULE__{bricks: bricks} 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/breakout/game_object.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.GameObject do 2 | alias Breakout.Renderer.Sprite 3 | # alias Breakout.Renderer.Texture2D 4 | alias Breakout.Math.Vec3 5 | alias Breakout.Math.Vec2 6 | 7 | @type t :: %__MODULE__{ 8 | position: Vec2.t(), 9 | size: Vec2.t(), 10 | velocity: Vec2.t(), 11 | color: Vec3.t(), 12 | rotation: float(), 13 | is_solid: boolean(), 14 | destroyed: boolean(), 15 | sprite: atom() 16 | } 17 | 18 | defstruct position: Vec2.new(0, 0), 19 | size: Vec2.new(1, 1), 20 | velocity: Vec2.new(0, 0), 21 | color: Vec3.new(1, 1, 1), 22 | rotation: 0.0, 23 | is_solid: false, 24 | destroyed: false, 25 | sprite: nil 26 | 27 | def new() do 28 | %__MODULE__{} 29 | end 30 | 31 | def new(position, size, sprite, color, velocity) do 32 | %__MODULE__{ 33 | position: position, 34 | size: size, 35 | sprite: sprite, 36 | color: color, 37 | velocity: velocity 38 | } 39 | end 40 | 41 | def draw( 42 | %__MODULE__{ 43 | sprite: _sprite, 44 | position: position, 45 | size: size, 46 | rotation: rotation, 47 | color: color 48 | } = _game_object, 49 | name, 50 | _renderer, 51 | state 52 | ) do 53 | Sprite.draw(state, name, position, size, rotation, color) 54 | end 55 | 56 | def check_collision(%__MODULE__{position: {ax, ay}, size: {aw, ah}}, %__MODULE__{ 57 | position: {bx, by}, 58 | size: {bw, bh} 59 | }) do 60 | collision_x = ax + aw >= bx and bx + bw >= ax 61 | collision_y = ay + ah >= by and by + bh >= ay 62 | 63 | collision_x and collision_y 64 | end 65 | 66 | def check_collision(%{game_object: %{position: ball_position}, radius: radius}, %__MODULE__{ 67 | size: {w, h}, 68 | position: {x, y} 69 | }) do 70 | center = Vec2.add(ball_position, Vec2.new(radius, radius)) 71 | 72 | {aabb_half_x, aabb_half_y} = aabb_half_extents = Vec2.new(w / 2, h / 2) 73 | aabb_center = Vec2.new(x + aabb_half_x, y + aabb_half_y) 74 | 75 | diff = Vec2.subtract(center, aabb_center) 76 | 77 | clamped = Vec2.clamp(diff, Vec2.scale(aabb_half_extents, -1.0), aabb_half_extents) 78 | 79 | closest = Vec2.add(aabb_center, clamped) 80 | 81 | diff = Vec2.subtract(closest, center) 82 | 83 | if Vec2.length(diff) < radius do 84 | {true, Vec2.direction(diff), diff} 85 | else 86 | {false, :up, Vec2.new(0, 0)} 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/breakout/image_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.ImageParser do 2 | require Logger 3 | def parse(data) do 4 | res = do_parse(data, %{}) 5 | {:ok, res} 6 | end 7 | 8 | defp do_parse(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, rest::binary>>, acc) do 9 | do_png_parse(rest, acc) 10 | |> decompress_png() 11 | |> reconstruct_png() 12 | end 13 | 14 | defp do_parse(<<0xFF, 0xD8, 0xFF, rest::binary>>, acc) do 15 | do_jpg_parse(rest, acc) 16 | end 17 | 18 | def do_jpg_parse(_rest, _acc) do 19 | end 20 | 21 | defp decompress_png(%{IDAT: data} = map) do 22 | Map.put(map, :IDAT, :zlib.uncompress(data)) 23 | end 24 | 25 | defp paeth_predictor(a, b, c) do 26 | p = a + b - c 27 | pa = abs(p - a) 28 | pb = abs(p - b) 29 | pc = abs(p - c) 30 | 31 | cond do 32 | pa <= pb and pa <= pc -> a 33 | pb <= pc -> b 34 | true -> c 35 | end 36 | end 37 | 38 | defp recon_a(recon, _width, _height, _stride, bytes_per_pixel, i, c) do 39 | if c >= bytes_per_pixel do 40 | :binary.at(recon, i - bytes_per_pixel) 41 | else 42 | 0 43 | end 44 | end 45 | 46 | defp recon_b(recon, _width, _height, stride, _bytes_per_pixel, i, _c) do 47 | if i >= stride do 48 | :binary.at(recon, i - stride) 49 | else 50 | 0 51 | end 52 | end 53 | 54 | defp recon_c(recon, _width, _height, stride, bytes_per_pixel, i, c) do 55 | if i >= stride and c >= bytes_per_pixel do 56 | :binary.at(recon, i - stride - bytes_per_pixel) 57 | else 58 | 0 59 | end 60 | end 61 | 62 | defp reconstruct_png(%{IDAT: data, IHDR: %{width: width, height: height, type: type, depth: depth}} = map) do 63 | if width == 256 and height == 96, do: dbg(map) 64 | bytes_per_pixel = case type do 65 | 0 -> depth / 8 66 | 2 -> 3 * depth / 8 67 | 3 -> depth / 8 68 | 4 -> 4 * depth / 8 69 | 6 -> 4 * depth / 8 70 | _ -> raise "unknown type #{type}" 71 | end 72 | |> trunc() 73 | 74 | stride = width * bytes_per_pixel 75 | 76 | res = reconstruct_rows(data, width, height, stride, bytes_per_pixel, <<>>, 0) 77 | Map.put(map, :IDAT, res) 78 | end 79 | 80 | defp reconstruct_rows(<<>>, _width, _height, _stride, _bytes_per_pixel, recon, _i), do: recon 81 | 82 | defp reconstruct_rows( 83 | <>, 84 | width, 85 | height, 86 | stride, 87 | bytes_per_pixel, 88 | recon, 89 | i 90 | ) do 91 | {recon, rest, i} = 92 | 0..(stride - 1) 93 | |> Enum.reduce({recon, rest, i}, fn c, {recon_acc, rest_acc, i_acc} -> 94 | <> = rest_acc 95 | 96 | recon_x = 97 | case filter_type do 98 | 0 -> 99 | filt_x 100 | 101 | 1 -> 102 | filt_x + recon_a(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c) 103 | 104 | 2 -> 105 | filt_x + recon_b(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c) 106 | 107 | 3 -> 108 | filt_x + 109 | div( 110 | recon_a(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c) + 111 | recon_b(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c), 112 | 2 113 | ) 114 | 115 | 4 -> 116 | filt_x + 117 | paeth_predictor( 118 | recon_a(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c), 119 | recon_b(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c), 120 | recon_c(recon_acc, width, height, stride, bytes_per_pixel, i_acc, c) 121 | ) 122 | 123 | _ -> 124 | raise "unknown filter type: #{filter_type}" 125 | end 126 | 127 | {<>)>>, rest_acc, i_acc + 1} 128 | end) 129 | 130 | reconstruct_rows(rest, width, height, stride, bytes_per_pixel, recon, i) 131 | end 132 | 133 | defp do_png_parse(<>, acc) do 134 | my_crc = :erlang.crc32(<>) 135 | 136 | unless my_crc == crc do 137 | raise "Invalid crc" 138 | end 139 | 140 | acc = 141 | case do_png_chunk(<>, <>) do 142 | {:IDAT, data} -> Map.put(acc, :IDAT, Map.get(acc, :IDAT, <<>>) <> data) 143 | {key, vals} -> Map.put(acc, key, vals) 144 | _ -> acc 145 | end 146 | 147 | do_png_parse(rest, acc) 148 | end 149 | 150 | defp do_png_parse(<<>>, acc), do: acc 151 | 152 | defp do_png_chunk( 153 | "IHDR", 154 | <> = 155 | _data 156 | ) do 157 | {:IHDR, 158 | %{ 159 | width: width, 160 | height: height, 161 | depth: depth, 162 | type: type, 163 | compression: compression, 164 | filter: filter, 165 | interlace: interlace 166 | }} 167 | end 168 | 169 | defp do_png_chunk("IDAT", data) do 170 | {:IDAT, data} 171 | end 172 | 173 | defp do_png_chunk("IEND", _) do 174 | :ok 175 | end 176 | 177 | defp do_png_chunk("sRGB", <>) do 178 | {:sRGB, %{intent: intent}} 179 | end 180 | 181 | defp do_png_chunk("pHYs", <>) do 182 | {:pHYs, %{x: x, y: y, unit: unit}} 183 | end 184 | 185 | defp do_png_chunk("tIME", <>) do 186 | {:tIME, %{time: NaiveDateTime.new(year, month, day, hour, minute, second)}} 187 | end 188 | 189 | defp do_png_chunk("tEXt", data) do 190 | [keyword, data] = :binary.split(data, <<0>>) 191 | {:tEXt, %{keyword: keyword, data: data}} 192 | end 193 | 194 | defp do_png_chunk("iTXt", data) do 195 | [keyword, data] = :binary.split(data, <<0>>) 196 | <> = data 197 | 198 | [lang_tag, data] = :binary.split(data, <<0>>) 199 | [translated_keyword, data] = :binary.split(data, <<0>>) 200 | 201 | xml = :xmerl_scan.string(:binary.bin_to_list(data)) 202 | 203 | {:iTXt, 204 | %{ 205 | keyword: keyword, 206 | data: xml, 207 | compression_flag: compression_flag, 208 | compression_method: compression_method, 209 | lang_tag: lang_tag, 210 | translated_keyword: translated_keyword 211 | }} 212 | end 213 | 214 | defp do_png_chunk("eXIf", data) do 215 | {:eXIf, data} 216 | end 217 | 218 | defp do_png_chunk("iCCP", data) do 219 | [profile_name, data] = :binary.split(data, <<0>>) 220 | 221 | <> = data 222 | 223 | uncompressed = :zlib.uncompress(compressed_profile) 224 | 225 | # File.write("uncompressed", uncompressed) 226 | <> = uncompressed 227 | 228 | << 229 | profile_size::unsigned-size(4)-unit(8), # 0-3 230 | preferred_cmm_type::binary-size(4), # 4-7 231 | # profile_version_raw::binary-size(2), # 8-11 232 | 0::size(4), 233 | profile_version_major::size(4), 234 | profile_version_minor::size(4), 235 | 0::size(4), 236 | 0::unit(8)-size(2), # Lowest two are reserved 237 | profile_class::binary-size(4), # 12-15 238 | color_space::binary-size(4), # 16-20 239 | pcs::binary-size(4), # 20-23 240 | creation_date_time::binary-size(12), # 24-35 241 | "acsp", # 36-39 242 | primary_platform_signature::binary-size(4), # 40-43 243 | # profile_flags::binary-size(4), # 44-47 244 | flag_embedded::size(1), 245 | flag_independent::size(1), 246 | 0::size(30), 247 | device_manufacturer::binary-size(4), # 48-51 248 | device_model::binary-size(4), # 52-55 249 | # device_attributes::binary-size(8), # 56-63 (really, 56-59) 250 | # TODO: these should probably be something like 251 | # attributes: %{reflective: true, glossy: false, ...} 252 | # or something. kind of awkward since they're mutually exclusive. 253 | reflective_or_transparency::size(1), 254 | glossy_or_matte::size(1), 255 | positive_or_negative_polarity::size(1), 256 | color_or_bw::size(1), 257 | 0::size(28), 258 | device_attributes::binary-size(4), # really, 60-63 259 | rendering_intent::binary-size(4), # 64-67 260 | nciexyz::binary-size(12), # 68-79 261 | signature::binary-size(4), # 80-83 262 | profile_id::binary-size(16), # 84-99 263 | 0::unit(8)-size(28) # 100-127 264 | >> = header 265 | 266 | profile_version = "#{profile_version_major}.#{profile_version_minor}" 267 | 268 | iccp = %{ 269 | profile_name: profile_name, 270 | compression_method: compression_method, 271 | compressed_profile: compressed_profile, 272 | uncompressed: uncompressed, 273 | profile_size: profile_size, 274 | preferred_cmm_type: preferred_cmm_type, 275 | profile_version_major: profile_version_major, 276 | profile_version_minor: profile_version_minor, 277 | profile_version: profile_version, 278 | profile_class_raw: profile_class, 279 | profile_class: case profile_class do 280 | "mntr" -> :monitor 281 | "scnr" -> :scanner 282 | "prtr" -> :printer 283 | "link" -> :link 284 | "abst" -> :abstract 285 | "spac" -> :space 286 | "nmcl" -> :named_color 287 | _ -> :unknown 288 | end, 289 | color_space_raw: color_space, 290 | color_space: case color_space do 291 | "XYZ " -> [:nciexyz, :pcsxyz] 292 | "Lab " -> [:cielab, :pcslab] 293 | "Luv " -> :cieluv 294 | "YCbr" -> :ycbcr 295 | "Yxy " -> :cieyxy 296 | "RGB " -> :rgb 297 | "GRAY" -> :gray 298 | "HSV " -> :hsv 299 | "HLS " -> :hls 300 | "CMYK" -> :cmyk 301 | "CMY " -> :cmy 302 | # 2-15 color 303 | <> when digit in ?0..?9//1 -> 304 | String.to_atom("color_#{[digit]}") 305 | <> when digit in ?A..?F//1 -> 306 | String.to_atom("color_#{digit - ?A + 10}") 307 | end, 308 | pcs_raw: pcs, 309 | # TODO: kind of awkward to duplicate this. 310 | pcs: case color_space do 311 | # oh, this is where I decide (n)cie/pcs, I think? 312 | "XYZ " -> [:nciexyz, :pcsxyz] 313 | "Lab " -> [:cielab, :pcslab] 314 | _ -> :unknown 315 | end, 316 | creation_date_time_raw: creation_date_time, 317 | creation_date_time: parse_iccp_date_time(creation_date_time), 318 | primary_platform_signature_raw: primary_platform_signature, 319 | primary_platform_signature: case primary_platform_signature do 320 | "MSFT" -> :microsoft 321 | "APPL" -> :apple 322 | "ADBE" -> :adobe # this isn't specified in icc.1:2022, but is in some random other icc profile parser (icc node package) 323 | "SUNW" -> :sun_microsystems 324 | "SGI " -> :silicon_graphics 325 | "TGNT" -> :taligent # this isn't specified in icc.1:2022, but is in some random other icc profile parser (icc node package) 326 | _ -> :unknown 327 | end, 328 | profile_flags: %{ 329 | embedded: flag_embedded, 330 | independent: flag_independent, 331 | }, 332 | device_manufacturer: device_manufacturer, 333 | device_model: device_model, 334 | device_attributes: device_attributes, 335 | reflective_or_transparency: reflective_or_transparency, 336 | glossy_or_matte: glossy_or_matte, 337 | positive_or_negative_polarity: positive_or_negative_polarity, 338 | color_or_bw: color_or_bw, 339 | rendering_intent: rendering_intent, 340 | nciexyz: nciexyz, 341 | signature: signature, 342 | profile_id: profile_id, 343 | # TODO: This is just a list of pointers to the rest of the data 344 | tag_table: parse_tag_table(tag_table), 345 | } 346 | 347 | {:iCCP, iccp} 348 | end 349 | 350 | defp do_png_chunk("cHRM", data) do 351 | << 352 | white_x::32, 353 | white_y::32, 354 | red_x::32, 355 | red_y::32, 356 | green_x::32, 357 | green_y::32, 358 | blue_x::32, 359 | blue_y::32 360 | >> = data 361 | 362 | chrm = %{ 363 | white_x: white_x / 100_000, 364 | white_y: white_y / 100_000, 365 | red_x: red_x / 100_000, 366 | red_y: red_y / 100_000, 367 | green_x: green_x / 100_000, 368 | green_y: green_y / 100_000, 369 | blue_x: blue_x / 100_000, 370 | blue_y: blue_y / 100_000, 371 | } 372 | 373 | {:cHRM, chrm} 374 | end 375 | 376 | defp do_png_chunk(type, data) do 377 | IO.puts("unknown type #{type}") 378 | {String.to_atom(type), %{data: data}} 379 | end 380 | 381 | defp parse_iccp_date_time(<>) do 382 | DateTime.new(Date.new!(year, month, day), Time.new!(hours, minutes, seconds)) 383 | end 384 | 385 | defp parse_tag_table(tag_table) do 386 | # File.write("tag_table_raw", tag_table) 387 | # <> = tag_table 388 | <> = tag_table 389 | 390 | table = do_parse_tag_table(tag_table, [], count) 391 | 392 | # File.write("tag_table", tag_table) 393 | 394 | <<_tag_table::binary-size(count * 12), data::binary>> = tag_table 395 | 396 | # File.write("data", data) 397 | 398 | first_offset = table |> hd() |> elem(1) 399 | table = Enum.map(table, fn {sig, offset, size} -> 400 | {sig, offset - first_offset, size} 401 | end) 402 | 403 | tagged_data = do_parse_tagged_data(table, data, %{}) 404 | 405 | {table, tagged_data} 406 | 407 | # Enum.reduce(tag_table, fn <> = el, acc -> 408 | # IO.inspect({signature, offset, size}, label: "tag") 409 | # [el | acc] 410 | # end) 411 | end 412 | 413 | defp do_parse_tagged_data([], _data, acc) do 414 | acc 415 | end 416 | 417 | defp do_parse_tagged_data(_table, <<>>, acc) do 418 | # this is happening for every image it seems, so just mute the warning for now 419 | # Logger.warning("tag table indicates there's more data, but no more data exists in the tagged element data", msg: table) 420 | acc 421 | end 422 | 423 | defp do_parse_tagged_data(table, <<0::8, data::binary>>, acc) do 424 | do_parse_tagged_data(table, data, acc) 425 | end 426 | 427 | defp do_parse_tagged_data([{sig, _offset, size} | rest], data, acc) do 428 | <> = data 429 | parsed = do_parse_signature(to_parse) 430 | acc = put_in(acc[sig], parsed) 431 | do_parse_tagged_data(rest, next, acc) 432 | end 433 | 434 | defp do_parse_signature(<<"text", 0::32, text::binary>>) do 435 | text 436 | |> :binary.part(0, byte_size(text) - 1) 437 | end 438 | 439 | defp do_parse_signature(<<"desc", 0::32, rest::binary>>) do 440 | # from what I can tell.. this is like.. 441 | # 0::32, length::32, data::size(length), 442 | # then.. 0::8, 0::8, length::32, repeat? 443 | # <> = rest 444 | do_parse_desc(rest, []) 445 | end 446 | 447 | defp do_parse_signature(<<"XYZ ", 0::32, x_sign::16, x::signed-16, y_sign::16, y::16, z_sign::16, z::16>>) do 448 | # TODO: this is wrong, but the spec is pretty unclear about how this is supposed to work. 449 | {x_sign + x, y_sign + y, z_sign + z} 450 | end 451 | 452 | defp do_parse_signature(<<"view", 0::32, illuminant::binary-size(12), surround::binary-size(12), type::binary>>) do 453 | {illuminant, surround, type} 454 | end 455 | 456 | defp do_parse_signature(<<"meas", 0::32, observer::32, tristimulus::binary-size(12), geo::32, flare::32, illuminant::32>>) do 457 | {observer, tristimulus, geo, flare, illuminant} 458 | end 459 | 460 | defp do_parse_signature(<<"sig ", 0::32, sig::binary>>) do 461 | sig 462 | end 463 | 464 | defp do_parse_signature(<<"curv", 0::32, count::32, curves::binary>>) do 465 | {count, curves} 466 | end 467 | 468 | defp do_parse_signature(<>) do 469 | Logger.warning("unknown signature #{data}") 470 | 471 | # continue parsing, why not 472 | rest 473 | end 474 | 475 | defp do_parse_desc(<<>>, acc), do: Enum.reverse(acc) 476 | 477 | defp do_parse_desc(<<0::32, rest::binary>>, acc) do 478 | # TODO: this is hacky. I'm not sure why it ends with a single null byte. 479 | if byte_size(rest) < 4 do 480 | do_parse_desc(<<>>, acc) 481 | else 482 | do_parse_desc(rest, acc) 483 | end 484 | end 485 | 486 | defp do_parse_desc(data, acc) do 487 | <> = data 488 | 489 | <> = next 490 | do_parse_desc(next, [res | acc]) 491 | end 492 | 493 | defp do_parse_tag_table(_data, acc, 0), do: Enum.reverse(acc) 494 | defp do_parse_tag_table(<<>>, _acc, _count), do: raise "premature end of data" 495 | 496 | defp do_parse_tag_table(<>, acc, count) do 497 | do_parse_tag_table(next, [{signature, offset, size} | acc], count - 1) 498 | end 499 | end 500 | defmodule BCD do 501 | def decode(val) do 502 | Integer.undigits(for <>, do: x) 503 | end 504 | end 505 | -------------------------------------------------------------------------------- /lib/breakout/input.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Input do 2 | require Logger 3 | 4 | import Breakout.WxRecords 5 | 6 | # we can just match on type, but matching on the specific mouse event type 7 | # (:motion, :left_down, :middle_dclick, etc.) lets us be more specific. 8 | # we need to connect the callback in Breakout.Window. 9 | def handler(wx(event: wxMouse(type: :motion, x: _x, y: _y)), state) do 10 | {:noreply, state} 11 | end 12 | 13 | def handler(wx(event: wxKey(type: type, x: _x, y: _y, keyCode: key_code)) = _request, state) do 14 | # IO.inspect(key_code, label: type) 15 | # send(Breakout.Game, {type, key_code}) 16 | :wx_object.cast(Breakout.Game, {type, key_code}) 17 | 18 | {:noreply, state} 19 | end 20 | 21 | def handler(request, state) do 22 | Logger.debug(request: request, state: state) 23 | 24 | {:noreply, state} 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/breakout/logger.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Logger do 2 | def format(level, message, {_date, time} = _timestamp, metadata) do 3 | # \t#{inspect(metadata)}\n" 4 | msg = "\n[#{level}] #{Logger.Formatter.format_time(time)}: #{message}\n\t" 5 | 6 | meta = 7 | for {k, v} <- metadata do 8 | "#{inspect(k)}, #{inspect(v)}" 9 | end 10 | |> Enum.join("\n\t") 11 | 12 | msg <> meta <> "\n" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/breakout/math/mat2.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Math.Mat2 do 2 | alias Breakout.Math.Vec2 3 | 4 | @type t :: {Vec2.t(), Vec2.t()} 5 | 6 | @spec flatten(matrix :: t()) :: 7 | {float(), float(), float(), float()} 8 | def flatten({{a0, a1}, {b0, b1}}) do 9 | {a0, a1, b0, b1} 10 | end 11 | 12 | @spec scale(mat :: t(), scalar :: number()) :: t() 13 | def scale({r0, r1}, scalar) do 14 | {Vec2.scale(r0, scalar), Vec2.scale(r1, scalar)} 15 | end 16 | 17 | @spec add(mat1 :: t(), mat2 :: t()) :: t() 18 | def add({r0, r1}, {s0, s1}) do 19 | {Vec2.add(r0, s0), Vec2.add(r1, s1)} 20 | end 21 | 22 | @spec determinant(mat :: t()) :: float() 23 | def determinant({{r0x, r0y}, {r1x, r1y}}) do 24 | r0x * r1y - r0y * r1x 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/breakout/math/mat3.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Math.Mat3 do 2 | alias Breakout.Math.Mat2 3 | alias Breakout.Math.Vec3 4 | 5 | @type t :: {Vec3.t(), Vec3.t(), Vec3.t()} 6 | 7 | @spec flatten(matrix :: t()) :: 8 | {float(), float(), float(), float(), float(), float(), float(), float(), float()} 9 | def flatten({{a0, a1, a2}, {b0, b1, b2}, {c0, c1, c2}}) do 10 | {a0, a1, a2, b0, b1, b2, c0, c1, c2} 11 | end 12 | 13 | @spec zero() :: t() 14 | def zero() do 15 | { 16 | Vec3.new(0, 0, 0), 17 | Vec3.new(0, 0, 0), 18 | Vec3.new(0, 0, 0) 19 | } 20 | end 21 | 22 | @spec identity() :: t() 23 | def identity() do 24 | { 25 | Vec3.new(1, 0, 0), 26 | Vec3.new(0, 1, 0), 27 | Vec3.new(0, 0, 1) 28 | } 29 | end 30 | 31 | @spec scale(mat :: t(), scalar :: number()) :: t() 32 | def scale({r0, r1, r2}, scalar) do 33 | { 34 | Vec3.scale(r0, scalar), 35 | Vec3.scale(r1, scalar), 36 | Vec3.scale(r2, scalar) 37 | } 38 | end 39 | 40 | @spec add(mat1 :: t(), mat2 :: t()) :: t() 41 | def add({r0, r1, r2}, {s0, s1, s2}) do 42 | { 43 | Vec3.add(r0, s0), 44 | Vec3.add(r1, s1), 45 | Vec3.add(r2, s2) 46 | } 47 | end 48 | 49 | @spec trace(mat :: t()) :: float() 50 | def trace({{r0x, _, _}, {_, r1y, _}, {_, _, r2z}}) do 51 | r0x * r0x + r1y * r1y + r2z * r2z 52 | end 53 | 54 | @spec determinant(mat :: t()) :: float() 55 | def determinant({{r0x, r0y, r0z}, {r1x, r1y, r1z}, {r2x, r2y, r2z}}) do 56 | i = r0x * (r1y * r2z - r1z * r2y) 57 | j = r0y * (r1x * r2z - r1z * r2x) 58 | k = r0z * (r1x * r2y - r1y * r2x) 59 | 60 | i - j + k 61 | end 62 | 63 | @spec transpose(mat :: t()) :: t() 64 | def transpose({ 65 | {r0x, r0y, r0z}, 66 | {r1x, r1y, r1z}, 67 | {r2x, r2y, r2z} 68 | }) do 69 | { 70 | {r0x, r1x, r2x}, 71 | {r0y, r1y, r2y}, 72 | {r0z, r1z, r2z} 73 | } 74 | end 75 | 76 | @spec inverse(mat :: t()) :: t() 77 | def inverse(m) do 78 | inv = { 79 | {cofactor(m, 0, 0), cofactor(m, 1, 0), cofactor(m, 2, 0)}, 80 | {cofactor(m, 0, 1), cofactor(m, 1, 1), cofactor(m, 2, 1)}, 81 | {cofactor(m, 0, 2), cofactor(m, 1, 2), cofactor(m, 2, 2)} 82 | } 83 | 84 | inv_det = 1 / determinant(m) 85 | 86 | scale(inv, inv_det) 87 | end 88 | 89 | @spec minor(mat :: t(), i :: integer(), j :: integer()) :: Mat2.t() 90 | def minor(mat, i, j) do 91 | rows = Tuple.to_list(mat) 92 | 93 | minor_rows = 94 | rows 95 | |> Enum.with_index() 96 | |> Enum.reject(fn {_row, y} -> y == j end) 97 | |> Enum.map(fn {row, _y} -> 98 | row 99 | |> Tuple.to_list() 100 | |> Enum.with_index() 101 | |> Enum.reject(fn {_value, x} -> x == i end) 102 | |> Enum.map(&elem(&1, 0)) 103 | |> List.to_tuple() 104 | end) 105 | 106 | List.to_tuple(minor_rows) 107 | end 108 | 109 | @spec cofactor(mat :: t(), i :: integer(), j :: integer()) :: float() 110 | def cofactor(mat, i, j) do 111 | :math.pow(-1, i + 1 + j + 1) * Mat2.determinant(minor(mat, i, j)) 112 | end 113 | 114 | @spec to_binary(matrix :: t()) :: binary() 115 | def to_binary({a, b, c}) do 116 | Vec3.to_binary(a) <> Vec3.to_binary(b) <> Vec3.to_binary(c) 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/breakout/math/mat4.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Math.Mat4 do 2 | alias Breakout.Math.Mat3 3 | alias Breakout.Math.Vec3 4 | alias Breakout.Math.Vec4 5 | 6 | @type t :: {Vec4.t(), Vec4.t(), Vec4.t(), Vec4.t()} 7 | 8 | @spec flatten(matrix :: t()) :: 9 | {float(), float(), float(), float(), float(), float(), float(), float(), float(), 10 | float(), float(), float(), float(), float(), float(), float()} 11 | def flatten({{a0, a1, a2, a3}, {b0, b1, b2, b3}, {c0, c1, c2, c3}, {d0, d1, d2, d3}}) do 12 | {a0, a1, a2, a3, b0, b1, b2, b3, c0, c1, c2, c3, d0, d1, d2, d3} 13 | end 14 | 15 | @spec zero() :: t() 16 | def zero() do 17 | { 18 | Vec4.new(0, 0, 0, 0), 19 | Vec4.new(0, 0, 0, 0), 20 | Vec4.new(0, 0, 0, 0), 21 | Vec4.new(0, 0, 0, 0) 22 | } 23 | end 24 | 25 | @spec identity() :: t() 26 | def identity() do 27 | { 28 | Vec4.new(1, 0, 0, 0), 29 | Vec4.new(0, 1, 0, 0), 30 | Vec4.new(0, 0, 1, 0), 31 | Vec4.new(0, 0, 0, 1) 32 | } 33 | end 34 | 35 | @spec trace(mat :: t()) :: float() 36 | def trace({{r0x, _, _, _}, {_, r1y, _, _}, {_, _, r2z, _}, {_, _, _, r3w}}) do 37 | r0x * r0x + r1y * r1y + r2z * r2z + r3w * r3w 38 | end 39 | 40 | @spec determinant(mat :: t()) :: float() 41 | def determinant(mat) do 42 | Enum.reduce(0..3, 0, fn j, acc -> 43 | minor_mat = minor(mat, 0, j) 44 | cofactor = elem(mat, 0) |> elem(j) 45 | sign = if rem(j, 2) == 0, do: 1, else: -1 46 | acc + cofactor * Mat3.determinant(minor_mat) * sign 47 | end) 48 | end 49 | 50 | @spec transpose(mat :: t()) :: t() 51 | def transpose(mat) do 52 | mat 53 | |> Tuple.to_list() 54 | |> Enum.map(&Tuple.to_list/1) 55 | |> Enum.zip() 56 | |> List.to_tuple() 57 | end 58 | 59 | @spec inverse(mat :: t()) :: t() 60 | def inverse(mat) do 61 | inv = 62 | for i <- 0..3 do 63 | for j <- 0..3 do 64 | cofactor(mat, i, j) 65 | end 66 | end 67 | |> Enum.map(&List.to_tuple/1) 68 | |> List.to_tuple() 69 | 70 | inv_det = 1 / determinant(mat) 71 | 72 | scale(inv, inv_det) 73 | end 74 | 75 | @spec minor(mat :: t(), i :: integer(), j :: integer()) :: Mat3.t() 76 | def minor(mat, i, j) do 77 | rows = Tuple.to_list(mat) 78 | 79 | minor_rows = 80 | rows 81 | |> Enum.with_index() 82 | |> Enum.reject(fn {_row, y} -> y == j end) 83 | |> Enum.map(fn {row, _y} -> 84 | row 85 | |> Tuple.to_list() 86 | |> Enum.with_index() 87 | |> Enum.reject(fn {_value, x} -> x == i end) 88 | |> Enum.map(&elem(&1, 0)) 89 | |> List.to_tuple() 90 | end) 91 | 92 | List.to_tuple(minor_rows) 93 | end 94 | 95 | @spec cofactor(mat :: t(), i :: integer(), j :: integer()) :: float() 96 | def cofactor(mat, i, j) do 97 | :math.pow(-1, i + 1 + j + 1) * Mat3.determinant(minor(mat, i, j)) 98 | end 99 | 100 | @spec orient(pos :: Vec3.t(), fwd :: Vec3.t(), up :: Vec3.t()) :: t() 101 | def orient({px, py, pz}, {fx, fy, fz} = fwd, {ux, uy, uz} = up) do 102 | {lx, ly, lz} = Vec3.cross(up, fwd) 103 | 104 | # this is for the physics coordinate system where 105 | # +x-axis = fwd 106 | # +y-axis = left 107 | # +z-axis = up 108 | # this I think will be weird with opengl? 109 | { 110 | Vec4.new(fx, lx, ux, px), 111 | Vec4.new(fy, ly, uy, py), 112 | Vec4.new(fz, lz, uz, pz), 113 | Vec4.new(0, 0, 0, 1) 114 | } 115 | end 116 | 117 | @spec scale(mat :: t(), scalar :: number()) :: t() 118 | def scale({r0, r1, r2, r3}, scalar) do 119 | { 120 | Vec4.scale(r0, scalar), 121 | Vec4.scale(r1, scalar), 122 | Vec4.scale(r2, scalar), 123 | Vec4.scale(r3, scalar) 124 | } 125 | end 126 | 127 | @spec add(m1 :: t(), m2 :: t()) :: t() 128 | def add({r0, r1, r2, r3}, {s0, s1, s2, s3}) do 129 | { 130 | Vec4.add(r0, s0), 131 | Vec4.add(r1, s1), 132 | Vec4.add(r2, s2), 133 | Vec4.add(r3, s3) 134 | } 135 | end 136 | 137 | @spec look_at(eye :: Vec3.t(), center :: Vec3.t(), up :: Vec3.t()) :: t() 138 | def look_at(eye, center, up) do 139 | f = 140 | center 141 | |> Vec3.subtract(eye) 142 | |> Vec3.normalize() 143 | 144 | s = 145 | f 146 | |> Vec3.cross(up) 147 | |> Vec3.normalize() 148 | 149 | u = Vec3.cross(s, f) 150 | 151 | {f0, f1, f2} = f 152 | {s0, s1, s2} = s 153 | {u0, u1, u2} = u 154 | 155 | # same not as with orient/2; this coordinate system is wrong for opengl 156 | # .. I think 157 | { 158 | {s0, u0, -f0, 0.0}, 159 | {s1, u1, -f1, 0.0}, 160 | {s2, u2, -f2, 0.0}, 161 | {-Vec3.dot(s, eye), -Vec3.dot(u, eye), Vec3.dot(f, eye), 1.0} 162 | } 163 | |> transpose 164 | end 165 | 166 | @spec perspective(fovy :: float(), aspect :: float(), near :: float(), far :: float()) :: t() 167 | def perspective(fovy, aspect, near, far) do 168 | fovy_radians = fovy * (:math.pi() / 180.0) 169 | f = 1.0 / :math.tan(fovy_radians * 0.5) 170 | y_scale = f / aspect 171 | 172 | { 173 | Vec4.new(f, 0, 0, 0), 174 | Vec4.new(0, y_scale, 0, 0), 175 | Vec4.new(0, 0, (far + near) / (near - far), 2 * far * near / (near - far)), 176 | Vec4.new(0, 0, -1, 0) 177 | } 178 | |> transpose 179 | 180 | # f = 1.0 / :math.tan(fovy / 2) 181 | # nf = 1 / (near - far) 182 | 183 | # { 184 | # {f / aspect, 0.0, 0.0, 0.0}, 185 | # {0.0, f, 0.0, 0.0}, 186 | # {0.0, 0.0, (far + near) * nf, -1.0}, 187 | # {0.0, 0.0, 2 * far * near * nf, 0.0} 188 | # } 189 | 190 | # { 191 | # {f, 0.0, 0.0, 0.0}, 192 | # {0.0, f, 0.0, 0.0}, 193 | # {0.0, 0.0, (far + near) * nf, -1.0}, 194 | # {0.0, 0.0, 2 * far * near * nf, 0.0} 195 | # } 196 | end 197 | 198 | @spec ortho( 199 | x_min :: float(), 200 | x_max :: float(), 201 | y_min :: float(), 202 | y_max :: float(), 203 | z_near :: float(), 204 | z_far :: float() 205 | ) :: t() 206 | def ortho(x_min, x_max, y_min, y_max, z_near, z_far) do 207 | width = x_max - x_min 208 | height = y_max - y_min 209 | depth = z_far - z_near 210 | 211 | tx = -(x_max + x_min) / width 212 | ty = -(y_max + y_min) / height 213 | tz = -(z_far + z_near) / depth 214 | 215 | { 216 | Vec4.new(2 / width, 0, 0, tx), 217 | Vec4.new(0, 2 / height, 0, ty), 218 | Vec4.new(0, 0, -2 / depth, tz), 219 | Vec4.new(0, 0, 0, 1) 220 | } 221 | |> transpose 222 | end 223 | 224 | @spec to_binary(matrix :: t()) :: binary() 225 | def to_binary({a, b, c, d}) do 226 | Vec4.to_binary(a) <> Vec4.to_binary(b) <> Vec4.to_binary(c) <> Vec4.to_binary(d) 227 | end 228 | 229 | @spec multiply_vec(matrix :: t(), vec :: Vec4.t()) :: Vec4.t() 230 | def multiply_vec({a, b, c, d}, vec) do 231 | Vec4.new( 232 | Vec4.dot(a, vec), 233 | Vec4.dot(b, vec), 234 | Vec4.dot(c, vec), 235 | Vec4.dot(d, vec) 236 | ) 237 | end 238 | 239 | @spec multiply_mat(mat1 :: t(), mat2 :: t()) :: t() 240 | # and is_float(b00) and is_float(b01) and is_float(b02) and is_float(b03) 241 | # and is_float(b10) and is_float(b11) and is_float(b12) and is_float(b13) 242 | # and is_float(b20) and is_float(b21) and is_float(b22) and is_float(b23) 243 | # and is_float(b30) and is_float(b31) and is_float(b32) and is_float(b33) 244 | def multiply_mat( 245 | {{a00, a01, a02, a03}, {a10, a11, a12, a13}, {a20, a21, a22, a23}, {a30, a31, a32, a33}}, 246 | {{b00, b01, b02, b03}, {b10, b11, b12, b13}, {b20, b21, b22, b23}, {b30, b31, b32, b33}} 247 | ) 248 | when is_float(a00) and is_float(a01) and is_float(a02) and is_float(a03) and 249 | is_float(a10) and is_float(a11) and is_float(a12) and is_float(a13) and 250 | is_float(a20) and is_float(a21) and is_float(a22) and is_float(a23) and 251 | is_float(a30) and is_float(a31) and is_float(a32) and is_float(a33) do 252 | # Task.async(fn -> 253 | { 254 | { 255 | a00 * b00 + a01 * b10 + a02 * b20 + a03 * b30, 256 | a00 * b01 + a01 * b11 + a02 * b21 + a03 * b31, 257 | a00 * b02 + a01 * b12 + a02 * b22 + a03 * b32, 258 | a00 * b03 + a01 * b13 + a02 * b23 + a03 * b33 259 | }, 260 | { 261 | a10 * b00 + a11 * b10 + a12 * b20 + a13 * b30, 262 | a10 * b01 + a11 * b11 + a12 * b21 + a13 * b31, 263 | a10 * b02 + a11 * b12 + a12 * b22 + a13 * b32, 264 | a10 * b03 + a11 * b13 + a12 * b23 + a13 * b33 265 | }, 266 | { 267 | a20 * b00 + a21 * b10 + a22 * b20 + a23 * b30, 268 | a20 * b01 + a21 * b11 + a22 * b21 + a23 * b31, 269 | a20 * b02 + a21 * b12 + a22 * b22 + a23 * b32, 270 | a20 * b03 + a21 * b13 + a22 * b23 + a23 * b33 271 | }, 272 | { 273 | a30 * b00 + a31 * b10 + a32 * b20 + a33 * b30, 274 | a30 * b01 + a31 * b11 + a32 * b21 + a33 * b31, 275 | a30 * b02 + a31 * b12 + a32 * b22 + a33 * b32, 276 | a30 * b03 + a31 * b13 + a32 * b23 + a33 * b33 277 | } 278 | } 279 | 280 | # end) 281 | # |> Task.await() 282 | 283 | # { 284 | # [ 285 | # Task.async(fn -> 286 | # { 287 | # a00 * b00 + a01 * b10 + a02 * b20 + a03 * b30, 288 | # a00 * b01 + a01 * b11 + a02 * b21 + a03 * b31, 289 | # a00 * b02 + a01 * b12 + a02 * b22 + a03 * b32, 290 | # a00 * b03 + a01 * b13 + a02 * b23 + a03 * b33 291 | # } 292 | # end), Task.async(fn -> 293 | # { 294 | # a10 * b00 + a11 * b10 + a12 * b20 + a13 * b30, 295 | # a10 * b01 + a11 * b11 + a12 * b21 + a13 * b31, 296 | # a10 * b02 + a11 * b12 + a12 * b22 + a13 * b32, 297 | # a10 * b03 + a11 * b13 + a12 * b23 + a13 * b33 298 | # } 299 | # end), Task.async(fn -> { 300 | # a20 * b00 + a21 * b10 + a22 * b20 + a23 * b30, 301 | # a20 * b01 + a21 * b11 + a22 * b21 + a23 * b31, 302 | # a20 * b02 + a21 * b12 + a22 * b22 + a23 * b32, 303 | # a20 * b03 + a21 * b13 + a22 * b23 + a23 * b33 304 | # } end), Task.async(fn -> { 305 | # a30 * b00 + a31 * b10 + a32 * b20 + a33 * b30, 306 | # a30 * b01 + a31 * b11 + a32 * b21 + a33 * b31, 307 | # a30 * b02 + a31 * b12 + a32 * b22 + a33 * b32, 308 | # a30 * b03 + a31 * b13 + a32 * b23 + a33 * b33 309 | # } end) 310 | # ] 311 | # |> Task.await_many() 312 | # |> List.to_tuple() 313 | # } 314 | 315 | # |> transpose 316 | end 317 | 318 | @spec translate(mat :: t(), vec :: Vec3.t()) :: t() 319 | def translate(mat, {x, y, z}) do 320 | # { 321 | # {a1, a2, a3, _a4}, 322 | # {b1, b2, b3, _b4}, 323 | # {c1, c2, c3, _c4}, 324 | # d 325 | # } = mat 326 | 327 | transform = { 328 | Vec4.new(1, 0, 0, x), 329 | Vec4.new(0, 1, 0, y), 330 | Vec4.new(0, 0, 1, z), 331 | Vec4.new(0, 0, 0, 1) 332 | } 333 | 334 | multiply_mat(mat, transform) 335 | # |> transpose 336 | end 337 | 338 | def scale_vec(mat, {x, y, z}) do 339 | # { 340 | # {a1, a2, a3, a4}, 341 | # {b1, b2, b3, b4}, 342 | # {c1, c2, c3, c4}, 343 | # d 344 | # } = mat 345 | 346 | transform = { 347 | Vec4.new(x, 0, 0, 0), 348 | Vec4.new(0, y, 0, 0), 349 | Vec4.new(0, 0, z, 0), 350 | Vec4.new(0, 0, 0, 1) 351 | } 352 | 353 | multiply_mat(mat, transform) 354 | # |> transpose 355 | end 356 | 357 | def rotate(mat, angle, {x, y, z}) do 358 | c = :math.cos(angle) 359 | s = :math.sin(angle) 360 | 361 | transform = { 362 | Vec4.new(c + x * x * (1 - c), x * y * (1 - c) - z * s, x * z * (1 - c) + y * s, 0), 363 | Vec4.new(y * x * (1 - c) + z * s, c + y * y * (1 - c), y * z * (1 - c) - x * s, 0), 364 | Vec4.new(z * x * (1 - c) - y * s, z * y * (1 - c) + x * s, c + z * z * (1 - c), 0), 365 | Vec4.new(0, 0, 0, 1) 366 | } 367 | 368 | multiply_mat(mat, transform) 369 | end 370 | end 371 | -------------------------------------------------------------------------------- /lib/breakout/math/quat.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Math.Quat do 2 | alias Breakout.Math.Vec4 3 | alias Breakout.Math.Vec3 4 | alias Breakout.Math.Mat3 5 | 6 | @type t :: {float(), float(), float(), float()} 7 | 8 | @spec new(n :: Vec3.t(), angle_rad :: number()) :: t() 9 | def new(n, angle_rad) do 10 | half_angle_rad = 0.5 * angle_rad 11 | 12 | w = :math.cos(half_angle_rad) 13 | half_sine = :math.sin(half_angle_rad) 14 | {x, y, z} = Vec3.normalize(n) 15 | 16 | {x * half_sine, y * half_sine, z * half_sine, w} 17 | end 18 | 19 | @spec multiply(lhs :: t(), rhs :: t()) :: t() 20 | def multiply({x1, y1, z1, w1}, {x2, y2, z2, w2}) do 21 | { 22 | x1 * w2 + w1 * x2 + y1 * z2 - z1 * y2, 23 | y1 * w2 + w1 * y2 + z1 * x2 - x1 * z2, 24 | z1 * w2 + w1 * z2 + x1 * y2 - y1 * x2, 25 | w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2 26 | } 27 | end 28 | 29 | @spec scale(quat :: t(), scalar :: float()) :: t() 30 | def scale({x, y, z, w}, scalar) do 31 | { 32 | x * scalar, 33 | y * scalar, 34 | z * scalar, 35 | w * scalar 36 | } 37 | end 38 | 39 | @spec normalize(quat :: t()) :: t() 40 | def normalize({x, y, z, w} = q) do 41 | inv_mag = 1 / magnitude(q) 42 | 43 | { 44 | x * inv_mag, 45 | y * inv_mag, 46 | z * inv_mag, 47 | w * inv_mag 48 | } 49 | end 50 | 51 | @spec invert(quat :: t()) :: t() 52 | def invert(q) do 53 | {x, y, z, w} = scale(q, 1 / magnitude_squared(q)) 54 | 55 | {-x, -y, -z, w} 56 | end 57 | 58 | # this is just invert, but the C++ source does a copy. unnecessary here. 59 | # curious to see which is used more. 60 | @spec inverse(quat :: t()) :: t() 61 | def inverse(q) do 62 | invert(q) 63 | end 64 | 65 | @spec magnitude(quat :: t()) :: float() 66 | def magnitude(q) do 67 | :math.sqrt(magnitude_squared(q)) 68 | end 69 | 70 | @spec magnitude_squared(quat :: t()) :: float() 71 | def magnitude_squared({x, y, z, w}) do 72 | x * x + y * y + z * z + w * w 73 | end 74 | 75 | @spec rotate_point(quat :: t(), vec :: Vec3.t()) :: Vec3.t() 76 | def rotate_point(q, {x, y, z}) do 77 | vector = {x, y, z, 0.0} 78 | 79 | {x, y, z, _} = 80 | q 81 | |> multiply(vector) 82 | |> multiply(inverse(q)) 83 | 84 | {x, y, z} 85 | end 86 | 87 | @spec rotate_matrix(quat :: t(), mat :: Mat3.t()) :: Mat3.t() 88 | def rotate_matrix(q, {r0, r1, r2}) do 89 | { 90 | rotate_point(q, r0), 91 | rotate_point(q, r1), 92 | rotate_point(q, r2) 93 | } 94 | end 95 | 96 | @spec to_mat3(quat :: t) :: Mat3.t() 97 | def to_mat3(q) do 98 | {r0, r1, r2} = Mat3.identity() 99 | 100 | { 101 | rotate_point(q, r0), 102 | rotate_point(q, r1), 103 | rotate_point(q, r2) 104 | } 105 | end 106 | 107 | def to_mat4(q) do 108 | {{a1, a2, a3}, {b1, b2, b3}, {c1, c2, c3}} = to_mat3(q) 109 | 110 | { 111 | Vec4.new(a1, a2, a3, 0), 112 | Vec4.new(b1, b2, b3, 0), 113 | Vec4.new(c1, c2, c3, 0), 114 | Vec4.new(0, 0, 0, 1) 115 | } 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/breakout/math/vec2.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Math.Vec2 do 2 | @type t :: {float(), float()} 3 | 4 | @spec new(x :: number(), y :: number()) :: t() 5 | def new(x, y) do 6 | {x + 0.0, y + 0.0} 7 | end 8 | 9 | @spec add(v1 :: t(), v2 :: t()) :: t() 10 | def add({x1, y1}, {x2, y2}) do 11 | {x1 + x2, y1 + y2} 12 | end 13 | 14 | @spec subtract(v1 :: t(), v2 :: t()) :: t() 15 | def subtract({x1, y1}, {x2, y2}) do 16 | {x1 - x2, y1 - y2} 17 | end 18 | 19 | @spec scale(v :: t(), scalar :: number()) :: t() 20 | def scale({x, y}, scalar) do 21 | {x * scalar, y * scalar} 22 | end 23 | 24 | @spec multiply(v1 :: t(), v2 :: t()) :: t() 25 | def multiply({x1, y1}, {x2, y2}) do 26 | {x1 * x2, y1 * y2} 27 | end 28 | 29 | @spec normalize(v :: t()) :: t() 30 | def normalize({x, y} = v) do 31 | mag = magnitude(v) 32 | 33 | inv_mag = 34 | try do 35 | 1 / mag 36 | rescue 37 | _ -> 0 38 | end 39 | 40 | {x * inv_mag, y * inv_mag} 41 | end 42 | 43 | @spec magnitude(v :: t()) :: float() 44 | def magnitude({x, y}) do 45 | :math.sqrt(x * x + y * y) 46 | end 47 | 48 | @spec length(v :: t()) :: float() 49 | def length(v) do 50 | magnitude(v) 51 | end 52 | 53 | @spec dot(v1 :: t(), v2 :: t()) :: float() 54 | def dot({x1, y1}, {x2, y2}) do 55 | x1 * x2 + y1 * y2 56 | end 57 | 58 | @spec clamp(value :: t(), min_val :: t(), max_val :: t()) :: t() 59 | def clamp({x, y}, {min_x, min_y}, {max_x, max_y}) do 60 | {max(min_x, min(max_x, x)), max(min_y, min(max_y, y))} 61 | end 62 | 63 | @compass [ 64 | {0.0, 1.0}, 65 | {1.0, 0.0}, 66 | {0.0, -1.0}, 67 | {-1.0, 0.0} 68 | ] 69 | @spec direction(target :: t()) :: :up | :right | :down | :left 70 | def direction(target) do 71 | target = normalize(target) 72 | 73 | {best_match, _} = 74 | @compass 75 | |> Enum.with_index() 76 | |> Enum.map(fn {direction, index} -> {index, dot(target, direction)} end) 77 | |> Enum.max_by(fn {_, dot_product} -> dot_product end) 78 | 79 | direction_from_index(best_match) 80 | end 81 | 82 | defp direction_from_index(0), do: :up 83 | defp direction_from_index(1), do: :right 84 | defp direction_from_index(2), do: :down 85 | defp direction_from_index(3), do: :left 86 | end 87 | -------------------------------------------------------------------------------- /lib/breakout/math/vec3.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Math.Vec3 do 2 | @type t :: {float(), float(), float()} 3 | 4 | @spec new(x :: number(), y :: number(), z :: number()) :: t() 5 | def new(x, y, z) do 6 | {x + 0.0, y + 0.0, z + 0.0} 7 | end 8 | 9 | @spec add(vec1 :: t(), vec2 :: t()) :: t() 10 | def add({x1, y1, z1}, {x2, y2, z2}) do 11 | {x1 + x2, y1 + y2, z1 + z2} 12 | end 13 | 14 | @spec cross(vec1 :: t(), vec2 :: t()) :: t() 15 | def cross({x1, y1, z1}, {x2, y2, z2}) do 16 | { 17 | y1 * z2 - z1 * y2, 18 | z1 * x2 - x1 * z2, 19 | x1 * y2 - y1 * x2 20 | } 21 | end 22 | 23 | @spec dot(vec1 :: t(), vec2 :: t()) :: float() 24 | def dot({x1, y1, z1}, {x2, y2, z2}) do 25 | x1 * x2 + y1 * y2 + z1 * z2 26 | end 27 | 28 | @spec normalize(vec :: t()) :: t() 29 | def normalize({x, y, z} = vec) do 30 | mag = magnitude(vec) 31 | inv_mag = if mag == 0.0, do: 0, else: 1 / mag 32 | {x * inv_mag, y * inv_mag, z * inv_mag} 33 | end 34 | 35 | @spec magnitude(vec :: t()) :: float() 36 | def magnitude({x, y, z}) do 37 | :math.sqrt(x * x + y * y + z * z) 38 | end 39 | 40 | @spec scale(vec :: t(), scalar :: float()) :: t() 41 | def scale({x, y, z}, scalar) do 42 | {x * scalar, y * scalar, z * scalar} 43 | end 44 | 45 | @spec get_ortho(this :: t()) :: {t(), t()} 46 | def get_ortho(n) do 47 | n = normalize(n) 48 | {_, _, nz} = n 49 | w = if nz * nz > 0.9 * 0.9, do: new(1, 0, 0), else: new(0, 0, 1) 50 | u = normalize(cross(w, n)) 51 | v = normalize(cross(n, u)) 52 | u = normalize(cross(v, n)) 53 | 54 | {u, v} 55 | end 56 | 57 | @spec subtract(vec1 :: t(), vec2 :: t()) :: t() 58 | def subtract({x1, y1, z1}, {x2, y2, z2}) do 59 | {x1 - x2, y1 - y2, z1 - z2} 60 | end 61 | 62 | @spec to_binary(vec :: t()) :: binary() 63 | def to_binary({x, y, z}) do 64 | <> 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/breakout/math/vec4.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Math.Vec4 do 2 | @type t :: {float(), float(), float(), float()} 3 | 4 | @spec new(x :: number(), y :: number(), z :: number(), w :: number()) :: t() 5 | def new(x, y, z, w) do 6 | {x + 0.0, y + 0.0, z + 0.0, w + 0.0} 7 | end 8 | 9 | @spec add(vec1 :: t(), vec2 :: t()) :: t() 10 | def add({x1, y1, z1, w1}, {x2, y2, z2, w2}) do 11 | {x1 + x2, y1 + y2, z1 + z2, w1 + w2} 12 | end 13 | 14 | @spec normalize(vec :: t()) :: t() 15 | def normalize({x, y, z, w} = vec) do 16 | inv_mag = 1 / magnitude(vec) 17 | {x * inv_mag, y * inv_mag, z * inv_mag, w * inv_mag} 18 | end 19 | 20 | @spec magnitude(vec :: t()) :: float() 21 | def magnitude({x, y, z, w}) do 22 | :math.sqrt(x * x + y * y + z * z + w * w) 23 | end 24 | 25 | @spec scale(vec :: t(), scalar :: float()) :: t() 26 | def scale({x, y, z, w}, scalar) do 27 | {x * scalar, y * scalar, z * scalar, w * scalar} 28 | end 29 | 30 | @spec subtract(vec1 :: t(), vec2 :: t()) :: t() 31 | def subtract({x1, y1, z1, w1}, {x2, y2, z2, w2}) do 32 | {x1 - x2, y1 - y2, z1 - z2, w1 - w2} 33 | end 34 | 35 | @spec dot(vec1 :: t(), vec2 :: t()) :: float() 36 | def dot({x1, y1, z1, w1}, {x2, y2, z2, w2}) do 37 | x1 * x2 + y1 * y2 + z1 * z2 + w1 * w2 38 | end 39 | 40 | @spec to_binary(vec :: t()) :: binary() 41 | def to_binary({x, y, z, w}) do 42 | <> 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/breakout/particle.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Particle do 2 | alias Breakout.Math.Vec2 3 | alias Breakout.Math.Vec4 4 | 5 | defstruct position: Vec2.new(0, 0), 6 | velocity: Vec2.new(0, 0), 7 | color: Vec4.new(1, 1, 1, 1), 8 | life: 0.0 9 | 10 | @type t :: %__MODULE__{ 11 | position: Vec2.t(), 12 | velocity: Vec2.t(), 13 | color: Vec4.t(), 14 | life: float() 15 | } 16 | end 17 | -------------------------------------------------------------------------------- /lib/breakout/particle_generator.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.ParticleGenerator do 2 | alias Breakout.Math.Vec4 3 | alias Breakout.Math.Vec2 4 | alias Breakout.GameObject 5 | alias Breakout.Renderer.Texture2D 6 | alias Breakout.Renderer.Shader 7 | alias Breakout.Particle 8 | alias Breakout.GameObject 9 | alias Breakout.Util 10 | 11 | defstruct particles: [], 12 | amount: 0, 13 | shader: nil, 14 | texture: nil, 15 | vao: 0 16 | 17 | @type t :: %__MODULE__{ 18 | particles: [Particle.t()], 19 | amount: non_neg_integer(), 20 | shader: Shader.t(), 21 | texture: Texture2D.t(), 22 | vao: non_neg_integer() 23 | } 24 | 25 | @spec new(shader :: Shader.t(), texture :: Texture2D.t(), amount :: non_neg_integer()) :: t() 26 | def new(shader, texture, amount) do 27 | %__MODULE__{ 28 | particles: [], 29 | amount: amount, 30 | shader: shader, 31 | texture: texture 32 | } 33 | |> init() 34 | end 35 | 36 | @spec update( 37 | pg :: t(), 38 | dt :: float(), 39 | object :: GameObject.t(), 40 | new_particles :: non_neg_integer(), 41 | offset :: Vec2.t() 42 | ) :: t() 43 | def update(pg, dt, object, new_particles, offset) do 44 | generator = 45 | Enum.reduce(1..new_particles, pg, fn _, acc -> 46 | unused_particle = first_unused_particle(acc) 47 | respawn_particle(acc, unused_particle, object, offset) 48 | end) 49 | 50 | updated_particles = 51 | Enum.map(generator.particles, fn particle -> 52 | update_particle(particle, dt) 53 | end) 54 | 55 | %{generator | particles: updated_particles} 56 | end 57 | 58 | def update_particle(%Particle{} = particle, dt) do 59 | life = particle.life - 0.001 * dt 60 | 61 | if life > 0.0 do 62 | position = Vec2.subtract(particle.position, Vec2.scale(particle.velocity, 0.1)) 63 | color = put_elem(particle.color, 3, (particle.color |> elem(3)) - 0.01) 64 | %Particle{particle | position: position, color: color, life: life} 65 | else 66 | %Particle{particle | life: life} 67 | end 68 | end 69 | 70 | def draw(%__MODULE__{} = pg) do 71 | :gl.blendFunc(:gl_const.gl_src_alpha(), :gl_const.gl_one()) 72 | Shader.use_shader(pg.shader) 73 | 74 | pg.particles 75 | |> Enum.each(fn particle -> 76 | if particle.life > 0.0 do 77 | pg.shader 78 | |> Shader.set(~c"offset", particle.position) 79 | |> Shader.set(~c"color", particle.color) 80 | 81 | Texture2D.bind(pg.texture) 82 | 83 | :gl.bindVertexArray(pg.vao) 84 | :gl.drawArrays(:gl_const.gl_triangles(), 0, 6) 85 | :gl.bindVertexArray(0) 86 | end 87 | end) 88 | 89 | :gl.blendFunc(:gl_const.gl_src_alpha(), :gl_const.gl_one_minus_src_alpha()) 90 | end 91 | 92 | defp init(pg) do 93 | particle_quad = 94 | Util.make_bits([ 95 | 0.0, 96 | 1.0, 97 | 0.0, 98 | 1.0, 99 | 1.0, 100 | 0.0, 101 | 1.0, 102 | 0.0, 103 | 0.0, 104 | 0.0, 105 | 0.0, 106 | 0.0, 107 | 0.0, 108 | 1.0, 109 | 0.0, 110 | 1.0, 111 | 1.0, 112 | 1.0, 113 | 1.0, 114 | 1.0, 115 | 1.0, 116 | 0.0, 117 | 1.0, 118 | 0.0 119 | ]) 120 | 121 | [vao] = :gl.genVertexArrays(1) 122 | [vbo] = :gl.genBuffers(1) 123 | :gl.bindVertexArray(vao) 124 | 125 | :gl.bindBuffer(:gl_const.gl_array_buffer(), vbo) 126 | 127 | :gl.bufferData( 128 | :gl_const.gl_array_buffer(), 129 | byte_size(particle_quad), 130 | particle_quad, 131 | :gl_const.gl_static_draw() 132 | ) 133 | 134 | :gl.enableVertexAttribArray(0) 135 | 136 | :gl.vertexAttribPointer( 137 | 0, 138 | 4, 139 | :gl_const.gl_float(), 140 | :gl_const.gl_false(), 141 | 4 * byte_size(<<0.0::float-native-size(32)>>), 142 | 0 143 | ) 144 | 145 | :gl.bindVertexArray(0) 146 | 147 | particles = 148 | for _ <- 0..pg.amount do 149 | %Particle{} 150 | end 151 | 152 | %__MODULE__{pg | vao: vao, particles: particles} 153 | end 154 | 155 | defp first_unused_particle(%__MODULE__{particles: particles}) do 156 | Enum.find_index(particles, fn p -> p.life <= 0 end) || 0 157 | end 158 | 159 | defp respawn_particle(%__MODULE__{} = pg, index, %GameObject{} = object, offset) do 160 | random = (:rand.uniform(100) - 50) / 10.0 161 | r_color = 0.5 + :rand.uniform() / 2.0 162 | position = Vec2.add(object.position, Vec2.add(offset, {random, random})) 163 | color = Vec4.new(r_color, r_color, r_color, 1) 164 | velocity = Vec2.scale(object.velocity, 0.1) 165 | 166 | particle = %Particle{position: position, color: color, life: 1.0, velocity: velocity} 167 | updated_particles = List.update_at(pg.particles, index, fn _ -> particle end) 168 | 169 | %__MODULE__{ 170 | pg 171 | | particles: updated_particles 172 | } 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /lib/breakout/player.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Player do 2 | alias Breakout.Math.Vec2 3 | 4 | @type t :: %__MODULE__{ 5 | size: Vec2.t(), 6 | velocity: number(), 7 | position: Vec2.t() 8 | } 9 | 10 | defstruct size: Vec2.new(100, 20), 11 | velocity: 500.0, 12 | position: Vec2.new(0, 0) 13 | end 14 | -------------------------------------------------------------------------------- /lib/breakout/power_up.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.PowerUp do 2 | alias Breakout.Renderer.Texture2D 3 | alias Breakout.Math.Vec2 4 | alias Breakout.Math.Vec3 5 | alias Breakout.GameObject 6 | 7 | @type power_up_types :: 8 | nil | :chaos | :confuse | :increase | :passthrough | :speed | :sticky 9 | 10 | @type t :: %__MODULE__{ 11 | type: power_up_types(), 12 | duration: float(), 13 | activated: boolean(), 14 | game_object: GameObject.t() 15 | } 16 | 17 | @enforce_keys [:game_object] 18 | defstruct type: nil, 19 | duration: 0.0, 20 | activated: false, 21 | game_object: GameObject.new() 22 | 23 | @spec new( 24 | type :: power_up_types(), 25 | color :: Vec3.t(), 26 | duration :: float(), 27 | position :: Vec2.t(), 28 | texture :: Texture2D.t() 29 | ) :: t() 30 | def new(type, color, duration, position, texture) do 31 | %__MODULE__{ 32 | type: type, 33 | duration: duration, 34 | game_object: 35 | GameObject.new( 36 | position, 37 | Vec2.new(60, 20), 38 | texture, 39 | color, 40 | Vec2.new(0, 150) 41 | ) 42 | } 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/breakout/renderer/open_gl.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Renderer.OpenGL do 2 | def init() do 3 | do_enables() 4 | 5 | :gl.blendFunc(:gl_const.gl_src_alpha(), :gl_const.gl_one_minus_src_alpha()) 6 | end 7 | 8 | defp do_enables() do 9 | # :gl.enable(:gl_const.gl_depth_test) 10 | # :gl.enable(:gl_const.gl_cull_face) 11 | :gl.enable(:gl_const.gl_multisample()) 12 | :gl.enable(:gl_const.gl_blend()) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/breakout/renderer/post_processor.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Renderer.PostProcessor do 2 | require Logger 3 | alias Breakout.Util 4 | alias Breakout.Renderer.{Shader, Texture2D} 5 | 6 | @type t :: %__MODULE__{ 7 | shader: Shader.t(), 8 | texture: Texture2D.t(), 9 | width: non_neg_integer(), 10 | height: non_neg_integer(), 11 | confuse: boolean(), 12 | chaos: boolean(), 13 | shake: boolean(), 14 | msfbo: non_neg_integer(), 15 | fbo: non_neg_integer(), 16 | rbo: non_neg_integer(), 17 | vao: non_neg_integer() 18 | } 19 | 20 | defstruct shader: nil, 21 | texture: nil, 22 | width: 0, 23 | height: 0, 24 | confuse: false, 25 | chaos: false, 26 | shake: false, 27 | msfbo: 0, 28 | fbo: 0, 29 | rbo: 0, 30 | vao: 0 31 | 32 | @spec new(shader :: Shader.t(), width :: non_neg_integer(), height :: non_neg_integer()) :: t() 33 | def new(shader, width, height) do 34 | pp = %__MODULE__{ 35 | shader: shader, 36 | width: width, 37 | height: height 38 | } 39 | 40 | [msfbo, fbo] = :gl.genFramebuffers(2) 41 | :gl.isFramebuffer(msfbo) 42 | :gl.isFramebuffer(fbo) 43 | [rbo] = :gl.genRenderbuffers(1) 44 | :gl.isRenderbuffer(rbo) 45 | 46 | :gl.bindFramebuffer(:gl_const.gl_framebuffer(), msfbo) 47 | 48 | :gl.flush() 49 | 50 | :gl.bindRenderbuffer(:gl_const.gl_renderbuffer(), rbo) 51 | 52 | :gl.renderbufferStorageMultisample( 53 | :gl_const.gl_renderbuffer(), 54 | 4, 55 | :gl_const.gl_rgb(), 56 | width, 57 | height 58 | ) 59 | 60 | :gl.framebufferRenderbuffer( 61 | :gl_const.gl_framebuffer(), 62 | :gl_const.gl_color_attachment0(), 63 | :gl_const.gl_renderbuffer(), 64 | rbo 65 | ) 66 | 67 | if :gl.checkFramebufferStatus(:gl_const.gl_framebuffer()) != 68 | :gl_const.gl_framebuffer_complete() do 69 | Logger.error("failed to init msfbo") 70 | end 71 | 72 | :gl.bindFramebuffer(:gl_const.gl_framebuffer(), fbo) 73 | 74 | tex = 75 | Texture2D.new() 76 | |> Texture2D.generate(width, height, 0) 77 | 78 | :gl.framebufferTexture2D( 79 | :gl_const.gl_framebuffer(), 80 | :gl_const.gl_color_attachment0(), 81 | :gl_const.gl_texture_2d(), 82 | tex.id, 83 | 0 84 | ) 85 | 86 | pp = %__MODULE__{pp | texture: tex, msfbo: msfbo, fbo: fbo, rbo: rbo} 87 | 88 | if :gl.checkFramebufferStatus(:gl_const.gl_framebuffer()) != 89 | :gl_const.gl_framebuffer_complete() do 90 | Logger.error("failed to init fbo") 91 | end 92 | 93 | :gl.bindFramebuffer(:gl_const.gl_framebuffer(), 0) 94 | 95 | pp = init_render_data(pp) 96 | 97 | Shader.set(pp.shader, ~c"scene", 0, true) 98 | offset = 1.0 / 300.0 99 | 100 | offsets = [ 101 | {-offset, offset}, 102 | {0.0, offset}, 103 | {offset, offset}, 104 | {-offset, 0.0}, 105 | {0.0, 0.0}, 106 | {offset, 0.0}, 107 | {-offset, -offset}, 108 | {0.0, -offset}, 109 | {offset, -offset} 110 | ] 111 | 112 | Shader.set(pp.shader, ~c"offsets", offsets) 113 | 114 | edge_kernel = [ 115 | -1, 116 | -1, 117 | -1, 118 | -1, 119 | 8, 120 | -1, 121 | -1, 122 | -1, 123 | -1 124 | ] 125 | 126 | Shader.set(pp.shader, ~c"edge_kernel", edge_kernel) 127 | 128 | blur_kernel = [ 129 | 1 / 16, 130 | 2 / 16, 131 | 1 / 16, 132 | 2 / 16, 133 | 4 / 16, 134 | 2 / 16, 135 | 1 / 16, 136 | 2 / 16, 137 | 1 / 16 138 | ] 139 | 140 | Shader.set(pp.shader, ~c"blur_kernel", blur_kernel) 141 | 142 | pp 143 | end 144 | 145 | @spec begin_render(post_processor :: t()) :: :ok 146 | def begin_render(%__MODULE__{msfbo: msfbo}) do 147 | :gl.bindFramebuffer(:gl_const.gl_framebuffer(), msfbo) 148 | :gl.clearColor(0.0, 0.0, 0.0, 1.0) 149 | :gl.clear(:gl_const.gl_color_buffer_bit()) 150 | end 151 | 152 | @spec end_render(post_processor :: t()) :: :ok 153 | def end_render(%__MODULE__{msfbo: msfbo, fbo: fbo, width: width, height: height}) do 154 | :gl.bindFramebuffer(:gl_const.gl_read_framebuffer(), msfbo) 155 | :gl.bindFramebuffer(:gl_const.gl_draw_framebuffer(), fbo) 156 | 157 | :gl.blitFramebuffer( 158 | 0, 159 | 0, 160 | width, 161 | height, 162 | 0, 163 | 0, 164 | width, 165 | height, 166 | :gl_const.gl_color_buffer_bit(), 167 | :gl_const.gl_nearest() 168 | ) 169 | 170 | :gl.bindFramebuffer(:gl_const.gl_framebuffer(), 0) 171 | end 172 | 173 | @spec render(post_processor :: t(), time :: float()) :: :ok 174 | def render( 175 | %__MODULE__{ 176 | shader: shader, 177 | confuse: confuse, 178 | chaos: chaos, 179 | shake: shake, 180 | texture: texture, 181 | vao: vao 182 | }, 183 | time 184 | ) 185 | when is_float(time) do 186 | Shader.use_shader(shader) 187 | |> Shader.set(~c"time", time) 188 | |> Shader.set(~c"confuse", confuse) 189 | |> Shader.set(~c"chaos", chaos) 190 | |> Shader.set(~c"shake", shake) 191 | 192 | :gl.activeTexture(:gl_const.gl_texture0()) 193 | Texture2D.bind(texture) 194 | 195 | :gl.bindVertexArray(vao) 196 | :gl.drawArrays(:gl_const.gl_triangles(), 0, 6) 197 | :gl.bindVertexArray(0) 198 | end 199 | 200 | @spec init_render_data(post_processor :: t()) :: t() 201 | def init_render_data(%__MODULE__{} = post_processor) do 202 | vertices = 203 | Util.make_bits([ 204 | -1, 205 | -1, 206 | 0, 207 | 0, 208 | 1, 209 | 1, 210 | 1, 211 | 1, 212 | -1, 213 | 1, 214 | 0, 215 | 1, 216 | -1, 217 | -1, 218 | 0, 219 | 0, 220 | 1, 221 | -1, 222 | 1, 223 | 0, 224 | 1, 225 | 1, 226 | 1, 227 | 1 228 | ]) 229 | 230 | [vao] = :gl.genVertexArrays(1) 231 | [vbo] = :gl.genBuffers(1) 232 | 233 | :gl.bindBuffer(:gl_const.gl_array_buffer(), vbo) 234 | 235 | :gl.bufferData( 236 | :gl_const.gl_array_buffer(), 237 | byte_size(vertices), 238 | vertices, 239 | :gl_const.gl_static_draw() 240 | ) 241 | 242 | :gl.bindVertexArray(vao) 243 | :gl.enableVertexAttribArray(0) 244 | 245 | :gl.vertexAttribPointer( 246 | 0, 247 | 4, 248 | :gl_const.gl_float(), 249 | :gl_const.gl_false(), 250 | 4 * byte_size(<<0.0::native-float-size(32)>>), 251 | 0 252 | ) 253 | 254 | :gl.bindBuffer(:gl_const.gl_array_buffer(), 0) 255 | :gl.bindVertexArray(0) 256 | 257 | %__MODULE__{post_processor | vao: vao} 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /lib/breakout/renderer/renderer.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Renderer do 2 | require Logger 3 | 4 | alias Breakout.Math.Vec3 5 | alias Breakout.Math.Vec2 6 | # alias Breakout.ResourceManager 7 | alias Breakout.Renderer.Sprite 8 | @spec draw(state :: Breakout.State.t()) :: :ok 9 | def draw(state) do 10 | case state.resources.textures[:face] do 11 | {:ok, texture} -> 12 | t = :erlang.monotonic_time(:millisecond) 13 | 14 | Sprite.draw( 15 | state.sprite_renderer, 16 | texture, 17 | Vec2.new(:math.cos(t / 1000) * 550 + 550, :math.sin(t / 1000) * 325 + 325), 18 | Vec2.new(100, 100), 19 | :math.sin(t / 1000) * :math.pi(), 20 | Vec3.new(0, 1, 0) 21 | ) 22 | 23 | nil -> 24 | nil 25 | end 26 | 27 | :ok 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/breakout/renderer/shader.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Renderer.Shader do 2 | require Logger 3 | 4 | @type t :: integer() 5 | @type shader_type :: non_neg_integer() 6 | @type uniform_type :: 7 | float() 8 | | integer() 9 | # Breakout.Vec2.t() | Breakout.Vec3.t() | Breakout.Vec4.t() | Breakout.Mat4.t() 10 | | {float(), float()} 11 | | {float(), float(), float()} 12 | | {float(), float(), float(), float()} 13 | 14 | @spec init(vertex_path :: Path.t(), fragment_path :: Path.t()) :: t() 15 | def init(vertex_path, fragment_path) do 16 | vertex_code = File.read!(vertex_path) 17 | fragment_code = File.read!(fragment_path) 18 | 19 | vertex_shader = compile_shader(vertex_code, :gl_const.gl_vertex_shader()) 20 | fragment_shader = compile_shader(fragment_code, :gl_const.gl_fragment_shader()) 21 | 22 | shader_program = :gl.createProgram() 23 | :gl.attachShader(shader_program, vertex_shader) 24 | :gl.attachShader(shader_program, fragment_shader) 25 | :gl.linkProgram(shader_program) 26 | check_program_linking!(shader_program) 27 | 28 | :gl.deleteShader(vertex_shader) 29 | :gl.deleteShader(fragment_shader) 30 | 31 | shader_program 32 | end 33 | 34 | @spec compile_shader(source :: binary(), type :: shader_type()) :: t() 35 | defp compile_shader(source, type) do 36 | shader = :gl.createShader(type) 37 | :gl.shaderSource(shader, [source <> <<0>>]) 38 | :gl.compileShader(shader) 39 | check_shader_compilation!(shader) 40 | 41 | shader 42 | end 43 | 44 | @spec check_shader_compilation!(shader :: integer()) :: :ok 45 | defp check_shader_compilation!(shader) do 46 | status = :gl.getShaderiv(shader, :gl_const.gl_compile_status()) 47 | 48 | unless status == :gl_const.gl_true() do 49 | buf_size = :gl.getShaderiv(shader, :gl_const.gl_info_log_length()) 50 | info_log = :gl.getShaderInfoLog(shader, buf_size) 51 | Logger.error(msg: info_log) 52 | raise "Shader compilation error: #{info_log}" 53 | end 54 | 55 | :ok 56 | end 57 | 58 | @spec check_program_linking!(program :: integer()) :: :ok 59 | defp check_program_linking!(program) do 60 | status = :gl.getProgramiv(program, :gl_const.gl_link_status()) 61 | 62 | unless status == :gl_const.gl_true() do 63 | buf_size = :gl.getProgramiv(program, :gl_const.gl_info_log_length()) 64 | info_log = :gl.getProgramInfoLog(program, buf_size) 65 | Logger.error(msg: info_log) 66 | raise "Program linking error: #{info_log}" 67 | end 68 | 69 | :ok 70 | end 71 | 72 | @spec use_shader(shader :: t()) :: t() 73 | def use_shader(shader) do 74 | :gl.useProgram(shader) 75 | 76 | shader 77 | end 78 | 79 | # @spec set(shader :: pos_integer(), name :: binary(), value :: uniform_type()) :: :ok 80 | def set(shader, name, value, use_shader \\ false) 81 | 82 | def set(shader, name, value, use_shader) when is_float(value) do 83 | if use_shader, do: use_shader(shader) 84 | 85 | :gl.uniform1f(:gl.getUniformLocation(shader, name), value) 86 | 87 | shader 88 | end 89 | 90 | def set(shader, name, value, use_shader) when is_integer(value) do 91 | if use_shader, do: use_shader(shader) 92 | 93 | :gl.uniform1i(:gl.getUniformLocation(shader, name), value) 94 | 95 | shader 96 | end 97 | 98 | def set(shader, name, true, use_shader), do: set(shader, name, 1, use_shader) 99 | 100 | def set(shader, name, false, use_shader), do: set(shader, name, 0, use_shader) 101 | 102 | def set(shader, name, {x, y}, use_shader) do 103 | if use_shader, do: use_shader(shader) 104 | 105 | :gl.uniform2f(:gl.getUniformLocation(shader, name), x, y) 106 | 107 | shader 108 | end 109 | 110 | def set(shader, name, {x, y, z}, use_shader) do 111 | if use_shader, do: use_shader(shader) 112 | 113 | :gl.uniform3f(:gl.getUniformLocation(shader, name), x, y, z) 114 | 115 | shader 116 | end 117 | 118 | def set(shader, name, {x, y, z, w}, use_shader) do 119 | if use_shader, do: use_shader(shader) 120 | 121 | :gl.uniform4f(:gl.getUniformLocation(shader, name), x, y, z, w) 122 | 123 | shader 124 | end 125 | 126 | def set(shader, name, value, use_shader) 127 | when is_list(value) and tuple_size(value |> hd()) == 16 do 128 | if use_shader, do: use_shader(shader) 129 | 130 | :gl.uniformMatrix4fv(:gl.getUniformLocation(shader, name), :gl_const.gl_false(), value) 131 | 132 | shader 133 | end 134 | 135 | def set(shader, name, value, use_shader) 136 | when is_list(value) and tuple_size(value |> hd()) == 2 do 137 | if use_shader, do: use_shader(shader) 138 | 139 | :gl.uniform2fv(:gl.getUniformLocation(shader, name), value) 140 | end 141 | 142 | def set(shader, name, value, use_shader) when is_list(value) and is_integer(value |> hd()) do 143 | if use_shader, do: use_shader(shader) 144 | 145 | :gl.uniform1iv(:gl.getUniformLocation(shader, name), value) 146 | end 147 | 148 | def set(shader, name, value, use_shader) when is_list(value) and is_float(value |> hd()) do 149 | if use_shader, do: use_shader(shader) 150 | 151 | :gl.uniform1fv(:gl.getUniformLocation(shader, name), value) 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/breakout/renderer/sprite.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Renderer.Sprite do 2 | alias Breakout.Renderer.Texture2D 3 | alias Breakout.Math.Vec3 4 | alias Breakout.Math.Mat4 5 | alias Breakout.Renderer.Shader 6 | alias Breakout.Util 7 | 8 | defstruct [:shader, :quadVAO] 9 | 10 | def new(shader) do 11 | %__MODULE__{ 12 | shader: shader 13 | } 14 | |> init_render_data() 15 | end 16 | 17 | def draw( 18 | # %__MODULE__{shader: shader} = sprite, 19 | %_{sprite_renderer: %{shader: shader} = sprite, resources: resources} = _state, 20 | texture, 21 | {x, y} = _position, 22 | {width, height} = _size, 23 | rotate, 24 | color 25 | ) do 26 | texture = resources.textures[texture] 27 | Shader.use_shader(shader) 28 | 29 | model = 30 | Mat4.identity() 31 | |> Mat4.translate(Vec3.new(x, y, 0)) 32 | |> Mat4.translate(Vec3.new(0.5 * width, 0.5 * height, 0)) 33 | |> Mat4.rotate(rotate, Vec3.new(0, 0, 1)) 34 | |> Mat4.translate(Vec3.new(-0.5 * width, -0.5 * height, 0)) 35 | |> Mat4.scale_vec(Vec3.new(width, height, 1)) 36 | |> Mat4.transpose() 37 | 38 | Shader.set(shader, ~c"model", [model |> Mat4.flatten()]) 39 | Shader.set(shader, ~c"spriteColor", color) 40 | 41 | :gl.activeTexture(:gl_const.gl_texture0()) 42 | Texture2D.bind(texture) 43 | 44 | :gl.bindVertexArray(sprite.quadVAO) 45 | :gl.drawArrays(:gl_const.gl_triangles(), 0, 6) 46 | 47 | :gl.bindVertexArray(0) 48 | end 49 | 50 | defp init_render_data(%__MODULE__{} = sprite) do 51 | [quadVAO] = :gl.genVertexArrays(1) 52 | sprite = %__MODULE__{sprite | quadVAO: quadVAO} 53 | 54 | vertices = 55 | Util.make_bits([ 56 | 0.0, 57 | 1.0, 58 | 0.0, 59 | 1.0, 60 | 1.0, 61 | 0.0, 62 | 1.0, 63 | 0.0, 64 | 0.0, 65 | 0.0, 66 | 0.0, 67 | 0.0, 68 | 0.0, 69 | 1.0, 70 | 0.0, 71 | 1.0, 72 | 1.0, 73 | 1.0, 74 | 1.0, 75 | 1.0, 76 | 1.0, 77 | 0.0, 78 | 1.0, 79 | 0.0 80 | ]) 81 | 82 | [vbo] = :gl.genBuffers(1) 83 | 84 | :gl.bindBuffer(:gl_const.gl_array_buffer(), vbo) 85 | 86 | :gl.bufferData( 87 | :gl_const.gl_array_buffer(), 88 | byte_size(vertices), 89 | vertices, 90 | :gl_const.gl_static_draw() 91 | ) 92 | 93 | :gl.bindVertexArray(quadVAO) 94 | :gl.enableVertexAttribArray(0) 95 | 96 | :gl.vertexAttribPointer( 97 | 0, 98 | 4, 99 | :gl_const.gl_float(), 100 | :gl_const.gl_false(), 101 | 4 * byte_size(<<0::native-float-size(32)>>), 102 | 0 103 | ) 104 | 105 | :gl.bindBuffer(:gl_const.gl_array_buffer(), 0) 106 | :gl.bindVertexArray(0) 107 | 108 | sprite 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/breakout/renderer/text.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Renderer.Text do 2 | defstruct [] 3 | end 4 | -------------------------------------------------------------------------------- /lib/breakout/renderer/texture2d.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Renderer.Texture2D do 2 | alias Breakout.ImageParser 3 | require Logger 4 | 5 | @type t :: %__MODULE__{ 6 | id: non_neg_integer(), 7 | width: non_neg_integer(), 8 | height: non_neg_integer(), 9 | internal_format: non_neg_integer(), 10 | image_format: non_neg_integer(), 11 | wrap_s: non_neg_integer(), 12 | wrap_t: non_neg_integer(), 13 | filter_min: non_neg_integer(), 14 | filter_max: non_neg_integer() 15 | } 16 | 17 | defstruct id: 0, 18 | width: 0, 19 | height: 0, 20 | internal_format: 0, 21 | image_format: 0, 22 | wrap_s: 0, 23 | wrap_t: 0, 24 | filter_min: 0, 25 | filter_max: 0 26 | 27 | @spec new() :: t() 28 | def new() do 29 | %__MODULE__{ 30 | id: :gl.genTextures(1) |> hd, 31 | internal_format: :gl_const.gl_rgb(), 32 | image_format: :gl_const.gl_rgb(), 33 | wrap_s: :gl_const.gl_repeat(), 34 | wrap_t: :gl_const.gl_repeat(), 35 | filter_min: :gl_const.gl_linear(), 36 | filter_max: :gl_const.gl_linear() 37 | } 38 | end 39 | 40 | def load_gray(file) do 41 | tex = new() 42 | tex = %__MODULE__{tex | internal_format: :gl_const.gl_luminance, image_format: :gl_const.gl_luminance} 43 | 44 | data = case File.read(file) do 45 | {:ok, data} -> data 46 | {:error, err} -> Logger.error(err: err, file: file) 47 | end 48 | 49 | {:ok, image} = ImageParser.parse(data) 50 | 51 | generate(tex, image[:IHDR].width, image[:IHDR].height, image[:IDAT]) 52 | end 53 | 54 | def load(file, alpha) do 55 | tex = new() 56 | 57 | tex = 58 | if alpha do 59 | %__MODULE__{tex | internal_format: :gl_const.gl_rgba(), image_format: :gl_const.gl_rgba()} 60 | else 61 | tex 62 | end 63 | 64 | # load file 65 | data = 66 | case File.read(file) do 67 | {:ok, data} -> data 68 | {:error, err} -> Logger.error(err: err, file: file) 69 | end 70 | 71 | {:ok, image} = ImageParser.parse(data) 72 | 73 | generate(tex, image[:IHDR].width, image[:IHDR].height, image[:IDAT]) 74 | end 75 | 76 | @spec generate( 77 | texture :: t(), 78 | width :: non_neg_integer(), 79 | height :: non_neg_integer(), 80 | data :: binary() | 0 81 | ) :: t() 82 | def generate(texture, width, height, data) do 83 | tex = %__MODULE__{texture | width: width, height: height} 84 | 85 | :gl.bindTexture(:gl_const.gl_texture_2d(), tex.id) 86 | 87 | :gl.texImage2D( 88 | :gl_const.gl_texture_2d(), 89 | 0, 90 | tex.internal_format, 91 | width, 92 | height, 93 | 0, 94 | tex.image_format, 95 | :gl_const.gl_unsigned_byte(), 96 | data 97 | ) 98 | 99 | :gl.texParameteri(:gl_const.gl_texture_2d(), :gl_const.gl_texture_wrap_s(), tex.wrap_s) 100 | :gl.texParameteri(:gl_const.gl_texture_2d(), :gl_const.gl_texture_wrap_t(), tex.wrap_t) 101 | 102 | :gl.texParameteri( 103 | :gl_const.gl_texture_2d(), 104 | :gl_const.gl_texture_min_filter(), 105 | tex.filter_min 106 | ) 107 | 108 | :gl.texParameteri( 109 | :gl_const.gl_texture_2d(), 110 | :gl_const.gl_texture_mag_filter(), 111 | tex.filter_max 112 | ) 113 | 114 | :gl.bindTexture(:gl_const.gl_texture_2d(), 0) 115 | 116 | tex 117 | end 118 | 119 | @spec bind(texture :: t()) :: :ok 120 | def bind(texture) do 121 | :gl.bindTexture(:gl_const.gl_texture_2d(), texture.id) 122 | :ok 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/breakout/renderer/window.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Renderer.Window do 2 | alias Breakout.Input 3 | 4 | defstruct [:frame, :canvas, :context] 5 | 6 | @type t :: %__MODULE__{ 7 | frame: :wxFrame.wxFrame(), 8 | canvas: :wxGLCanvas.wxGLCanvas(), 9 | context: :wxGLContext.wxGLContext() 10 | } 11 | 12 | @spec init(width :: pos_integer(), height :: pos_integer()) :: t() 13 | def init(width, height) do 14 | opts = [size: {width, height}] 15 | 16 | wx = :wx.new() 17 | 18 | frame = :wxFrame.new(wx, :wx_const.wx_id_any(), ~c"Elixir Breakout", opts) 19 | 20 | :wxWindow.connect(frame, :close_window) 21 | 22 | :wxFrame.show(frame) 23 | 24 | gl_attrib = [ 25 | attribList: [ 26 | :wx_const.wx_gl_core_profile(), 27 | :wx_const.wx_gl_major_version(), 28 | 4, 29 | :wx_const.wx_gl_minor_version(), 30 | 1, 31 | :wx_const.wx_gl_doublebuffer(), 32 | # :wx_const.wx_gl_depth_size, 24, 33 | :wx_const.wx_gl_sample_buffers(), 34 | 1, 35 | :wx_const.wx_gl_samples(), 36 | 4, 37 | 0 38 | ] 39 | ] 40 | 41 | canvas = :wxGLCanvas.new(frame, opts ++ gl_attrib) 42 | ctx = :wxGLContext.new(canvas) 43 | 44 | # cursor = :wxCursor.new(:wx_const.wx_cursor_blank) 45 | # :wxWindow.setCursor(canvas, cursor) 46 | 47 | # :wxWindow.captureMouse(canvas) 48 | 49 | :wxGLCanvas.setFocus(canvas) 50 | 51 | :wxGLCanvas.setCurrent(canvas, ctx) 52 | 53 | :wxGLCanvas.connect(canvas, :key_down, callback: &Input.handler/2) 54 | :wxGLCanvas.connect(canvas, :key_up, callback: &Input.handler/2) 55 | :wxGLCanvas.connect(canvas, :motion, callback: &Input.handler/2) 56 | :wxGLCanvas.connect(canvas, :mousewheel) 57 | 58 | %__MODULE__{ 59 | frame: frame, 60 | canvas: canvas, 61 | context: ctx 62 | } 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/breakout/resource_manager.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.ResourceManager do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | alias Breakout.Renderer.{Shader, Texture2D} 7 | 8 | defstruct shaders: %{}, textures: %{} 9 | 10 | @type t :: %__MODULE__{ 11 | shaders: %{atom() => Shader.t()}, 12 | textures: %{atom() => Texture2D.t()} 13 | } 14 | 15 | def start_link(_arg) do 16 | GenServer.start_link(__MODULE__, [], name: __MODULE__) 17 | end 18 | 19 | @impl GenServer 20 | def init(_arg) do 21 | {:ok, new()} 22 | end 23 | 24 | def new() do 25 | %__MODULE__{} 26 | end 27 | 28 | @impl GenServer 29 | def handle_cast({:put_shader, name, shader}, state) do 30 | state = put_in(state.shaders[name], shader) 31 | 32 | {:noreply, state} 33 | end 34 | 35 | @impl GenServer 36 | def handle_cast({:put_texture, name, texture}, state) do 37 | state = put_in(state.textures[name], texture) 38 | 39 | {:noreply, state} 40 | end 41 | 42 | @impl GenServer 43 | def handle_call({:get_shader, name}, _from, state) do 44 | reply = Map.get(state.shaders, name) 45 | 46 | {:reply, {:ok, reply}, state} 47 | end 48 | 49 | @impl GenServer 50 | def handle_call({:get_texture, name}, _from, state) do 51 | reply = Map.get(state.textures, name) 52 | 53 | {:reply, {:ok, reply}, state} 54 | end 55 | 56 | def put_shader(shader, name) do 57 | GenServer.cast(__MODULE__, {:put_shader, name, shader}) 58 | 59 | shader 60 | end 61 | 62 | def get_shader(name) do 63 | GenServer.call(__MODULE__, {:get_shader, name}) 64 | end 65 | 66 | def put_texture(texture, name) do 67 | GenServer.cast(__MODULE__, {:put_texture, name, texture}) 68 | 69 | texture 70 | end 71 | 72 | def get_texture(name) do 73 | GenServer.call(__MODULE__, {:get_texture, name}) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/breakout/state.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.State do 2 | alias Breakout.PowerUp 3 | alias Breakout.Renderer.PostProcessor 4 | alias Breakout.ParticleGenerator 5 | alias Breakout.Renderer.Shader 6 | alias Breakout.BallObject 7 | alias Breakout.Renderer.Texture2D 8 | alias Breakout.GameObject 9 | alias Breakout.GameLevel 10 | 11 | @game_width 1200 12 | @game_height 800 13 | 14 | @type game_state :: :active | :menu | :win | nil 15 | 16 | @type t :: %__MODULE__{ 17 | game_state: game_state(), 18 | keys: MapSet.t(integer()), 19 | keys_processed: MapSet.t(integer()), 20 | width: pos_integer(), 21 | height: pos_integer(), 22 | window: :wxWindow.wxWindow() | nil, 23 | t: pos_integer(), 24 | dt: non_neg_integer(), 25 | shader_program: non_neg_integer(), 26 | sprite_renderer: non_neg_integer(), 27 | levels: [GameLevel.t()], 28 | level: non_neg_integer(), 29 | player: GameObject.t(), 30 | background_texture: Texture2D.t(), 31 | ball: BallObject.t(), 32 | resources: %{shaders: %{atom() => Shader.t()}, textures: %{atom() => Texture2D.t()}}, 33 | particle_generator: ParticleGenerator.t(), 34 | post_processor: PostProcessor.t(), 35 | elapsed: float(), 36 | start: float(), 37 | shake_time: float(), 38 | power_ups: [PowerUp.t()], 39 | font: :wxFont.wxFont(), 40 | brush: :wxBrush.wxBrush(), 41 | lives: pos_integer(), 42 | menu_string_size: {non_neg_integer(), non_neg_integer()}, 43 | } 44 | 45 | defstruct [ 46 | # :active | :menu | :win 47 | game_state: :active, 48 | keys: MapSet.new(), 49 | keys_processed: MapSet.new(), 50 | width: @game_width, 51 | height: @game_height, 52 | window: nil, 53 | t: 0, 54 | dt: 0, 55 | shader_program: 0, 56 | sprite_renderer: 0, 57 | levels: [], 58 | level: 0, 59 | player: GameObject.new(), 60 | background_texture: nil, 61 | ball: BallObject.new(), 62 | resources: %{shaders: %{}, textures: %{}}, 63 | particle_generator: nil, 64 | post_processor: nil, 65 | elapsed: 0.0, 66 | start: 0.0, 67 | shake_time: 0.0, 68 | power_ups: [], 69 | font: nil, 70 | brush: nil, 71 | lives: 3, 72 | menu_string_size: {200, 200}, 73 | ] 74 | end 75 | -------------------------------------------------------------------------------- /lib/breakout/util.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.Util do 2 | def make_bits(list) do 3 | list 4 | |> Enum.reduce(<<>>, fn el, acc -> acc <> <> end) 5 | end 6 | 7 | def make_bits_unsigned(list) do 8 | list 9 | |> Enum.reduce(<<>>, fn el, acc -> acc <> <> end) 10 | end 11 | 12 | def to_priv(path) do 13 | :code.priv_dir(:breakout) 14 | |> Path.join(path) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/breakout/wx_records.ex: -------------------------------------------------------------------------------- 1 | defmodule Breakout.WxRecords do 2 | require Record 3 | 4 | for {type, record} <- Record.extract_all(from_lib: "wx/include/wx.hrl") do 5 | Record.defrecord(type, record) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Breakout.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :breakout, 7 | version: "0.1.0", 8 | elixir: "~> 1.17", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger, :wx, :observer, :runtime_tools, :tools, :xmerl, :debugger], 18 | mod: {Breakout.Application, []} 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | {:membrane_core, "~> 1.1"}, 26 | {:membrane_file_plugin, "~> 0.17.2"}, 27 | {:membrane_portaudio_plugin, "~> 0.19.2"}, 28 | {:membrane_ffmpeg_swresample_plugin, "~> 0.20.2"}, 29 | {:membrane_mp3_mad_plugin, "~> 0.18.3"}, 30 | 31 | {:membrane_funnel_plugin, "~> 0.9.0"}, 32 | # {:exsync, "~> 0.4", only: :dev} 33 | # {:dep_from_hexpm, "~> 0.3.0"}, 34 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 35 | ] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bimap": {:hex, :bimap, "1.3.0", "3ea4832e58dc83a9b5b407c6731e7bae87458aa618e6d11d8e12114a17afa4b3", [:mix], [], "hexpm", "bf5a2b078528465aa705f405a5c638becd63e41d280ada41e0f77e6d255a10b4"}, 3 | "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, 4 | "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"}, 5 | "bundlex": {:hex, :bundlex, "1.5.3", "35d01e5bc0679510dd9a327936ffb518f63f47175c26a35e708cc29eaec0890b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "debd0eac151b404f6216fc60222761dff049bf26f7d24d066c365317650cd118"}, 6 | "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, 7 | "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, 8 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, 9 | "exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"}, 10 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 11 | "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, 12 | "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, 13 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 14 | "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, 15 | "membrane_common_c": {:hex, :membrane_common_c, "0.16.0", "caf3f29d2f5a1d32d8c2c122866110775866db2726e4272be58e66dfdf4bce40", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "a3c7e91de1ce1f8b23b9823188a5d13654d317235ea0ca781c05353ed3be9b1c"}, 16 | "membrane_core": {:hex, :membrane_core, "1.1.1", "4dcff6e9f3b2ecd4f437c20e201e53957731772c0f15b3005062c41f7f58f500", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 3.0 or ~> 4.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3802f3fc071505c59d48792487d9927e803d4edb4039710ffa52cdb60bb0aecc"}, 17 | "membrane_ffmpeg_swresample_plugin": {:hex, :membrane_ffmpeg_swresample_plugin, "0.20.2", "2e669f0b25418d10b51a73bc52d2e12e4a3a26b416c5c1199d852c3f781a18b3", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.2", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.12.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:mockery, "~> 2.1", [hex: :mockery, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "6c8d3bcd61d568dd94cabb9b45f29e8926e0076e4432d8f419378e004e02147c"}, 18 | "membrane_file_plugin": {:hex, :membrane_file_plugin, "0.17.2", "650e134c2345d946f930082fac8bac9f5aba785a7817d38a9a9da41ffc56fa92", [:mix], [{:logger_backends, "~> 1.0", [hex: :logger_backends, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "df50c6040004cd7b901cf057bd7e99c875bbbd6ae574efc93b2c753c96f43b9d"}, 19 | "membrane_funnel_plugin": {:hex, :membrane_funnel_plugin, "0.9.0", "9cfe09e44d65751f7d9d8d3c42e14797f7be69e793ac112ea63cd224af70a7bf", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "988790aca59d453a6115109f050699f7f45a2eb6a7f8dc5c96392760cddead54"}, 20 | "membrane_mp3_mad_plugin": {:hex, :membrane_mp3_mad_plugin, "0.18.3", "7cf02b7185439918f2deb794af53c13dbc07a7cf61ff81500731b4cf1d62eb5b", [:mix], [{:bundlex, "~> 1.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_mpegaudio_format, "~> 0.3.0", [hex: :membrane_mpegaudio_format, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.12.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "0917fb3aa33529e331ce2d363f2479e0efc648ec791ceed1c2ce7f54139bf9ca"}, 21 | "membrane_mpegaudio_format": {:hex, :membrane_mpegaudio_format, "0.3.0", "d4fee77fad9f953171c52acd6d53b6646cfc7fbb827c63caa7c6a1efeb86450a", [:mix], [], "hexpm", "dec903efd0086133402b44515d04301790832b4f39995747b0e712c8f966d50d"}, 22 | "membrane_portaudio_plugin": {:hex, :membrane_portaudio_plugin, "0.19.2", "d67e1aba624f7b87c4932b2016082d2be5740d537429ac37330e03dacead37e6", [:mix], [{:bunch, "~> 1.5", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.5", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.12.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:mockery, "~> 2.1", [hex: :mockery, repo: "hexpm", optional: false]}], "hexpm", "216adf08fbc9c0cd0cee791a25a44fa2da1a90537bd4fdb0457b6701ea2b0715"}, 23 | "membrane_precompiled_dependency_provider": {:hex, :membrane_precompiled_dependency_provider, "0.1.2", "8af73b7dc15ba55c9f5fbfc0453d4a8edfb007ade54b56c37d626be0d1189aba", [:mix], [{:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "7fe3e07361510445a29bee95336adde667c4162b76b7f4c8af3aeb3415292023"}, 24 | "membrane_raw_audio_format": {:hex, :membrane_raw_audio_format, "0.12.0", "b574cd90f69ce2a8b6201b0ccf0826ca28b0fbc8245b8078d9f11cef65f7d5d5", [:mix], [{:bimap, "~> 1.1", [hex: :bimap, repo: "hexpm", optional: false]}, {:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "6e6c98e3622a2b9df19eab50ba65d7eb45949b1ba306fa8423df6cdb12fd0b44"}, 25 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 26 | "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, 27 | "mockery": {:hex, :mockery, "2.3.3", "3dba87bd0422a513e6af6e0d811383f38f82ac6be5d3d285a5fcca9c299bd0ac", [:mix], [], "hexpm", "17282be00613286254298117cd25e607a39f15ac03b41c631f60e52f5b5ec974"}, 28 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 29 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 30 | "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, 31 | "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, 32 | "ratio": {:hex, :ratio, "4.0.1", "3044166f2fc6890aa53d3aef0c336f84b2bebb889dc57d5f95cc540daa1912f8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "c60cbb3ccdff9ffa56e7d6d1654b5c70d9f90f4d753ab3a43a6bf40855b881ce"}, 33 | "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"}, 34 | "shmex": {:hex, :shmex, "0.5.1", "81dd209093416bf6608e66882cb7e676089307448a1afd4fc906c1f7e5b94cf4", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "c29f8286891252f64c4e1dac40b217d960f7d58def597c4e606ff8fbe71ceb80"}, 35 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 36 | "unifex": {:hex, :unifex, "1.1.2", "ed3366515b6612a5a08d24a38658dea18a6c6001e79cf41e3a2edd07004d3c6d", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "c25b9d4d1a1c76716ecdf68d0873553fdab4105f418ef76f646a5cb47e0396ab"}, 37 | "zarex": {:hex, :zarex, "1.0.5", "58239e3ee5d75f343262bb4df5cf466555a1c689f920e5d3651a9333972f7c7e", [:mix], [], "hexpm", "9fb72ef0567c2b2742f5119a1ba8a24a2fabb21b8d09820aefbf3e592fa9a46a"}, 38 | } 39 | -------------------------------------------------------------------------------- /priv/audio/block.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/audio/block.mp3 -------------------------------------------------------------------------------- /priv/audio/breakout.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/audio/breakout.mp3 -------------------------------------------------------------------------------- /priv/audio/paddle.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/audio/paddle.mp3 -------------------------------------------------------------------------------- /priv/audio/powerup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/audio/powerup.mp3 -------------------------------------------------------------------------------- /priv/audio/solid.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/audio/solid.mp3 -------------------------------------------------------------------------------- /priv/levels/four.lvl: -------------------------------------------------------------------------------- 1 | 1 2 1 2 1 2 1 2 1 2 1 2 1 2 | 2 2 2 2 2 2 2 2 2 2 2 2 2 3 | 2 1 3 1 4 1 5 1 4 1 3 1 2 4 | 2 3 3 4 4 5 5 5 4 4 3 3 2 5 | 2 1 3 1 4 1 5 1 4 1 3 1 2 6 | 2 2 3 3 4 4 5 4 4 3 3 2 2 7 | -------------------------------------------------------------------------------- /priv/levels/one.lvl: -------------------------------------------------------------------------------- 1 | 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 2 | 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 3 | 4 4 4 4 4 0 0 0 0 0 4 4 4 4 4 4 | 4 1 4 1 4 0 0 1 0 0 4 1 4 1 4 5 | 3 3 3 3 3 0 0 0 0 0 3 3 3 3 3 6 | 3 3 1 3 3 3 3 3 3 3 3 3 1 3 3 7 | 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 8 | 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 -------------------------------------------------------------------------------- /priv/levels/three.lvl: -------------------------------------------------------------------------------- 1 | 0 0 0 0 0 0 0 0 0 0 0 0 0 2 | 0 0 2 0 0 0 0 0 0 0 2 0 0 3 | 0 0 0 2 0 0 0 0 0 2 0 0 0 4 | 0 0 0 5 5 5 5 5 5 5 0 0 0 5 | 0 0 5 5 0 5 5 5 0 5 5 0 0 6 | 0 5 5 5 5 5 5 5 5 5 5 5 0 7 | 0 3 0 1 1 1 1 1 1 1 0 3 0 8 | 0 3 0 3 0 0 0 0 0 3 0 3 0 9 | 0 0 0 0 4 4 0 4 4 0 0 0 0 10 | -------------------------------------------------------------------------------- /priv/levels/two.lvl: -------------------------------------------------------------------------------- 1 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 | 1 0 5 5 0 5 5 0 5 5 0 5 5 0 1 3 | 1 5 5 5 5 5 5 5 5 5 5 5 5 5 1 4 | 1 0 3 3 0 3 3 0 3 3 0 3 3 0 1 5 | 1 3 3 3 3 3 3 3 3 3 3 3 3 3 1 6 | 1 0 2 2 0 2 2 0 2 2 0 2 2 0 1 7 | 1 2 2 2 2 2 2 2 2 2 2 2 2 2 1 8 | 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 9 | -------------------------------------------------------------------------------- /priv/shaders/particle/fragment.fs: -------------------------------------------------------------------------------- 1 | #version 410 core 2 | 3 | in vec2 TexCoords; 4 | in vec4 ParticleColor; 5 | out vec4 color; 6 | 7 | uniform sampler2D sprite; 8 | 9 | void main() { 10 | color = (texture(sprite, TexCoords) * ParticleColor); 11 | } -------------------------------------------------------------------------------- /priv/shaders/particle/vertex.vs: -------------------------------------------------------------------------------- 1 | #version 410 core 2 | layout (location = 0) in vec4 vertex; 3 | 4 | out vec2 TexCoords; 5 | out vec4 ParticleColor; 6 | 7 | uniform mat4 projection; 8 | uniform vec2 offset; 9 | uniform vec4 color; 10 | 11 | void main() { 12 | float scale = 10.0f; 13 | TexCoords = vertex.zw; 14 | ParticleColor = color; 15 | gl_Position = projection * vec4((vertex.xy * scale) + offset, 0.0, 1.0); 16 | } -------------------------------------------------------------------------------- /priv/shaders/post_processor/fragment.fs: -------------------------------------------------------------------------------- 1 | #version 410 core 2 | in vec2 TexCoords; 3 | out vec4 color; 4 | 5 | uniform sampler2D scene; 6 | uniform vec2 offsets[9]; 7 | uniform int edge_kernel[9]; 8 | uniform float blur_kernel[9]; 9 | 10 | uniform bool chaos; 11 | uniform bool confuse; 12 | uniform bool shake; 13 | 14 | #define SIZE 9 15 | 16 | void main() { 17 | color = vec4(0.0f); // , 0.0f, 0.0f, 1.0f); 18 | vec3 samplex[9]; 19 | 20 | if (chaos || shake) { 21 | for (int i = 0; i < 9; i++) { 22 | samplex[i] = vec3(texture(scene, TexCoords.st + offsets[i])); 23 | } 24 | } 25 | 26 | if (chaos) { 27 | for (int i = 0; i < 9; i++) { 28 | color += vec4(samplex[i] * edge_kernel[i], 0.0f); 29 | } 30 | 31 | color.a = 1.0f; 32 | } else if (confuse) { 33 | color = vec4(1.0 - texture(scene, TexCoords).rgb, 1.0); 34 | } else if (shake) { 35 | for (int i = 0; i < 9; i++) { 36 | color += vec4(samplex[i] * blur_kernel[i], 0.0f); 37 | } 38 | color.a = 1.0f; 39 | } else { 40 | color = texture(scene, TexCoords); 41 | } 42 | } -------------------------------------------------------------------------------- /priv/shaders/post_processor/vertex.vs: -------------------------------------------------------------------------------- 1 | #version 410 core 2 | layout (location = 0) in vec4 vertex; 3 | 4 | out vec2 TexCoords; 5 | 6 | uniform bool chaos; 7 | uniform bool confuse; 8 | uniform bool shake; 9 | uniform float time; 10 | 11 | void main() { 12 | gl_Position = vec4(vertex.xy, 0.0f, 1.0f); 13 | vec2 texture = vertex.zw; 14 | if (chaos) { 15 | float strength = 0.3; 16 | vec2 pos = vec2(texture.x + sin(time) * strength, texture.y + cos(time) * strength); 17 | TexCoords = pos; 18 | } else if (confuse) { 19 | TexCoords = vec2(1.0 - texture.x, 1.0 - texture.y); 20 | } else { 21 | TexCoords = texture; 22 | } 23 | 24 | if (shake) { 25 | float strength = 0.01; 26 | gl_Position.x += cos(time * 10) * strength; 27 | gl_Position.y += cos(time * 15) * strength; 28 | } 29 | } -------------------------------------------------------------------------------- /priv/shaders/sprite/fragment.fs: -------------------------------------------------------------------------------- 1 | #version 410 core 2 | 3 | in vec2 TexCoords; 4 | out vec4 color; 5 | 6 | uniform sampler2D image; 7 | uniform vec3 spriteColor; 8 | 9 | void main() 10 | { 11 | color = vec4(spriteColor, 1.0) * texture(image, TexCoords); 12 | } -------------------------------------------------------------------------------- /priv/shaders/sprite/vertex.vs: -------------------------------------------------------------------------------- 1 | #version 410 core 2 | 3 | layout (location = 0) in vec4 vertex; // 4 | 5 | out vec2 TexCoords; 6 | 7 | uniform mat4 model; 8 | uniform mat4 projection; 9 | 10 | void main() 11 | { 12 | TexCoords = vertex.zw; 13 | gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0); 14 | } -------------------------------------------------------------------------------- /priv/sprites/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/sprites/test.png -------------------------------------------------------------------------------- /priv/textures/ascii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/ascii.png -------------------------------------------------------------------------------- /priv/textures/ascii_rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/ascii_rgb.png -------------------------------------------------------------------------------- /priv/textures/awesomeface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/awesomeface.png -------------------------------------------------------------------------------- /priv/textures/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/background.jpg -------------------------------------------------------------------------------- /priv/textures/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/background.png -------------------------------------------------------------------------------- /priv/textures/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/block.png -------------------------------------------------------------------------------- /priv/textures/block_solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/block_solid.png -------------------------------------------------------------------------------- /priv/textures/paddle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/paddle.png -------------------------------------------------------------------------------- /priv/textures/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/particle.png -------------------------------------------------------------------------------- /priv/textures/powerup_chaos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_chaos.png -------------------------------------------------------------------------------- /priv/textures/powerup_confuse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_confuse.png -------------------------------------------------------------------------------- /priv/textures/powerup_increase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_increase.png -------------------------------------------------------------------------------- /priv/textures/powerup_passthrough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_passthrough.png -------------------------------------------------------------------------------- /priv/textures/powerup_speed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_speed.png -------------------------------------------------------------------------------- /priv/textures/powerup_sticky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrisi/elixir_breakout/85fe1aef2af7462ea25b5403584887e20ccba00c/priv/textures/powerup_sticky.png -------------------------------------------------------------------------------- /src/gl_const.erl: -------------------------------------------------------------------------------- 1 | -module(gl_const). 2 | -compile(nowarn_export_all). 3 | -compile(export_all). 4 | 5 | -include_lib("wx/include/gl.hrl"). 6 | 7 | gl_depth_test() -> ?GL_DEPTH_TEST. 8 | 9 | gl_lequal() -> ?GL_LEQUAL. 10 | gl_color_buffer_bit() -> ?GL_COLOR_BUFFER_BIT. 11 | 12 | gl_depth_buffer_bit() -> ?GL_DEPTH_BUFFER_BIT. 13 | 14 | gl_triangles() -> ?GL_TRIANGLES. 15 | gl_array_buffer() -> ?GL_ARRAY_BUFFER. 16 | gl_element_array_buffer() -> ?GL_ELEMENT_ARRAY_BUFFER. 17 | gl_static_draw() -> ?GL_STATIC_DRAW. 18 | 19 | gl_vertex_shader() -> ?GL_VERTEX_SHADER. 20 | gl_fragment_shader() -> ?GL_FRAGMENT_SHADER. 21 | 22 | gl_compile_status() -> ?GL_COMPILE_STATUS. 23 | gl_link_status() -> ?GL_LINK_STATUS. 24 | 25 | gl_float() -> ?GL_FLOAT. 26 | gl_false() -> ?GL_FALSE. 27 | gl_true() -> ?GL_TRUE. 28 | gl_unsigned_int() -> ?GL_UNSIGNED_INT. 29 | gl_unsigned_byte() -> ?GL_UNSIGNED_BYTE. 30 | 31 | gl_front_and_back() -> ?GL_FRONT_AND_BACK. 32 | gl_line() -> ?GL_LINE. 33 | gl_fill() -> ?GL_FILL. 34 | gl_debug_output() -> ?GL_DEBUG_OUTPUT. 35 | gl_texture_2d() -> ?GL_TEXTURE_2D. 36 | gl_texture_wrap_s() -> ?GL_TEXTURE_WRAP_S. 37 | gl_texture_wrap_t() -> ?GL_TEXTURE_WRAP_T. 38 | gl_texture_min_filter() -> ?GL_TEXTURE_MIN_FILTER. 39 | gl_texture_mag_filter() -> ?GL_TEXTURE_MAG_FILTER. 40 | gl_rgb() -> ?GL_RGB. 41 | gl_rgba() -> ?GL_RGBA. 42 | gl_multisample() -> ?GL_MULTISAMPLE. 43 | gl_luminance() -> ?GL_LUMINANCE. 44 | 45 | gl_texture0() -> ?GL_TEXTURE0. 46 | 47 | gl_cull_face() -> ?GL_CULL_FACE. 48 | gl_back() -> ?GL_BACK. 49 | gl_front() -> ?GL_FRONT. 50 | gl_ccw() -> ?GL_CCW. 51 | gl_cw() -> ?GL_CW. 52 | 53 | gl_info_log_length() -> ?GL_INFO_LOG_LENGTH. 54 | 55 | gl_blend() -> ?GL_BLEND. 56 | gl_src_alpha() -> ?GL_SRC_ALPHA. 57 | gl_one() -> ?GL_ONE. 58 | gl_one_minus_src_alpha() -> ?GL_ONE_MINUS_SRC_ALPHA. 59 | 60 | gl_repeat() -> ?GL_REPEAT. 61 | gl_linear() -> ?GL_LINEAR. 62 | gl_nearest() -> ?GL_NEAREST. 63 | 64 | gl_framebuffer() -> ?GL_FRAMEBUFFER. 65 | gl_renderbuffer() -> ?GL_RENDERBUFFER. 66 | gl_color_attachment0() -> ?GL_COLOR_ATTACHMENT0. 67 | gl_framebuffer_complete() -> ?GL_FRAMEBUFFER_COMPLETE. 68 | gl_read_framebuffer() -> ?GL_READ_FRAMEBUFFER. 69 | gl_draw_framebuffer() -> ?GL_DRAW_FRAMEBUFFER. 70 | 71 | gl_texture_env() -> ?GL_TEXTURE_ENV. 72 | gl_texture_env_mode() -> ?GL_TEXTURE_ENV_MODE. 73 | gl_replace() -> ?GL_REPLACE. -------------------------------------------------------------------------------- /src/wx_const.erl: -------------------------------------------------------------------------------- 1 | -module(wx_const). 2 | -compile(nowarn_export_all). 3 | -compile(export_all). 4 | 5 | -include_lib("wx/include/wx.hrl"). 6 | 7 | wx_id_any() -> ?wxID_ANY. 8 | wx_gl_rgba() -> ?WX_GL_RGBA. 9 | 10 | wx_gl_doublebuffer() -> ?WX_GL_DOUBLEBUFFER. 11 | wx_gl_depth_size() -> ?WX_GL_DEPTH_SIZE. 12 | wx_gl_forward_compat() -> ?WX_GL_FORWARD_COMPAT. 13 | 14 | wxk_left() -> ?WXK_LEFT. 15 | wxk_right() -> ?WXK_RIGHT. 16 | wxk_up() -> ?WXK_UP. 17 | wxk_down() -> ?WXK_DOWN. 18 | wxk_space() -> ?WXK_SPACE. 19 | wxk_raw_control() -> ?WXK_RAW_CONTROL. 20 | 21 | wx_gl_major_version() -> ?WX_GL_MAJOR_VERSION. 22 | 23 | wx_gl_minor_version() -> ?WX_GL_MINOR_VERSION. 24 | 25 | wx_gl_core_profile() -> ?WX_GL_CORE_PROFILE. 26 | wx_gl_sample_buffers() -> ?WX_GL_SAMPLE_BUFFERS. 27 | 28 | wx_gl_samples() -> ?WX_GL_SAMPLES. 29 | 30 | wx_null_cursor() -> ?wxNullCursor. 31 | wx_cursor_blank() -> ?wxCURSOR_BLANK. 32 | wx_cursor_cross() -> ?wxCURSOR_CROSS. 33 | 34 | wx_fontfamily_default() -> ?wxFONTFAMILY_DEFAULT. 35 | wx_fontfamily_teletype() -> ?wxFONTFAMILY_TELETYPE. 36 | wx_normal() -> ?wxNORMAL. 37 | wx_fontstyle_normal() -> ?wxFONTSTYLE_NORMAL. 38 | wx_fontweight_bold() -> ?wxFONTWEIGHT_BOLD. 39 | wx_fontweight_normal() -> ?wxFONTWEIGHT_NORMAL. -------------------------------------------------------------------------------- /test/breakout_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BreakoutTest do 2 | use ExUnit.Case 3 | doctest Breakout 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------