harmonica 0.0.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d12e0ae1f838550ab02b756bbe2881b24804079fadab5f9da68712effd6d31b4
4
- data.tar.gz: e5713b0f482006a2b40cf5f39dce6cfa4bf738c536ed4c00d3b14ee28dcffe9f
3
+ metadata.gz: a44f86b9f43f12624407af947f1e81c1fa191bccc81263cb2b4228af8582f377
4
+ data.tar.gz: 414eabc26823d5e03fe022406753472f8dc1e9e0a4b99ebce150b6b8a624cbab
5
5
  SHA512:
6
- metadata.gz: 2c10d14d813f1d1d66a47fb03253a78f82a57bb123502b7f4c655ea5f80446f0509ee5680e6bad2cc110d84e389c64b2eb6560a71e466c7c01d0a128b1ceb58e
7
- data.tar.gz: 5dac6408c89fd254986ac093fcf74fabe993461f49eb40022c9e970d249e0ff5218f0057bb4982319a9c79f4803ec3eb72a8c97ff9f8a0987e55fd7a99b87c7b
6
+ metadata.gz: 1225b332b512135e1044afd04b1d4c74c281f93992b26999abd46147d5d55da7830df1fadd5527a5db2cc996c828345a42e0d9dac27e380eeb6d674a298e6ec3
7
+ data.tar.gz: f4057ff79e4105e693e8359423dd7d1249ca3cc803ba2839519f7478f5b303691540d603617327c8caa765eaa4842680873c9b7ce9e9dce6527546c08fc7ff71
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Marco Roth
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 CHANGED
@@ -1,39 +1,265 @@
1
- # Harmonica
1
+ <div align="center">
2
+ <h1>Harmonica for Ruby</h1>
3
+ <h4>A simple, physics-based animation library for Ruby.</h4>
2
4
 
3
- TODO: Delete this and the text below, and describe your gem
5
+ <p>
6
+ <a href="https://rubygems.org/gems/harmonica"><img alt="Gem Version" src="https://img.shields.io/gem/v/harmonica"></a>
7
+ <a href="https://github.com/marcoroth/harmonica-ruby/blob/main/LICENSE.txt"><img alt="License" src="https://img.shields.io/github/license/marcoroth/harmonica-ruby"></a>
8
+ </p>
4
9
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/harmonica`. To experiment with that code, run `bin/console` for an interactive prompt.
10
+ <p>Ruby implementation of <a href="https://github.com/charmbracelet/harmonica">charmbracelet/harmonica</a>.<br/>A simple, efficient spring animation library for smooth, natural motion.</p>
11
+ </div>
6
12
 
7
13
  ## Installation
8
14
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
15
+ **Add to your Gemfile:**
10
16
 
11
- Install the gem and add to the application's Gemfile by executing:
12
-
13
- ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
17
+ ```ruby
18
+ gem "harmonica"
15
19
  ```
16
20
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
21
+ **Or install directly:**
18
22
 
19
23
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
24
+ gem install harmonica
21
25
  ```
22
26
 
27
+ ## Components
28
+
29
+ | Component | Description |
30
+ |-----------|-------------|
31
+ | [Spring](#spring) | Damped harmonic oscillator for smooth UI animations |
32
+ | [Projectile](#projectile) | Physics projectile motion for particles |
33
+ | [Point](#point) | 3D point coordinates |
34
+ | [Vector](#vector) | 3D vector with magnitude and direction |
35
+
23
36
  ## Usage
24
37
 
25
- TODO: Write usage instructions here
38
+ ### Spring
39
+
40
+ Spring provides smooth, realistic motion using a damped harmonic oscillator. Perfect for UI animations like scroll positions, element transitions, and interactive feedback.
41
+
42
+ **Basic usage:**
43
+
44
+ ```ruby
45
+ require "harmonica"
46
+
47
+ spring = Harmonica::Spring.new(
48
+ delta_time: Harmonica.fps(60), # 60 FPS
49
+ angular_frequency: 6.0, # Speed of motion
50
+ damping_ratio: 0.5 # Smoothness
51
+ )
52
+
53
+ position = 0.0
54
+ velocity = 0.0
55
+ target = 100.0
56
+
57
+ loop do
58
+ position, velocity = spring.update(position, velocity, target)
59
+
60
+ break if (position - target).abs < 0.01
61
+ end
62
+ ```
63
+
64
+ **Damping ratios:**
65
+
66
+ | Ratio | Behavior |
67
+ |-------|----------|
68
+ | `< 1.0` | Under-damped: oscillates before settling (bouncy) |
69
+ | `= 1.0` | Critically-damped: fastest without oscillation |
70
+ | `> 1.0` | Over-damped: slow approach, no oscillation |
71
+
72
+ **Example with Bubbletea:**
73
+
74
+ ```ruby
75
+ class ScrollModel
76
+ def initialize
77
+ @scroll_position = 0.0
78
+ @scroll_velocity = 0.0
79
+ @target_scroll = 0.0
80
+
81
+ @spring = Harmonica::Spring.new(
82
+ delta_time: Harmonica.fps(60),
83
+ angular_frequency: 5.0,
84
+ damping_ratio: 0.8
85
+ )
86
+ end
87
+
88
+ def update(message)
89
+ case message
90
+ when Bubbletea::KeyMessage
91
+ @target_scroll += 10 if message.to_s == "down"
92
+ @target_scroll -= 10 if message.to_s == "up"
93
+ end
94
+
95
+ @scroll_position, @scroll_velocity = @spring.update(
96
+ @scroll_position,
97
+ @scroll_velocity,
98
+ @target_scroll
99
+ )
100
+
101
+ [self, nil]
102
+ end
103
+ end
104
+ ```
105
+
106
+ ### Projectile
107
+
108
+ Projectile simulates physics motion with position, velocity, and acceleration. Great for particles, falling objects, and game physics.
109
+
110
+ #### Basic usage
111
+
112
+ ```ruby
113
+ require "harmonica"
114
+
115
+ projectile = Harmonica::Projectile.new(
116
+ delta_time: Harmonica.fps(60),
117
+ position: Harmonica::Point.new(0, 100, 0),
118
+ velocity: Harmonica::Vector.new(10, 20, 0),
119
+ acceleration: Harmonica::GRAVITY
120
+ )
121
+
122
+ loop do
123
+ position = projectile.update
124
+
125
+ puts "Position: #{position.x}, #{position.y}"
126
+ break if position.y <= 0
127
+ end
128
+ ```
129
+
130
+ #### Gravity constants
131
+
132
+ ```ruby
133
+ Harmonica::GRAVITY # Vector(0, -9.81, 0) - origin at bottom-left
134
+ Harmonica::TERMINAL_GRAVITY # Vector(0, 9.81, 0) - origin at top-left
135
+ ```
136
+
137
+ #### Custom acceleration
138
+
139
+ **No gravity (space)**
140
+
141
+ ```ruby
142
+ projectile = Harmonica::Projectile.new(
143
+ delta_time: Harmonica.fps(60),
144
+ position: Harmonica::Point.new(0, 0, 0),
145
+ velocity: Harmonica::Vector.new(5, 5, 0),
146
+ acceleration: Harmonica::Vector.new(0, 0, 0)
147
+ )
148
+ ```
149
+
150
+ **Strong gravity**
151
+
152
+ ```ruby
153
+ projectile = Harmonica::Projectile.new(
154
+ delta_time: Harmonica.fps(60),
155
+ position: Harmonica::Point.new(0, 100, 0),
156
+ velocity: Harmonica::Vector.new(0, 0, 0),
157
+ acceleration: Harmonica::Vector.new(0, -20, 0)
158
+ )
159
+ ```
160
+
161
+ ### Point
162
+
163
+ Point represents a position in 3D space.
164
+
165
+ ```ruby
166
+ point = Harmonica::Point.new(10.0, 20.0, 30.0)
167
+
168
+ point.x # => 10.0
169
+ point.y # => 20.0
170
+ point.z # => 30.0
171
+
172
+ point.to_a # => [10.0, 20.0, 30.0]
173
+ ```
174
+
175
+ ### Vector
176
+
177
+ Vector represents direction and magnitude in 3D space.
178
+
179
+ ```ruby
180
+ vector = Harmonica::Vector.new(3.0, 4.0, 0.0)
181
+
182
+ vector.magnitude # => 5.0
183
+ vector.normalize # => Vector(0.6, 0.8, 0.0)
184
+
185
+ v1 = Harmonica::Vector.new(1, 2, 3)
186
+ v2 = Harmonica::Vector.new(4, 5, 6)
187
+
188
+ v1 + v2 # => Vector(5, 7, 9)
189
+ v1 - v2 # => Vector(-3, -3, -3)
190
+ v1 * 2 # => Vector(2, 4, 6)
191
+ ```
192
+
193
+ ## Frame Rate Helper
194
+
195
+ Use `Harmonica.fps` to calculate the time delta for a given frame rate:
196
+
197
+ ```ruby
198
+ Harmonica.fps(60) # => 0.01666... (1/60 second)
199
+ Harmonica.fps(30) # => 0.03333... (1/30 second)
200
+ Harmonica.fps(120) # => 0.00833... (1/120 second)
201
+ ```
202
+
203
+ ## Complete Example
204
+
205
+ Animate a value from 0 to 100 with spring physics:
206
+
207
+ ```ruby
208
+ require "harmonica"
209
+
210
+ spring = Harmonica::Spring.new(
211
+ delta_time: Harmonica.fps(60),
212
+ angular_frequency: 6.0,
213
+ damping_ratio: 0.3 # bouncy
214
+ )
215
+
216
+ position = 0.0
217
+ velocity = 0.0
218
+ target = 100.0
219
+
220
+ 60.times do |frame|
221
+ position, velocity = spring.update(position, velocity, target)
222
+
223
+ bar_length = (position / 2).to_i
224
+ bar = "#" * bar_length
225
+
226
+ puts "\r#{bar.ljust(50)} #{position.round(1)}"
227
+ sleep(1.0 / 60)
228
+ end
229
+ ```
26
230
 
27
231
  ## Development
28
232
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
233
+ **Requirements:**
234
+ - Ruby 3.0+
235
+
236
+ **Install dependencies:**
237
+
238
+ ```bash
239
+ bundle install
240
+ ```
30
241
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
242
+ **Run tests:**
243
+
244
+ ```bash
245
+ bundle exec rake test
246
+ ```
247
+
248
+ **Run demos:**
249
+
250
+ ```bash
251
+ ./demo/spring
252
+ ./demo/damping
253
+ ```
32
254
 
33
255
  ## Contributing
34
256
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/harmonica. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/marcoroth/harmonica/blob/main/CODE_OF_CONDUCT.md).
257
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/harmonica-ruby.
258
+
259
+ ## License
260
+
261
+ The gem is available as open source under the terms of the MIT License.
36
262
 
37
- ## Code of Conduct
263
+ ## Acknowledgments
38
264
 
39
- Everyone interacting in the Harmonica project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/marcoroth/harmonica/blob/main/CODE_OF_CONDUCT.md).
265
+ This gem is a Ruby implementation of [charmbracelet/harmonica](https://github.com/charmbracelet/harmonica), part of the excellent [Charm](https://charm.sh) ecosystem. The spring algorithm is based on Ryan Juckett's [damped springs](https://www.ryanjuckett.com/damped-springs/).
data/harmonica.gemspec CHANGED
@@ -8,31 +8,25 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Marco Roth"]
9
9
  spec.email = ["marco.roth@intergga.ch"]
10
10
 
11
- spec.summary = "Ruby bindings for the Harmonica physics-based animation library."
12
- spec.description = spec.summary
11
+ spec.summary = "A simple, physics-based animation library for Ruby."
12
+ spec.description = "Ruby implementation of Charm's Harmonica. A simple, efficient spring animation library for smooth, natural motion."
13
13
  spec.homepage = "https://github.com/marcoroth/harmonica-ruby"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.1.0"
15
+ spec.required_ruby_version = ">= 3.2.0"
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = spec.homepage
19
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/releases"
20
20
  spec.metadata["rubygems_mfa_required"] = "true"
21
21
 
22
22
  spec.files = Dir[
23
23
  "harmonica.gemspec",
24
24
  "LICENSE.txt",
25
25
  "README.md",
26
- "CHANGELOG.md",
27
- "lib/**/*.rb",
28
- "go/**/*.{go,mod,sum}",
29
- "go/build/**/*"
26
+ "lib/**/*.rb"
30
27
  ]
31
28
 
32
29
  spec.bindir = "exe"
33
30
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
34
31
  spec.require_paths = ["lib"]
35
- spec.extensions = []
36
-
37
- spec.add_dependency "lipgloss", "~> 0.1"
38
32
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Harmonica
5
+ # Point represents a point in 3D space.
6
+ #
7
+ # @example
8
+ # point = Harmonica::Point.new(10.0, 20.0, 0.0)
9
+ # point.x # => 10.0
10
+ # point.y # => 20.0
11
+ # point.z # => 0.0
12
+ class Point
13
+ # @rbs @x: Float
14
+ # @rbs @y: Float
15
+ # @rbs @z: Float
16
+
17
+ attr_accessor :x #: Float
18
+ attr_accessor :y #: Float
19
+ attr_accessor :z #: Float
20
+
21
+ # @rbs x: Integer | Float -- x coordinate
22
+ # @rbs y: Integer | Float -- y coordinate
23
+ # @rbs z: Integer | Float -- z coordinate
24
+ # @rbs return: void
25
+ def initialize(x = 0.0, y = 0.0, z = 0.0)
26
+ @x = x.to_f
27
+ @y = y.to_f
28
+ @z = z.to_f
29
+ end
30
+
31
+ #: () -> Array[Float]
32
+ def to_a
33
+ [@x, @y, @z]
34
+ end
35
+
36
+ #: (untyped other) -> bool
37
+ def ==(other)
38
+ return false unless other.is_a?(Point)
39
+
40
+ @x == other.x && @y == other.y && @z == other.z
41
+ end
42
+
43
+ #: () -> String
44
+ def to_s
45
+ "Point(#{@x}, #{@y}, #{@z})"
46
+ end
47
+
48
+ #: () -> String
49
+ def inspect
50
+ to_s
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Harmonica
5
+ # Projectile simulates physics projectile motion.
6
+ #
7
+ # @example
8
+ # projectile = Harmonica::Projectile.new(
9
+ # delta_time: Harmonica.fps(60),
10
+ # position: Harmonica::Point.new(0, 100, 0),
11
+ # velocity: Harmonica::Vector.new(2, 0, 0),
12
+ # acceleration: Harmonica::GRAVITY
13
+ # )
14
+ #
15
+ # loop do
16
+ # position = projectile.update
17
+ # break if position.y <= 0
18
+ # end
19
+ class Projectile
20
+ # @rbs @delta_time: Float
21
+ # @rbs @position: Point
22
+ # @rbs @velocity: Vector
23
+ # @rbs @acceleration: Vector
24
+
25
+ attr_reader :position #: Point
26
+ attr_reader :velocity #: Vector
27
+ attr_reader :acceleration #: Vector
28
+ attr_reader :delta_time #: Float
29
+
30
+ # Create a new Projectile.
31
+ #
32
+ # @rbs delta_time: Float -- time step per frame (use Harmonica.fps)
33
+ # @rbs position: Point -- initial position
34
+ # @rbs velocity: Vector -- initial velocity
35
+ # @rbs acceleration: Vector -- constant acceleration (e.g., gravity)
36
+ # @rbs return: void
37
+ def initialize(delta_time:, position:, velocity:, acceleration:)
38
+ @delta_time = delta_time.to_f
39
+ @position = position
40
+ @velocity = velocity
41
+ @acceleration = acceleration
42
+ end
43
+
44
+ # Update the projectile position and velocity for one time step.
45
+ #
46
+ #: () -> Point
47
+ def update
48
+ @position = Point.new(
49
+ @position.x + (@velocity.x * @delta_time),
50
+ @position.y + (@velocity.y * @delta_time),
51
+ @position.z + (@velocity.z * @delta_time)
52
+ )
53
+
54
+ @velocity = Vector.new(
55
+ @velocity.x + (@acceleration.x * @delta_time),
56
+ @velocity.y + (@acceleration.y * @delta_time),
57
+ @velocity.z + (@acceleration.z * @delta_time)
58
+ )
59
+
60
+ @position
61
+ end
62
+
63
+ # Reset the projectile to a new state.
64
+ #
65
+ # @rbs position: Point -- new position
66
+ # @rbs velocity: Vector -- new velocity
67
+ # @rbs return: void
68
+ def reset(position:, velocity:)
69
+ @position = position
70
+ @velocity = velocity
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Harmonica
5
+ # Spring is a damped harmonic oscillator for smooth, realistic spring-like motion.
6
+ #
7
+ # This is ported from Ryan Juckett's simple damped harmonic motion, originally
8
+ # written in C++. For background on the algorithm see:
9
+ # https://www.ryanjuckett.com/damped-springs/
10
+ #
11
+ # @example
12
+ # # Initialize once
13
+ # spring = Harmonica::Spring.new(
14
+ # delta_time: Harmonica.fps(60),
15
+ # angular_frequency: 6.0,
16
+ # damping_ratio: 0.2
17
+ # )
18
+ #
19
+ # # Update on every frame
20
+ # position = 0.0
21
+ # velocity = 0.0
22
+ # target = 100.0
23
+ #
24
+ # loop do
25
+ # position, velocity = spring.update(position, velocity, target)
26
+ # end
27
+ class Spring
28
+ EPSILON = Float::EPSILON #: Float
29
+
30
+ # @rbs @delta_time: Float
31
+ # @rbs @damping_ratio: Float
32
+ # @rbs @angular_frequency: Float
33
+ # @rbs @position_position_coefficient: Float
34
+ # @rbs @position_velocity_coefficient: Float
35
+ # @rbs @velocity_position_coefficient: Float
36
+ # @rbs @velocity_velocity_coefficient: Float
37
+
38
+ attr_reader :position_position_coefficient #: Float
39
+ attr_reader :position_velocity_coefficient #: Float
40
+ attr_reader :velocity_position_coefficient #: Float
41
+ attr_reader :velocity_velocity_coefficient #: Float
42
+
43
+ # Create a new Spring with precomputed coefficients.
44
+ #
45
+ # @rbs delta_time: Float -- time step (use Harmonica.fps for frame rate)
46
+ # @rbs angular_frequency: Float -- angular frequency of motion (affects speed)
47
+ # @rbs damping_ratio: Float -- damping ratio (> 1: over-damped, = 1: critical, < 1: under-damped)
48
+ # @rbs return: void
49
+ def initialize(delta_time:, angular_frequency:, damping_ratio:)
50
+ @delta_time = delta_time
51
+ @angular_frequency = [0.0, angular_frequency].max
52
+ @damping_ratio = [0.0, damping_ratio].max
53
+
54
+ compute_coefficients
55
+ end
56
+
57
+ # Update position and velocity towards equilibrium position.
58
+ #
59
+ # @rbs position: Float -- current position
60
+ # @rbs velocity: Float -- current velocity
61
+ # @rbs equilibrium_position: Float -- target/equilibrium position
62
+ # @rbs return: [Float, Float]
63
+ def update(position, velocity, equilibrium_position)
64
+ old_position = position - equilibrium_position
65
+ old_velocity = velocity
66
+
67
+ new_position = (old_position * @position_position_coefficient) +
68
+ (old_velocity * @position_velocity_coefficient) +
69
+ equilibrium_position
70
+
71
+ new_velocity = (old_position * @velocity_position_coefficient) +
72
+ (old_velocity * @velocity_velocity_coefficient)
73
+
74
+ [new_position, new_velocity]
75
+ end
76
+
77
+ private
78
+
79
+ #: () -> void
80
+ def compute_coefficients
81
+ if @angular_frequency < EPSILON
82
+ @position_position_coefficient = 1.0
83
+ @position_velocity_coefficient = 0.0
84
+ @velocity_position_coefficient = 0.0
85
+ @velocity_velocity_coefficient = 1.0
86
+ return
87
+ end
88
+
89
+ if @damping_ratio > 1.0 + EPSILON
90
+ compute_over_damped
91
+ elsif @damping_ratio < 1.0 - EPSILON
92
+ compute_under_damped
93
+ else
94
+ compute_critically_damped
95
+ end
96
+ end
97
+
98
+ #: () -> void
99
+ def compute_over_damped
100
+ za = -@angular_frequency * @damping_ratio
101
+ zb = @angular_frequency * Math.sqrt((@damping_ratio * @damping_ratio) - 1.0)
102
+ z1 = za - zb
103
+ z2 = za + zb
104
+
105
+ e1 = Math.exp(z1 * @delta_time)
106
+ e2 = Math.exp(z2 * @delta_time)
107
+
108
+ inverse_two_zb = 1.0 / (2.0 * zb)
109
+
110
+ e1_over_two_zb = e1 * inverse_two_zb
111
+ e2_over_two_zb = e2 * inverse_two_zb
112
+
113
+ z1e1_over_two_zb = z1 * e1_over_two_zb
114
+ z2e2_over_two_zb = z2 * e2_over_two_zb
115
+
116
+ @position_position_coefficient = (e1_over_two_zb * z2) - z2e2_over_two_zb + e2
117
+ @position_velocity_coefficient = -e1_over_two_zb + e2_over_two_zb
118
+
119
+ @velocity_position_coefficient = (z1e1_over_two_zb - z2e2_over_two_zb + e2) * z2
120
+ @velocity_velocity_coefficient = -z1e1_over_two_zb + z2e2_over_two_zb
121
+ end
122
+
123
+ #: () -> void
124
+ def compute_under_damped
125
+ omega_zeta = @angular_frequency * @damping_ratio
126
+ alpha = @angular_frequency * Math.sqrt(1.0 - (@damping_ratio * @damping_ratio))
127
+
128
+ exponential_term = Math.exp(-omega_zeta * @delta_time)
129
+ cosine_term = Math.cos(alpha * @delta_time)
130
+ sine_term = Math.sin(alpha * @delta_time)
131
+
132
+ inverse_alpha = 1.0 / alpha
133
+
134
+ exponential_sine = exponential_term * sine_term
135
+ exponential_cosine = exponential_term * cosine_term
136
+ exponential_omega_zeta_sine_over_alpha = exponential_term * omega_zeta * sine_term * inverse_alpha
137
+
138
+ @position_position_coefficient = exponential_cosine + exponential_omega_zeta_sine_over_alpha
139
+ @position_velocity_coefficient = exponential_sine * inverse_alpha
140
+
141
+ @velocity_position_coefficient = (-exponential_sine * alpha) - (omega_zeta * exponential_omega_zeta_sine_over_alpha)
142
+ @velocity_velocity_coefficient = exponential_cosine - exponential_omega_zeta_sine_over_alpha
143
+ end
144
+
145
+ #: () -> void
146
+ def compute_critically_damped
147
+ exponential_term = Math.exp(-@angular_frequency * @delta_time)
148
+ time_exponential = @delta_time * exponential_term
149
+ time_exponential_frequency = time_exponential * @angular_frequency
150
+
151
+ @position_position_coefficient = time_exponential_frequency + exponential_term
152
+ @position_velocity_coefficient = time_exponential
153
+
154
+ @velocity_position_coefficient = -@angular_frequency * time_exponential_frequency
155
+ @velocity_velocity_coefficient = -time_exponential_frequency + exponential_term
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Harmonica
5
+ # Vector represents a vector in 3D space with magnitude and direction.
6
+ #
7
+ # @example
8
+ # velocity = Harmonica::Vector.new(2.0, 0.0, 0.0)
9
+ # gravity = Harmonica::GRAVITY
10
+ class Vector
11
+ # @rbs @x: Float
12
+ # @rbs @y: Float
13
+ # @rbs @z: Float
14
+
15
+ attr_accessor :x #: Float
16
+ attr_accessor :y #: Float
17
+ attr_accessor :z #: Float
18
+
19
+ # @rbs x: Integer | Float -- x component
20
+ # @rbs y: Integer | Float -- y component
21
+ # @rbs z: Integer | Float -- z component
22
+ # @rbs return: void
23
+ def initialize(x = 0.0, y = 0.0, z = 0.0)
24
+ @x = x.to_f
25
+ @y = y.to_f
26
+ @z = z.to_f
27
+ end
28
+
29
+ #: () -> Array[Float]
30
+ def to_a
31
+ [@x, @y, @z]
32
+ end
33
+
34
+ #: (untyped other) -> bool
35
+ def ==(other)
36
+ return false unless other.is_a?(Vector)
37
+
38
+ @x == other.x && @y == other.y && @z == other.z
39
+ end
40
+
41
+ #: () -> String
42
+ def to_s
43
+ "Vector(#{@x}, #{@y}, #{@z})"
44
+ end
45
+
46
+ #: () -> String
47
+ def inspect
48
+ to_s
49
+ end
50
+
51
+ # Vector addition
52
+ #
53
+ #: (Vector other) -> Vector
54
+ def +(other)
55
+ Vector.new(@x + other.x, @y + other.y, @z + other.z)
56
+ end
57
+
58
+ # Vector subtraction
59
+ #
60
+ #: (Vector other) -> Vector
61
+ def -(other)
62
+ Vector.new(@x - other.x, @y - other.y, @z - other.z)
63
+ end
64
+
65
+ # Scalar multiplication
66
+ #
67
+ #: (Integer | Float other) -> Vector
68
+ def *(other)
69
+ Vector.new(@x * other, @y * other, @z * other)
70
+ end
71
+
72
+ # Magnitude (length) of the vector
73
+ #
74
+ #: () -> Float
75
+ def magnitude
76
+ Math.sqrt((@x * @x) + (@y * @y) + (@z * @z))
77
+ end
78
+
79
+ # Normalize the vector (unit vector)
80
+ #
81
+ #: () -> Vector
82
+ def normalize
83
+ mag = magnitude
84
+ return Vector.new(0, 0, 0) if mag.zero?
85
+
86
+ Vector.new(@x / mag, @y / mag, @z / mag)
87
+ end
88
+ end
89
+
90
+ # origin at bottom-left, Y pointing up
91
+ GRAVITY = Vector.new(0, -9.81, 0).freeze #: Vector
92
+
93
+ # origin at top-left, Y pointing down
94
+ TERMINAL_GRAVITY = Vector.new(0, 9.81, 0).freeze #: Vector
95
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
4
  module Harmonica
4
- VERSION = "0.0.1"
5
+ VERSION = "0.1.0" #: String
5
6
  end
data/lib/harmonica.rb CHANGED
@@ -1,8 +1,22 @@
1
1
  # frozen_string_literal: true
2
+ # rbs_inline: enabled
2
3
 
3
4
  require_relative "harmonica/version"
5
+ require_relative "harmonica/spring"
6
+ require_relative "harmonica/point"
7
+ require_relative "harmonica/vector"
8
+ require_relative "harmonica/projectile"
4
9
 
5
10
  module Harmonica
6
- class Error < StandardError; end
7
- # Your code goes here...
11
+ # Calculate time delta for a given frames per second.
12
+ # Use this when initializing Spring or Projectile.
13
+ #
14
+ # @example
15
+ # spring = Harmonica::Spring.new(delta_time: Harmonica.fps(60), ...)
16
+ #
17
+ # @rbs frames_per_second: Integer -- frames per second
18
+ # @rbs return: Float
19
+ def self.fps(frames_per_second)
20
+ 1.0 / frames_per_second
21
+ end
8
22
  end
metadata CHANGED
@@ -1,39 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: harmonica
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marco Roth
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: lipgloss
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '0.1'
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'
26
- description: Ruby bindings for the Harmonica physics-based animation library.
11
+ dependencies: []
12
+ description: Ruby implementation of Charm's Harmonica. A simple, efficient spring
13
+ animation library for smooth, natural motion.
27
14
  email:
28
15
  - marco.roth@intergga.ch
29
16
  executables: []
30
17
  extensions: []
31
18
  extra_rdoc_files: []
32
19
  files:
33
- - CHANGELOG.md
20
+ - LICENSE.txt
34
21
  - README.md
35
22
  - harmonica.gemspec
36
23
  - lib/harmonica.rb
24
+ - lib/harmonica/point.rb
25
+ - lib/harmonica/projectile.rb
26
+ - lib/harmonica/spring.rb
27
+ - lib/harmonica/vector.rb
37
28
  - lib/harmonica/version.rb
38
29
  homepage: https://github.com/marcoroth/harmonica-ruby
39
30
  licenses:
@@ -41,7 +32,7 @@ licenses:
41
32
  metadata:
42
33
  homepage_uri: https://github.com/marcoroth/harmonica-ruby
43
34
  source_code_uri: https://github.com/marcoroth/harmonica-ruby
44
- changelog_uri: https://github.com/marcoroth/harmonica-ruby/blob/main/CHANGELOG.md
35
+ changelog_uri: https://github.com/marcoroth/harmonica-ruby/releases
45
36
  rubygems_mfa_required: 'true'
46
37
  rdoc_options: []
47
38
  require_paths:
@@ -50,14 +41,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
50
41
  requirements:
51
42
  - - ">="
52
43
  - !ruby/object:Gem::Version
53
- version: 3.1.0
44
+ version: 3.2.0
54
45
  required_rubygems_version: !ruby/object:Gem::Requirement
55
46
  requirements:
56
47
  - - ">="
57
48
  - !ruby/object:Gem::Version
58
49
  version: '0'
59
50
  requirements: []
60
- rubygems_version: 3.6.9
51
+ rubygems_version: 4.0.3
61
52
  specification_version: 4
62
- summary: Ruby bindings for the Harmonica physics-based animation library.
53
+ summary: A simple, physics-based animation library for Ruby.
63
54
  test_files: []
data/CHANGELOG.md DELETED
@@ -1,5 +0,0 @@
1
- ## [Unreleased]
2
-
3
- ## [0.1.0] - 2025-12-23
4
-
5
- - Initial release