libgd-gis 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f3f45fc1508b900f78e5b11cd5fff6854ff81e1482ca26b8478be88957ba086
4
- data.tar.gz: 30df288e61f15f66d0c0f210f9842cc1897ae35b1e6c0a442d6238dbb0c1ce4c
3
+ metadata.gz: 72c8b1d43f60ad8615302e22054c3d5e00be6bd746c757f00d980e781f760b0c
4
+ data.tar.gz: 74d09fb46fe02d7d53c75f4c2a9acb6bbab24383b51ef9bd9ec3ad8c89e0253f
5
5
  SHA512:
6
- metadata.gz: ca3069e841e805116089bc949fa4f14a708775100eed78dbc488fa1c1bba0d0fa66a7acb2fce50711d134e9d98695c2b3c0ecc7f28cdf723b4936da37e22825b
7
- data.tar.gz: d12da69bd604584ba37b2b250de904e622a93a325437afec6c6f79f81c53c4863043c2b837e2f73d965423e62229f4838458d485f7f2d44bdf746e7df8c44075
6
+ metadata.gz: e09c62bf8ee7218019a2e69676cc9b7461982568842bb8afac861f38f290d7fec658ee15269915ec5a3ad925de4d5ef4e6dcdce0415d7b97d438d91795d022a5
7
+ data.tar.gz: e7a1a6f47da4bbf9db7e18de9c3184e4af3498f636f51ba8b8ee9aa25b1c57c9532be24e71ee76e2201f5f7a240cddc62e33f16d886a1673062d845163831664
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # LibGD-GIS
1
+ # LibGD GIS
2
2
 
3
3
  <p align="center">
4
4
  <a href="https://rubystacknews.com/2026/01/07/ruby-can-now-draw-maps-and-i-started-with-ice-cream/">
@@ -33,156 +33,190 @@
33
33
  [![Test Coverage](https://coveralls.io/repos/githublibgd-gis/libgd-gis/badge.svg?branch=master)](https://coveralls.io/github/libgd-gis/libgd-gis?branch=master)
34
34
  [![Gem Version](https://img.shields.io/gem/v/libgd-gis.svg)](https://rubygems.org/gems/libgd-gis)
35
35
 
36
+ ---
37
+
38
+ **libgd-gis** is a lightweight Ruby GIS rendering library built on top of **GD**.
39
+ It renders geographic data (GeoJSON, points, lines, polygons) into raster images using Web Mercator tiles and a simple, explicit rendering pipeline.
36
40
 
37
- | Examples | Examples | Examples |
38
- | :----: | :----: | :--: |
39
- | <img src="docs/examples/parana.png" height="250"> | <img src="docs/examples/nyc.png" height="250"> | <img src="docs/examples/paris.png" height="250"> |
40
- | <img src="examples/nyc/nyc.png" height="250"> | <img src="docs/examples/tokyo_solarized.png" height="250"> | <img src="examples/parana/parana.png" height="250"> |
41
- | <img src="docs/examples/america.png" height="250"> | <img src="docs/examples/argentina_museum.png" height="250"> | <img src="docs/examples/museos_parana.png" height="250"> |
42
- | <img src="docs/examples/asia.png" height="250"> | <img src="docs/examples/europe.png" height="250"> | <img src="docs/examples/icecream_parana.png" height="250"> |
43
- | <img src="docs/examples/argentina_cities.png" height="250"> | <img src="docs/examples/tanzania_hydro.png" height="250"> | <img src="docs/examples/parana_polygon.png" height="250"> |
44
- | <img src="docs/examples/parana_carto_dark.png" height="250"> | <img src="docs/examples/ramirez_avenue.png" height="250"> | <img src="examples/paris/paris.png" height="250"> |
41
+ This library is designed for **map visualization**, not for spatial analysis.
45
42
 
46
43
  ---
47
44
 
48
- > **libgd-gis is evolving very fast**, so some examples may temporarily stop working.
49
- > Please report issues or ask for help — feedback is very welcome.
50
- > https://github.com/ggerman/libgd-gis/issues or ggerman@gmail.com
45
+ ## Features
51
46
 
52
- --
47
+ - Web Mercator tile rendering (OSM, CARTO, ESRI, Stamen, etc.)
48
+ - CRS normalization (CRS84, EPSG:4326, EPSG:3857, Gauss–Krüger Argentina)
49
+ - Layered rendering pipeline
50
+ - YAML-based styling
51
+ - Rule-based semantic classification (ontology)
52
+ - Points, lines, and polygons support
53
+ - No heavy GIS dependencies
53
54
 
54
- ## A geospatial raster engine for Ruby.
55
+ ---
55
56
 
56
- libgd-gis allows Ruby to render real maps, GeoJSON layers, vector features, and geospatial tiles using a native raster backend powered by **libgd**.
57
+ ## Non-Goals
57
58
 
58
- It restores something Ruby lost over time:
59
- the ability to generate **maps, tiles, and GIS-grade visualizations natively**, without relying on external tools like QGIS, Mapnik, ImageMagick, or Mapbox.
59
+ libgd-gis intentionally does **not** aim to be:
60
60
 
61
- Built on top of **ruby-libgd**, this project turns Ruby into a **map rendering engine**, capable of producing spatial graphics, tiled maps, and geospatial outputs directly inside Ruby processes.
61
+ - a spatial analysis engine
62
+ - a replacement for PostGIS / GEOS
63
+ - a full map server
64
+ - a vector tile generator
62
65
 
63
- - No external renderers.
64
- - No shelling out.
65
- - Just Ruby, raster, and GIS.
66
+ If you need projections beyond Web Mercator or topological correctness,
67
+ use a full GIS stack.
66
68
 
67
69
  ---
68
70
 
69
- ## What is this?
71
+ ## Installation
70
72
 
71
- `libgd-gis` is a **geospatial rendering engine** for Ruby built on top of [`ruby-libgd`](https://github.com/ggerman/ruby-libgd).
73
+ Add to your Gemfile:
72
74
 
73
- It allows you to:
75
+ ```ruby
76
+ gem "libgd-gis"
77
+ ```
74
78
 
75
- - Load GeoJSON, CSV, or any dataset with coordinates
76
- - Fetch real basemap tiles
77
- - Reproject WGS84 (lat/lon) into Web Mercator
78
- - Render points, icons, and layers onto a raster map
79
- - Generate PNG maps or map tiles
79
+ Then run:
80
80
 
81
- This is the same type of pipeline used by professional GIS systems — implemented in Ruby.
81
+ ```bash
82
+ bundle install
83
+ ```
82
84
 
83
- ---
85
+ You must also have **GD** available on your system.
84
86
 
85
- ## Installation
87
+ ---
86
88
 
87
- ### System dependency
89
+ ## Basic Usage
88
90
 
89
- `libgd-gis` depends on **libgd**, via `ruby-libgd`.
91
+ ### Create a map
90
92
 
91
- Install libgd first:
93
+ ```ruby
94
+ require "gd/gis"
92
95
 
93
- **Ubuntu / Debian**
96
+ map = GD::GIS::Map.new(
97
+ bbox: [-58.45, -34.7, -58.35, -34.55],
98
+ zoom: 13,
99
+ basemap: :carto_light,
100
+ width: 1024,
101
+ height: 768
102
+ )
94
103
  ```
95
- sudo apt install libgd-dev
104
+
105
+ ### Load a style
106
+
107
+ ```ruby
108
+ map.style = GD::GIS::Style.load("default", from: "styles")
96
109
  ```
97
110
 
98
- **macOS**
111
+ ### Load GeoJSON
112
+
113
+ ```ruby
114
+ map.add_geojson("data/roads.geojson")
115
+ map.add_geojson("data/water.geojson")
99
116
  ```
100
- brew install gd
117
+
118
+ ### Render
119
+
120
+ ```ruby
121
+ map.render
122
+ map.save("map.png")
101
123
  ```
102
124
 
103
125
  ---
104
126
 
105
- ### Ruby gems
127
+ ## Styles Are Mandatory
106
128
 
107
- ```
108
- gem install ruby-libgd
109
- gem install libgd-gis
110
- ```
129
+ libgd-gis requires an explicit **style definition** in order to render a map.
111
130
 
112
- ---
131
+ A `GD::GIS::Map` instance **will not render without a style**, and calling
132
+ `map.render` before assigning one will raise an error.
133
+
134
+ This is intentional.
113
135
 
114
- ## Quick Example
136
+ Styles define how semantic layers (roads, water, parks, points, etc.) are mapped
137
+ to visual properties such as colors, stroke widths, fills, and drawing order.
138
+ No implicit or default styling is applied.
115
139
 
116
- Render hydroelectric plants from a GeoJSON file:
140
+ ### Example:
117
141
 
118
142
  ```ruby
119
- require "json"
120
143
  require "gd/gis"
121
144
 
122
- # ---------------------------
123
- # Bounding box mundial
124
- # ---------------------------
125
- AMERICA = [-170, -60, -30, 75]
126
-
127
- # ---------------------------
128
- # Crear mapa
129
- # ---------------------------
130
145
  map = GD::GIS::Map.new(
131
- bbox: AMERICA,
132
- zoom: 4,
146
+ bbox: PARIS,
147
+ zoom: 13,
133
148
  basemap: :carto_light
134
149
  )
135
150
 
136
- # Cargar datos
137
- # ---------------------------
138
- peaks = JSON.parse(File.read("picks.json"))
139
-
140
- # ---------------------------
141
- # Agregar capa de puntos
142
- # ---------------------------
143
- map.add_points(
144
- peaks,
145
- lon: ->(p) { p["longitude"] },
146
- lat: ->(p) { p["latitude"] },
147
- icon: "peak.png",
148
- label: ->(p) { p["name"] },
149
- font: "./fonts/DejaVuSans.ttf",
150
- size: 10,
151
- color: [0,0,0]
152
- )
151
+ map.style = GD::GIS::Style.load("default")
153
152
 
154
- # ---------------------------
155
- # Renderizar y guardar
156
- # ---------------------------
157
153
  map.render
158
- map.save("output/america.png")
159
-
160
- puts "Saved output/america.png"
161
-
162
154
  ```
155
+ ### Example:
156
+
157
+ ```yml
158
+ # styles/default.yml
159
+
160
+ roads:
161
+ motorway:
162
+ stroke: [255, 255, 255]
163
+ stroke_width: 10
164
+ fill: [60, 60, 60]
165
+ fill_width: 6
166
+
167
+ primary:
168
+ stroke: [200, 200, 200]
169
+ stroke_width: 7
170
+ fill: [80, 80, 80]
171
+ fill_width: 4
172
+
173
+ street:
174
+ stroke: [120, 120, 120]
175
+ stroke_width: 1
176
+
177
+ rail:
178
+ stroke: [255, 255, 255]
179
+ stroke_width: 6
180
+ fill: [220, 50, 50]
181
+ fill_width: 4
182
+ center: [255, 255, 255]
183
+ center_width: 1
184
+
185
+ water:
186
+ fill: [120, 180, 255]
187
+ fill_width: 4
188
+ stroke: [80, 140, 220]
189
+
190
+ park:
191
+ fill: [40, 80, 40]
192
+
193
+ order:
194
+ - water
195
+ - park
196
+ - street
197
+ - primary
198
+ - motorway
199
+ - rail
200
+ ```
201
+ This design ensures predictable rendering and makes all visual decisions explicit
202
+ and reproducible.
163
203
 
164
- ---
165
204
 
166
- ## Features
205
+ ---
167
206
 
168
- - Real basemap tiles
169
- - WGS84 → Web Mercator projection
170
- - GeoJSON point rendering
171
- - CSV / JSON support
172
- - Icon-based symbol layers
173
- - Automatic bounding box fitting
174
- - Raster output (PNG)
207
+ ## CRS Support
175
208
 
176
- ---
209
+ Supported input CRS:
177
210
 
178
- ## License
211
+ - CRS84
212
+ - EPSG:4326
213
+ - EPSG:3857
214
+ - EPSG:22195 (Gauss–Krüger Argentina, zone 5)
179
215
 
180
- MIT
216
+ All coordinates are normalized internally to **CRS84 (lon, lat)**.
181
217
 
182
218
  ---
183
219
 
184
- ## Author
220
+ ## License
185
221
 
186
- Germán Silva
187
- https://github.com/ggerman
188
- https://rubystacknews.com
222
+ MIT
@@ -1,18 +1,55 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "net/http"
2
4
  require "fileutils"
3
5
 
4
6
  module GD
5
7
  module GIS
8
+ # Fetches and manages raster basemap tiles using the XYZ
9
+ # Web Mercator tiling scheme.
10
+ #
11
+ # A Basemap is responsible for:
12
+ # - Converting geographic coordinates to tile coordinates
13
+ # - Downloading raster tiles from public tile providers
14
+ # - Caching tiles on disk
15
+ # - Exposing pixel origins for map composition
16
+ #
17
+ # All tiles are assumed to be 256×256 pixels.
18
+ #
19
+ # @example Fetch tiles for a bounding box
20
+ # bbox = [-3.8, 40.3, -3.6, 40.5] # west, south, east, north
21
+ # basemap = GD::GIS::Basemap.new(12, bbox, :carto_light)
22
+ # tiles, origin_x, origin_y = basemap.fetch_tiles
23
+ #
6
24
  class Basemap
25
+ # Tile size in pixels (Web Mercator standard)
7
26
  TILE_SIZE = 256
8
- attr_reader :origin_x, :origin_y
9
27
 
10
- def initialize(zoom, bbox, provider=:carto_light)
28
+ # @return [Integer] pixel X origin of the basemap
29
+ attr_reader :origin_x
30
+
31
+ # @return [Integer] pixel Y origin of the basemap
32
+ attr_reader :origin_y
33
+
34
+ # Creates a new basemap tile source.
35
+ #
36
+ # @param zoom [Integer] zoom level
37
+ # @param bbox [Array<Float>] bounding box [west, south, east, north]
38
+ # @param provider [Symbol] tile provider/style
39
+ def initialize(zoom, bbox, provider = :carto_light)
11
40
  @zoom = zoom
12
41
  @bbox = bbox
13
42
  @provider = provider
14
43
  end
15
44
 
45
+ # Builds a tile URL for a given provider and tile coordinate.
46
+ #
47
+ # @param z [Integer] zoom level
48
+ # @param x [Integer] tile X coordinate
49
+ # @param y [Integer] tile Y coordinate
50
+ # @param style [Symbol] tile style/provider
51
+ # @return [String] tile URL
52
+ # @raise [RuntimeError] if the provider is unknown
16
53
  def url(z, x, y, style = :osm)
17
54
  case style
18
55
 
@@ -55,9 +92,6 @@ module GD
55
92
  when :esri_terrain
56
93
  "https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/#{z}/#{y}/#{x}"
57
94
 
58
- when :esri_hybrid
59
- "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/#{z}/#{y}/#{x}"
60
-
61
95
  # ==============================
62
96
  # STAMEN 503
63
97
  # ==============================
@@ -102,15 +136,34 @@ module GD
102
136
  end
103
137
  end
104
138
 
139
+ # Converts longitude to tile X coordinate.
140
+ #
141
+ # @param lon [Float] longitude in degrees
142
+ # @return [Integer] tile X coordinate
105
143
  def lon2tile(lon)
106
- ((lon + 180.0) / 360.0 * (2 ** @zoom)).floor
144
+ ((lon + 180.0) / 360.0 * (2**@zoom)).floor
107
145
  end
108
146
 
147
+ # Converts latitude to tile Y coordinate.
148
+ #
149
+ # @param lat [Float] latitude in degrees
150
+ # @return [Integer] tile Y coordinate
109
151
  def lat2tile(lat)
110
152
  rad = lat * Math::PI / 180
111
- ((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math::PI) / 2 * (2 ** @zoom)).floor
153
+ ((1 - (Math.log(Math.tan(rad) + (1 / Math.cos(rad))) / Math::PI)) / 2 * (2**@zoom)).floor
112
154
  end
113
155
 
156
+ # Downloads all tiles required to cover the bounding box.
157
+ #
158
+ # Tiles are cached on disk under `tmp/tiles`.
159
+ #
160
+ # @return [Array]
161
+ # - tiles [Array<Array(Integer, Integer, String)>]
162
+ # list of [x, y, file_path]
163
+ # - origin_x [Integer] pixel X origin
164
+ # - origin_y [Integer] pixel Y origin
165
+ #
166
+ # @raise [RuntimeError] if a tile cannot be fetched
114
167
  def fetch_tiles
115
168
  west, south, east, north = @bbox
116
169
 
@@ -133,8 +186,14 @@ module GD
133
186
  (y_min..y_max).each do |y|
134
187
  path = nil
135
188
 
136
- unless File.exist?("tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png") ||
137
- File.exist?("tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.jpg")
189
+ if File.exist?("tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png") ||
190
+ File.exist?("tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.jpg")
191
+ path = if File.exist?("tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png")
192
+ "tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png"
193
+ else
194
+ "tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.jpg"
195
+ end
196
+ else
138
197
 
139
198
  uri = URI(url(@zoom, x, y, @provider))
140
199
 
@@ -159,15 +218,9 @@ module GD
159
218
  path = "tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.#{ext}"
160
219
  File.binwrite(path, res.body)
161
220
  end
162
- else
163
- if File.exist?("tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png")
164
- path = "tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.png"
165
- else
166
- path = "tmp/tiles/#{@provider}_#{@zoom}_#{x}_#{y}.jpg"
167
- end
168
221
  end
169
222
 
170
- tiles << [x,y,path]
223
+ tiles << [x, y, path]
171
224
  end
172
225
  end
173
226
 
@@ -1,6 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GD
2
4
  module GIS
5
+ # Classifies geographic features based on their properties.
6
+ #
7
+ # This class provides a set of stateless helpers used to
8
+ # infer semantic categories (roads, water, parks, rails)
9
+ # from feature attribute tags, typically originating from
10
+ # OpenStreetMap or similar datasets.
11
+ #
12
+ # All methods are pure functions and return symbols or booleans
13
+ # suitable for styling or rendering decisions.
14
+ #
3
15
  class Classifier
16
+ # Classifies a road feature into a road category.
17
+ #
18
+ # The classification is based on the `highway` tag.
19
+ #
20
+ # @param feature [GD::GIS::Feature]
21
+ # @return [Symbol, nil]
22
+ # one of:
23
+ # - :motorway
24
+ # - :primary
25
+ # - :secondary
26
+ # - :street
27
+ # - :minor
28
+ # - nil if the feature is not a road
4
29
  def self.road(feature)
5
30
  tags = feature.properties || {}
6
31
 
@@ -11,37 +36,52 @@ module GD
11
36
  :primary
12
37
  when "secondary", "secondary_link"
13
38
  :secondary
14
- when "tertiary"
15
- :street
16
- when "residential", "living_street"
39
+ when "tertiary", "residential", "living_street"
17
40
  :street
18
41
  when "service", "track"
19
42
  :minor
20
- else
21
- nil
22
43
  end
23
44
  end
24
45
 
46
+ # Determines whether a feature represents water.
47
+ #
48
+ # @param feature [GD::GIS::Feature]
49
+ # @return [Boolean] true if the feature is water-related
25
50
  def self.water?(feature)
26
51
  p = feature.properties
27
52
 
28
53
  p["waterway"] ||
29
- p["natural"] == "water" ||
30
- p["fclass"] == "river" ||
31
- p["fclass"] == "stream"
54
+ p["natural"] == "water" ||
55
+ p["fclass"] == "river" ||
56
+ p["fclass"] == "stream"
32
57
  end
33
58
 
59
+ # Determines whether a feature represents a railway.
60
+ #
61
+ # @param feature [GD::GIS::Feature]
62
+ # @return [Boolean] true if the feature is a rail feature
34
63
  def self.rail?(feature)
35
64
  tags = feature.properties || {}
36
65
  tags["railway"]
37
66
  end
38
67
 
68
+ # Determines whether a feature represents a park or green area.
69
+ #
70
+ # @param feature [GD::GIS::Feature]
71
+ # @return [Boolean] true if the feature is a park or green space
39
72
  def self.park?(feature)
40
73
  tags = feature.properties || {}
41
74
  %w[park recreation_ground garden].include?(tags["leisure"]) ||
42
75
  %w[park grass forest].include?(tags["landuse"])
43
76
  end
44
77
 
78
+ # Classifies the type of water feature.
79
+ #
80
+ # @param feature [GD::GIS::Feature]
81
+ # @return [Symbol]
82
+ # - :river
83
+ # - :stream
84
+ # - :minor (default / fallback)
45
85
  def self.water_kind(feature)
46
86
  p = feature.properties
47
87
 
@@ -51,7 +91,6 @@ module GD
51
91
  else :minor
52
92
  end
53
93
  end
54
-
55
94
  end
56
95
  end
57
96
  end
@@ -1,9 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GD
2
4
  module GIS
5
+ # Utility helpers for generating colors compatible with GD.
6
+ #
7
+ # This module provides convenience methods for creating
8
+ # random RGB / RGBA colors and vivid colors suitable for
9
+ # map rendering and styling.
10
+ #
11
+ # All methods return instances of {GD::Color}.
12
+ #
3
13
  module ColorHelpers
4
- # --------------------------------------------------
5
- # Random RGB color
6
- # --------------------------------------------------
14
+ # Generates a random RGB color.
15
+ #
16
+ # @param min [Integer] minimum channel value (0–255)
17
+ # @param max [Integer] maximum channel value (0–255)
18
+ # @return [GD::Color]
7
19
  def self.random_rgb(min: 0, max: 255)
8
20
  GD::Color.rgb(
9
21
  rand(min..max),
@@ -12,9 +24,12 @@ module GD
12
24
  )
13
25
  end
14
26
 
15
- # --------------------------------------------------
16
- # Random RGBA color
17
- # --------------------------------------------------
27
+ # Generates a random RGBA color.
28
+ #
29
+ # @param min [Integer] minimum channel value (0–255)
30
+ # @param max [Integer] maximum channel value (0–255)
31
+ # @param alpha [Integer, nil] alpha channel (0–255), random if nil
32
+ # @return [GD::Color]
18
33
  def self.random_rgba(min: 0, max: 255, alpha: nil)
19
34
  GD::Color.rgba(
20
35
  rand(min..max),
@@ -24,9 +39,12 @@ module GD
24
39
  )
25
40
  end
26
41
 
27
- # --------------------------------------------------
28
- # Random vivid color (avoid gray/mud)
29
- # --------------------------------------------------
42
+ # Generates a random vivid RGB color.
43
+ #
44
+ # Vivid colors avoid low saturation and brightness values,
45
+ # making them suitable for distinguishing map features.
46
+ #
47
+ # @return [GD::Color]
30
48
  def self.random_vivid
31
49
  h = rand
32
50
  s = rand(0.6..1.0)
@@ -36,15 +54,21 @@ module GD
36
54
  GD::Color.rgb(r, g, b)
37
55
  end
38
56
 
39
- # --------------------------------------------------
40
- # HSV → RGB
41
- # --------------------------------------------------
57
+ # Converts HSV color values to RGB.
58
+ #
59
+ # Hue, saturation, and value are expected to be in the
60
+ # range 0.0–1.0.
61
+ #
62
+ # @param h [Float] hue
63
+ # @param s [Float] saturation
64
+ # @param v [Float] value
65
+ # @return [Array<Integer>] RGB values in the range 0–255
42
66
  def self.hsv_to_rgb(h, s, v)
43
67
  i = (h * 6).floor
44
- f = h * 6 - i
68
+ f = (h * 6) - i
45
69
  p = v * (1 - s)
46
- q = v * (1 - f * s)
47
- t = v * (1 - (1 - f) * s)
70
+ q = v * (1 - (f * s))
71
+ t = v * (1 - ((1 - f) * s))
48
72
 
49
73
  r, g, b =
50
74
  case i % 6
@@ -58,8 +82,6 @@ module GD
58
82
 
59
83
  [(r * 255).to_i, (g * 255).to_i, (b * 255).to_i]
60
84
  end
61
-
62
85
  end
63
86
  end
64
87
  end
65
-