gesso 0.1.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 +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +94 -0
- data/Rakefile +11 -0
- data/examples/bubbles.rb +10 -0
- data/examples/gui_sketch.rb +18 -0
- data/exe/gesso +39 -0
- data/lib/gesso/auto.rb +39 -0
- data/lib/gesso/version.rb +5 -0
- data/lib/gesso.rb +490 -0
- data/sig/gesso.rbs +4 -0
- metadata +110 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8870e15d24fbbc45d4f17e9f9311b6b16927857d1bbe95b557e8a2225080d86e
|
|
4
|
+
data.tar.gz: 0f1c0ae36c4a67a1556a5963fd567254949522159f49f0e340fcd09392108d39
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f9db6ac499d2c5c5240742ff29b371a18bbbe3f6183af2cc2d8f95aa58f1d42d7c7d949d48806058820216a39ff80d06f70bc1ebdb39138218d23ba92cfe5152
|
|
7
|
+
data.tar.gz: e88716ed0d0d65e048abde230c90cfdffd25c6c16583a8ca8ccb7336a6a8fe30ac08ef9fe8cf2ecc26c68444d9f3e709c91d3c0ad812a2eedcc46af7071d60fd
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ydah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Gesso
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/gesso)
|
|
4
|
+
[](https://rubygems.org/gems/gesso)
|
|
5
|
+
[](https://github.com/rbgfx/gesso/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.ruby-lang.org/)
|
|
7
|
+
[](LICENSE.txt)
|
|
8
|
+
|
|
9
|
+
> Processing-style 2D drawing for Ruby, backed by Tessel and rbgl.
|
|
10
|
+
|
|
11
|
+
Gesso is a small creative coding DSL for sketches that can run in an rbgl
|
|
12
|
+
window or render deterministic PNG frames without a window.
|
|
13
|
+
|
|
14
|
+
**[Features](#features) · [Installation](#installation) · [Quick start](#quick-start) · [CLI](#cli) · [Development](#development)**
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Immediate-mode setup and draw callbacks.
|
|
19
|
+
- Lines, shapes, images, transforms, text, and configurable stroke styles.
|
|
20
|
+
- RGB and HSB color modes with corner, center, and radius shape modes.
|
|
21
|
+
- Seeded random values and deterministic smooth noise.
|
|
22
|
+
- Optional rlsl shader backgrounds and Twiddle control panels.
|
|
23
|
+
- Interactive windows and headless frame rendering.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Add Gesso to your Gemfile:
|
|
28
|
+
|
|
29
|
+
~~~ruby
|
|
30
|
+
gem "gesso"
|
|
31
|
+
~~~
|
|
32
|
+
|
|
33
|
+
Then run:
|
|
34
|
+
|
|
35
|
+
~~~sh
|
|
36
|
+
bundle install
|
|
37
|
+
~~~
|
|
38
|
+
|
|
39
|
+
Or install the released gem:
|
|
40
|
+
|
|
41
|
+
~~~sh
|
|
42
|
+
gem install gesso
|
|
43
|
+
~~~
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
~~~ruby
|
|
48
|
+
require "gesso"
|
|
49
|
+
|
|
50
|
+
sketch = Gesso.run(width: 320, height: 200) do
|
|
51
|
+
setup { background 16, 24, 39 }
|
|
52
|
+
|
|
53
|
+
draw do
|
|
54
|
+
fill 240, 120, 80
|
|
55
|
+
circle width / 2, height / 2, 30
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sketch.canvas.write("shot.png")
|
|
60
|
+
~~~
|
|
61
|
+
|
|
62
|
+
For a top-level sketch file, load the automatic DSL:
|
|
63
|
+
|
|
64
|
+
~~~ruby
|
|
65
|
+
require "gesso/auto"
|
|
66
|
+
|
|
67
|
+
size 320, 200
|
|
68
|
+
draw do
|
|
69
|
+
background "#101827"
|
|
70
|
+
fill "#f07850"
|
|
71
|
+
circle 160, 100, 30
|
|
72
|
+
end
|
|
73
|
+
~~~
|
|
74
|
+
|
|
75
|
+
## CLI
|
|
76
|
+
|
|
77
|
+
~~~sh
|
|
78
|
+
gesso run examples/bubbles.rb
|
|
79
|
+
gesso render examples/bubbles.rb --frames 3 -o frames
|
|
80
|
+
~~~
|
|
81
|
+
|
|
82
|
+
Use <code>--backend file</code> for a headless window run. The
|
|
83
|
+
<code>examples/gui_sketch.rb</code> example shows the optional Twiddle panel.
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
~~~sh
|
|
88
|
+
bundle install
|
|
89
|
+
bundle exec rake verify
|
|
90
|
+
~~~
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
[MIT](LICENSE.txt)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
|
7
|
+
task.ruby_opts = %w[-I../tessel/lib -I../glyphic/lib -I../twiddle/lib -I../rbgl/lib -I../larb/lib -I../rlsl/lib]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
task default: :spec
|
|
11
|
+
task verify: :spec
|
data/examples/bubbles.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gesso/auto"
|
|
4
|
+
|
|
5
|
+
size 320, 200
|
|
6
|
+
speed = 1.0
|
|
7
|
+
|
|
8
|
+
draw do
|
|
9
|
+
background "#101827"
|
|
10
|
+
fill "#f07850"
|
|
11
|
+
circle 160 + Math.sin(frame_count * 0.05 * speed) * 80, 100, 24
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
gui do |ui|
|
|
15
|
+
ui.window("Controls") do
|
|
16
|
+
speed = ui.slider("Speed", speed, 0.0..4.0)
|
|
17
|
+
end
|
|
18
|
+
end
|
data/exe/gesso
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "gesso/auto"
|
|
7
|
+
|
|
8
|
+
command = ARGV.shift
|
|
9
|
+
case command
|
|
10
|
+
when "render"
|
|
11
|
+
path = ARGV.shift
|
|
12
|
+
frames = 1
|
|
13
|
+
output = "out"
|
|
14
|
+
OptionParser.new do |opts|
|
|
15
|
+
opts.on("--frames N", Integer) { |value| frames = value }
|
|
16
|
+
opts.on("-o DIR") { |value| output = value }
|
|
17
|
+
end.parse!(ARGV)
|
|
18
|
+
load path
|
|
19
|
+
Gesso::Auto.sync!
|
|
20
|
+
FileUtils.mkdir_p(output)
|
|
21
|
+
sketch = Gesso::Auto.sketch
|
|
22
|
+
frames.times { |index| sketch.frame.write(File.join(output, format("frame_%05d.png", index))) }
|
|
23
|
+
when "run"
|
|
24
|
+
path = ARGV.shift
|
|
25
|
+
backend = :auto
|
|
26
|
+
frames = nil
|
|
27
|
+
output_dir = "."
|
|
28
|
+
OptionParser.new do |opts|
|
|
29
|
+
opts.on("--backend NAME") { |value| backend = value.to_sym }
|
|
30
|
+
opts.on("--frames N", Integer) { |value| frames = value }
|
|
31
|
+
opts.on("-o DIR") { |value| output_dir = value }
|
|
32
|
+
end.parse!(ARGV)
|
|
33
|
+
load path
|
|
34
|
+
Gesso::Auto.sync!
|
|
35
|
+
Gesso::Runner::Window.run(Gesso::Auto.sketch, backend: backend, frames: frames, output_dir: output_dir)
|
|
36
|
+
else
|
|
37
|
+
warn "usage: gesso render FILE [--frames N] [-o DIR] | run FILE [--backend NAME]"
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
data/lib/gesso/auto.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../gesso"
|
|
4
|
+
|
|
5
|
+
module Gesso
|
|
6
|
+
module Auto
|
|
7
|
+
@sketch = Sketch.new
|
|
8
|
+
class << self
|
|
9
|
+
attr_reader :sketch
|
|
10
|
+
|
|
11
|
+
def sync!
|
|
12
|
+
target = TOPLEVEL_BINDING.eval("self")
|
|
13
|
+
%i[setup draw].each do |name|
|
|
14
|
+
next unless @sketch.instance_variable_get(:"@#{name}_block").nil?
|
|
15
|
+
|
|
16
|
+
method = target.method(name)
|
|
17
|
+
next if method.owner == Gesso::Auto
|
|
18
|
+
|
|
19
|
+
@sketch.public_send(name) { target.__send__(name) }
|
|
20
|
+
rescue NameError
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
@sketch
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def method_missing(name, *args, &block)
|
|
28
|
+
sketch = Gesso::Auto.sketch
|
|
29
|
+
return super unless sketch.respond_to?(name)
|
|
30
|
+
sketch.public_send(name, *args, &block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def respond_to_missing?(name, include_private = false)
|
|
34
|
+
Gesso::Auto.sketch.respond_to?(name) || super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
TOPLEVEL_BINDING.eval("self").extend(Gesso::Auto)
|
data/lib/gesso.rb
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tessel"
|
|
4
|
+
|
|
5
|
+
require_relative "gesso/version"
|
|
6
|
+
|
|
7
|
+
module Gesso
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
class Sketch
|
|
11
|
+
attr_reader :canvas, :width, :height, :frame_count, :mouse_x, :mouse_y, :key
|
|
12
|
+
|
|
13
|
+
def initialize(width: 640, height: 480, seed: Random.new_seed)
|
|
14
|
+
@width = width
|
|
15
|
+
@height = height
|
|
16
|
+
@canvas = Tessel::Image.new(width, height)
|
|
17
|
+
@random = Random.new(seed)
|
|
18
|
+
@noise_seed = seed
|
|
19
|
+
@frame_count = 0
|
|
20
|
+
@mouse_x = @mouse_y = 0
|
|
21
|
+
@pmouse_x = @pmouse_y = 0
|
|
22
|
+
@pressed = false
|
|
23
|
+
@fill = Tessel::Color.pack([255, 255, 255, 255])
|
|
24
|
+
@stroke = Tessel::Color.pack([0, 0, 0, 255])
|
|
25
|
+
@color_mode = :rgb
|
|
26
|
+
@color_ranges = [255.0, 255.0, 255.0, 255.0]
|
|
27
|
+
@rect_mode = :corner
|
|
28
|
+
@ellipse_mode = :corner
|
|
29
|
+
@fill_enabled = true
|
|
30
|
+
@stroke_enabled = true
|
|
31
|
+
@stroke_weight = 1
|
|
32
|
+
@stroke_cap = :round
|
|
33
|
+
@stroke_join = :round
|
|
34
|
+
@text_align = [:left, :top]
|
|
35
|
+
@matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
|
|
36
|
+
@matrices = []
|
|
37
|
+
@draw_block = nil
|
|
38
|
+
@setup_block = nil
|
|
39
|
+
@setup_ran = false
|
|
40
|
+
@handlers = Hash.new { |hash, key| hash[key] = [] }
|
|
41
|
+
@ui_events = []
|
|
42
|
+
@shader_cache = {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def size(width, height)
|
|
46
|
+
width = Integer(width)
|
|
47
|
+
height = Integer(height)
|
|
48
|
+
raise ArgumentError, "size must be positive" unless width.positive? && height.positive?
|
|
49
|
+
@width, @height = width, height
|
|
50
|
+
@canvas = Tessel::Image.new(width, height)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def setup(&block) = (@setup_block = block)
|
|
54
|
+
def draw(&block) = (@draw_block = block)
|
|
55
|
+
def gui(&block) = (@gui_block = block)
|
|
56
|
+
|
|
57
|
+
def prepare!
|
|
58
|
+
return self if @setup_ran
|
|
59
|
+
|
|
60
|
+
@setup_ran = true
|
|
61
|
+
@setup_block&.call
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
def shader_background(name = nil, &block)
|
|
65
|
+
raise ArgumentError, "shader_background requires a block" unless block
|
|
66
|
+
|
|
67
|
+
require "rlsl"
|
|
68
|
+
key = name || block.source_location
|
|
69
|
+
shader = (@shader_cache[key] ||= begin
|
|
70
|
+
builder = RLSL::ShaderBuilder.new(:"gesso_#{@shader_cache.length}")
|
|
71
|
+
builder.uniforms { float :time; vec4 :mouse }
|
|
72
|
+
builder.fragment(&block)
|
|
73
|
+
builder.compile_and_load
|
|
74
|
+
end)
|
|
75
|
+
buffer = "\0".b * (@width * @height * 4)
|
|
76
|
+
shader.render(buffer, @width, @height, time: millis / 1000.0, mouse: [@mouse_x.to_f, (@height - @mouse_y).to_f, 0.0, 0.0])
|
|
77
|
+
rgba = buffer.bytes.each_slice(4).flat_map { |blue, green, red, alpha| [red, green, blue, alpha] }.pack("C*")
|
|
78
|
+
@canvas = Tessel::Image.from_rgba(@width, @height, rgba)
|
|
79
|
+
end
|
|
80
|
+
def key_pressed(&block) = @handlers[:key_pressed] << block
|
|
81
|
+
def key_released(&block) = @handlers[:key_released] << block
|
|
82
|
+
def mouse_pressed(&block) = @handlers[:mouse_pressed] << block
|
|
83
|
+
def mouse_released(&block) = @handlers[:mouse_released] << block
|
|
84
|
+
def mouse_moved(&block) = @handlers[:mouse_moved] << block
|
|
85
|
+
def mouse_dragged(&block) = @handlers[:mouse_dragged] << block
|
|
86
|
+
|
|
87
|
+
def frame
|
|
88
|
+
prepare!
|
|
89
|
+
@draw_block&.call
|
|
90
|
+
if @gui_block
|
|
91
|
+
require "twiddle"
|
|
92
|
+
require "glyphic"
|
|
93
|
+
@ui ||= Twiddle::Context.new(font: Glyphic.default)
|
|
94
|
+
@ui.frame(events: @ui_events) { |context| @gui_block.call(context) }
|
|
95
|
+
@ui.render(@canvas)
|
|
96
|
+
@ui_events.clear
|
|
97
|
+
end
|
|
98
|
+
@frame_count += 1
|
|
99
|
+
@canvas
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def handle(event)
|
|
103
|
+
@ui_events << event if @gui_block
|
|
104
|
+
case event[:type]&.to_sym
|
|
105
|
+
when :mouse_move
|
|
106
|
+
@pmouse_x, @pmouse_y, @mouse_x, @mouse_y = @mouse_x, @mouse_y, event[:x], event[:y]
|
|
107
|
+
@handlers[:mouse_moved].each(&:call)
|
|
108
|
+
@handlers[:mouse_dragged].each(&:call) if @pressed
|
|
109
|
+
when :mouse_press then @pressed = true; @handlers[:mouse_pressed].each(&:call)
|
|
110
|
+
when :mouse_release then @pressed = false; @handlers[:mouse_released].each(&:call)
|
|
111
|
+
when :key_press then @key = event[:key]; @handlers[:key_pressed].each(&:call)
|
|
112
|
+
when :key_release then @key = event[:key]; @handlers[:key_released].each(&:call)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def background(*args)
|
|
117
|
+
@canvas.clear(color(*args))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def color_mode(mode = :rgb, max1 = 255, max2 = max1, max3 = max1, max_alpha = 255)
|
|
121
|
+
mode = mode.to_sym
|
|
122
|
+
raise ArgumentError, "color mode must be :rgb or :hsb" unless %i[rgb hsb].include?(mode)
|
|
123
|
+
ranges = [max1, max2, max3, max_alpha].map(&:to_f)
|
|
124
|
+
raise ArgumentError, "color ranges must be positive" unless ranges.all?(&:positive?)
|
|
125
|
+
@color_mode = mode
|
|
126
|
+
@color_ranges = ranges
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def fill(*args) = (@fill_enabled = true; @fill = color(*args))
|
|
130
|
+
def no_fill = (@fill_enabled = false)
|
|
131
|
+
def stroke(*args) = (@stroke_enabled = true; @stroke = color(*args))
|
|
132
|
+
def no_stroke = (@stroke_enabled = false)
|
|
133
|
+
def stroke_weight(value) = (@stroke_weight = [value.to_f, 1].max)
|
|
134
|
+
def stroke_cap(mode)
|
|
135
|
+
mode = mode.to_sym
|
|
136
|
+
raise ArgumentError, "stroke cap must be :round or :square" unless %i[round square].include?(mode)
|
|
137
|
+
@stroke_cap = mode
|
|
138
|
+
end
|
|
139
|
+
def stroke_join(mode)
|
|
140
|
+
mode = mode.to_sym
|
|
141
|
+
raise ArgumentError, "stroke join must be :round or :square" unless %i[round square].include?(mode)
|
|
142
|
+
@stroke_join = mode
|
|
143
|
+
end
|
|
144
|
+
def mouse_pressed? = @pressed
|
|
145
|
+
def pmouse_x = @pmouse_x
|
|
146
|
+
def pmouse_y = @pmouse_y
|
|
147
|
+
def frame_rate(value = nil)
|
|
148
|
+
return (@frame_rate || 60.0) unless value
|
|
149
|
+
value = value.to_f
|
|
150
|
+
raise ArgumentError, "frame rate must be positive" unless value.positive?
|
|
151
|
+
@frame_rate = value
|
|
152
|
+
end
|
|
153
|
+
def millis = (@frame_count * 1000.0 / frame_rate).round
|
|
154
|
+
|
|
155
|
+
def point(x, y)
|
|
156
|
+
draw_line(x, y, x, y, @stroke) if @stroke_enabled
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def line(x1, y1, x2, y2)
|
|
160
|
+
draw_line(x1, y1, x2, y2, @stroke) if @stroke_enabled
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def rect(x, y, width, height)
|
|
164
|
+
x, y = x - width / 2.0, y - height / 2.0 if @rect_mode == :center
|
|
165
|
+
polygon([[x, y], [x + width, y], [x + width, y + height], [x, y + height]])
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def rect_mode(mode)
|
|
169
|
+
mode = mode.to_sym
|
|
170
|
+
raise ArgumentError, "rect mode must be :corner or :center" unless %i[corner center].include?(mode)
|
|
171
|
+
@rect_mode = mode
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def square(x, y, size) = rect(x, y, size, size)
|
|
175
|
+
|
|
176
|
+
def ellipse(x, y, width, height)
|
|
177
|
+
case @ellipse_mode
|
|
178
|
+
when :center then x, y = x - width / 2.0, y - height / 2.0
|
|
179
|
+
when :radius then width, height = width * 2.0, height * 2.0; x, y = x - width / 2.0, y - height / 2.0
|
|
180
|
+
end
|
|
181
|
+
cx = x + width / 2.0
|
|
182
|
+
cy = y + height / 2.0
|
|
183
|
+
rx = width / 2.0
|
|
184
|
+
ry = height / 2.0
|
|
185
|
+
return if rx <= 0 || ry <= 0
|
|
186
|
+
unless @matrix == [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
|
|
187
|
+
count = [((2 * Math::PI * [rx, ry].max) / 2).ceil, 16].max
|
|
188
|
+
return polygon(count.times.map { |index| angle = index * 2 * Math::PI / count; [cx + Math.cos(angle) * rx, cy + Math.sin(angle) * ry] })
|
|
189
|
+
end
|
|
190
|
+
([(y).floor, 0].max..[(y + height).ceil, @height - 1].min).each do |row|
|
|
191
|
+
dy = (row + 0.5 - cy) / ry
|
|
192
|
+
next if dy.abs > 1
|
|
193
|
+
half = (rx * Math.sqrt(1 - dy * dy)).floor
|
|
194
|
+
@canvas.hspan((cx - half).floor, (cx + half).floor, row, @fill, blend: @fill.getbyte(3) == 255 ? :copy : :alpha) if @fill_enabled
|
|
195
|
+
end
|
|
196
|
+
ellipse_outline(cx, cy, rx, ry) if @stroke_enabled
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def circle(x, y, radius)
|
|
200
|
+
case @ellipse_mode
|
|
201
|
+
when :corner then ellipse(x - radius, y - radius, radius * 2, radius * 2)
|
|
202
|
+
when :center then ellipse(x, y, radius * 2, radius * 2)
|
|
203
|
+
when :radius then ellipse(x, y, radius, radius)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def ellipse_mode(mode)
|
|
208
|
+
mode = mode.to_sym
|
|
209
|
+
raise ArgumentError, "ellipse mode must be :corner, :center, or :radius" unless %i[corner center radius].include?(mode)
|
|
210
|
+
@ellipse_mode = mode
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def triangle(*points) = polygon(points.each_slice(2).to_a)
|
|
214
|
+
def quad(*points) = polygon(points.each_slice(2).to_a)
|
|
215
|
+
|
|
216
|
+
def begin_shape
|
|
217
|
+
@shape = []
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def vertex(x, y)
|
|
221
|
+
@shape << [x, y]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def end_shape(close: true)
|
|
225
|
+
points = @shape
|
|
226
|
+
if close
|
|
227
|
+
polygon(points)
|
|
228
|
+
elsif @stroke_enabled
|
|
229
|
+
points.each_cons(2) { |first, second| draw_line(*first, *second, @stroke) }
|
|
230
|
+
end
|
|
231
|
+
@shape = nil
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def arc(x, y, width, height, start_angle, stop_angle)
|
|
235
|
+
count = [(stop_angle - start_angle).abs * [width, height].max / 4, 8].max.to_i
|
|
236
|
+
points = (0..count).map do |index|
|
|
237
|
+
angle = start_angle + (stop_angle - start_angle) * index / count
|
|
238
|
+
[x + Math.cos(angle) * width / 2, y + Math.sin(angle) * height / 2]
|
|
239
|
+
end
|
|
240
|
+
polygon(points)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def push = @matrices << @matrix.dup
|
|
244
|
+
def pop
|
|
245
|
+
raise RuntimeError, "transform stack is empty" if @matrices.empty?
|
|
246
|
+
@matrix = @matrices.pop
|
|
247
|
+
end
|
|
248
|
+
def reset_matrix = (@matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
|
|
249
|
+
def translate(x, y) = multiply_matrix([1, 0, 0, 1, x, y])
|
|
250
|
+
def scale(x, y = x) = multiply_matrix([x, 0, 0, y, 0, 0])
|
|
251
|
+
def rotate(angle) = multiply_matrix([Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0])
|
|
252
|
+
|
|
253
|
+
def random_seed(seed) = (@random = Random.new(seed))
|
|
254
|
+
def random(maximum = 1, maximum2 = nil) = maximum2 ? @random.rand(maximum.to_f...maximum2.to_f) : @random.rand(maximum.to_f)
|
|
255
|
+
def noise_seed(seed) = (@noise_seed = Integer(seed))
|
|
256
|
+
def noise(x, y = 0, z = 0)
|
|
257
|
+
coordinates = [x, y, z].map(&:to_f)
|
|
258
|
+
base = coordinates.map(&:floor)
|
|
259
|
+
fraction = coordinates.each_with_index.map { |value, index| smooth(value - base[index]) }
|
|
260
|
+
layers = [0, 1].product([0, 1], [0, 1]).map do |offsets|
|
|
261
|
+
noise_value(*base.zip(offsets).map { |value, offset| value + offset })
|
|
262
|
+
end
|
|
263
|
+
x0 = interpolate(layers[0], layers[4], fraction[0])
|
|
264
|
+
x1 = interpolate(layers[1], layers[5], fraction[0])
|
|
265
|
+
x2 = interpolate(layers[2], layers[6], fraction[0])
|
|
266
|
+
x3 = interpolate(layers[3], layers[7], fraction[0])
|
|
267
|
+
y0 = interpolate(x0, x2, fraction[1])
|
|
268
|
+
y1 = interpolate(x1, x3, fraction[1])
|
|
269
|
+
interpolate(y0, y1, fraction[2])
|
|
270
|
+
end
|
|
271
|
+
def map(value, start1, stop1, start2, stop2) = start2 + (value - start1).to_f / (stop1 - start1) * (stop2 - start2)
|
|
272
|
+
def lerp(start, stop, amount) = start + (stop - start) * amount
|
|
273
|
+
def constrain(value, minimum, maximum) = [[value, minimum].max, maximum].min
|
|
274
|
+
def dist(x1, y1, x2, y2) = Math.hypot(x2 - x1, y2 - y1)
|
|
275
|
+
def degrees(radians) = radians * 180 / Math::PI
|
|
276
|
+
def radians(degrees) = degrees * Math::PI / 180
|
|
277
|
+
|
|
278
|
+
def image(source, x, y, width = nil, height = nil)
|
|
279
|
+
source = source.scale_nearest(width, height) if width && height
|
|
280
|
+
@canvas.blit(source, x, y, blend: :alpha)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def load_image(path) = Tessel.read(path)
|
|
284
|
+
def save(path) = @canvas.write(path)
|
|
285
|
+
def get(x, y) = @canvas[x, y]
|
|
286
|
+
def set(x, y, value) = @canvas[x, y] = value
|
|
287
|
+
|
|
288
|
+
def text(value, x, y, color: @fill)
|
|
289
|
+
require "glyphic"
|
|
290
|
+
font = @font || Glyphic.default
|
|
291
|
+
value = value.to_s
|
|
292
|
+
scale = @font_size ? @font_size.to_f / font.line_height : 1.0
|
|
293
|
+
if scale == 1.0 && @text_align == [:left, :top]
|
|
294
|
+
font.draw(@canvas, x, y, value, color: color)
|
|
295
|
+
return
|
|
296
|
+
end
|
|
297
|
+
image = font.render(value, color: color)
|
|
298
|
+
image = image.scale_nearest([(image.width * scale).round, 1].max, [(image.height * scale).round, 1].max) if scale != 1.0
|
|
299
|
+
draw_x = x - (@text_align[0] == :center ? image.width / 2.0 : @text_align[0] == :right ? image.width : 0)
|
|
300
|
+
draw_y = case @text_align[1]
|
|
301
|
+
when :center then y - image.height / 2.0
|
|
302
|
+
when :bottom then y - image.height
|
|
303
|
+
when :baseline then y - (@font || Glyphic.default).ascent * scale
|
|
304
|
+
else y
|
|
305
|
+
end
|
|
306
|
+
@canvas.blit(image, draw_x.round, draw_y.round, blend: :alpha)
|
|
307
|
+
end
|
|
308
|
+
def text_font(path, size: nil)
|
|
309
|
+
require "glyphic"
|
|
310
|
+
@font_size = size.to_f if size
|
|
311
|
+
@font = Glyphic.load(path, size: size || 16)
|
|
312
|
+
end
|
|
313
|
+
def text_align(horizontal = :left, vertical = :top)
|
|
314
|
+
horizontal = horizontal.to_sym
|
|
315
|
+
vertical = vertical.to_sym
|
|
316
|
+
raise ArgumentError, "text horizontal alignment must be :left, :center, or :right" unless %i[left center right].include?(horizontal)
|
|
317
|
+
raise ArgumentError, "text vertical alignment must be :top, :center, :bottom, or :baseline" unless %i[top center bottom baseline].include?(vertical)
|
|
318
|
+
@text_align = [horizontal, vertical]
|
|
319
|
+
end
|
|
320
|
+
def text_size(size)
|
|
321
|
+
size = Float(size)
|
|
322
|
+
raise ArgumentError, "text size must be positive" unless size.positive?
|
|
323
|
+
@font_size = size
|
|
324
|
+
end
|
|
325
|
+
def text_width(value)
|
|
326
|
+
require "glyphic"
|
|
327
|
+
font = @font || Glyphic.default
|
|
328
|
+
width = font.text_width(value.to_s)
|
|
329
|
+
@font_size ? (width * @font_size / font.line_height).round : width
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
private
|
|
333
|
+
|
|
334
|
+
def color(*args)
|
|
335
|
+
args = args.first if args.length == 1 && args.first.is_a?(Array)
|
|
336
|
+
return Tessel::Color.pack(args) if args.is_a?(String)
|
|
337
|
+
args = [args] if args.is_a?(Numeric)
|
|
338
|
+
args = Array(args)
|
|
339
|
+
return Tessel::Color.pack(args.first) if args.length == 1 && args.first.is_a?(String)
|
|
340
|
+
raise ArgumentError, "color needs 1 to 4 channels" unless (1..4).cover?(args.length)
|
|
341
|
+
alpha = args.length == 2 ? args[1] : (args.length == 4 ? args.pop : @color_ranges[3])
|
|
342
|
+
channels = args.length <= 2 ? [args[0]] * 3 : args
|
|
343
|
+
channels = if @color_mode == :hsb
|
|
344
|
+
hsb_to_rgb(channels).map { |value| (value * 255.0).round }
|
|
345
|
+
else
|
|
346
|
+
channels.each_with_index.map { |value, index| scale_color(value, @color_ranges[index]) }
|
|
347
|
+
end
|
|
348
|
+
channels << scale_color(alpha, @color_ranges[3])
|
|
349
|
+
Tessel::Color.pack(channels)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def multiply_matrix(other)
|
|
353
|
+
a, b, c, d, e, f = @matrix
|
|
354
|
+
oa, ob, oc, od, oe, of = other
|
|
355
|
+
@matrix = [a * oa + c * ob, b * oa + d * ob, a * oc + c * od, b * oc + d * od, a * oe + c * of + e, b * oe + d * of + f]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def smooth(value) = value * value * (3 - 2 * value)
|
|
359
|
+
|
|
360
|
+
def interpolate(first, second, amount) = first + (second - first) * amount
|
|
361
|
+
|
|
362
|
+
def noise_value(x, y, z)
|
|
363
|
+
value = Math.sin(x * 12.9898 + y * 78.233 + z * 37.719 + @noise_seed * 0.12345) * 43_758.5453
|
|
364
|
+
value - value.floor
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def scale_color(value, maximum)
|
|
368
|
+
[[Float(value) / maximum * 255.0, 0.0].max, 255.0].min.round
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def hsb_to_rgb(channels)
|
|
372
|
+
hue, saturation, brightness = channels.map.with_index { |value, index| Float(value) / @color_ranges[index] }
|
|
373
|
+
saturation = [[saturation, 0.0].max, 1.0].min
|
|
374
|
+
brightness = [[brightness, 0.0].max, 1.0].min
|
|
375
|
+
return [brightness, brightness, brightness] if saturation.zero?
|
|
376
|
+
|
|
377
|
+
hue = (hue % 1.0) * 6.0
|
|
378
|
+
sector = hue.floor
|
|
379
|
+
fraction = hue - sector
|
|
380
|
+
low = brightness * (1.0 - saturation)
|
|
381
|
+
down = brightness * (1.0 - saturation * fraction)
|
|
382
|
+
up = brightness * (1.0 - saturation * (1.0 - fraction))
|
|
383
|
+
case sector
|
|
384
|
+
when 0 then [brightness, up, low]
|
|
385
|
+
when 1 then [down, brightness, low]
|
|
386
|
+
when 2 then [low, brightness, up]
|
|
387
|
+
when 3 then [low, down, brightness]
|
|
388
|
+
when 4 then [up, low, brightness]
|
|
389
|
+
else [brightness, low, down]
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def transform(point)
|
|
394
|
+
a, b, c, d, e, f = @matrix
|
|
395
|
+
[a * point[0] + c * point[1] + e, b * point[0] + d * point[1] + f]
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def polygon(points)
|
|
399
|
+
return if points.length < 3
|
|
400
|
+
points = points.map { |point| transform(point) }
|
|
401
|
+
min_y = [[points.map(&:last).min.floor, 0].max, @height - 1].min
|
|
402
|
+
max_y = [[points.map(&:last).max.ceil, 0].max, @height - 1].min
|
|
403
|
+
edges = points.each_cons(2).to_a + [[points[-1], points[0]]]
|
|
404
|
+
(min_y..max_y).each do |y|
|
|
405
|
+
intersections = edges.filter_map do |first, second|
|
|
406
|
+
next if first[1] == second[1] || y + 0.5 < [first[1], second[1]].min || y + 0.5 >= [first[1], second[1]].max
|
|
407
|
+
first[0] + (y + 0.5 - first[1]) * (second[0] - first[0]) / (second[1] - first[1])
|
|
408
|
+
end.sort
|
|
409
|
+
intersections.each_slice(2) { |left, right| @canvas.hspan(left.ceil, right.floor - 1, y, @fill, blend: @fill.getbyte(3) == 255 ? :copy : :alpha) if @fill_enabled && right }
|
|
410
|
+
end
|
|
411
|
+
edges.each { |first, second| draw_line(*first, *second, @stroke, transformed: true) if @stroke_enabled }
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def draw_line(x1, y1, x2, y2, color, transformed: false)
|
|
415
|
+
first = transformed ? [x1, y1] : transform([x1, y1])
|
|
416
|
+
last = transformed ? [x2, y2] : transform([x2, y2])
|
|
417
|
+
if @stroke_cap == :square && first != last
|
|
418
|
+
radius = @stroke_weight / 2.0
|
|
419
|
+
length = Math.hypot(last[0] - first[0], last[1] - first[1])
|
|
420
|
+
unit = [(last[0] - first[0]) / length, (last[1] - first[1]) / length]
|
|
421
|
+
first = [first[0] - unit[0] * radius, first[1] - unit[1] * radius]
|
|
422
|
+
last = [last[0] + unit[0] * radius, last[1] + unit[1] * radius]
|
|
423
|
+
end
|
|
424
|
+
x, y = first.map(&:round)
|
|
425
|
+
target_x, target_y = last.map(&:round)
|
|
426
|
+
dx = (target_x - x).abs
|
|
427
|
+
sx = x < target_x ? 1 : -1
|
|
428
|
+
dy = -(target_y - y).abs
|
|
429
|
+
sy = y < target_y ? 1 : -1
|
|
430
|
+
error = dx + dy
|
|
431
|
+
loop do
|
|
432
|
+
if @stroke_weight <= 1
|
|
433
|
+
@canvas[x, y] = color if x.between?(0, @width - 1) && y.between?(0, @height - 1)
|
|
434
|
+
else
|
|
435
|
+
radius = @stroke_weight / 2.0
|
|
436
|
+
((y - radius).ceil..(y + radius).floor).each do |row|
|
|
437
|
+
distance = @stroke_join == :square ? radius : Math.sqrt([radius * radius - (row - y) ** 2, 0].max)
|
|
438
|
+
@canvas.hspan((x - distance).ceil, (x + distance).floor, row, color, blend: color.getbyte(3) == 255 ? :copy : :alpha)
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
break if x == target_x && y == target_y
|
|
442
|
+
twice = 2 * error
|
|
443
|
+
if twice >= dy then error += dy; x += sx end
|
|
444
|
+
if twice <= dx then error += dx; y += sy end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def ellipse_outline(cx, cy, rx, ry)
|
|
449
|
+
points = 32.times.map { |index| angle = index * 2 * Math::PI / 32; [cx + Math.cos(angle) * rx, cy + Math.sin(angle) * ry] }
|
|
450
|
+
points.each_cons(2) { |first, second| draw_line(*first, *second, @stroke) }
|
|
451
|
+
draw_line(*points[-1], *points[0], @stroke)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
module Runner
|
|
456
|
+
class Headless
|
|
457
|
+
def self.run(sketch, frames: 1)
|
|
458
|
+
sketch.prepare!
|
|
459
|
+
Array.new(frames) { sketch.frame.dup }
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
class Window
|
|
464
|
+
def self.run(sketch, backend: :auto, frames: nil, output_dir: ".")
|
|
465
|
+
require "rbgl"
|
|
466
|
+
sketch.prepare!
|
|
467
|
+
options = backend.to_sym == :file ? { format: :ppm, max_frames: frames || 1, output_dir: output_dir } : {}
|
|
468
|
+
window = RBGL::GUI::Window.new(width: sketch.width, height: sketch.height, title: "gesso", backend: backend, **options)
|
|
469
|
+
until window.should_close?
|
|
470
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
471
|
+
Array(window.poll_events_raw).each { |event| sketch.handle(event) }
|
|
472
|
+
window.set_pixels(sketch.frame.bytes)
|
|
473
|
+
delay = 1.0 / sketch.frame_rate - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
|
|
474
|
+
sleep(delay) if delay.positive? && backend.to_sym != :file
|
|
475
|
+
end
|
|
476
|
+
ensure
|
|
477
|
+
window&.close
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
module_function
|
|
483
|
+
|
|
484
|
+
def run(width: 640, height: 480, seed: Random.new_seed, &block)
|
|
485
|
+
sketch = Sketch.new(width: width, height: height, seed: seed)
|
|
486
|
+
sketch.instance_eval(&block)
|
|
487
|
+
Runner::Headless.run(sketch, frames: 1).first
|
|
488
|
+
sketch
|
|
489
|
+
end
|
|
490
|
+
end
|
data/sig/gesso.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gesso
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ydah
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: tessel
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.1.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: glyphic
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.1.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.1.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rbgl
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 1.0.0
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 1.0.0
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: larb
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 1.0.0
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 1.0.0
|
|
68
|
+
description: A small creative coding DSL backed by Tessel RGBA8 images.
|
|
69
|
+
email:
|
|
70
|
+
- t.yudai92@gmail.com
|
|
71
|
+
executables:
|
|
72
|
+
- gesso
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- CHANGELOG.md
|
|
77
|
+
- LICENSE.txt
|
|
78
|
+
- README.md
|
|
79
|
+
- Rakefile
|
|
80
|
+
- examples/bubbles.rb
|
|
81
|
+
- examples/gui_sketch.rb
|
|
82
|
+
- exe/gesso
|
|
83
|
+
- lib/gesso.rb
|
|
84
|
+
- lib/gesso/auto.rb
|
|
85
|
+
- lib/gesso/version.rb
|
|
86
|
+
- sig/gesso.rbs
|
|
87
|
+
homepage: https://github.com/rbgfx/gesso
|
|
88
|
+
licenses:
|
|
89
|
+
- MIT
|
|
90
|
+
metadata:
|
|
91
|
+
homepage_uri: https://github.com/rbgfx/gesso
|
|
92
|
+
source_code_uri: https://github.com/rbgfx/gesso/tree/main
|
|
93
|
+
rdoc_options: []
|
|
94
|
+
require_paths:
|
|
95
|
+
- lib
|
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: 3.1.0
|
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
requirements: []
|
|
107
|
+
rubygems_version: 4.0.16
|
|
108
|
+
specification_version: 4
|
|
109
|
+
summary: Processing style 2D drawing for Ruby
|
|
110
|
+
test_files: []
|