
Not a tutorial, not a benchmark — just experimenting with 2D and 3D rendering in Ruby and confirming that the foundation is already mature and reliable.
Links:
# frozen_string_literal: true
require "gd"
W = 1400
H = 500
TEXT = "Ruby-LibGD v0.2.4"
FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
SIZE = 72
DEPTH = 32
img =
begin
GD::Image.new(W, H, true)
rescue
GD::Image.new(W, H)
end
# -------------------------------------------------
# 🌌 Background gradient
# -------------------------------------------------
(0...H).each do |y|
t = y.to_f / H
r = (20 + 40 * Math.sin(t * Math::PI)).to_i
g = (20 + 30 * t).to_i
b = (40 + 80 * (1 - t)).to_i
c = GD::Color.rgb(r, g, b)
img.line(0, y, W, y, c)
end
# -------------------------------------------------
# ✨ Stars
# -------------------------------------------------
star_colors = [
GD::Color.rgb(255, 255, 255),
GD::Color.rgb(255, 220, 180),
GD::Color.rgb(180, 220, 255)
]
300.times do
x = rand(W)
y = rand(H)
c = star_colors.sample
img.filled_rectangle(x, y, x + rand(2), y + rand(2), c)
end
# -------------------------------------------------
# 📐 Measure text (YOUR API)
# -------------------------------------------------
tw, th = img.text_bbox(TEXT, font: FONT, size: SIZE)
x0 = (W - tw) / 2
y0 = (H + th) / 2
# -------------------------------------------------
# 🌈 Rainbow 3D extrusion
# -------------------------------------------------
DEPTH.downto(1) do |i|
hue = i.to_f / DEPTH
r = (200 + 55 * Math.sin(hue * 2 * Math::PI)).to_i
g = (200 + 55 * Math.sin(hue * 2 * Math::PI + 2)).to_i
b = (200 + 55 * Math.sin(hue * 2 * Math::PI + 4)).to_i
color = GD::Color.rgb(r, g, b)
img.text_ft(TEXT, {
x: x0 + i * 1.4,
y: y0 + i * 0.9,
size: SIZE,
font: FONT,
color: color
})
end
# -------------------------------------------------
# 🖤 Soft outline
# -------------------------------------------------
outline = GD::Color.rgb(20, 20, 20)
[-2, -1, 1, 2].each do |d|
img.text_ft(TEXT, {
x: x0 + d,
y: y0,
size: SIZE,
font: FONT,
color: outline
})
end
# -------------------------------------------------
# 🔴 Front face (Ruby red)
# -------------------------------------------------
front = GD::Color.rgb(220, 40, 40)
img.text_ft(TEXT, {
x: x0,
y: y0,
size: SIZE,
font: FONT,
color: front
})
img.save("ruby_libgd_festival.png")
puts "✨ ruby_libgd_festival.png generated"

SPONSOR LOGISTIC INTELLIGENCE:

# frozen_string_literal: true
require "gd"
# -----------------------
# Helpers
# -----------------------
def clamp01(x)
return 0.0 if x < 0.0
return 1.0 if x > 1.0
x
end
def hsv_to_rgb(h, s, v)
h = h % 1.0
s = clamp01(s)
v = clamp01(v)
i = (h * 6.0).floor
f = h * 6.0 - i
p = v * (1.0 - s)
q = v * (1.0 - f * s)
t = v * (1.0 - (1.0 - f) * s)
r, g, b =
case i % 6
when 0 then [v, t, p]
when 1 then [q, v, p]
when 2 then [p, v, t]
when 3 then [p, q, v]
when 4 then [t, p, v]
when 5 then [v, p, q]
end
[(r * 255).round, (g * 255).round, (b * 255).round]
end
def rot_x(y, z, a)
[y * Math.cos(a) - z * Math.sin(a), y * Math.sin(a) + z * Math.cos(a)]
end
def rot_y(x, z, a)
[x * Math.cos(a) + z * Math.sin(a), -x * Math.sin(a) + z * Math.cos(a)]
end
def rot_z(x, y, a)
[x * Math.cos(a) - y * Math.sin(a), x * Math.sin(a) + y * Math.cos(a)]
end
def set_px(img, x, y, color)
if img.respond_to?(:set_pixel)
img.set_pixel(x, y, color)
elsif img.respond_to?(:pixel)
img.pixel(x, y, color)
else
img[x, y] = color
end
end
# -----------------------
# Canvas
# -----------------------
w = 900
h = 900
cx = w / 2
cy = h / 2
img =
begin
GD::Image.new(w, h, true)
rescue ArgumentError
GD::Image.new(w, h)
end
bg = GD::Color.rgb(14, 14, 18)
if img.respond_to?(:filled_rectangle)
img.filled_rectangle(0, 0, w - 1, h - 1, bg)
else
(0...h).each { |yy| (0...w).each { |xx| set_px(img, xx, yy, bg) } }
end
cache = {}
# -----------------------
# Torus + Camera parameters
# -----------------------
R = 1.55
r = 0.65
ax = 0.9
ay = 0.6
az = 0.2
z_offset = 3.2
screen_scale = 260.0
fov = 2.2
lx, ly, lz = -0.35, -0.45, 0.82
llen = Math.sqrt(lx * lx + ly * ly + lz * lz)
lx /= llen; ly /= llen; lz /= llen
steps_u = 220
steps_v = 220
dot = 2
points = []
(0...steps_u).each do |iu|
u = iu * (2.0 * Math::PI) / steps_u
cu = Math.cos(u)
su = Math.sin(u)
(0...steps_v).each do |iv|
v = iv * (2.0 * Math::PI) / steps_v
cv = Math.cos(v)
sv = Math.sin(v)
x = (R + r * cu) * cv
y = (R + r * cu) * sv
z = r * su
nx = cu * cv
ny = cu * sv
nz = su
y, z = rot_x(y, z, ax)
x, z = rot_y(x, z, ay)
x, y = rot_z(x, y, az)
ny, nz = rot_x(ny, nz, ax)
nx, nz = rot_y(nx, nz, ay)
nx, ny = rot_z(nx, ny, az)
zc = z + z_offset
p = fov / (fov + zc)
sx = (x * p * screen_scale + cx).round
sy = (y * p * screen_scale + cy).round
next if sx < 0 || sy < 0 || sx >= w || sy >= h
hue = v / (2.0 * Math::PI)
base_r, base_g, base_b = hsv_to_rgb(hue, 0.92, 1.0)
ndotl = nx * lx + ny * ly + nz * lz
diff = clamp01(ndotl)
spec = clamp01((diff ** 20) * 1.2)
shade = 0.18 + 0.82 * diff
rr = (base_r * shade + 255 * spec).round
gg = (base_g * shade + 255 * spec).round
bb = (base_b * shade + 255 * spec).round
points << [zc, sx, sy, rr, gg, bb]
end
end
points.sort_by!(&:first)
points.each do |_, x, y, rr, gg, bb|
key = (rr << 16) | (gg << 8) | bb
color = cache[key] ||= GD::Color.rgb(rr, gg, bb)
(0...dot).each do |dy|
(0...dot).each do |dx|
xx = x + dx
yy = y + dy
next if xx < 0 || yy < 0 || xx >= w || yy >= h
set_px(img, xx, yy, color)
end
end
end
out = "donut_3d.png"
if img.respond_to?(:png)
img.png(out)
elsif img.respond_to?(:save)
img.save(out)
else
raise "Export method not found"
end
puts "Wrote #{out}"

