Skip to content

Instantly share code, notes, and snippets.

@TheEyesightDim
Last active May 31, 2024 03:17
Show Gist options
  • Save TheEyesightDim/c98fa7035d1309670771911f2ff5c120 to your computer and use it in GitHub Desktop.
Save TheEyesightDim/c98fa7035d1309670771911f2ff5c120 to your computer and use it in GitHub Desktop.
Main code for Orthopaint: https://orthohedron.itch.io/orthopaint
require "app/strings.rb"
def undo_last_stroke(args)
args.state.counts.pop
stroke = args.state.counts.pop
args.state.commands.slice!(0 - stroke, stroke) if stroke
end
def tick(args)
args.state.help_layer ||= :shown
# Bind inputs to new names for easier access.
mousewheel = args.inputs.mouse.wheel ? args.inputs.mouse.wheel.y : 0
mx = args.inputs.mouse.x
my = args.inputs.mouse.y
key_i = args.inputs.keyboard.key_down.i
key_u = args.inputs.keyboard.key_down.u
key_h = args.inputs.keyboard.h
key_s = args.inputs.keyboard.s
key_l = args.inputs.keyboard.l
key_a = args.inputs.keyboard.a
args.state.click = :up
args.state.click = :down if args.inputs.mouse.button_left
# Draw the bg first. This is the base layer and it always exists.
args.outputs.primitives << {
x: 0,
y: 0,
w: 1280,
h: 720,
r: 32,
g: 32,
b: 32,
}.solid
# Our canvas for painting It should be erased on a reset.
# EDIT: Now I'm using a command list because I don't know how to hold the texture otherwise.
args.state.commands ||= []
# Array that holds frame counts for brush strokes
# used for undo.
args.state.counts ||= [0]
# we need to keep updating the last value of `counts` if it matches our current state,
# or else push a new value to start a new 'stroke.'
if args.state.click == :down
args.state.counts[-1] += 1
else
args.state.counts << 0 unless args.state.counts[-1] == 0
end
# Set brush size default.
brush = args.state.brush_size ||= 8
# Modify the brush size based on mousewheel movement.
args.state.brush_size = (args.state.brush_size + (mousewheel * 4)).clamp(4, 256)
# Make a hash for a rect dims to reuse later.
rect = { x: (mx - brush * 0.5), y: (my - brush * 0.5), w: brush, h: brush }
####### NOTES ABOUT COLORS #######
# 2 color formats: RGBA (for DR) and HSLA (for the interface)
# RGBA is all in 0..255
# HSLA is [360 deg, 0..1, 0..1, 0..255]
# Set the default color.
# Only the ones in `args.state` get saved between ticks.
args.state.hsl_color ||= { h: rand(360), s: 0.999, l: 0.5, a: 255 }
args.state.rgb_color ||= hsl_to_rgb(args.state.hsl_color)
# Now modify the color based on inputs. Be sure to modify the ones held in `args.state`.
if key_h || key_s || key_l || key_a
shifts = [key_h, key_s, key_l, key_a].map { |k| if k then 1 else 0 end }
args.state.hsl_color = shift_hsla(args.state.hsl_color, *shifts)
args.state.rgb_color = hsl_to_rgb(args.state.hsl_color)
end
undo_last_stroke(args) if key_u
# Painting happens here.
# First, draw the solid to the render target...
color = args.state.rgb_color
if args.state.click == :down
args.state.commands << rect.merge(color)
end
# Then draw the render target to main output.
args.state.commands.each do |comm|
args.outputs.primitives << comm.solid
end
# Done painting.
# Draw the selection border. Note the last merge: it should never be transparent.
args.outputs.primitives << rect.merge(color).merge({ a: 255 }).border
# Draw color preview square in the bottom-left corner.
# First, draw the background...
args.outputs.primitives << { x: 30, y: 30, w: 180, h: 100, a: 168 }.solid
# Then, draw the active color.
args.outputs.primitives << { x: 35, y: 35, w: 70, h: 90 }.merge(color).solid
# Now print the HSL values all neatly.
hsl = args.state.hsl_color
args.outputs.labels << { text: "H:", x: 110, y: 120, size_enum: -2 }.merge($lb_common)
args.outputs.labels << { text: "S:", x: 110, y: 100, size_enum: -2 }.merge($lb_common)
args.outputs.labels << { text: "L:", x: 110, y: 80, size_enum: -2 }.merge($lb_common)
args.outputs.labels << { text: "A:", x: 110, y: 60, size_enum: -2 }.merge($lb_common)
args.outputs.labels << { text: "#{"%.3f" % hsl[:h]}", x: 140, y: 125 }.merge($lb_common)
args.outputs.labels << { text: "#{"%.3f" % hsl[:s]}", x: 140, y: 105 }.merge($lb_common)
args.outputs.labels << { text: "#{"%.3f" % hsl[:l]}", x: 140, y: 85 }.merge($lb_common)
args.outputs.labels << { text: "#{hsl[:a]}", x: 140, y: 65 }.merge($lb_common)
# Toggle help text.
if key_i
if args.state.help_layer == :shown
args.state.help_layer = :hidden
else
args.state.help_layer = :shown
end
end
if args.state.help_layer == :shown
print_help_text args
end
# Reset button.
if args.inputs.keyboard.key_down.enter
args.gtk.reset Time.now.to_i
end
end
def shift_hsla(color_hash, h_shift, s_shift, l_shift, a_shift)
{
h: (color_hash[:h] + h_shift) % 360,
s: fractional(color_hash[:s] + s_shift * 0.0101),
l: fractional(color_hash[:l] + l_shift * 0.0101),
a: (color_hash[:a] + a_shift) % 256,
}
end
def rgb_to_hsl(color_hash)
r, g, b = [color_hash[:r], color_hash[:g], color_hash[:b]].map { |c| (c / 255.0) }
max = [r, g, b].max
min = [r, g, b].min
delta = (max - min).to_f
f = 1 - (max + max - delta - 1).abs
h = 0
if delta != 0
case max
when r
h = (g - b) / delta
when g
h = 2 + (b - r) / delta
when b
h = 4 + (r - g) / delta
end
else
h = 0
end
h = 60 * (h < 0 ? h + 6 : h)
s = f != 0 ? delta / f : 0
l = (max + max - min) / 2
{ h: h, s: s, l: l, a: color_hash[:a] }
end
def hsl_to_rgb(color_hash)
h, s, l = [color_hash[:h], color_hash[:s], color_hash[:l]]
c = s * [l, 1 - l].min
fn = ->(n, k = (n + h / 30.0) % 12) { l - c * [[k - 3, 9 - k, 1].min, -1].max }
{ r: fn.(0) * 255, g: fn.(8) * 255, b: fn.(4) * 255, a: color_hash[:a] }
end
def fractional(num)
num - num.to_i
end
def print_help_text(args)
# Print labels against a translucent bg.
args.outputs.primitives << { x: 0, y: 600, w: 1280, h: 600, a: 168 }.solid
# Print help text.
args.outputs.labels << $lb_hash[:help_splash_1].merge($lb_common)
args.outputs.labels << $lb_hash[:help_splash_2].merge($lb_common)
args.outputs.labels << $lb_hash[:help_splash_3].merge($lb_common)
# Other stuff, like perf.
# Calculating frame time average over last ten frames...
args.state.ftimes ||= [Time.now.to_f]
args.state.ftimes << Time.now.to_f
args.state.ftimes.slice!(0..-10)
buf = []
args.state.ftimes.each_cons(2) { |a| buf << (a[-1] - a[0]) }
avg = (buf.reduce(0) { |m, x| m + x } / buf.length) * 1000
args.outputs.labels << { text: "ms/F (avg. of 10): #{"%2.2f" % avg}", x: 1080, y: 690 }.merge($lb_common)
# Mouse coords
x, y = args.inputs.mouse.x, args.inputs.mouse.y
args.outputs.labels << { text: "x: #{x} | y: #{y}", x: 1080, y: 660 }.merge($lb_common)
#button
args.outputs.labels << { text: "#{args.state.click}", x: 1080, y: 630 }.merge($lb_common)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment