Extract 2d and 3d functions into separate libraries

This commit is contained in:
Alex Clink 2024-10-07 01:02:31 -04:00
parent 5fc507fbaf
commit 0bf101777e
No known key found for this signature in database
GPG key ID: 081CC9920757FABF
55 changed files with 346 additions and 2088 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 B

View file

@ -1,34 +1,35 @@
require "pf3d"
require "../src/game"
require "../src/controller"
require "../src/sprite"
require "../src/pixel"
require "../src/vector"
require "../src/sprite"
require "../src/3d/*"
class ThreeDee < PF::Game
@projector : PF::Projector
@camera : PF::Camera
@projector : PF3d::Projector
@camera : PF3d::Camera
@paused = false
@speed = 10.0
@controller : PF::Controller(PF::Keys)
@depth_buffer : PF::DepthBuffer
@depth_buffer : PF3d::DepthBuffer
@font = Pixelfont::Font.new("#{__DIR__}/../lib/pixelfont/fonts/pixel-5x7.txt")
def initialize(*args, **kwargs)
super
@projector = PF::Projector.new(width, height)
@depth_buffer = PF::DepthBuffer.new(width, height)
@projector = PF3d::Projector.new(width, height)
@depth_buffer = PF3d::DepthBuffer.new(width, height)
@camera = @projector.camera
@model = PF::Mesh.load_obj("./assets/pixelfaucet.obj")
@model = PF3d::Mesh.load_obj("./assets/pixelfaucet.obj")
@model_texture = PF::Sprite.new("./assets/bricks.png")
@model.position.z = @model.position.z + 2.0
@cube_model = PF::Mesh.load_obj("./assets/cube.obj")
@cube_model = PF3d::Mesh.load_obj("./assets/cube.obj")
@cube_model_texture = PF::Sprite.new("./assets/bricks.png")
@cube_model.position.z = @cube_model.position.z + 2.5
@sprite = PF::Sprite.new("./assets/bricks.png")
@controller = PF::Controller(PF::Keys).new({
PF::Keys::RIGHT => "Rotate Right",
@ -67,7 +68,7 @@ class ThreeDee < PF::Game
@camera.position.y = @camera.position.y - @speed * dt
end
# Control the camera pitch instead of elevation -
# Control the camera pitch instead of elevation
# TODO: this needs to account for where the camera is pointing
# if @controller.held?("Up")
@ -94,54 +95,43 @@ class ThreeDee < PF::Game
@camera.position = @camera.position - (forward * @speed * dt)
end
@model.rotation.x = @model.rotation.x + 1.0 * dt
# @model.rotation.x = @model.rotation.x + 1.0 * dt
@cube_model.rotation.x = @cube_model.rotation.x + 1.0 * dt
end
def draw
# clear(25, 50, 25)
clear
@depth_buffer.clear
clear(25, 50, 25)
# clear
cube_tris = @projector.project(@cube_model.tris)
cube_tris.each do |tri|
fill_triangle(
@depth_buffer.clear
tri_count = 0
@projector.project(@cube_model.tris).each do |tri|
tri_count += 1
paint_triangle(
tri.p1.to_i, tri.p2.to_i, tri.p3.to_i, # Points
tri.t1, tri.t2, tri.t3, # Texture Points
@sprite,
@cube_model_texture,
@depth_buffer,
tri.color
PF::Pixel::White * tri.shade
)
if tri.clipped
p1 = PF::Vector[tri.p1.x.to_i, tri.p1.y.to_i]
p2 = PF::Vector[tri.p2.x.to_i, tri.p2.y.to_i]
p3 = PF::Vector[tri.p3.x.to_i, tri.p3.y.to_i]
draw_triangle(p1, p2, p3, PF::Pixel.new(255, 255, 0))
end
end
tris = @projector.project(@model.tris, sort: true)
tris.each do |tri|
# Rasterize all triangles
@projector.project(@model.tris).each do |tri|
tri_count += 1
p1 = PF::Vector[tri.p1.x.to_i, tri.p1.y.to_i]
p2 = PF::Vector[tri.p2.x.to_i, tri.p2.y.to_i]
p3 = PF::Vector[tri.p3.x.to_i, tri.p3.y.to_i]
fill_triangle(
p1,
p2,
p3,
pixel: tri.color # buffer: @depth_buffer
paint_triangle(
tri.p1.to_i, tri.p2.to_i, tri.p3.to_i, # Points
tri.t1, tri.t2, tri.t3, # Texture Points
nil,
@depth_buffer,
PF::Pixel::White * tri.shade
)
if tri.clipped
draw_triangle(p1, p2, p3, PF::Pixel.new(255, 0, 0))
end
end
string = String.build do |io|
io << "Triangles: " << cube_tris.size + tris.size
io << "Triangles: " << tri_count
io << "\nPosition: "
io << "x: " << @camera.position.x.round(2)
io << "y: " << @camera.position.y.round(2)
@ -152,9 +142,10 @@ class ThreeDee < PF::Game
io << "z: " << @camera.rotation.z.round(2)
end
draw_string(string, 3, 3)
draw_string(string, 3, 3, @font, PF::Pixel::White)
end
end
engine = ThreeDee.new(640, 480, 2)
# engine = ThreeDee.new(256, 240, 4)
engine = ThreeDee.new(256 * 2, 240 * 2, 2)
engine.run!

View file

@ -1,12 +1,11 @@
require "../src/game"
require "../src/sprite"
require "../src/transform2d"
module PF
class Affine < Game
@bricks : Sprite
@top_left : Vector2(Int32) = Vector[0, 0]
@transform : Transform2d = Transform2d.new
@top_left : PF2d::Vec2(Int32) = PF2d::Vec[0, 0]
@transform : PF2d::Transform = PF2d::Transform.new
@angle = 0.0
@size = 1.0
@zoom = 0.5

View file

@ -8,6 +8,7 @@ module PF
super
@person = Animation.new("assets/walking.png", 32, 64, 10)
@cat = Animation.new("assets/black-cat.png", 18, 14, 15)
@font = Pixelfont::Font.new("#{__DIR__}/../lib/pixelfont/fonts/pixel-5x7.txt")
end
def update(dt)
@ -17,7 +18,7 @@ module PF
def draw
clear(60, 120, 200)
draw_string("Frame: #{@person.frame}", 5, 5)
draw_string("Frame: #{@person.frame}", 5, 5, @font, Pixel::White)
fill_rect(0, 65, width - 1, height - 1, Pixel.new(100, 100, 100))
@person.draw_to(screen, (viewport // 2) - @person.size // 2)
@cat.draw_to(screen, 30, 56)

View file

@ -7,7 +7,7 @@ module PF
class Ball < Entity
include CircleCollision
getter frame : Array(Vector2(Float64))
getter frame : Array(PF2d::Vec2(Float64))
getter color = Pixel.random
def initialize(size : Float64)
@ -21,6 +21,7 @@ module PF
ADD_BALL = 2.0
@balls : Array(Ball) = [] of Ball
@ball_clock = ADD_BALL
@font = Pixelfont::Font.new("#{__DIR__}/../lib/pixelfont/fonts/pixel-5x7.txt")
def initialize(*args, **kwargs)
super
@ -28,10 +29,10 @@ module PF
end
def add_ball
position = Vector[rand(0.0_f64..width.to_f64), rand(0.0_f64..height.to_f64)]
position = PF2d::Vec[rand(0.0_f64..width.to_f64), rand(0.0_f64..height.to_f64)]
ball = Ball.new(rand(10.0..30.0))
ball.position = position
ball.velocity = Vector[rand(-50.0..50.0), rand(-50.0..50.0)]
ball.velocity = PF2d::Vec[rand(-50.0..50.0), rand(-50.0..50.0)]
@balls << ball
end
@ -71,7 +72,7 @@ module PF
@balls.each do |ball|
fill_shape(Shape.translate(ball.frame, translation: ball.position).map(&.to_i32), ball.color)
end
draw_string("Balls: #{@balls.size}", 5, 5, Pixel::White)
draw_string("Balls: #{@balls.size}", 5, 5, @font, Pixel::White)
end
end
end

View file

@ -1,84 +1,83 @@
require "../src/game"
require "../src/bezier"
module PF
class CubicBezier < PF::Game
FONT_COLOR = Pixel.new(0xFFFFFFFF)
POINT_COLOR = Pixel.new(0xFF0000FF)
CTL_COLOR = Pixel.new(0x505050FF)
CURVE_COLOR = Pixel.new(0x0077FFFF)
SEL_COLOR = Pixel.new(0xFFFF00FF)
EXT_X_COLOR = Pixel.new(0xFF00FFFF)
EXT_Y_COLOR = Pixel.new(0x00FF00FF)
class CubicBezier < PF::Game
FONT_COLOR = PF::Pixel.new(0xFFFFFFFF)
POINT_COLOR = PF::Pixel.new(0xFF0000FF)
CTL_COLOR = PF::Pixel.new(0x505050FF)
CURVE_COLOR = PF::Pixel.new(0x0077FFFF)
SEL_COLOR = PF::Pixel.new(0xFFFF00FF)
EXT_X_COLOR = PF::Pixel.new(0xFF00FFFF)
EXT_Y_COLOR = PF::Pixel.new(0x00FF00FF)
@curve : Bezier::Cubic(Float64)
@curve : PF2d::Bezier::Cubic(Float64)
@hover_point : Vector2(Float64)*? = nil
@selected_point : Vector2(Float64)*? = nil
@hover_point : PF2d::Vec2(Float64)*? = nil
@selected_point : PF2d::Vec2(Float64)*? = nil
def initialize(*args, **kwargs)
super
@curve = Bezier::Cubic.new(
Vector[width * 0.25, height * 0.7],
Vector[width * 0.33, height * 0.3],
Vector[width * 0.66, height * 0.3],
Vector[width * 0.75, height * 0.7]
)
@font = Pixelfont::Font.new("#{__DIR__}/../lib/pixelfont/fonts/pixel-5x7.txt")
def initialize(*args, **kwargs)
super
@curve = PF2d::Bezier::Cubic.new(
PF2d::Vec[width * 0.25, height * 0.7],
PF2d::Vec[width * 0.33, height * 0.3],
PF2d::Vec[width * 0.66, height * 0.3],
PF2d::Vec[width * 0.75, height * 0.7]
)
end
def on_mouse_motion(cursor)
if point = @selected_point
point.value = cursor.to_f
else
@hover_point = @curve.points.find { |p| cursor.distance(p.value) < 4 }
end
end
def on_mouse_motion(cursor)
if point = @selected_point
point.value = cursor.to_f
def on_mouse_button(event)
if event.button == 1
if event.pressed?
@selected_point = @hover_point
else
@hover_point = @curve.points.find { |p| cursor.distance(p.value) < 4 }
@selected_point = nil
end
end
end
def update(dt)
end
def draw
clear
draw_line(@curve.p0, @curve.p1, CTL_COLOR)
draw_line(@curve.p3, @curve.p2, CTL_COLOR)
draw_string("Length: " + @curve.length.round(2).to_s, 5, 5, @font, FONT_COLOR)
draw_rect(*@curve.rect.map(&.to_i), CTL_COLOR)
draw_curve(@curve, CURVE_COLOR)
@curve.extremeties.each do |point|
point.try do |p|
draw_circle(p.to_i, 3, EXT_Y_COLOR)
end
end
def on_mouse_button(event)
if event.button == 1
if event.pressed?
@selected_point = @hover_point
else
@selected_point = nil
end
end
end
fill_circle(@curve.p0.to_i, 2, POINT_COLOR)
fill_circle(@curve.p1.to_i, 2, POINT_COLOR)
fill_circle(@curve.p2.to_i, 2, POINT_COLOR)
fill_circle(@curve.p3.to_i, 2, POINT_COLOR)
def update(dt)
end
draw_string("P1 (#{@curve.p0.x.to_i}, #{@curve.p0.y.to_i})", @curve.p0, @font, FONT_COLOR)
draw_string("P2 (#{@curve.p1.x.to_i}, #{@curve.p1.y.to_i})", @curve.p1, @font, FONT_COLOR)
draw_string("P3 (#{@curve.p2.x.to_i}, #{@curve.p2.y.to_i})", @curve.p2, @font, FONT_COLOR)
draw_string("P4 (#{@curve.p3.x.to_i}, #{@curve.p3.y.to_i})", @curve.p3, @font, FONT_COLOR)
def draw
clear
draw_line(@curve.p0, @curve.p1, CTL_COLOR)
draw_line(@curve.p3, @curve.p2, CTL_COLOR)
draw_string("Length: " + @curve.length.round(2).to_s, 5, 5, FONT_COLOR)
draw_rect(*@curve.rect.map(&.to_i), CTL_COLOR)
draw_curve(@curve, CURVE_COLOR)
@curve.extremeties.each do |point|
point.try do |p|
draw_circle(p.to_i, 3, EXT_Y_COLOR)
end
end
fill_circle(@curve.p0.to_i, 2, POINT_COLOR)
fill_circle(@curve.p1.to_i, 2, POINT_COLOR)
fill_circle(@curve.p2.to_i, 2, POINT_COLOR)
fill_circle(@curve.p3.to_i, 2, POINT_COLOR)
draw_string("P1", @curve.p0.to_i, color: FONT_COLOR)
draw_string("P2", @curve.p1.to_i, color: FONT_COLOR)
draw_string("P3", @curve.p2.to_i, color: FONT_COLOR)
draw_string("P4", @curve.p3.to_i, color: FONT_COLOR)
if point = @hover_point
draw_circle(point.value.to_i, 5, SEL_COLOR)
end
if point = @hover_point
draw_circle(point.value.to_i, 5, SEL_COLOR)
end
end
end
engine = PF::CubicBezier.new(500, 500, 2).run!
engine = CubicBezier.new(500, 500, 2).run!

81
examples/draw_line.cr Normal file
View file

@ -0,0 +1,81 @@
require "../src/game"
require "../src/pixel"
class DrawLine < PF::Game
include PF2d
@color = PF::Pixel.random
@p1 : Vec2(Float64)
@d1 : Vec2(Float64)
@p2 : Vec2(Float64)
@d2 : Vec2(Float64)
@font = Pixelfont::Font.new("#{__DIR__}/../lib/pixelfont/fonts/pixel-5x7.txt")
def initialize(*args, **kwargs)
super
@p1 = Vec[rand(0.0...width.to_f), rand(0.0...height.to_f)]
@d1 = Vec[rand(-100.0..100.0), rand(-100.0..100.0)]
@p2 = Vec[rand(0.0...width.to_f), rand(0.0...height.to_f)]
@d2 = Vec[rand(-100.0..100.0), rand(-100.0..100.0)]
end
def update(dt)
@p1 += (@d1 * dt)
if @p1.x < 0
@p1.x = 0
@d1.x = -@d1.x
end
if @p1.x > width
@p1.x = width
@d1.x = -@d1.x
end
if @p1.y < 0
@p1.y = 0
@d1.y = -@d1.y
end
if @p1.y > height
@p1.y = height
@d1.y = -@d1.y
end
@p2 += (@d2 * dt)
if @p2.x < 0
@p2.x = 0
@d2.x = -@d2.x
end
if @p2.x > width
@p2.x = width
@d2.x = -@d2.x
end
if @p2.y < 0
@p2.y = 0
@d2.y = -@d2.y
end
if @p2.y > height
@p2.y = height
@d2.y = -@d2.y
end
end
def draw
clear(0, 0, 100)
# draw_string("P1: (#{@p1.x.to_i32},#{@p1.y.to_i32})", @p1, @font, @color)
# draw_string("P2: (#{@p2.x.to_i32},#{@p2.y.to_i32})", @p2, @font, @color)
draw_line(@p1, @p2, @color)
end
end
engine = DrawLine.new(200, 200, 3)
engine.run!

View file

@ -2,27 +2,32 @@ require "../src/game"
require "../src/pixel"
class FillShape < PF::Game
@color = PF::Pixel.random
def initialize(*args, **kwargs)
super
end
def update(dt)
if elapsed_milliseconds.to_i % 100 == 1
@color = PF::Pixel.random
end
end
def draw
clear(0, 0, 100)
fill_shape(PF::Vector[15, 15], PF::Vector[50, 10], PF::Vector[60, 55], PF::Vector[10, 60])
fill_shape(PF::Vector[100, 10], PF::Vector[150, 10], PF::Vector[150, 60], PF::Vector[100, 60])
fill_shape(
PF::Vector[10, 100],
PF::Vector[20, 110],
PF::Vector[30, 100],
PF::Vector[40, 110],
PF::Vector[50, 100],
PF::Vector[50, 150],
PF::Vector[10, 150],
)
fill_shape(PF::Vector[115, 115], PF::Vector[150, 120], PF::Vector[160, 155], PF::Vector[110, 160])
fill_shape({PF2d::Vec[15, 15], PF2d::Vec[50, 10], PF2d::Vec[60, 55], PF2d::Vec[10, 60]}, @color)
fill_shape({PF2d::Vec[100, 10], PF2d::Vec[150, 10], PF2d::Vec[150, 60], PF2d::Vec[100, 60]}, @color)
fill_shape({
PF2d::Vec[10, 100],
PF2d::Vec[20, 110],
PF2d::Vec[30, 100],
PF2d::Vec[40, 110],
PF2d::Vec[50, 100],
PF2d::Vec[50, 150],
PF2d::Vec[10, 150],
}, @color)
fill_shape({PF2d::Vec[115, 115], PF2d::Vec[150, 120], PF2d::Vec[160, 155], PF2d::Vec[110, 160]}, @color)
end
end

View file

@ -5,6 +5,7 @@ require "../src/audio/*"
module PF
class Piano < Game
@font = Pixelfont::Font.new("#{__DIR__}/../lib/pixelfont/fonts/pixel-5x7.txt")
@instrument : UInt8 = 0
@base_note : UInt8 = 69 # (in MIDI) - A4 / 440.0Hz
@ -15,8 +16,8 @@ module PF
@key_width : Int32
@middle : Int32
@keys : UInt32 = 16
@white_keys = [] of Tuple(Vector2(Int32), Vector2(Int32), String)
@black_keys = [] of Tuple(Vector2(Int32), Vector2(Int32), String)
@white_keys = [] of Tuple(PF2d::Vec2(Int32), PF2d::Vec2(Int32), String)
@black_keys = [] of Tuple(PF2d::Vec2(Int32), PF2d::Vec2(Int32), String)
@instruments : Array(Instrument) = [RetroVoice.new, SineVoice.new, PianoVoice.new, Flute.new, KickDrum.new, SnareDrum.new, Harmonica.new]
@ -103,8 +104,8 @@ module PF
unless note.accidental?
# Calculate the position of a white key
top_left = Vector[@key_width * pos, @middle - @key_size]
bottom_right = Vector[(@key_width * pos) + @key_width, @middle + @key_size]
top_left = PF2d::Vec[@key_width * pos, @middle - @key_size]
bottom_right = PF2d::Vec[(@key_width * pos) + @key_width, @middle + @key_size]
@white_keys << {top_left, bottom_right, name}
# position from the left is increased by 1 for every white key
pos += 1
@ -114,8 +115,8 @@ module PF
shrinkage = (@key_width // 8)
# black keys are at the same position as the last, but half as tall and offset by half the width.
left = (@key_width * pos) - (@key_width // 2) + shrinkage
top_left = Vector[left, @middle - @key_size]
bottom_right = Vector[left + @key_width - (shrinkage * 2), @middle]
top_left = PF2d::Vec[left, @middle - @key_size]
bottom_right = PF2d::Vec[left + @key_width - (shrinkage * 2), @middle]
@black_keys << {top_left, bottom_right, name}
end
end
@ -160,7 +161,7 @@ module PF
def draw
clear
draw_string(<<-TEXT, 5, 5, @text_color)
draw_string(<<-TEXT, 5, 5, @font, @text_color)
Press up/down to change octave, Bottom row of keyboard plays notes
#{@instruments.map(&.name).join(", ")}
Octave: #{@base_note // 12 - 1}, Voice: #{@instruments[@instrument].name}, Echo: #{@echo ? "on" : "off"}
@ -171,14 +172,14 @@ module PF
top_left, bottom_right, name = key
fill_rect(top_left, bottom_right, @keysdown[name]? ? @highlight : Pixel::White)
draw_rect(top_left, bottom_right, Pixel.new(127, 127, 127))
draw_string(name, top_left.x + 2, top_left.y + (@key_size * 2) - Sprite::CHAR_HEIGHT - 2, @keysdown[name]? ? @text_hl : @text_color)
draw_string(name, top_left.x + 2, top_left.y + (@key_size * 2) - @font.line_height - 2, @font, @keysdown[name]? ? @text_hl : @text_color)
end
@black_keys.each do |key|
top_left, bottom_right, name = key
fill_rect(top_left, bottom_right, @keysdown[name]? ? @highlight : Pixel::Black)
draw_rect(top_left, bottom_right, Pixel.new(127, 127, 127))
draw_string(name, top_left.x + 2, top_left.y + @key_size - Sprite::CHAR_HEIGHT - 2, @keysdown[name]? ? @text_hl : @text_color)
draw_string(name, top_left.x + 2, top_left.y + @key_size - @font.line_height - 2, @font, @keysdown[name]? ? @text_hl : @text_color)
end
fill_rect(0, @middle - @key_size - 2, width, @middle - @key_size, Pixel.new(200, 20, 20))

View file

@ -5,8 +5,9 @@ module PF
class Proceedural < Game
@buffer_size : Int32
@buffer : Pointer(UInt32)
@pan : Vector2(Float64) = PF::Vector[0.0, 0.0]
@pan : PF2d::Vec2(Float64) = PF2d::Vec[0.0, 0.0]
@seed : UInt32
@font = Pixelfont::Font.new("#{__DIR__}/../lib/pixelfont/fonts/pixel-5x7.txt")
def initialize(*args, **kwargs)
super
@ -59,7 +60,7 @@ module PF
end
end
time = elapsed_milliseconds - start
draw_string("frame: #{time.round(2)}ms", 5, 5, Pixel::White, bg: Pixel::Black)
draw_string("frame: #{time.round(2)}ms", 5, 5, @font, Pixel::White)
end
end
end

View file

@ -2,7 +2,6 @@ require "../src/game"
require "../src/controller"
require "../src/sprite"
require "../src/pixel"
require "../src/vector"
class Wind
property width : Int32
@ -12,10 +11,10 @@ class Wind
@step : Float64?
struct Gust
property position : PF::Vector2(Float64)
property strength : PF::Vector2(Float64)
property position : PF2d::Vec2(Float64)
property strength : PF2d::Vec2(Float64)
def initialize(@position, @strength = PF::Vector[rand(-5.0..5.0), rand(-5.0..5.0)])
def initialize(@position, @strength = PF2d::Vec[rand(-5.0..5.0), rand(-5.0..5.0)])
end
end
@ -33,7 +32,7 @@ class Wind
while y < @height
x = step / 2
while x < @width
@gusts << Gust.new(PF::Vector[x, y])
@gusts << Gust.new(PF2d::Vec[x, y])
x += step
end
y += step
@ -43,12 +42,12 @@ end
class Flake
property shape : UInt8
property position : PF::Vector2(Float64)
property position : PF2d::Vec2(Float64)
property z_pos : Float64
property velocity : PF::Vector2(Float64)
property velocity : PF2d::Vec2(Float64)
def initialize(@position, @shape = rand(0_u8..2_u8), @z_pos = rand(0.0..1.0), velocity : PF::Vector2(Float64)? = nil)
@velocity = velocity || PF::Vector[rand(-2.0..2.0), rand(10.0..20.0)]
def initialize(@position, @shape = rand(0_u8..2_u8), @z_pos = rand(0.0..1.0), velocity : PF2d::Vec2(Float64)? = nil)
@velocity = velocity || PF2d::Vec[rand(-2.0..2.0), rand(10.0..20.0)]
end
def update(dt)
@ -67,7 +66,7 @@ class Snow < PF::Game
@wind = Wind.new(width, height)
500.times do
@flakes << Flake.new(position: PF::Vector[rand(0.0..width.to_f64), rand(0.0..height.to_f64)])
@flakes << Flake.new(position: PF2d::Vec[rand(0.0..width.to_f64), rand(0.0..height.to_f64)])
end
clear(0, 0, 15)
end
@ -77,7 +76,7 @@ class Snow < PF::Game
if @last_flake >= 0.025
@last_flake = 0.0
@flakes << Flake.new(position: PF::Vector[rand(0.0..width.to_f64), 0.0])
@flakes << Flake.new(position: PF2d::Vec[rand(0.0..width.to_f64), 0.0])
end
@flakes.reject! do |flake|

View file

@ -7,7 +7,7 @@ module PF
def initialize(*args, **kwargs)
super
@sprite = Sprite.new("./assets/pf-font.png")
@sprite = Sprite.new("./assets/walking.png")
end
def update(dt)

View file

@ -2,22 +2,11 @@ require "../src/game"
module PF
class Static < Game
@buffer_size : Int32
@buffer : Pointer(UInt32)
def initialize(*args, **kwargs)
super
@buffer_size = width * height
@buffer = screen.pixel_pointer(0, 0)
end
def update(dt)
end
def draw
0.upto(@buffer_size) do |n|
(@buffer + n).value = PF::Pixel.random.to_u32
end
screen.pixels.fill { PF::Pixel.random.to_u32 }
end
end
end

View file

@ -8,8 +8,8 @@ class TextGame < PF::Game
@dx = 50.0
@dy = 50.0
@msg = "Hello, World!"
# @msg = "HI"
@color = PF::Pixel.random
@font = Pixelfont::Font.new("#{__DIR__}/../lib/pixelfont/fonts/pixel-5x7.txt")
end
def update(dt)
@ -22,8 +22,8 @@ class TextGame < PF::Game
@color = PF::Pixel.random
end
if @x > width - (@msg.size * PF::Sprite::CHAR_WIDTH)
@x = width - (@msg.size * PF::Sprite::CHAR_WIDTH)
if @x > width - @font.width_of(@msg)
@x = width - @font.width_of(@msg)
@dx = -@dx
@color = PF::Pixel.random
end
@ -34,8 +34,8 @@ class TextGame < PF::Game
@color = PF::Pixel.random
end
if @y > height - (PF::Sprite::CHAR_HEIGHT)
@y = height - (PF::Sprite::CHAR_HEIGHT)
if @y > height - @font.line_height
@y = height - @font.line_height
@dy = -@dy
@color = PF::Pixel.random
end
@ -43,7 +43,7 @@ class TextGame < PF::Game
def draw
clear(0, 0, 50)
draw_string(@msg, @x.to_i, @y.to_i, @color)
draw_string(@msg, @x.to_i, @y.to_i, @font, @color)
end
end

View file

@ -3,13 +3,12 @@ require "../src/controller"
require "../src/entity"
require "../src/pixel"
require "../src/shape"
require "../src/vector"
class Triangle < PF::Entity
property frame : Array(PF::Vector2(Float64))
property frame : Array(PF2d::Vec2(Float64))
def initialize(*args, **kwargs)
@frame = [] of PF::Vector2(Float64)
@frame = [] of PF2d::Vec2(Float64)
end
def update(dt)

View file

@ -1,49 +1,48 @@
#!/usr/bin/env ruby
v_major, v_minor, v_patch = RUBY_VERSION.split('.').map(&:to_i)
unless v_major == 3 && v_minor >= 2
$stderr.puts "Warn: script designed for Ruby 3.2.x, running: #{RUBY_VERSION}"
end
require 'optparse'
require 'fileutils'
OUT_PATH = 'examples/build'
options = {
release: true,
debug: false,
clean: false,
}
OptionParser.new do |opts|
opts.banner = "Usage: build_examples.rb [options]"
opts.on("--[no-]release", "Build in release mode (default: #{options[:release]})") do |value|
options[:release] = value
opts.on("--release", "Build in release mode") do
options[:release] = true
end
opts.on("--[no-]debug", "Include debug information (default: #{options[:debug]})") do |value|
options[:debug] = value
opts.on("--no-release", "Build faster") do
options[:release] = false
end
opts.on("--clean", "Remove built examples") do
options[:clean] = true
end
end.parse!
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
unless options[:clean]
cmd = "crystal build"
flags = []
flags << "--release" if options[:release]
flags << "--no-debug" unless options[:debug]
cmd = "crystal build"
flags = []
flags << "--release" if options[:release]
flags << "--no-debug" unless options[:debug]
Dir.chdir File.join(__dir__, '..')
FileUtils.mkdir_p(OUT_PATH)
unless File.exist?("#{OUT_PATH}/assets")
FileUtils.ln_s("../../assets", "#{OUT_PATH}/assets")
end
Dir.glob("examples/*.cr").each do |path|
bin_name = File.basename(path, ".cr")
full_cmd = %'#{cmd} #{flags.join(" ")} "#{path}" -o #{OUT_PATH}/#{bin_name}'
puts full_cmd
system full_cmd
Dir.chdir File.join(__dir__, '..')
FileUtils.mkdir_p("examples/build")
unless File.exist?("examples/build/assets")
FileUtils.ln_s("../../assets", "examples/build/assets")
end
Dir.glob("examples/*.cr").each do |path|
full_cmd = %'#{cmd} #{flags.join(" ")} "#{path}"'
puts full_cmd
system full_cmd
bin_name = File.basename(path, ".cr")
FileUtils.mv(bin_name, "examples/build/#{bin_name}")
end
else
# TODO
end

View file

@ -1,26 +0,0 @@
require "../src/game"
mapping : String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?().,/\\[]{}$#+-“”‘’'\"@=><_"
tiles = PF::Sprite.load_tiles("assets/pf-font.png", 7, 8)
puts "CHARS = {"
tiles.each_with_index do |tile, i|
if letter = mapping[i]?
if ['\\', '\''].includes? letter
print " '\\#{letter}' => "
else
print " '#{letter}' => "
end
n = 0u64
mask = 1_u64 << (7 * 8)
tile.pixels.each do |pixel|
n |= mask if pixel >> 8 <= 127
mask >>= 1
end
puts "0x#{n.to_s(16).rjust(16, '0')}_u64,"
end
end
puts "}"

View file

@ -1,13 +1,20 @@
name: pixelfaucet
version: 0.0.7
version: 0.0.8
authors:
- Alex Clink <alexclink@gmail.com>
crystal: 1.8.2
crystal: 1.3.2
license: MIT
dependencies:
sdl:
github: SleepingInsomniac/sdl.cr
version: 0.1.0
pf2d:
github: SleepingInsomniac/pf2d
pf3d:
github: SleepingInsomniac/pf3d
pixelfont:
github: SleepingInsomniac/pixelfont

View file

@ -1,16 +0,0 @@
require "./spec_helper"
require "../src/g3d"
require "../src/vector"
describe "line_intersects_plane" do
it "intersects a plane at a known point" do
line_start = PF::Vector[0.0, 0.0, -5.0]
line_end = PF::Vector[0.0, 0.0, 5.0]
plane_normal = PF::Vector[0.0, 0.0, 1.0]
plane_point = PF::Vector[0.0, 0.0, 0.0]
intersect = PF::G3d.line_intersects_plane(plane_point, plane_normal, line_start, line_end)
intersect.should eq({PF::Vector[0.0, 0.0, 0.0], 0.5})
end
end

View file

@ -1,7 +0,0 @@
require "./spec_helper"
require "../src/line"
include PF
describe Line do
end

View file

@ -1,83 +0,0 @@
require "./spec_helper"
require "../src/matrix"
include PF
describe Matrix do
it "Creates a square matrix with bracket notation" do
m = Matrix[
0, 1,
1, 0,
]
m.class.should eq(Matrix(Int32, 4))
m[1, 0].should eq(1)
m[0, 1].should eq(1)
end
describe "#*" do
it "returns the same matrix when multiplied by identity" do
mat = Matrix[
1.0, 2.0, 3.0, 4.0,
1.0, 2.0, 3.0, 4.0,
1.0, 2.0, 3.0, 4.0,
1.0, 2.0, 3.0, 4.0,
]
mat.class.should eq(Matrix(Float64, 16))
ident = Matrix[
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
]
result = mat * ident
result.should eq(mat)
end
it "multiplies different types" do
m1 = Matrix[1, 2, 3, 4]
m2 = Matrix[2.0, 0.0, 0.0, 2.0]
m3 = Matrix[2.0, 4.0, 6.0, 8.0]
(m1 * m2).should eq(m3)
end
end
describe "#size" do
it "returns the size of the matrix" do
mat = Matrix[
1, 2,
3, 4,
]
mat.width.should eq(2)
end
end
describe "#==" do
it "accurately show equality" do
m1 = Matrix[
1, 1, 1,
1, 1, 1,
1, 1, 1,
]
m2 = Matrix[
1, 1, 1,
1, 1, 1,
1, 1, 1,
]
m3 = Matrix[
2, 2, 2,
2, 2, 2,
2, 2, 2,
]
(m1 == m2).should eq(true)
(m1 == m3).should eq(false)
end
end
end

View file

@ -1,20 +0,0 @@
require "./spec_helper"
require "../src/transform2d"
include PF
describe Transform2d do
describe "#translate" do
it "creates the same matrix as matrix multiplication" do
t = Transform2d.new
t.translate(-1.0, -2.0).rotate(0.5).scale(1.1).translate(1.0, 2.0)
m = Transform2d.translation(-1.0, -2.0)
m = Transform2d.rotation(0.5) * m
m = Transform2d.scale(1.1, 1.1) * m
m = Transform2d.translation(1.0, 2.0) * m
t.matrix.should eq(m)
end
end
end

View file

@ -1,134 +0,0 @@
require "./spec_helper"
require "../src/vector"
include PF
describe Vector do
describe "#*" do
it "multiplies 2 vectors" do
v1 = Vector[1, 2]
v2 = Vector[2, 2]
(v1 * v2).should eq(Vector[2, 4])
end
end
describe "#magnitude" do
it "returns the magnitude a vector" do
v1 = Vector[2, 2]
v1.magnitude.should eq(2.8284271247461903)
end
end
describe "#dot" do
it "returns a known dot product" do
v1 = Vector[6, 2, -1]
v2 = Vector[5, -8, 2]
v1.dot(v2).should eq(12)
end
end
describe "#cross" do
it "returns a known cross product" do
v1 = Vector[0, 0, 2]
v2 = Vector[0, 2, 0]
v1.cross(v2).should eq(Vector[-4, 0, 0])
end
end
describe "#x" do
it "returns the x positional value" do
v1 = Vector[1, 2]
v1.x.should eq(1)
end
end
describe "standard operations" do
it "adds" do
v1 = Vector[1, 2]
v2 = Vector[3, 4]
(v1 + v2).should eq(Vector[4, 6])
end
it "substracts" do
v1 = Vector[4, 5]
v2 = Vector[3, 4]
(v1 - v2).should eq(Vector[1, 1])
end
it "does modulus" do
v1 = Vector[5, 10]
v2 = Vector[3, 3]
(v1 % v2).should eq(Vector[2, 1])
end
it "does division" do
v1 = Vector[5, 5]
(v1 / 2).should eq(Vector[2.5, 2.5])
end
it "does integer division" do
v1 = Vector[5, 5]
(v1 // 2).should eq(Vector[2, 2])
end
it "applies exponents" do
v = Vector[2, 2] ** 5
v.should eq(Vector[32, 32])
end
end
describe "#-" do
it "negates" do
v = Vector[1, 1]
v = -v
v.should eq(Vector[-1, -1])
end
end
describe "#sum" do
it "returns all components added together" do
v = Vector[1, 2, 3]
v.sum.should eq(6)
end
end
describe "#abs" do
it "returns the absolute value" do
v = Vector[-1, -1]
v.abs.should eq(Vector[1, 1])
end
end
describe "type conversion" do
it "converts float to int" do
v = Vector[1.5, 2.5]
v.to_i32.should eq(Vector[1, 2])
end
end
describe "Matrix multiplication" do
it "returns the scaled value when multiplied by an identity matrix" do
v = Vector[1, 2]
m = Matrix[
1, 0,
0, 1,
]
(v * m).should eq(v)
m = Matrix[
2, 0,
0, 1,
]
(v * m).should eq(Vector[2, 2])
end
it "multiplies correctly" do
v = Vector[2, 1, 3]
m = Matrix[
1, 2, 3,
4, 5, 6,
7, 8, 9,
]
(v * m).should eq(Vector[13, 31, 49])
end
end
end

View file

@ -30,10 +30,10 @@ module PF
end
setter tris = [] of Tri
property origin : Vector3(Float64) = Vector[0.0, 0.0, 0.0]
property rotation : Vector3(Float64) = Vector[0.0, 0.0, 0.0]
property position : Vector3(Float64) = Vector[0.0, 0.0, 0.0]
property scale : Vector3(Float64) = Vector[1.0, 1.0, 1.0]
property origin : Vector3(Float64) = PF2d::Vec[0.0, 0.0, 0.0]
property rotation : Vector3(Float64) = PF2d::Vec[0.0, 0.0, 0.0]
property position : Vector3(Float64) = PF2d::Vec[0.0, 0.0, 0.0]
property scale : Vector3(Float64) = PF2d::Vec[1.0, 1.0, 1.0]
# Load an obj file
# TODO: Load meshes specified by the obj file
@ -60,13 +60,13 @@ module PF
# Vertex coord
# EX: v 0.0 1.0 1.0
w = parts[4]?.try { |n| n.to_f64 } || 1.0
verticies << Vector[parts[1].to_f64, parts[2].to_f64, parts[3].to_f64]
verticies << PF2d::Vec[parts[1].to_f64, parts[2].to_f64, parts[3].to_f64]
when "vt"
# Vertex Texture coord
# EX: vt 0.0 1.0
v = parts[2]?.try { |n| n.to_f64 } || 0.0
# w = parts[3]?.try { |n| n.to_f64 } || 1.0
texture_verticies << Vector[parts[1].to_f64, v, 1.0]
texture_verticies << PF2d::Vec[parts[1].to_f64, v, 1.0]
when "vn"
# Vertex Normal
if use_normals

View file

@ -8,9 +8,9 @@ module PF
property p2 : Vector3(Float64)
property p3 : Vector3(Float64)
property t1 : Vector3(Float64) = Vector[0.0, 0.0, 0.0]
property t2 : Vector3(Float64) = Vector[0.0, 0.0, 0.0]
property t3 : Vector3(Float64) = Vector[0.0, 0.0, 0.0]
property t1 : Vector3(Float64) = PF2d::Vec[0.0, 0.0, 0.0]
property t2 : Vector3(Float64) = PF2d::Vec[0.0, 0.0, 0.0]
property t3 : Vector3(Float64) = PF2d::Vec[0.0, 0.0, 0.0]
property color : Pixel

View file

@ -32,7 +32,7 @@ module PF
current_frame.draw_to(sprite, x, y)
end
def draw_to(sprite : Sprite, pos : Vector2(Int))
def draw_to(sprite : Sprite, pos : PF2d::Vec)
draw_to(sprite, pos.x, pos.y)
end
end

View file

@ -1,24 +0,0 @@
module PF
module Bezier
alias Curve = Quad(Float64) | Cubic(Float64)
module Aproximations
# Get the length of the curve by calculating the length of line segments
# Increase *steps* for accuracy
def length(steps : UInt32 = 10)
_length = 0.0
seg_p0 = Vector[@p0.x, @p0.y]
0.upto(steps) do |n|
t = n / steps
seg_p1 = at(t)
_length += seg_p0.distance(seg_p1)
seg_p0 = seg_p1
end
_length
end
end
end
end
require "./bezier/*"

View file

@ -1,115 +0,0 @@
require "../bezier"
module PF
module Bezier
# Cubic bezier is a type of spline segment with 4 control points.
# The curve intersects points 0 and 3, while points 1 and 2 control the curve
#
# For information on the implementation see https://pomax.github.io/bezierinfo
struct Cubic(T)
include Aproximations
def self.point(t : Float64, p0 : Number, p1 : Number, p2 : Number, p3 : Number)
(1 - t) ** 3 * p0 + 3 * (1 - t) ** 2 * t * p1 + 3 * (1 - t) * t ** 2 * p2 + t ** 3 * p3
end
def self.derivative(t : Float64, p0 : Number, p1 : Number, p2 : Number, p3 : Number)
3 * (1 - t) ** 2 * (p1 - p0) + 6 * (1 - t) * t * (p2 - p1) + 3 * t ** 2 * (p3 - p2)
end
def self.second_derivative(t : Float64, p0 : Number, p1 : Number, p2 : Number, p3 : Number)
6 * (1 - t) * (p2 - 2 * p1 + p0) + 6 * t * (p3 - 2 * p2 + p1)
end
def self.extremeties(p0 : Number, p1 : Number, p2 : Number, p3 : Number)
a = 3 * p3 - 9 * p2 + 9 * p1 - 3 * p0
b = 6 * p0 - 12 * p1 + 6 * p2
c = 3 * p1 - 3 * p0
disc = b * b - 4 * a * c
return {nil, nil} unless disc >= 0
t1 = (-b + Math.sqrt(disc)) / (2 * a)
t2 = (-b - Math.sqrt(disc)) / (2 * a)
accept_1 = t1 >= 0 && t1 <= 1
accept_2 = t2 >= 0 && t2 <= 1
if accept_1 && accept_2
{t1, t2}
elsif accept_1
{t1, nil}
elsif accept_2
{nil, t2}
else
{0.5, nil}
end
end
property p0 : Vector2(T)
property p1 : Vector2(T)
property p2 : Vector2(T)
property p3 : Vector2(T)
def initialize(@p0 : Vector2(T), @p1 : Vector2(T), @p2 : Vector2(T), @p3 : Vector2(T))
end
def points
{pointerof(@p0), pointerof(@p1), pointerof(@p2), pointerof(@p3)}
end
# Get the point at percentage *t* < 0 < 1 of the curve
def at(t : Float64)
Vector[
T.new(self.class.point(t, @p0.x, @p1.x, @p2.x, @p3.x)),
T.new(self.class.point(t, @p0.y, @p1.y, @p2.y, @p3.y)),
]
end
# Get the tangent to a point at *t* < 0 < 1 on the spline
def tangent(t : Float64)
Vector[
T.new(self.class.derivative(t, @p0.x, @p1.x, @p2.x, @p3.x)),
T.new(self.class.derivative(t, @p0.y, @p1.y, @p2.y, @p3.y)),
].normalized
end
# Get the normal to a point at *t* < 0 < 1 on the spline
def normal(t : Float64)
Vector[
T.new(self.class.derivative(t, @p0.y, @p1.y, @p2.y, @p3.y)),
T.new(-self.class.derivative(t, @p0.x, @p1.x, @p2.x, @p3.x)),
].normalized
end
# Get the points at the extremeties of this curve
# note: Will return 4 values which are either Float64 | nil
def extremeties
exts = self.class.extremeties(@p0.x, @p1.x, @p2.x, @p3.x) +
self.class.extremeties(@p0.y, @p1.y, @p2.y, @p3.y)
exts.map { |e| e ? at(e) : e }
end
def rect
tl, br = @p0, @p3
tl.x = @p3.x if @p3.x < tl.x
tl.y = @p3.y if @p3.y < tl.y
br.x = @p0.x if @p0.x > br.x
br.y = @p0.y if @p0.y > br.y
extremeties.each do |e|
e.try do |e|
tl.x = e.x if e.x < tl.x
tl.y = e.y if e.y < tl.y
br.x = e.x if e.x > br.x
br.y = e.y if e.y > br.y
end
end
{tl, br}
end
end
end
end

View file

@ -1,30 +0,0 @@
module PF
module Bezier
struct Quad(T)
include Aproximations
def self.point(t : Float64, p0 : Number, p1 : Number, p2 : Number)
(1 - t) ** 2 * p0 + 2 * (1 - t) * t * p1 + t ** 2 * p2
end
property p0 : Vector2(T)
property p1 : Vector2(T)
property p2 : Vector2(T)
def initialize(@p0 : Vector2(T), @p1 : Vector2(T), @p2 : Vector2(T))
end
def points
{pointerof(@p0), pointerof(@p1), pointerof(@p2)}
end
# Get the point at percentage *t* of the curve
def at(t : Float64)
Vector[
self.class.point(t, @p0.x, @p1.x, @p2.x),
self.class.point(t, @p0.y, @p1.y, @p2.y),
]
end
end
end
end

View file

@ -22,7 +22,7 @@ module PF
end
direction = rand((@rotation - @emit_angle)..(@rotation + @emit_angle))
particle.velocity = @velocity + Vector[Math.cos(direction), Math.sin(direction)] * @strength
particle.velocity = @velocity + PF2d::Vec[Math.cos(direction), Math.sin(direction)] * @strength
particle.lifespan = @max_age
particle
end

View file

@ -5,8 +5,8 @@ module PF
class Entity
property sprite : Sprite? = nil
property position : Vector2(Float64) = Vector[0.0, 0.0]
property velocity : Vector2(Float64) = Vector[0.0, 0.0]
property position : PF2d::Vec2(Float64) = PF2d::Vec[0.0, 0.0]
property velocity : PF2d::Vec2(Float64) = PF2d::Vec[0.0, 0.0]
property rotation : Float64 = 0.0
property rotation_speed : Float64 = 0.0
property mass : Float64 = 1.0

View file

@ -28,7 +28,7 @@ module PF
# Calculate the new velocities
normal_vec = (position - other.position) / d
tangental_vec = Vector[-normal_vec.y, normal_vec.x]
tangental_vec = PF2d::Vec[-normal_vec.y, normal_vec.x]
# Dot product of velocity with the tangent
# (the direction in which to bounce towards)

View file

@ -1,17 +0,0 @@
module PF
module G3d
# Given a point on a plane *plane_point*, and a normal to the plane *plane_normal*,
# see if a line from *line_start* to *line_end* intersects a plane, and return the
# point at intersection
def self.line_intersects_plane(plane_point : Vector3(Float64), plane_normal : Vector3(Float64), line_start : Vector3(Float64), line_end : Vector3(Float64))
plane_normal = plane_normal.normalized
plane_dot_product = -plane_normal.dot(plane_point)
ad = line_start.dot(plane_normal)
bd = line_end.dot(plane_normal)
t = (-plane_dot_product - ad) / (bd - ad)
line_start_to_end = line_end - line_start
line_to_intersect = line_start_to_end * t
{line_start + line_to_intersect, t}
end
end
end

View file

@ -1,3 +1,5 @@
require "pf2d"
require "./lib_sdl"
require "./flags"
require "./fps"
@ -12,8 +14,8 @@ module PF
SHOW_FPS = true
property title : String
property viewport : Vector2(Int32) = Vector[0, 0]
getter scale : Vector2(Int32) = Vector[1, 1]
property viewport : PF2d::Vec2(Int32) = PF2d::Vec[0, 0]
getter scale : PF2d::Vec2(Int32) = PF2d::Vec[1, 1]
getter window : SDL::Window
getter renderer : SDL::Renderer
@ -22,7 +24,7 @@ module PF
property controllers = [] of PF::Controller(Keys)
delegate :draw_point, :draw_line, :draw_curve, :scan_line, :draw_circle, :draw_triangle, :draw_rect, :draw_shape,
:fill_triangle, :fill_rect, :fill_circle, :fill_shape, :draw_string, to: @screen
:fill_triangle, :paint_triangle, :fill_rect, :fill_circle, :fill_shape, :draw_string, to: @screen
@milliseconds : Float64 = Time.monotonic.total_milliseconds
@last_ms : Float64 = Time.monotonic.total_milliseconds
@ -36,8 +38,8 @@ module PF
render_flags = Flags::Render::ACCELERATED, window_flags = Flags::Window::SHOWN
)
SDL.init(SDL::Init::EVERYTHING)
@scale = Vector[scale, scale]
@viewport = Vector[width, height]
@scale = PF2d::Vec[scale, scale]
@viewport = PF2d::Vec[width, height]
winsize = @viewport * @scale
@window = SDL::Window.new(@title, winsize.x, winsize.y, flags: window_flags)
@renderer = SDL::Renderer.new(@window, flags: render_flags)
@ -123,7 +125,7 @@ module PF
# Called when the mouse is moved
# override in your subclass to hook into this behavior
def on_mouse_motion(cursor : Vector2(Int32))
def on_mouse_motion(cursor : PF2d::Vec)
end
# Called when the mouse is clicked
@ -152,7 +154,7 @@ module PF
while event = Event.poll
case event
when Event::MouseMotion
on_mouse_motion(Vector[event.x, event.y] // scale)
on_mouse_motion(PF2d::Vec[event.x, event.y] // scale)
when Event::MouseButton
on_mouse_button(event)
when Event::Keyboard

View file

@ -1,6 +1,5 @@
require "sdl"
@[Link("SDL2")]
lib LibSDL
fun queue_audio = SDL_QueueAudio(dev : AudioDeviceID, data : Int16*, len : UInt32)
end

View file

@ -1,93 +0,0 @@
require "./vector"
module PF
struct Line(T)
property p1 : T, p2 : T
def initialize(@p1 : T, @p2 : T)
end
def rise
@p2.y - @p1.y
end
def run
@p2.x - @p1.x
end
def slope
return 0.0 if run == 0
rise / run
end
def inv_slope
return 0.0 if rise == 0
run / rise
end
def left
@p1.x < @p2.x ? @p1.x : @p2.x
end
def right
@p1.x > @p2.x ? @p1.x : @p2.x
end
def top
@p1.y > @p2.y ? @p2.y : @p1.y
end
def bottom
@p1.y > @p2.y ? @p1.y : @p2.y
end
def contains_y?(y)
if @p1.y < @p2.y
top, bottom = @p1.y, @p2.y
else
top, bottom = @p2.y, @p1.y
end
y >= top && y <= bottom
end
def y_at(x)
return p1.y if slope == 1.0
x * slope + p1.y
end
def x_at(y)
return p1.x if slope == 0.0
(y - p1.y) / slope + p1.x
end
# Linearly interpolate
def lerp(t : Float64)
(@p2 - @p1) * t + @p1
end
# Return the length of the line
def length
Math.sqrt(((@p2 - @p1) ** 2).sum)
end
def /(n : (Float | Int))
Line.new(@p1 / n, @p2 / n)
end
# Convert this line into a normalized vector
def to_vector
(@p2 - @p1).normalized
end
# Find the normal axis to this line
def normal
Vector[-rise, run].normalized
end
# Normal counter clockwise
def normal_cc
Vector[rise, -run].normalized
end
end
end

View file

@ -1,91 +0,0 @@
module PF
struct Matrix(T, S)
include Indexable::Mutable(T)
getter values : StaticArray(T, S)
getter width : UInt8
getter height : UInt8
# Creates a new square `Matrix` with the given *args*
#
# ```
# m = Matrix[1, 2, 3, 4] # => Matrix(Int32, 4) 2x2 [1, 2, 3, 4]
# ```
macro [](*args)
# width and height are the isqrt of args.size
{% if args.size == 4 %}
PF::Matrix(typeof({{args.splat}}), 4).new(2, 2, StaticArray[{{args.splat}}])
{% elsif args.size == 9 %}
PF::Matrix(typeof({{args.splat}}), 9).new(3, 3, StaticArray[{{args.splat}}])
{% elsif args.size == 16 %}
PF::Matrix(typeof({{args.splat}}), 16).new(4, 4, StaticArray[{{args.splat}}])
{% else %}
raise "Cannot determine width and height of matrix with {{ args.size }} elements, " \
"please provide them explicitly Matrix(Int32, 16).new(4, 4, StaticArray[...])"
{% end %}
end
def initialize(@width : UInt8, @height : UInt8)
@values = StaticArray(T, S).new(T.new(0))
end
def initialize(@width : UInt8, @height : UInt8, @values : StaticArray(T, S))
end
delegate :fill, to: @values
def index(col : Int, row : Int)
row * width + col
end
def size
S
end
def unsafe_fetch(index : Int)
@values.unsafe_fetch(index)
end
def unsafe_put(index : Int, value : T)
@values.unsafe_put(index, value)
end
# Fetch a value at a specified *column* and *row*
def [](col : Int, row : Int)
unsafe_fetch(index(col, row))
end
# Put a value at a specified *column* and *row*
def []=(col : Int, row : Int, value : T)
unsafe_put(index(col, row), value)
end
# Tests the equality of two matricies
def ==(other : Matrix)
self.values == other.values
end
def *(other : Matrix)
result = Matrix(typeof(@values.unsafe_fetch(0) * other.values.unsafe_fetch(0)), S).new(width, height)
(0...height).each do |row|
(0...width).each do |col|
(0...width).each do |n|
result[col, row] = result[col, row] + self[n, row] * other[col, n]
end
end
end
result
end
def to_s(io)
io << {{@type}} << ' ' << width << "x" << height << " ["
{% for i in 0...S %}
io << unsafe_fetch({{i}})
{% if i != S - 1 %}
io << ", "
{% end %}
{% end %}
io << ']'
end
end
end

View file

@ -7,37 +7,37 @@ module PF
x = size + rand(-jitter..jitter)
rc = Math.cos(angle)
rs = Math.sin(angle)
Vector[0.0 * rc - x * rs, x * rc + 0.0 * rs]
PF2d::Vec[0.0 * rc - x * rs, x * rc + 0.0 * rs]
end.to_a
end
# Rotate points by *rotation*
def self.rotate(points : Enumerable(Vector2), rotation : Float64)
def self.rotate(points : Enumerable(PF2d::Vec), rotation : Float64)
rc = Math.cos(rotation)
rs = Math.sin(rotation)
points.map do |point|
Vector[point.x * rc - point.y * rs, point.y * rc + point.x * rs]
PF2d::Vec[point.x * rc - point.y * rs, point.y * rc + point.x * rs]
end
end
# Translate points by *translation*
def self.translate(points : Enumerable(Vector2), translation : Vector2)
def self.translate(points : Enumerable(PF2d::Vec), translation : PF2d::Vec)
points.map { |p| p + translation }
end
# ditto
def self.translate(*points : Vector2, translation : Vector2)
def self.translate(*points : PF2d::Vec, translation : PF2d::Vec)
self.translation(points, translation: translation)
end
# Scale points by a certain *amount*
def self.scale(points : Enumerable(Vector2), amount : Vector2)
def self.scale(points : Enumerable(PF2d::Vec), amount : PF2d::Vec)
points.map { |p| p * amount }
end
# calculate length from center for all points, and then get the average
def self.average_radius(points : Enumerable(Vector2))
def self.average_radius(points : Enumerable(PF2d::Vec))
points.map(&.length).reduce { |t, p| t + p } / points.size
end
end

View file

@ -1,6 +1,5 @@
require "pixelfont"
require "sdl/image"
require "./vector"
require "./sprite/*"
module PF
class Sprite
@ -16,7 +15,7 @@ module PF
sx = tx * tile_width
sy = ty * tile_height
sprite = Sprite.new(tile_width, tile_height)
sheet.draw_to(sprite, Vector[sx, sy], Vector[tile_width, tile_height], Vector[0, 0])
sheet.draw_to(sprite, PF2d::Vec[sx, sy], PF2d::Vec[tile_width, tile_height], PF2d::Vec[0, 0])
sprites << sprite
end
end
@ -24,6 +23,9 @@ module PF
sprites
end
include PF2d::Drawable(UInt32 | Pixel)
include PF2d::Viewable(Pixel)
property surface : SDL::Surface
delegate :fill, :lock, :format, to: @surface
@ -50,8 +52,8 @@ module PF
@surface.height
end
def size
Vector[width, height]
def size : PF2d::Vec2
PF2d::Vec[width, height]
end
# Convert the color mode of this sprite to another for optimization
@ -75,12 +77,12 @@ module PF
end
# ditto
def draw_to(dest : SDL::Surface | Sprite, at : Vector2(Int))
def draw_to(dest : SDL::Surface | Sprite, at : PF2d::Vec)
draw_to(dest, at.x, at.y)
end
# Draw this sprite to another given a source rect and destination
def draw_to(sprite : Sprite, source : Vector2(Int), size : Vector2(Int), dest : Vector2(Int))
def draw_to(sprite : Sprite, source : PF2d::Vec, size : PF2d::Vec, dest : PF2d::Vec)
@surface.blit(sprite.surface, SDL::Rect.new(source.x, source.y, size.x, size.y), SDL::Rect.new(dest.x, dest.y, size.x, size.y))
end
@ -95,10 +97,14 @@ module PF
end
# ditto
def peak(point : Vector2(Int))
def peak(point : PF2d::Vec)
pixel_pointer(point.x, point.y).value
end
def get_point(x : Number, y : Number) : Pixel
sample(x.to_i, y.to_i)
end
# Sample a color at an *x* and *y* position
def sample(x : Int, y : Int)
raw_pixel = peak(x, y)
@ -107,12 +113,12 @@ module PF
end
# ditto
def sample(point : Vector2(Int))
def sample(point : PF2d::Vec)
sample(point.x, point.y)
end
# Sample a color with alhpa
def sample(x : Int, y : Int, alpha : Boolean)
def sample(x : Int, y : Int, alpha : Bool)
return sample(x, y) unless alpha
raw_pixel = pixel_pointer(x, y).value
LibSDL.get_rgba(raw_pixel, format, out r, out g, out b, out a)
@ -120,7 +126,7 @@ module PF
end
# ditto
def sample(point : Vector2(Int), alpha : Boolean)
def sample(point : PF2d::Vec, alpha : Bool)
sample(point.x, point.y, alpha)
end
@ -129,5 +135,22 @@ module PF
target = @surface.pixels + (y * @surface.pitch) + (x * sizeof(UInt32))
target.as(Pointer(UInt32))
end
# Implements PF2d::Drawable(UInt32)
def draw_point(x, y, value : UInt32 | Pixel)
if x >= 0 && x < width && y >= 0 && y < height
pixel_pointer(x.to_i32, y.to_i32).value = value.to_u32
end
end
def draw_string(string : String, x : Number, y : Number, font : Pixelfont::Font, pixel)
font.draw(string) do |px, py, on|
draw_point(px + x, py + y, pixel) if on
end
end
def draw_string(string : String, pos : PF2d::Vec, font : Pixelfont::Font, pixel)
draw_string(string, pos.x, pos.y, font, pixel)
end
end
end

View file

@ -1,35 +0,0 @@
module PF
class Sprite
# Draw a circle using Bresenhams Algorithm
def draw_circle(cx : Int, cy : Int, r : Int, pixel : Pixel = Pixel.new)
x, y = 0, r
d = 3 - 2 * r
loop do
draw_point(cx + x, cy + y, pixel)
draw_point(cx - x, cy + y, pixel)
draw_point(cx + x, cy - y, pixel)
draw_point(cx - x, cy - y, pixel)
draw_point(cx + y, cy + x, pixel)
draw_point(cx - y, cy + x, pixel)
draw_point(cx + y, cy - x, pixel)
draw_point(cx - y, cy - x, pixel)
break if x > y
x += 1
if d > 0
y -= 1
d = d + 4 * (x - y) + 10
else
d = d + 4 * x + 6
end
end
end
def draw_circle(c : Vector2(Int), r : Int, pixel : Pixel = Pixel.new)
draw_circle(c.x, c.y, r, pixel)
end
end
end

View file

@ -1,17 +0,0 @@
module PF
class Sprite
def draw_curve(curve : Bezier::Cubic | Bezier::Quad, samples : Int = 100, pixel : Pixel = Pixel.new)
point = curve.p0
0.upto(samples) do |x|
t = x / samples
next_point = curve.at(t)
draw_line(point.to_i, next_point.to_i, pixel)
point = next_point
end
end
def draw_curve(curve : Bezier::Cubic | Bezier::Quad, pixel : Pixel)
draw_curve(curve, pixel: pixel)
end
end
end

View file

@ -1,65 +0,0 @@
module PF
class Sprite
# Draw a line using Bresenhams Algorithm
def draw_line(x1 : Int, y1 : Int, x2 : Int, y2 : Int, pixel : Pixel = Pixel.new)
# The slope for each axis
slope = Vector[(x2 - x1).abs, -(y2 - y1).abs]
# The step direction in both axis
step = Vector[x1 < x2 ? 1 : -1, y1 < y2 ? 1 : -1]
# The final decision accumulation
# Initialized to the height of x and y
decision = slope.x + slope.y
point = Vector[x1, y1]
loop do
draw_point(point.x, point.y, pixel)
# Break if we've reached the ending point
break if point.x == x2 && point.y == y2
# Square the decision to avoid floating point calculations
decision_squared = decision + decision
# if decision_squared is greater than
if decision_squared >= slope.y
decision += slope.y
point.x += step.x
end
if decision_squared <= slope.x
decision += slope.x
point.y += step.y
end
end
end
# ditto
def draw_line(x1 : Number, y1 : Number, x2 : Number, y2 : Number, pixel : Pixel = Pixel.new)
draw_line(x1.to_i, y1.to_i, x2.to_i, y2.to_i, pixel)
end
# ditto
def draw_line(p1 : Vector2(Int), p2 : Vector2(Int), pixel : Pixel = Pixel.new)
draw_line(p1.x, p1.y, p2.x, p2.y, pixel)
end
# ditto
def draw_line(p1 : Vector2(Number), p2 : Vector2(Number), pixel : Pixel = Pixel.new)
draw_line(p1.to_i32, p2.to_i32, pixel)
end
# ditto
def draw_line(line : Line, pixel : Pixel = Pixel.new)
draw_line(line.p1.to_i32, line.p2.to_i32, pixel)
end
# Draw a horizontal line to a certain *width*
def scan_line(x : Int, y : Int, width : Int, pixel : Pixel = Pixel.new)
0.upto(width) do |n|
draw_point(x + n, y, pixel)
end
end
end
end

View file

@ -1,25 +0,0 @@
module PF
class Sprite
# Draw a single point
def draw_point(x : Int32, y : Int32, color : UInt32)
if x >= 0 && x < width && y >= 0 && y < height
pixel_pointer(x, y).value = color
end
end
# ditto
def draw_point(x : Int32, y : Int32, pixel : Pixel = Pixel.new)
draw_point(x, y, pixel.format(format))
end
# ditto
def draw_point(point : Vector2(Int), pixel : Pixel = Pixel.new)
draw_point(point.x, point.y, pixel)
end
# ditto
def draw_point(point : Vector2(Float), pixel : Pixel = Pixel.new)
draw_point(point.to_i32, pixel)
end
end
end

View file

@ -1,30 +0,0 @@
module PF
class Sprite
# Draw the outline of a square rect
def draw_rect(x1 : Int, y1 : Int, x2 : Int, y2 : Int, pixel : Pixel = Pixel.new)
# draw from top left to bottom right
y1, y2 = y2, y1 if y1 > y2
x1, x2 = x2, x1 if x1 > x2
x1.upto(x2) do |x|
draw_point(x, y1, pixel)
draw_point(x, y2, pixel)
end
y1.upto(y2) do |y|
draw_point(x1, y, pixel)
draw_point(x2, y, pixel)
end
end
# ditto
def draw_rect(p1 : PF::Vector2(Int), p2 : PF::Vector2(Int), pixel : Pixel = Pixel.new)
draw_rect(p1.x, p1.y, p2.x, p2.y, pixel)
end
# ditto
def draw_rect(size : PF::Vector2(Int), pixel : Pixel = Pixel.new)
draw_rect(0, 0, size.x, size.y, pixel)
end
end
end

View file

@ -1,15 +0,0 @@
module PF
class Sprite
# Draw lines enclosing a shape
def draw_shape(points : Enumerable(Vector2), pixel : Pixel = Pixel.new)
0.upto(points.size - 1) do |n|
draw_line(points[n], points[(n + 1) % points.size], pixel)
end
end
# Ditto
def draw_shape(*points : Vector2, color : Pixel = Pixel.new)
draw_shape(points, color)
end
end
end

View file

@ -1,131 +0,0 @@
module PF
class Sprite
CHARS = {
'A' => 0x007112244f912200_u64,
'B' => 0x00f1122788913c00_u64,
'C' => 0x0071120408111c00_u64,
'D' => 0x00f1122448913c00_u64,
'E' => 0x00f1020708103c00_u64,
'F' => 0x00f1020788102000_u64,
'G' => 0x0071122409911c00_u64,
'H' => 0x00891227c8912200_u64,
'I' => 0x0070408102041c00_u64,
'J' => 0x0078204081121800_u64,
'K' => 0x009122860a122400_u64,
'L' => 0x0081020408103c00_u64,
'M' => 0x0089b2a448912200_u64,
'N' => 0x008992a4c8912200_u64,
'O' => 0x0071122448911c00_u64,
'P' => 0x00f1122788102000_u64,
'Q' => 0x007112244a931e00_u64,
'R' => 0x00f112278a122200_u64,
'S' => 0x0071120380911c00_u64,
'T' => 0x00f8408102040800_u64,
'U' => 0x0089122448911c00_u64,
'V' => 0x00891224488a0800_u64,
'W' => 0x01064c9ab5512200_u64,
'X' => 0x0089114105112200_u64,
'Y' => 0x0089114102040800_u64,
'Z' => 0x00f8104104103e00_u64,
'a' => 0x0000018087121c00_u64,
'b' => 0x0081038489123800_u64,
'c' => 0x0000018488121800_u64,
'd' => 0x001021c489121c00_u64,
'e' => 0x000001848e101c00_u64,
'f' => 0x0061220708102000_u64,
'g' => 0x00000184890e0470_u64,
'h' => 0x0081038489122400_u64,
'i' => 0x0000400102040800_u64,
'j' => 0x0000200081021410_u64,
'k' => 0x008102450c142400_u64,
'l' => 0x0000810204080800_u64,
'm' => 0x000003454a912200_u64,
'n' => 0x000002468b122400_u64,
'o' => 0x0000018489121800_u64,
'p' => 0x00000184891c2040_u64,
'q' => 0x000001c4890e0408_u64,
'r' => 0x0000018488102000_u64,
's' => 0x000001c406023800_u64,
't' => 0x0040838204080800_u64,
'u' => 0x0000022448911c00_u64,
'v' => 0x00000224488a0800_u64,
'w' => 0x000002244a951400_u64,
'x' => 0x00000222820a2200_u64,
'y' => 0x00000224488f0238_u64,
'z' => 0x000003e082083e00_u64,
'0' => 0x007112654c911c00_u64,
'1' => 0x0060408102041c00_u64,
'2' => 0x0071102184103e00_u64,
'3' => 0x0071102180911c00_u64,
'4' => 0x00891223c0810200_u64,
'5' => 0x00f9020780911c00_u64,
'6' => 0x0071120788911c00_u64,
'7' => 0x00f8104104081000_u64,
'8' => 0x0071122388911c00_u64,
'9' => 0x00711223c0911c00_u64,
'!' => 0x0020408102000800_u64,
'?' => 0x0071122182000800_u64,
'(' => 0x0020810204080800_u64,
')' => 0x0040408102041000_u64,
'.' => 0x0000000000000800_u64,
',' => 0x0000000000000410_u64,
'/' => 0x0010408204102000_u64,
'\\' => 0x0080810102020400_u64,
'[' => 0x00c1020408103000_u64,
']' => 0x00c0810204083000_u64,
'{' => 0x0060810404081800_u64,
'}' => 0x00c0810104083000_u64,
'$' => 0x0020f283829e0800_u64,
'#' => 0x0050a3e28f8a1400_u64,
'+' => 0x00004087c2040000_u64,
'-' => 0x00000007c0000000_u64,
'“' => 0x0041228100000000_u64,
'”' => 0x0021214200000000_u64,
'' => 0x0041020000000000_u64,
'' => 0x0080810000000000_u64,
'\'' => 0x0040810000000000_u64,
'"' => 0x00a1428000000000_u64,
'@' => 0x007112e54b901e00_u64,
'=' => 0x000003e00f800000_u64,
'>' => 0x0040404041041000_u64,
'<' => 0x0008208202020200_u64,
'_' => 0x0000000000003e00_u64,
}
CHAR_WIDTH = 7
CHAR_HEIGHT = 8
def draw_string(msg : String, x : Int, y : Int, color : Pixel = Pixel.new, bg : Pixel? = nil, leading : Int = 2)
cur_y = 0
cur_x = 0
msg.chars.each do |c|
if c == '\n'
cur_y += 1
cur_x = 0
next
end
if char = CHARS[c]?
mask = 1_u64 << (CHAR_WIDTH * CHAR_HEIGHT)
0.upto(CHAR_HEIGHT - 1) do |cy|
0.upto(CHAR_WIDTH - 1) do |cx|
if mask & char > 0
draw_point(x + cx + (cur_x * CHAR_WIDTH), y + cy + (cur_y * (CHAR_HEIGHT + leading)), color)
elsif background = bg
draw_point(x + cx + (cur_x * CHAR_WIDTH), y + cy + (cur_y * (CHAR_HEIGHT + leading)), background)
end
mask >>= 1
end
end
end
cur_x += 1
end
end
def draw_string(msg : String, pos : Vector2(Int), color : Pixel = Pixel::Black)
draw_string(msg, pos.x, pos.y, color)
end
end
end

View file

@ -1,10 +0,0 @@
module PF
class Sprite
# Draws 3 lines
def draw_triangle(p1 : Vector2(Int), p2 : Vector2(Int), p3 : Vector2(Int), pixel : Pixel = Pixel.new)
draw_line(p1, p2, pixel)
draw_line(p2, p3, pixel)
draw_line(p3, p1, pixel)
end
end
end

View file

@ -1,34 +0,0 @@
module PF
class Sprite
# Fill a circle using Bresenhams Algorithm
def fill_circle(cx : Int, cy : Int, r : Int, pixel : Pixel = Pixel.new)
x, y = 0, r
balance = 0 - r
while x <= y
p0 = cx - x
p1 = cx - y
w0 = x + x
w1 = y + y
scan_line(p0, cy + y, w0, pixel)
scan_line(p0, cy - y, w0, pixel)
scan_line(p1, cy + x, w1, pixel)
scan_line(p1, cy - x, w1, pixel)
x += 1
balance += x + x
if balance >= 0
y -= 1
balance -= (y + y)
end
end
end
def fill_circle(c : Vector2(Int), r : Int, pixel : Pixel = Pixel.new)
fill_circle(c.x, c.y, r, pixel)
end
end
end

View file

@ -1,21 +0,0 @@
module PF
class Sprite
# Fill a rect
def fill_rect(x1 : Int, y1 : Int, x2 : Int, y2 : Int, pixel : Pixel = Pixel.new)
# draw from top left to bottom right
y1, y2 = y2, y1 if y1 > y2
x1, x2 = x2, x1 if x1 > x2
y1.upto(y2) do |y|
x1.upto(x2) do |x|
draw_point(x, y, pixel)
end
end
end
# ditto
def fill_rect(p1 : PF::Vector2(Int), p2 : PF::Vector2(Int), pixel : Pixel = Pixel.new)
fill_rect(p1.x, p1.y, p2.x, p2.y, pixel)
end
end
end

View file

@ -1,68 +0,0 @@
module PF
class Sprite
# Fill an abitrary polygon. Expects a clockwise winding of points
def fill_shape(points : Enumerable(Vector2), color : Pixel = Pixel.new)
return if points.empty?
return draw_point(points[0], color) if points.size == 1
return draw_line(points[0], points[1], color) if points.size == 2
return draw_triangle(points[0], points[1], points[2], color) if points.size == 3
# set initial bounding box
top = points[0].y
bottom = points[-1].y
left = points[0].x
right = points[-1].x
# find bounding box
points.each do |point|
top = point.y if point.y < top
bottom = point.y if point.y > bottom
left = point.x if point.x < left
right = point.x if point.x > right
end
# Form lines from the points
lines = [] of Line(Vector2(Int32))
0.upto(points.size - 1) do |n|
lines << Line.new(points[n], points[(n + 1) % points.size])
end
# Start at the top of the bounding box and draw scanlines until the end
top.upto(bottom) do |y|
intercepts = [] of Tuple(Int32, Bool) # TODO: use deque?
# Get the x intercepts for each line at this y level
lines.each do |line|
next unless line.contains_y?(y)
x = line.x_at(y).round.to_i
is_ascending = line.p2.y >= line.p1.y
intercepts << {x, is_ascending}
end
# sort x intercepts from left to right
intercepts.sort! { |a, b| a[0] <=> b[0] }
n = 0 # count which intercepts we've crossed on the scanline
# Start at the left boundary
intercepts[0][0].upto(right) do |x|
break if n >= intercepts.size # No need to draw if we reach the right shape boundary
# Only draw points within x values of an ascending slope,
# descending slope indicates that the point is outside of the shape
if intercepts[n][1] || x == intercepts[n][0] # Always draw the border itself
draw_point(x, y, color)
end
# # While condition for overlapping points
while n != intercepts.size && x == intercepts[n][0]
n += 1
end
end
end
end
def fill_shape(*points : Vector2, color : Pixel = Pixel.new)
fill_shape(points, color)
end
end
end

View file

@ -1,219 +0,0 @@
require "../line"
module PF
class Sprite
private def sort_verticies(p1 : Vector2, p2 : Vector2, p3 : Vector2)
# Sort points from top to bottom
p1, p2 = p2, p1 if p2.y < p1.y
p1, p3 = p3, p1 if p3.y < p1.y
p2, p3 = p3, p2 if p3.y < p2.y
{p1, p2, p3}
end
private def sort_verticies(p1 : Vector3, p2 : Vector3, p3 : Vector3, t1 : Vector3, t2 : Vector3, t3 : Vector3)
# Sort points from top to bottom
p1, p2, t1, t2 = p2, p1, t2, t1 if p2.y < p1.y
p1, p3, t1, t3 = p3, p1, t3, t1 if p3.y < p1.y
p2, p3, t2, t3 = p3, p2, t3, t2 if p3.y < p2.y
{p1, p2, p3, t1, t2, t3}
end
# Draw a filled in triangle
def fill_triangle(p1 : Vector2, p2 : Vector2, p3 : Vector2, pixel : Pixel = Pixel.new)
p1, p2, p3 = sort_verticies(p1, p2, p3)
# sort left and right edges by run / rise
line_left = PF::Line.new(p1, p2)
line_right = PF::Line.new(p1, p3)
if line_left.run / line_left.rise > line_right.run / line_right.rise
line_left, line_right = line_right, line_left
end
# calculate line slopes
slope_left = line_left.slope
slope_right = line_right.slope
offset = p1.y # height offset from 0
height = p3.y - p1.y # height of the triangle
mid = p2.y - p1.y # where the flat bottom triangle ends
start = 0
fin = mid
# Draw the triangle in two halfs
# 0 - Flat bottom triangle
# 1 - Flat top triangle
2.times do |half|
start.upto(fin) do |y|
if slope_left == 0
# When there is no rise, set the x value directly
x_left = line_left.p2.x
else
x_left = ((y - (line_left.p1.y - p1.y)) / slope_left).round.to_i + line_left.p1.x
end
if slope_right == 0
x_right = line_right.p2.x
else
x_right = ((y - (line_right.p1.y - p1.y)) / slope_right).round.to_i + line_right.p1.x
end
x_left.upto(x_right) do |x|
draw_point(x, y + offset, pixel)
end
end
start = fin + 1
fin = height
# Depending on which point is the middle
if line_left.p2 == p2
line_left = PF::Line.new(p2, p3)
slope_left = line_left.slope
else
line_right = PF::Line.new(p2, p3)
slope_right = line_right.slope
end
end
end
# ditto
def fill_triangle(points : Enumerable(Vector2), pixel : Pixel = Pixel.new)
fill_triangle(points[0], points[1], points[2], pixel)
end
# Draw a textured triangle
def fill_triangle(p1 : Vector3, p2 : Vector3, p3 : Vector3, t1 : Vector3, t2 : Vector3, t3 : Vector3, sprite : Sprite, buffer : DepthBuffer, color : Pixel = Pixel::White)
p1, p2, p3, t1, t2, t3 = sort_verticies(p1, p2, p3, t1, t2, t3)
# z = (p1.z + p2.z + p3.z) // 3
z = p1.z
# Create lines starting at p1 to the other lower points
line_left = PF::Line.new(p1, p2)
line_right = PF::Line.new(p1, p3)
tl_left = PF::Line.new(t1, t2)
tl_right = PF::Line.new(t1, t3)
# Sort left and right edges by run / rise
# if the first line goes to the right more than the right, then swap (first line is on the right)
if line_left.run / line_left.rise > line_right.run / line_right.rise
line_left, line_right = line_right, line_left
tl_left, tl_right = tl_right, tl_left
end
# if the left line ends at the middle, the left line changes
# otherwise this will be false and the right line will change
switch_left = line_left.p2 == p2
# calculate line slopes
slope_left = line_left.slope
slope_right = line_right.slope
c = p1.y # offset from 0
height = p3.y - p1.y # triangle height
mid = p2.y - p1.y # where the shorter line ends
start = 0
fin = mid
# Draw the triangle in two halfs
# 0 - Flat bottom triangle
# 1 - Flat top triangle
2.times do |half|
start.upto(fin) do |y|
# Check if the slope is 0, this would cause a divide by 0
if slope_left == 0
# When there is no rise, set the x value directly
x_left = line_left.p2.x
else
x_left = ((y - (line_left.p1.y - p1.y)) / slope_left).round.to_i + line_left.p1.x
end
if slope_right == 0
x_right = line_right.p2.x
t_right = tl_right.p2.x
else
x_right = ((y - (line_right.p1.y - p1.y)) / slope_right).round.to_i + line_right.p1.x
end
# Get the normalized t value for this height level
ty = height > 0 ? y / height : 0.0
# LERP both texture edges at the y position to create a new line
tyl =
if switch_left
# Line left is the 2 part segment
if half == 0
# still in the first segment (percent over the midpoint)
mid == 0 ? 0.0 : y / mid
else
# in the second part, pecentage of middle to end
height == 0 ? 0.0 : (y - mid) / (height - mid)
end
else
height == 0 ? 0.0 : y / height
end
tyr =
unless switch_left
if half == 0
mid == 0 ? 1.0 : y / mid
else
height == 0 ? 1.0 : (y - mid) / (height - mid)
end
else
height == 0 ? 1.0 : y / height
end
texture_line = PF::Line.new(tl_left.lerp(tyl), tl_right.lerp(tyr))
# Get the width of the scan line
scan_size = x_right - x_left
x_left.upto(x_right) do |x|
# LERP the line between the texture edges
t = scan_size == 0 ? 0.0 : (x - x_left) / scan_size
texture_point = texture_line.lerp(t)
if texture_point.z > buffer[x, y + c]
buffer[x, y + c] = texture_point.z
# Get the x and y of the texture coords, divide by z for perspective, then
# multiply the point by the size of the sprite to get the final texture point
sample_point = ((Vector[texture_point.x, texture_point.y] / texture_point.z) * sprite.size)
# Invert the y axis for the sprite
sample_point.y = sprite.height - sample_point.y
sample_point %= sprite.size
pixel = sprite.sample((sample_point).to_i)
# Blend the pixel sample with the provided color
pixel = pixel.darken(color)
# Darken by distance
d = (((50.0 - z) / 100.0) + 0.5).clamp(0.0..1.0)
pixel *= d
draw_point(x, y + c, pixel)
end
end
end
start = fin + 1
fin = height
# Once we hit the point where a line changes, we need a new slope for that line
if switch_left
line_left = PF::Line.new(p2, p3)
tl_left = PF::Line.new(t2, t3)
slope_left = line_left.slope
else
line_right = PF::Line.new(p2, p3)
tl_right = PF::Line.new(t2, t3)
slope_right = line_right.slope
end
end
end
end
end

View file

@ -1,182 +0,0 @@
require "./matrix"
require "./vector"
module PF
class Transform2d
property matrix : Matrix(Float64, 9)
def self.identity
Matrix[
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
]
end
# Returns a matrix representing a 2d translation
def self.translation(x : Float | Int, y : Float | Int)
Matrix[
1.0, 0.0, x.to_f64,
0.0, 1.0, y.to_f64,
0.0, 0.0, 1.0,
]
end
# Returns a matrix representing a 2d scaling
def self.scale(x : Float | Int, y : Float | Int)
Matrix[
x.to_f64, 0.0, 0.0,
0.0, y.to_f64, 0.0,
0.0, 0.0, 1.0,
]
end
# Returns a matrix representing a 2d rotation
def self.rotation(angle : Float | Int)
cos = Math.cos(angle)
sin = Math.sin(angle)
Matrix[
cos, -sin, 0.0,
sin, cos, 0.0,
0.0, 0.0, 1.0,
]
end
# Returns a matrix representing a 2d shear
def self.shear(x : Float | Int, y : Float | Int)
Matrix[
1.0, x.to_f64, 0.0,
y.to_f64, 1.0, 0.0,
0.0, 0.0, 1.0,
]
end
# Return a new inverted version of the given *matrix*
def self.invert(matrix : Matrix)
det = matrix[0, 0] * (matrix[1, 1] * matrix[2, 2] - matrix[1, 2] * matrix[2, 1]) -
matrix[1, 0] * (matrix[0, 1] * matrix[2, 2] - matrix[2, 1] * matrix[0, 2]) +
matrix[2, 0] * (matrix[0, 1] * matrix[1, 2] - matrix[1, 1] * matrix[0, 2])
idet = 1.0 / det
Matrix[
(matrix[1, 1] * matrix[2, 2] - matrix[1, 2] * matrix[2, 1]) * idet,
(matrix[2, 0] * matrix[1, 2] - matrix[1, 0] * matrix[2, 2]) * idet,
(matrix[1, 0] * matrix[2, 1] - matrix[2, 0] * matrix[1, 1]) * idet,
(matrix[2, 1] * matrix[0, 2] - matrix[0, 1] * matrix[2, 2]) * idet,
(matrix[0, 0] * matrix[2, 2] - matrix[2, 0] * matrix[0, 2]) * idet,
(matrix[0, 1] * matrix[2, 0] - matrix[0, 0] * matrix[2, 1]) * idet,
(matrix[0, 1] * matrix[1, 2] - matrix[0, 2] * matrix[1, 1]) * idet,
(matrix[0, 2] * matrix[1, 0] - matrix[0, 0] * matrix[1, 2]) * idet,
(matrix[0, 0] * matrix[1, 1] - matrix[0, 1] * matrix[1, 0]) * idet,
]
end
def initialize
@matrix = PF::Transform2d.identity
end
def initialize(@matrix)
end
# =============
# Reset the transformation to the identity matrix
def reset
@matrix = PF::Transform2d.identity
self
end
# =============
# = translate =
# =============
# Translate by *x* and *y*
def translate(x : Number, y : Number)
@matrix = PF::Transform2d.translation(x, y) * @matrix
self
end
# ditto
def translate(point : Vector2)
translate(point.x, point.y)
end
# ==========
# = rotate =
# ==========
# Rotate by *angle* (in radians)
def rotate(angle : Float | Int)
@matrix = PF::Transform2d.rotation(angle) * @matrix
self
end
# =========
# = scale =
# =========
# Scale by *x* and *y*
def scale(x : Float | Int, y : Float | Int)
@matrix = PF::Transform2d.scale(x, y) * @matrix
self
end
# ditto
def scale(point : Vector2)
scale(point.x, point.y)
end
# Scale both x and y by *n*
def scale(n : Number)
scale(n, n)
end
# =========
# = shear =
# =========
# Shear by *x* and *y*
def shear(x : Float | Int, y : Float | Int)
@matrix = PF::Transform2d.shear(x, y) * @matrix
self
end
# ditto
def shear(point : Vector2)
shear(point.x, point.y)
end
# ==========
# Return the boudning box of the current transformation matrix
def bounding_box(x : Float | Int, y : Float | Int)
top_left = apply(0.0, 0.0)
top_right = apply(x.to_f, 0.0)
bot_right = apply(x.to_f, y.to_f)
bot_left = apply(0.0, y.to_f)
xs = Float64[top_left.x, top_right.x, bot_right.x, bot_left.x]
ys = Float64[top_left.y, top_right.y, bot_right.y, bot_left.y]
{Vector[xs.min, ys.min], Vector[xs.max, ys.max]}
end
# Invert the transformation
def invert
@matrix = PF::Transform2d.invert(@matrix)
self
end
def apply(x : Float | Int, y : Float | Int)
result = Vector[x, y, 1.0] * @matrix
Vector[result.x, result.y]
end
def apply(point : Vector2)
apply(point.x, point.y)
end
end
end

View file

@ -1,163 +0,0 @@
require "./matrix"
require "./vector"
module PF
class Transform3d
property matrix : Matrix(Float64, 16)
def self.identity
Matrix[
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
]
end
def self.rot_x(theta : Float64)
cox, sox = Math.cos(theta), Math.sin(theta)
Matrix[
1.0, 0.0, 0.0, 0.0,
0.0, cox, sox, 0.0,
0.0, -sox, cox, 0.0,
0.0, 0.0, 0.0, 1.0,
]
end
def self.rot_y(theta : Float64)
coy, soy = Math.cos(theta), Math.sin(theta)
Matrix[
coy, 0.0, soy, 0.0,
0.0, 1.0, 0.0, 0.0,
-soy, 0.0, coy, 0.0,
0.0, 0.0, 0.0, 1.0,
]
end
def self.rot_z(theta : Float64)
coz, siz = Math.cos(theta), Math.sin(theta)
Matrix[
coz, siz, 0.0, 0.0,
-siz, coz, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
]
end
def self.rotation(x : Float64, y : Float64, z : Float64)
self.rot_x(x) * self.rot_y(y) * self.rot_z(z)
end
def self.rotation(angle : Vector3(Float64))
self.rotation(angle.x, angle.y, angle.z)
end
def self.translation(x : Float64, y : Float64, z : Float64)
Matrix[
1.0, 0.0, 0.0, x,
0.0, 1.0, 0.0, y,
0.0, 0.0, 1.0, z,
0.0, 0.0, 0.0, 1.0,
]
end
def self.translation(pos : Vector3(Float64))
self.translation(pos.x, pos.y, pos.z)
end
def self.scale(scale : Vector3(Float64))
Matrix[
scale.x, 0.0, 0.0, 0.0,
0.0, scale.y, 0.0, 0.0,
0.0, 0.0, scale.z, 0.0,
0.0, 0.0, 0.0, 1.0,
]
end
# Does not work for scaling, only for rotation / translation
def self.quick_inverse(other : Matrix)
matrix = Matrix(Float64, 16).new(4, 4)
matrix[0, 0] = other[0, 0]; matrix[0, 1] = other[1, 0]; matrix[0, 2] = other[2, 0]; matrix[0, 3] = 0.0
matrix[1, 0] = other[0, 1]; matrix[1, 1] = other[1, 1]; matrix[1, 2] = other[2, 1]; matrix[1, 3] = 0.0
matrix[2, 0] = other[0, 2]; matrix[2, 1] = other[1, 2]; matrix[2, 2] = other[2, 2]; matrix[2, 3] = 0.0
matrix[3, 0] = -(other[3, 0] * matrix[0, 0] + other[3, 1] * matrix[1, 0] + other[3, 2] * matrix[2, 0])
matrix[3, 1] = -(other[3, 0] * matrix[0, 1] + other[3, 1] * matrix[1, 1] + other[3, 2] * matrix[2, 1])
matrix[3, 2] = -(other[3, 0] * matrix[0, 2] + other[3, 1] * matrix[1, 2] + other[3, 2] * matrix[2, 2])
matrix[3, 3] = 1.0
matrix
end
def self.point_at(position : Vector3(Float64), target : Vector3(Float64), up : Vector3(Float64) = Vector[0.0, 1.0, 0.0])
new_forward = (target - position).normalized
new_up = (up - new_forward * up.dot(new_forward)).normalized
new_right = new_up.cross(new_forward)
Matrix[
new_right.x, new_up.x, new_forward.x, position.x,
new_right.y, new_up.y, new_forward.y, position.y,
new_right.z, new_up.z, new_forward.z, position.z,
0.0, 0.0, 0.0, 1.0,
]
end
def self.apply(point : Vector3(Float64), matrix : Matrix(Float64, 16))
vec = Vector3.new(
point.x * matrix[0, 0] + point.y * matrix[1, 0] + point.z * matrix[2, 0] + matrix[3, 0],
point.x * matrix[0, 1] + point.y * matrix[1, 1] + point.z * matrix[2, 1] + matrix[3, 1],
point.x * matrix[0, 2] + point.y * matrix[1, 2] + point.z * matrix[2, 2] + matrix[3, 2]
)
w = point.x * matrix[0, 3] + point.y * matrix[1, 3] + point.z * matrix[2, 3] + matrix[3, 3]
vec /= w unless w == 0.0
{vec, w}
end
def initialize
@matrix = PF::Transform3d.identity
end
def initialize(@matrix)
end
def reset
@matrix = PF::Transform3d.identity
self
end
def rot_x(theta : Float64)
@matrix = PF::Transform3d.rot_x(theta) * @matrix
self
end
def rot_y(theta : Float64)
@matrix = PF::Transform3d.rot_y(theta) * @matrix
self
end
def rot_z(theta : Float64)
@matrix = PF::Transform3d.rot_z(theta) * @matrix
self
end
def self.rotate(r : Vector3(Float64))
rot_x(r.x)
rot_y(r.y)
rot_z(r.z)
self
end
def translate(pos : Vector3(Float64))
@matrix = PF::Transform3d.translation * @matrix
self
end
# Does not work for scaling, only for rotation / translation
def quick_invert
@matrix = PF::Transform3d.quick_inverse(@matrix)
self
end
def apply(point : Vector3(Float64))
PF::Transform3d.apply(point, @matrix)
end
end
end

View file

@ -1,167 +0,0 @@
require "./matrix"
module PF
abstract struct Vector
# Creates a new `Vector` with the given *args*
#
# ```
# PF::Vector[1, 2] # => PF::Vector2(Int32)(@x=1, @y=2)
# ```
macro [](*args)
PF::Vector{{args.size}}(typeof({{args.splat}})).new(
{% for arg in args %}
{{ arg }},
{% end %}
)
end
end
{% for i in 2..4 %}
{% vars = %w[x y z w] %}
struct Vector{{i}}(T) < Vector
{% for arg in 0...i %}
property {{vars[arg].id}} : T
{% end %}
def initialize({% for arg in 0...i %} @{{vars[arg].id}} : T, {% end %})
end
# Returns the size of this vector
# ```
# PF::Vector{{i}}.new(...).size => {{i}}
# ```
def size
{{ i.id }}
end
# Converts this Vector into a `StaticArray(T, {{i}})`
def to_a
StaticArray[{% for arg in 0...i %} @{{vars[arg].id}}, {% end %}]
end
{% for op in %w[> < >= <= ==] %}
# Tests if all components of each vector meet the `{{op.id}}` condition
def {{ op.id }}(other : Vector{{i}})
{% for arg in 0...i %}
return false unless @{{vars[arg].id}} {{op.id}} other.{{vars[arg].id}}
{% end %}
true
end
# Tests if all components of this vector meet the `{{op.id}}` condition with the given *n*
def {{ op.id }}(n : (Int | Float))
{% for arg in 0...i %}
return false unless @{{vars[arg].id}} {{op.id}} n
{% end %}
true
end
{% end %}
{% for op in %w[- abs] %}
# Calls `{{ op.id }}` on all components of this vector
def {{op.id}}
Vector{{i}}(T).new({% for arg in 0...i %} @{{vars[arg].id}}.{{op.id}}, {% end %})
end
{% end %}
{% for op in %w[* / // + - % **] %}
# Applies `{{op.id}}` to all component of this vector with the corresponding component of *other*
def {{ op.id }}(other : Vector{{i}})
Vector[{% for arg in 0...i %} @{{vars[arg].id}} {{op.id}} other.{{vars[arg].id}}, {% end %}]
end
# Applies `{{op.id}}` to all component of this vector with *n*
def {{ op.id }}(n : (Int | Float))
Vector[{% for arg in 0...i %} @{{vars[arg].id}} {{op.id}} n, {% end %}]
end
{% end %}
# Add all components together
def sum
{% for arg in 0...i %}
@{{vars[arg].id}} {% if arg != i - 1 %} + {% end %}
{% end %}
end
# The length or magnitude of the vector calculated by the Pythagorean theorem
def magnitude
Math.sqrt({% for arg in 0...i %} @{{vars[arg].id}} ** 2 {% if arg != i - 1 %} + {% end %}{% end %})
end
# Returns a new normalized unit `Vector{{i}}`
def normalized
m = magnitude
return self if m == 0
i = (1.0 / m)
Vector[{% for arg in 0...i %} @{{vars[arg].id}} * i, {% end %}]
end
# Returns the dot product of this vector and *other*
def dot(other : Vector{{i}})
{% for arg in 0...i %} @{{vars[arg].id}} * other.{{vars[arg].id}} {% if arg != i - 1 %} + {% end %}{% end %}
end
# Calculates the cross product of this vector and *other*
def cross(other : Vector{{i}})
{% if i == 2 %}
Vector[
x * other.y - y * other.x,
y * other.x - x * other.y,
]
{% elsif i == 3 %}
Vector[
y * other.z - z * other.y,
z * other.x - x * other.z,
x * other.y - y * other.x,
]
{% elsif i == 4 %}
Vector[
y * other.z - z * other.y,
z * other.x - x * other.z,
x * other.y - y * other.x,
T.new(0),
]
{% end %}
end
# Returns normalized value at a normal to the current vector
def normal(other : Vector{{i}})
cross(other).normalized
end
# Returns the distance between this vector and *other*
def distance(other : Vector{{i}})
(self - other).magnitude
end
# Multiply this vector by a *matrix*
#
# ```
# v = PF::Vector[1, 2, 3]
# m = PF::Matrix[
# 1, 0, 0,
# 0, 2, 0,
# 0, 0, 1,
# ]
# # => PF::Vector3(Int32)(@x=1, @y=4, @z=3)
# ```
def *(matrix : Matrix)
PF::Vector[{% for row in 0...i %}
{% for col in 0...i %} @{{ vars[col].id }} * matrix[{{col}}, {{row}}] {% if col != i - 1 %} + {% end %}{% end %},
{% end %}]
end
{% for method, type in {
to_i: Int32, to_u: UInt32, to_f: Float64,
to_i8: Int8, to_i16: Int16, to_i32: Int32, to_i64: Int64, to_i128: Int128,
to_u8: UInt8, to_u16: UInt16, to_u32: UInt32, to_u64: UInt64, to_u128: UInt128,
to_f32: Float32, to_f64: Float64,
} %}
# Convert the components in this vector to {{ type }}
def {{ method }}
Vector{{i}}({{ type }}).new({% for arg in 0...i %} @{{vars[arg].id}}.{{method}}, {% end %})
end
{% end %}
end
{% end %}
end

View file

@ -1,3 +1,3 @@
module PF
VERSION = "0.0.7"
VERSION = {% `shards version`.chomp.stringify %}
end