# frozen_string_literal: true
require "gd"
# ---------- helpers ----------
def clamp01(x)
return 0.0 if x 1.0
x
end
def hsv_to_rgb(h, s, v)
h = h % 1.0
s = clamp01(s)
v = clamp01(v)
i = (h * 6.0).floor
f = h * 6.0 - i
p = v * (1.0 - s)
q = v * (1.0 - f * s)
t = v * (1.0 - (1.0 - f) * s)
r, g, b =
case i % 6
when 0 then [v, t, p]
when 1 then [q, v, p]
when 2 then [p, v, t]
when 3 then [p, q, v]
when 4 then [t, p, v]
when 5 then [v, p, q]
end
[(r * 255).round, (g * 255).round, (b * 255).round]
end
# ---------- render params ----------
w = 900
h = 900
cx = w / 2.0
cy = h / 2.0
radius = 360.0
bg = 18
# Light direction (normalized-ish)
lx, ly, lz = -0.4, -0.35, 0.85
l_len = Math.sqrt(lx*lx + ly*ly + lz*lz)
lx /= l_len; ly /= l_len; lz /= l_len
# ---------- image ----------
img = GD::Image.new(w, h)
# background
bg_color = GD::Color.rgb(bg, bg, bg)
# If your binding has "filled_rectangle":
if img.respond_to?(:filled_rectangle)
img.filled_rectangle(0, 0, w - 1, h - 1, bg_color)
end
# cache colors (libgd has a limited palette in some modes; truecolor is best)
color_cache = {}
# ---------- draw sphere ----------
# We'll iterate over a bounding square and fill pixels inside the circle.
min_x = (cx - radius).floor
max_x = (cx + radius).ceil
min_y = (cy - radius).floor
max_y = (cy + radius).ceil
( min_y..max_y ).each do |py|
dy = (py - cy)
( min_x..max_x ).each do |px|
dx = (px - cx)
rr = dx*dx + dy*dy
next if rr > radius*radius
# Sphere surface normal from screen-space x,y
# x,y in [-1,1]
nx = dx / radius
ny = dy / radius
# z is positive hemisphere
v = 1.0 - nx*nx - ny*ny
nz = Math.sqrt([v, 0.0].max)
# Use spherical coordinates for color:
# longitude (atan2) -> hue
# latitude (asin) -> saturation/value modulation
lon = Math.atan2(ny, nx) # [-pi, pi]
lat = Math.asin(nz) # [0, pi/2] (front hemisphere)
hue = (lon + Math::PI) / (2.0 * Math::PI) # 0..1
sat = 0.95
val = 0.95
# Lighting: lambertian shading (dot of normal and light)
ndotl = nx*lx + ny*ly + nz*lz
shade = clamp01(0.15 + 0.85 * ndotl) # ambient + diffuse
# Add subtle rim darkening for depth
rim = clamp01(nz) # 0 at edge, 1 at center
shade *= (0.65 + 0.35 * rim)
r, g, b = hsv_to_rgb(hue, sat, val)
# Apply shading
r = (r * shade).round
g = (g * shade).round
b = (b * shade).round
key = (r << 16) | (g << 8) | b
color = color_cache[key] ||= GD::Color.rgb(r, g, b)
# set pixel
if img.respond_to?(:set_pixel)
img.set_pixel(px, py, color)
elsif img.respond_to?(:pixel)
img.pixel(px, py, color)
else
# fallback name you might have
img[pX, py] = color
end
end
end
# Optional highlight specular dot (adds pop)
if img.respond_to?(:filled_ellipse)
highlight = GD::Color.rgb(255, 255, 255)
img.filled_ellipse((cx - radius*0.25).round, (cy - radius*0.25).round, (radius*0.25).round, (radius*0.18).round, highlight)
end
# ---------- export ----------
out = "sphere_multicolor.png"
if img.respond_to?(:png)
img.png(out)
elsif img.respond_to?(:save)
img.save(out)
else
raise "Don't know how to export PNG with this GD binding"
end
puts "Wrote #{out}"
