CSS Gradient Generator — Ruby source
Build linear, radial, and conic CSS gradients with multiple color stops and positions. Live preview and copy-ready CSS.
This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# =============================================================================
# css-gradient-generator.rb — CosmoDev polyglot showcase port of the
# `css-gradient-generator` tool
# -----------------------------------------------------------------------------
# Language : Ruby (3.2, standard library only)
# Source: ported from src/lib/cssGradient.ts (the canonical, live TypeScript
# lib); mirrors src/tool-sources/css-gradient-generator/{python.py,rust.rs}
# License : display source — part of CosmoDev's polyglot tool pages
# (dev.cosmolabs.org). Shown verbatim alongside the JS/TS/Go/Rust/
# Python ports and the other language ports.
# -----------------------------------------------------------------------------
# Pure CSS-gradient builder. Build linear / radial / conic CSS gradient
# strings from a small config struct. Deterministic and side-effect free;
# invalid input degrades gracefully (unknown colors → solid black, too few
# stops → black/white default ramp) rather than raising.
# =============================================================================
module CssGradient
# One color anchor on the gradient ramp. Position is a percentage 0..100.
Stop = Struct.new(:color, :position, keyword_init: true)
# Full input to build_gradient. `radial_shape` is only meaningful for
# type :radial; nil falls back to "circle" (mirroring the TypeScript
# `?? 'circle'` — an explicit "" passes through unchanged).
Config = Struct.new(:type, :angle, :stops, :radial_shape, keyword_init: true)
# Outcome of parse_color: an ok flag plus a human message (nil when ok).
ColorResult = Struct.new(:ok, :error, keyword_init: true)
# Named CSS colors this tool accepts. The full CSS spec defines ~148, but we
# intentionally accept only the common, unambiguous set so output stays
# predictable (mirrors the TypeScript allow-list).
NAMED_COLORS = %w[
transparent black white red green blue yellow orange purple pink gray
grey brown cyan magenta none currentcolor
].freeze
# Regexes mirror the TypeScript source exactly (\A/\z anchor like ^/$).
HEX_3_OR_6 = /\A#[0-9a-f]{3}([0-9a-f]{3})?\z/
HEX_8 = /\A#[0-9a-f]{8}\z/
RGB_FUNC = /\Argba?\([^)]+\)\z/
HSL_FUNC = /\Ahsla?\([^)]+\)\z/
module_function
# Validate a CSS color string.
#
# Accepts named colors, #RGB / #RRGGBB / #RRGGBBAA hex, and rgb()/rgba()/
# hsl()/hsla() functional forms. The input is trimmed and downcased before
# testing.
def parse_color(color)
c = color.to_s.strip.downcase
return ColorResult.new(ok: false, error: 'empty color') if c.empty?
return ColorResult.new(ok: true) if NAMED_COLORS.include?(c)
return ColorResult.new(ok: true) if c.match?(HEX_3_OR_6) || c.match?(HEX_8)
return ColorResult.new(ok: true) if c.match?(RGB_FUNC) || c.match?(HSL_FUNC)
ColorResult.new(ok: false, error: "invalid color: #{color}")
end
# Coerce a possibly-invalid color to a safe value: valid → the trimmed
# original (casing preserved), invalid → solid black. Guarantees the
# gradient always has a usable color value.
def normalize_color(color)
parse_color(color).ok ? color.strip : '#000000'
end
# Round the way JavaScript's Math.round does (half toward +infinity). Ruby's
# Float#round rounds half away from zero, which would disagree on negative
# .5 values; (x + 0.5).floor matches Math.round for all non-negative inputs —
# the gradient-position domain.
def round_like_js(x)
(x + 0.5).floor
end
# Render a float the way JavaScript's template literal does. Float#to_s is
# Ruby's shortest round-tripping decimal; we drop the trailing ".0" whole
# floats carry so 90.0 renders as "90", matching String(90).
def format_number(x)
s = x.to_s
s.end_with?('.0') ? s[0..-3] : s
end
# Render a complete CSS gradient string.
#
# Stops are sorted ascending by position. Array#sort_by is not guaranteed
# stable, so we sort on [position, index] to keep equal positions in input
# order — matching modern JavaScript's Array.sort. Fewer than two stops
# collapse to a black → white default ramp so the output is always
# renderable.
def build_gradient(config)
stops = config.stops.each_with_index.sort_by { |stop, index| [stop.position, index] }
.map(&:first)
if stops.length < 2
stops = [
Stop.new(color: '#000000', position: 0),
Stop.new(color: '#ffffff', position: 100),
]
end
stops_str = stops.map { |s| "#{normalize_color(s.color)} #{round_like_js(s.position)}%" }
.join(', ')
angle = format_number(config.angle)
case config.type
when :linear
"linear-gradient(#{angle}deg, #{stops_str})"
when :radial
# `?? 'circle'`: only nil falls back to "circle"; an explicit "" passes
# through (and would produce a degenerate gradient).
shape = config.radial_shape.nil? ? 'circle' : config.radial_shape
"radial-gradient(#{shape}, #{stops_str})"
when :conic
"conic-gradient(from #{angle}deg, #{stops_str})"
else
'' # mirrors the TypeScript, which falls off the switch
end
end
end
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →