rubysketch 0.9.5 → 0.10.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: ff5b8ecc70091bc532c6ddcc047df2ffb396bd8552c18e4d77dbf6538e7ae5f9
4
- data.tar.gz: cfe92a8493db486fc33212097b0675bd37524ebcc769d4d9cdc52b1c44d65558
3
+ metadata.gz: c4f8d3591f90ee825cf8f8c1d6379c7841d7a03e90c211fdb5be0ba378514e74
4
+ data.tar.gz: c82032ec68d87488fcf2036177ebd6aea6168e91c3d6e6de7508af17a0c9daf9
5
5
  SHA512:
6
- metadata.gz: 52857a9f308b4951971d78d3818d804526652f1c764ec226de92e9e0eb48d334520f5eed4ca04353da4c8c6ad086de09bb704034844f2a49116b82ac22b1c673
7
- data.tar.gz: 92bbaa51e072847f188f54ba79370dcf549e71b6bd78ac53b055b42e5b8087bf193196f999278c76347b79e9b210f4afdd0b50a2f8b7786bfbd54d6e5daabc68
6
+ metadata.gz: 9c3e94f02d383ff622f082058e8146dbf1b913e26b902f94c7a4dc6d46b95d3ab743a5b638cc48d50d13237689650d7e4db3a3312466bad7044ed76cd17af99a
7
+ data.tar.gz: ae869f7c81595a77724f02410b65eb5f638ccfaa77184572b5ad13ca990ea9a33fd256f240af578a1b06c4846fc72faa1e320e1b58df959090b8a98bb34d9958
@@ -26,8 +26,17 @@ jobs:
26
26
  - name: setup dependencies
27
27
  run: "ruby -I.github/workflows -rutils -e 'setup_dependencies'"
28
28
 
29
+ - name: packages
30
+ run: bundle exec rake packages
31
+
32
+ - name: setup zig
33
+ if: github.event.repository.name == 'reflex-terminal'
34
+ uses: mlugg/setup-zig@v2
35
+ with:
36
+ version: 0.16.0
37
+
29
38
  - name: test
30
- run: bundle exec rake packages test
39
+ run: bundle exec rake test
31
40
 
32
41
  - name: create gem
33
42
  id: gem
@@ -27,6 +27,15 @@ jobs:
27
27
  - name: packages
28
28
  run: bundle exec rake verbose packages
29
29
 
30
+ - name: setup zig
31
+ if: github.event.repository.name == 'reflex-terminal'
32
+ uses: mlugg/setup-zig@v2
33
+ with:
34
+ version: 0.16.0
35
+
36
+ - name: vendor
37
+ run: bundle exec rake vendor
38
+
30
39
  - name: lib
31
40
  run: bundle exec rake verbose lib
32
41
 
data/ChangeLog.md CHANGED
@@ -1,6 +1,19 @@
1
1
  # rubysketch ChangeLog
2
2
 
3
3
 
4
+ ## [v0.10.0] - 2026-08-04
5
+
6
+ - [BREAKING] Remove the context: parameter from Sprite.new and SpriteWorld#createSprite
7
+ - Add constraint mechanism backed by Reflex constraints
8
+ - Add mouseOver/mouseOut blocks to Sprite, and Sprite#mouseOver to read the state
9
+ - Add sprite(world) support to draw all sprites in a world
10
+ - Add position/size methods to SpriteWorld
11
+ - Deprecate addSprite/removeSprite array params in favor of to:/from: keywords
12
+ - Reject adding a sprite to two worlds, or removing it from another world
13
+
14
+ - Fix SpriteWorld#oy=
15
+
16
+
4
17
  ## [v0.9.5] - 2026-06-23
5
18
 
6
19
  - Add packager support with bin/rubysketch CLI
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.5
1
+ 0.10.0
data/bin/rubysketch CHANGED
@@ -11,7 +11,7 @@ profile = Reflex::Packager::Profile.new(
11
11
  libraries: %w[Xot Rucy Beeps Rays Reflex Processing RubySketch],
12
12
  extensions: %w[beeps_ext rays_ext reflex_ext],
13
13
  config_files: %w[rubysketch.yml rubysketch.yaml],
14
- template: <<~RUBY)
14
+ templates: {'main.rb': <<~MAIN, 'rubysketch.yml': <<~CONFIG})
15
15
  require 'rubysketch'
16
16
  using RubySketch
17
17
 
@@ -21,9 +21,13 @@ profile = Reflex::Packager::Profile.new(
21
21
 
22
22
  draw do
23
23
  background 100
24
- fill 255, 0, 0
25
- ellipse width / 2, height / 2, 100, 100
24
+ text 'hello, world!', 100, 100
26
25
  end
27
- RUBY
26
+ MAIN
27
+ name: {{name}}
28
+ version: 1.0.0
29
+ #bundle_id: org.xord.rubysketch.example.{{name_id}}
30
+ #icon: icon.png
31
+ CONFIG
28
32
 
29
33
  Reflex::Packager::CLI.new(profile).run ARGV.dup
@@ -0,0 +1,89 @@
1
+ %w[xot rucy beeps rays reflex processing rubysketch]
2
+ .map {|s| File.expand_path "../../#{s}/lib", __dir__}
3
+ .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)}
4
+
5
+ require 'rubysketch'
6
+ using RubySketch
7
+
8
+
9
+ def setup()
10
+ noStroke
11
+ gravity 0, 1000
12
+
13
+ $statics = [
14
+ createSprite(0, height - 10, width, 10),
15
+ createSprite(0, 0, 10, height),
16
+ createSprite(width - 10, 0, 10, height)
17
+ ]
18
+ walls = $statics[1..]
19
+ $balls = []
20
+
21
+ # pendulum: snap the hanging ball to the anchor
22
+ pivot = anchor 100, 100
23
+ weight = ball 170, 100, 30
24
+ pivot.snap weight
25
+
26
+ # motored arm: half a turn per second
27
+ hinge = anchor 250, 100
28
+ arm = ball 250, 140, 20
29
+ hinge.snap arm, motor: PI
30
+
31
+ # spring chain: balls linked with springs
32
+ prev = anchor 400, 60
33
+ 4.times do |i|
34
+ node = ball 400, 100 + i * 40, 20
35
+ node.link prev, spring: 4, damping: 0.5, collide: true
36
+ prev = node
37
+ end
38
+
39
+ # car: each tire rides on the body with a wheel constraint -- a vertical
40
+ # suspension spring and a drive motor, reversed whenever the body bumps
41
+ # into a wall
42
+ body = createSprite 60, height - 80, 60, 14
43
+ body.dynamic = true
44
+ axles = [10, 50].map do |x|
45
+ tire = ball 50 + x, height - 60, 18
46
+ tire.pin(9, 9).wheel body.pin(x, 12),
47
+ axis: [0, 1], spring: 6, damping: 0.7, motor: TWO_PI
48
+ end
49
+ body.contact do |o|
50
+ axles.each {|a| a.motor = -a.motor} if walls.include?(o)
51
+ end
52
+ $car = body
53
+
54
+ # chaser: follows the mouse pointer with a soft spring
55
+ chaser = ball 100, 300, 24
56
+ chaser.gravityScale = 0
57
+ chaser.sensor = true # do not bump the pendulum
58
+ $chase = chaser.chase [100, 300], spring: 2, damping: 0.7
59
+ end
60
+
61
+ def draw()
62
+ background 0
63
+
64
+ $chase.target = [mouseX, mouseY]
65
+
66
+ fill 200
67
+ sprite *$statics
68
+ fill 240, 240, 240
69
+ sprite $car
70
+ fill 150, 240, 150
71
+ sprite *$balls
72
+
73
+ textSize 16
74
+ fill 255, 200, 150
75
+ text "#{frameRate.to_i} FPS - the ball chases the mouse pointer", 20, 30
76
+ end
77
+
78
+ def ball(x, y, size)
79
+ sp = createSprite x, y, shape: Circle.new(0, 0, size)
80
+ sp.dynamic = true
81
+ $balls << sp
82
+ sp
83
+ end
84
+
85
+ def anchor(x, y)
86
+ sp = createSprite x, y, 10, 10 # static by default
87
+ $statics << sp
88
+ sp
89
+ end
@@ -18,6 +18,8 @@ require 'rubysketch/window'
18
18
 
19
19
  require 'rubysketch/sprite'
20
20
  require 'rubysketch/shape'
21
+ require 'rubysketch/pin'
22
+ require 'rubysketch/constraint'
21
23
  require 'rubysketch/sound'
22
24
  require 'rubysketch/mml'
23
25
  require 'rubysketch/easings'
@@ -0,0 +1,422 @@
1
+ module RubySketch
2
+
3
+
4
+ # Constrains the motion of sprites.
5
+ # Constraints are created with Sprite#snap, #link, #wheel and #chase
6
+ # (or the same verbs on Sprite#pin), not with new.
7
+ #
8
+ class Constraint
9
+
10
+ include Xot::Setter
11
+
12
+ # @private
13
+ def initialize(constraint)
14
+ @constraint__ = constraint
15
+ end
16
+
17
+ # Removes the constraint permanently.
18
+ #
19
+ # @return [Constraint] self
20
+ #
21
+ def remove()
22
+ @constraint__.remove
23
+ self
24
+ end
25
+
26
+ # Sets the spring stiffness in hertz.
27
+ #
28
+ # @param [Numeric, nil] hertz stiffness, or nil to make it rigid
29
+ #
30
+ # @return [Numeric, nil] hertz
31
+ #
32
+ def spring=(hertz)
33
+ @constraint__.spring = hertz
34
+ end
35
+
36
+ # Returns the spring stiffness in hertz.
37
+ #
38
+ # @return [Numeric, nil] hertz, or nil if the constraint is rigid
39
+ #
40
+ def spring()
41
+ @constraint__.spring
42
+ end
43
+
44
+ # Sets the spring damping ratio.
45
+ #
46
+ # @param [Numeric] ratio 0 (no damping) to 1 (critical damping)
47
+ #
48
+ # @return [Numeric] damping ratio
49
+ #
50
+ def damping=(ratio)
51
+ @constraint__.damping = ratio
52
+ end
53
+
54
+ # Returns the spring damping ratio.
55
+ #
56
+ # @return [Numeric] damping ratio
57
+ #
58
+ def damping()
59
+ @constraint__.damping
60
+ end
61
+
62
+ # Sets whether the constrained sprites collide with each other.
63
+ #
64
+ # @param [Boolean] bool collide or not
65
+ #
66
+ # @return [Boolean] collide or not
67
+ #
68
+ def collide=(bool)
69
+ @constraint__.collide = bool
70
+ end
71
+
72
+ # Returns whether the constrained sprites collide with each other.
73
+ #
74
+ # @return [Boolean] collide or not
75
+ #
76
+ def collide?()
77
+ @constraint__.collide?
78
+ end
79
+
80
+ # Sets the maximum force (or torque) that drives the constraint.
81
+ #
82
+ # @param [Numeric, nil] force
83
+ # maximum force, or nil for the default strength.
84
+ # 0 makes the drive powerless
85
+ #
86
+ # @return [Numeric, nil] force
87
+ #
88
+ def force=(force)
89
+ @constraint__.force = force
90
+ end
91
+
92
+ # Returns the maximum force that drives the constraint.
93
+ #
94
+ # @return [Numeric, nil] force, or nil for the default strength
95
+ #
96
+ def force()
97
+ @constraint__.force
98
+ end
99
+
100
+ universal_accessor :spring, :damping, :force, collide: {reader: :collide?}
101
+
102
+ # Returns whether the constraint is active in a physics world.
103
+ #
104
+ # @return [Boolean] active or not
105
+ #
106
+ def active?()
107
+ @constraint__.active?
108
+ end
109
+
110
+ # Returns whether the constraint was removed.
111
+ #
112
+ # @return [Boolean] removed or not
113
+ #
114
+ def removed?()
115
+ @constraint__.removed?
116
+ end
117
+
118
+ # Returns the sprites the constraint is fastened to.
119
+ #
120
+ # @return [Array<Sprite, nil>] nil for a world anchor
121
+ #
122
+ def sprites()
123
+ @sprites__ ||= @constraint__.views.map {_1&.sprite}.freeze
124
+ end
125
+
126
+ # @private
127
+ def ==(o)
128
+ o.is_a?(Constraint) && @constraint__ == o.getInternal__
129
+ end
130
+
131
+ alias eql? ==
132
+
133
+ # @private
134
+ def hash()
135
+ @constraint__.hash
136
+ end
137
+
138
+ # @private
139
+ def getInternal__()
140
+ @constraint__
141
+ end
142
+
143
+ # @private
144
+ def self.wrap__(constraint)
145
+ klass =
146
+ case constraint
147
+ when Reflex::SnapConstraint then SnapConstraint
148
+ when Reflex::LinkConstraint then LinkConstraint
149
+ when Reflex::WheelConstraint then WheelConstraint
150
+ when Reflex::ChaseConstraint then ChaseConstraint
151
+ else raise ArgumentError, "unknown constraint type: #{constraint.class}"
152
+ end
153
+ klass.new constraint
154
+ end
155
+
156
+ private
157
+
158
+ def getContext__()
159
+ sprites.first&.getContext__ || Processing.context
160
+ end
161
+
162
+ def toDegrees__(angle)
163
+ getContext__.toDegrees__ angle
164
+ end
165
+
166
+ def fromDegrees__(degrees)
167
+ getContext__.fromDegrees__ degrees
168
+ end
169
+
170
+ end# Constraint
171
+
172
+
173
+ # Snaps two points together like a hinge.
174
+ #
175
+ class SnapConstraint < Constraint
176
+
177
+ # Sets the range of the relative rotation angle.
178
+ #
179
+ # @param [Range, Numeric, nil] angle
180
+ # radians or degrees depending on angleMode().
181
+ # nil rotates freely, 0 fixes the rotation
182
+ #
183
+ # @return [Range, Numeric, nil] angle
184
+ #
185
+ def angle=(angle)
186
+ @constraint__.angle =
187
+ case angle
188
+ when Range then Range.new toDegrees__(angle.begin), toDegrees__(angle.end)
189
+ when Numeric then toDegrees__ angle
190
+ else angle
191
+ end
192
+ end
193
+
194
+ # Returns the range of the relative rotation angle.
195
+ #
196
+ # @return [Range, nil] radians or degrees depending on angleMode()
197
+ #
198
+ def angle()
199
+ angle = @constraint__.angle
200
+ angle ? Range.new(fromDegrees__(angle.begin), fromDegrees__(angle.end)) : nil
201
+ end
202
+
203
+ # Sets the motor speed that drives the relative rotation.
204
+ #
205
+ # @param [Numeric, nil] speed
206
+ # angular velocity per second, radians or degrees depending on
207
+ # angleMode(). nil stops the motor
208
+ #
209
+ # @return [Numeric, nil] speed
210
+ #
211
+ def motor=(speed)
212
+ @constraint__.motor = speed && toDegrees__(speed)
213
+ end
214
+
215
+ # Returns the motor speed.
216
+ #
217
+ # @return [Numeric, nil] angular velocity per second
218
+ #
219
+ def motor()
220
+ speed = @constraint__.motor
221
+ speed && fromDegrees__(speed)
222
+ end
223
+
224
+ universal_accessor :angle, :motor
225
+
226
+ end# SnapConstraint
227
+
228
+
229
+ # Keeps two points apart at a distance, or slides them along an axis
230
+ # like a rail.
231
+ #
232
+ class LinkConstraint < Constraint
233
+
234
+ # Sets the axis to slide along.
235
+ # With an axis, the separation is measured along it instead of
236
+ # radially, and the relative rotation is locked like a rail.
237
+ #
238
+ # @param [Vector, Array<Numeric>, nil] axis
239
+ # direction in the target local space, or nil to link radially
240
+ #
241
+ # @return [Vector, Array<Numeric>, nil] axis
242
+ #
243
+ def axis=(axis)
244
+ @constraint__.axis = RubySketch.unwrap__ axis
245
+ end
246
+
247
+ # Returns the axis to slide along.
248
+ #
249
+ # @return [Vector, nil] axis, or nil for a radial link
250
+ #
251
+ def axis()
252
+ @constraint__.axis&.toVector
253
+ end
254
+
255
+ # Sets the distance to keep.
256
+ #
257
+ # @param [Numeric] distance distance in pixels
258
+ #
259
+ # @return [Numeric] distance
260
+ #
261
+ def distance=(distance)
262
+ @constraint__.distance = distance
263
+ end
264
+
265
+ # Returns the distance to keep.
266
+ #
267
+ # @return [Numeric] distance in pixels
268
+ #
269
+ def distance()
270
+ @constraint__.distance
271
+ end
272
+
273
+ # Returns the current distance between the two points.
274
+ #
275
+ # @return [Numeric] distance in pixels, or 0 if not active in a world
276
+ #
277
+ def currentDistance()
278
+ @constraint__.current_distance
279
+ end
280
+
281
+ # Sets the range of the separation.
282
+ #
283
+ # @param [Range, Numeric, nil] range
284
+ # distance range like a rope, or the translation range along the axis
285
+ #
286
+ # @return [Range, Numeric, nil] range
287
+ #
288
+ def range=(range)
289
+ @constraint__.range = range
290
+ end
291
+
292
+ # Returns the range of the separation.
293
+ #
294
+ # @return [Range, nil] separation range
295
+ #
296
+ def range()
297
+ @constraint__.range
298
+ end
299
+
300
+ # Sets the motor speed that drives the separation.
301
+ #
302
+ # @param [Numeric, nil] speed speed in pixels per second, or nil to stop
303
+ #
304
+ # @return [Numeric, nil] speed
305
+ #
306
+ def motor=(speed)
307
+ @constraint__.motor = speed
308
+ end
309
+
310
+ # Returns the motor speed.
311
+ #
312
+ # @return [Numeric, nil] speed in pixels per second
313
+ #
314
+ def motor()
315
+ @constraint__.motor
316
+ end
317
+
318
+ universal_accessor :axis, :distance, :range, :motor
319
+
320
+ alias dist= distance=
321
+ alias dist distance
322
+
323
+ end# LinkConstraint
324
+
325
+
326
+ # Rides on the target like a wheel: slides along an axis for the
327
+ # suspension while spinning freely.
328
+ #
329
+ class WheelConstraint < Constraint
330
+
331
+ # Sets the axis to slide along for the suspension.
332
+ #
333
+ # @param [Vector, Array<Numeric>] axis direction in the target local space
334
+ #
335
+ # @return [Vector, Array<Numeric>] axis
336
+ #
337
+ def axis=(axis)
338
+ @constraint__.axis = RubySketch.unwrap__ axis
339
+ end
340
+
341
+ # Returns the axis to slide along.
342
+ #
343
+ # @return [Vector] axis
344
+ #
345
+ def axis()
346
+ @constraint__.axis&.toVector
347
+ end
348
+
349
+ # Sets the range of the translation along the axis.
350
+ #
351
+ # @param [Range, Numeric, nil] range translation range in pixels
352
+ #
353
+ # @return [Range, Numeric, nil] range
354
+ #
355
+ def range=(range)
356
+ @constraint__.range = range
357
+ end
358
+
359
+ # Returns the range of the translation along the axis.
360
+ #
361
+ # @return [Range, nil] translation range
362
+ #
363
+ def range()
364
+ @constraint__.range
365
+ end
366
+
367
+ # Sets the motor speed that spins the wheel.
368
+ #
369
+ # @param [Numeric, nil] speed
370
+ # angular velocity per second, radians or degrees depending on
371
+ # angleMode(). nil stops the motor
372
+ #
373
+ # @return [Numeric, nil] speed
374
+ #
375
+ def motor=(speed)
376
+ @constraint__.motor = speed && toDegrees__(speed)
377
+ end
378
+
379
+ # Returns the motor speed.
380
+ #
381
+ # @return [Numeric, nil] angular velocity per second
382
+ #
383
+ def motor()
384
+ speed = @constraint__.motor
385
+ speed && fromDegrees__(speed)
386
+ end
387
+
388
+ universal_accessor :axis, :range, :motor
389
+
390
+ end# WheelConstraint
391
+
392
+
393
+ # Moves a point toward the target every frame.
394
+ #
395
+ class ChaseConstraint < Constraint
396
+
397
+ # Sets the target to chase.
398
+ #
399
+ # @param [Sprite, Pin, Vector, Array<Numeric>, nil] target
400
+ # the center of a sprite, a pin, or a point in the world.
401
+ # nil chases nothing
402
+ #
403
+ # @return [Sprite, Pin, Vector, Array<Numeric>, nil] target
404
+ #
405
+ def target=(target)
406
+ @constraint__.target = RubySketch.unwrap__ target
407
+ end
408
+
409
+ # Returns the target to chase.
410
+ #
411
+ # @return [Pin] the target pin
412
+ #
413
+ def target()
414
+ Pin.new nil, pin__: @constraint__.target
415
+ end
416
+
417
+ universal_accessor :target
418
+
419
+ end# ChaseConstraint
420
+
421
+
422
+ end# RubySketch
@@ -265,29 +265,29 @@ module RubySketch
265
265
  # @return [Sprite] the new sprite object
266
266
  #
267
267
  def createSprite(*args, **kwargs)
268
- @world__.createSprite(*args, context: self, **kwargs)
268
+ @world__.createSprite(*args, **kwargs)
269
269
  end
270
270
 
271
271
  # Adds sprite to the physics engine.
272
272
  #
273
- # @param [Array] array user array to store the sprite
274
273
  # @param [Sprite] sprite sprite object
274
+ # @param [Array] to user array to store the sprite
275
275
  #
276
276
  # @return [Sprite] the added sprite
277
277
  #
278
- def addSprite(array = nil, sprite)
279
- @world__.addSprite array, sprite
278
+ def addSprite(...)
279
+ @world__.addSprite(...)
280
280
  end
281
281
 
282
282
  # Removes sprite from the physics engine.
283
283
  #
284
- # @param [Array] array user array to remove the sprite
285
284
  # @param [Sprite] sprite sprite object
285
+ # @param [Array] from user array to remove the sprite
286
286
  #
287
287
  # @return [Sprite] the removed sprite
288
288
  #
289
- def removeSprite(array = nil, sprite)
290
- @world__.removeSprite array, sprite
289
+ def removeSprite(...)
290
+ @world__.removeSprite(...)
291
291
  end
292
292
 
293
293
  alias drawSprite sprite
@@ -307,7 +307,11 @@ module RubySketch
307
307
  # @return [SpriteWorld] first added world
308
308
  #
309
309
  def addWorld(*worlds)
310
- worlds.each {@window__.add_overlay _1.getInternal__}
310
+ raise ArgumentError if worlds.any? {_1.getContext__}
311
+ worlds.each do |world|
312
+ @window__.add_overlay world.getInternal__
313
+ world.attached__
314
+ end
311
315
  worlds.first
312
316
  end
313
317
 
@@ -318,7 +322,11 @@ module RubySketch
318
322
  # @return [SpriteWorld] first removed world
319
323
  #
320
324
  def removeWorld(*worlds)
321
- worlds.each {@window__.remove_overlay _1.getInternal__}
325
+ raise ArgumentError if worlds.any? {_1.getContext__ != self}
326
+ worlds.each do |world|
327
+ @window__.remove_overlay world.getInternal__
328
+ world.detached__
329
+ end
322
330
  worlds.first
323
331
  end
324
332
 
@@ -7,7 +7,7 @@ module RubySketch
7
7
  module_function
8
8
 
9
9
  def name(downcase = false)
10
- super().split('::')[-2].then {|s|
10
+ super().split('::')[..-2].join.then {|s|
11
11
  downcase ? s.downcase : s
12
12
  }
13
13
  end
@@ -4,3 +4,13 @@ class Rays::Point
4
4
  Processing::Vector.new x, y, z
5
5
  end
6
6
  end
7
+
8
+
9
+ module RubySketch
10
+
11
+ # @private
12
+ def self.unwrap__(arg)
13
+ arg.respond_to?(:getInternal__) ? arg.getInternal__ : arg
14
+ end
15
+
16
+ end# RubySketch