charming 0.2.3 → 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 +4 -4
- data/README.md +1 -1
- data/lib/charming/application.rb +7 -0
- data/lib/charming/controller/class_methods.rb +13 -2
- data/lib/charming/controller/timers.rb +25 -0
- data/lib/charming/controller.rb +6 -1
- data/lib/charming/internal/event_loop.rb +25 -2
- data/lib/charming/internal/timer_control.rb +48 -0
- data/lib/charming/projectile.rb +64 -0
- data/lib/charming/runtime.rb +12 -2
- data/lib/charming/spring.rb +126 -0
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +6 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb4da799ae87f28a2495a56dd47dcb049c5b532397993976a5d56fcb2a51a518
|
|
4
|
+
data.tar.gz: 070767c2b15058ff97db6d4aec1da299f0f8a7df9e249ac323ef9ff1329764ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c4a76d6edd2439e200ea88d1ec13f8be7d4a74974770fd091de041f3f0c03c953edc0ae72b53ca0aed3dd9648f924d3c693d9a696ea5a727d2d351dc6f04e660
|
|
7
|
+
data.tar.gz: da98baee0f32faa72560af2b6f1f05f80b61996a272739145774103d42d672d536c5b00634082042ee0a3d4d919eb9f53080f2c143ffb1b7d7563d9b7da86b89
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
A Rails-inspired terminal user interface framework for **Ruby 4+**.
|
|
6
6
|
|
|
7
|
-
Charming gives terminal apps familiar application structure: routes, controllers, state objects, templates, layouts, reusable components, themes, keyboard bindings, command palettes, timers, background tasks, cross-platform audio playback, inline image display (Kitty graphics protocol), system clipboard / desktop notifications / window title, braille charts and sparklines, and testable terminal backends.
|
|
7
|
+
Charming gives terminal apps familiar application structure: routes, controllers, state objects, templates, layouts, reusable components, themes, keyboard bindings, command palettes, timers, physics-based animation (springs and projectiles, ported from charmbracelet/harmonica), background tasks, cross-platform audio playback, inline image display (Kitty graphics protocol), system clipboard / desktop notifications / window title, braille charts and sparklines, and testable terminal backends.
|
|
8
8
|
|
|
9
9
|
## Project Status
|
|
10
10
|
|
data/lib/charming/application.rb
CHANGED
|
@@ -163,6 +163,13 @@ module Charming
|
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
attr_accessor :logger, :task_executor
|
|
166
|
+
attr_writer :timer_control
|
|
167
|
+
|
|
168
|
+
# The runtime-injected timer control used by `Controller#start_timer` and
|
|
169
|
+
# friends. Defaults to a null object so controllers work outside a Runtime.
|
|
170
|
+
def timer_control
|
|
171
|
+
@timer_control ||= Internal::TimerControl::Null.new
|
|
172
|
+
end
|
|
166
173
|
attr_reader :session
|
|
167
174
|
|
|
168
175
|
# Initializes the session hash for per-request state storage, restoring a
|
|
@@ -24,10 +24,21 @@ module Charming
|
|
|
24
24
|
|
|
25
25
|
# Declares a timer that fires every *every* seconds and dispatches *action* on the controller.
|
|
26
26
|
# The runtime builds a TimerEvent and routes it to the active controller's dispatch_timer.
|
|
27
|
-
|
|
27
|
+
# Timers run from boot by default; declare `autostart: false` to schedule one only when an
|
|
28
|
+
# action calls `start_timer`.
|
|
29
|
+
def timer(name, every:, action:, autostart: true)
|
|
28
30
|
raise ArgumentError, "timer interval must be positive (got #{every.inspect})" unless every.is_a?(Numeric) && every.positive?
|
|
29
31
|
|
|
30
|
-
timer_bindings[name.to_sym] = TimerBinding.new(name: name.to_sym, interval: every, action: action)
|
|
32
|
+
timer_bindings[name.to_sym] = TimerBinding.new(name: name.to_sym, interval: every, action: action, autostart: autostart)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Declares an animation: a stopped timer ticking *action* at *fps* frames per second.
|
|
36
|
+
# Begin motion with `start_timer(name)`; the action calls `stop_timer(name)` once the
|
|
37
|
+
# motion settles, returning the app to zero idle cost.
|
|
38
|
+
def animate(name, action:, fps: 30)
|
|
39
|
+
raise ArgumentError, "fps must be positive (got #{fps.inspect})" unless fps.is_a?(Numeric) && fps.positive?
|
|
40
|
+
|
|
41
|
+
timer(name, every: 1.0 / fps, action: action, autostart: false)
|
|
31
42
|
end
|
|
32
43
|
|
|
33
44
|
# Declares a task handler for async work submitted via `run_task(:name)`. When the task emits
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
class Controller
|
|
5
|
+
# Timer control helpers mixed into Controller. Controllers are ephemeral, so
|
|
6
|
+
# starting and stopping named timers is delegated to the runtime-owned
|
|
7
|
+
# TimerControl exposed on the application (a null object outside a runtime).
|
|
8
|
+
module Timers
|
|
9
|
+
# Schedules the named declared timer. Idempotent while the timer is running.
|
|
10
|
+
def start_timer(name)
|
|
11
|
+
application.timer_control.start(name)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Unschedules the named timer. Idempotent when it is not running.
|
|
15
|
+
def stop_timer(name)
|
|
16
|
+
application.timer_control.stop(name)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# True while the named timer is scheduled.
|
|
20
|
+
def timer_running?(name)
|
|
21
|
+
application.timer_control.running?(name)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/charming/controller.rb
CHANGED
|
@@ -5,7 +5,11 @@ module Charming
|
|
|
5
5
|
# It provides the action dispatch pipeline, key/command/timer/task bindings, sidebar navigation,
|
|
6
6
|
# command palette management, and view rendering with layout composition.
|
|
7
7
|
class Controller
|
|
8
|
-
TimerBinding = Data.define(:name, :interval, :action)
|
|
8
|
+
TimerBinding = Data.define(:name, :interval, :action, :autostart) do
|
|
9
|
+
def initialize(name:, interval:, action:, autostart: true)
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
end
|
|
9
13
|
TaskBinding = Data.define(:name, :action)
|
|
10
14
|
|
|
11
15
|
extend ClassMethods
|
|
@@ -18,6 +22,7 @@ module Charming
|
|
|
18
22
|
include ComponentDispatching
|
|
19
23
|
include Dispatching
|
|
20
24
|
include Terminal
|
|
25
|
+
include Timers
|
|
21
26
|
|
|
22
27
|
attr_reader :application, :event, :params, :screen, :route
|
|
23
28
|
|
|
@@ -51,6 +51,24 @@ module Charming
|
|
|
51
51
|
@timers = build_timers(timer_bindings)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Schedules *binding* one interval from now. Idempotent: starting a timer
|
|
55
|
+
# that is already running keeps its current deadline (no phase reset).
|
|
56
|
+
def start_timer(binding)
|
|
57
|
+
return if timer_running?(binding.name)
|
|
58
|
+
|
|
59
|
+
@timers << {binding: binding, next_at: clock_now + binding.interval}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Removes the named timer from the schedule. Idempotent for unknown names.
|
|
63
|
+
def stop_timer(name)
|
|
64
|
+
@timers.reject! { |timer| timer.fetch(:binding).name == name }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# True while the named timer is scheduled.
|
|
68
|
+
def timer_running?(name)
|
|
69
|
+
@timers.any? { |timer| timer.fetch(:binding).name == name }
|
|
70
|
+
end
|
|
71
|
+
|
|
54
72
|
private
|
|
55
73
|
|
|
56
74
|
# The next due event in priority order: task results, then timers, then input.
|
|
@@ -74,13 +92,18 @@ module Charming
|
|
|
74
92
|
end
|
|
75
93
|
|
|
76
94
|
# Returns a TimerEvent for the first due timer and advances its next fire
|
|
77
|
-
# time. Returns nil if no timers are ready or registered.
|
|
95
|
+
# time. Returns nil if no timers are ready or registered. Rescheduling is
|
|
96
|
+
# accumulator-based so fire times stay on the interval grid (no drift);
|
|
97
|
+
# after a stall the deadline snaps forward, skipping missed ticks instead
|
|
98
|
+
# of firing a catch-up burst.
|
|
78
99
|
def next_timer_event
|
|
79
100
|
timer = due_timer
|
|
80
101
|
return unless timer
|
|
81
102
|
|
|
82
103
|
now = clock_now
|
|
83
|
-
|
|
104
|
+
interval = timer.fetch(:binding).interval
|
|
105
|
+
timer[:next_at] += interval
|
|
106
|
+
timer[:next_at] = now + interval if timer[:next_at] <= now
|
|
84
107
|
Events::TimerEvent.new(name: timer.fetch(:binding).name, now: now)
|
|
85
108
|
end
|
|
86
109
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Internal
|
|
5
|
+
# TimerControl is the runtime-injected surface that lets ephemeral controllers
|
|
6
|
+
# start and stop the EventLoop's scheduled timers by name (the same pattern as
|
|
7
|
+
# Application#task_executor). *bindings* is a callable returning the current
|
|
8
|
+
# route's timer bindings, so navigation needs no re-wiring.
|
|
9
|
+
class TimerControl
|
|
10
|
+
def initialize(event_loop:, bindings:)
|
|
11
|
+
@event_loop = event_loop
|
|
12
|
+
@bindings = bindings
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Schedules the named declared timer on the event loop.
|
|
16
|
+
def start(name)
|
|
17
|
+
binding = @bindings.call[name.to_sym]
|
|
18
|
+
raise ArgumentError, "unknown timer #{name.to_sym.inspect}" unless binding
|
|
19
|
+
|
|
20
|
+
@event_loop.start_timer(binding)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Unschedules the named timer.
|
|
24
|
+
def stop(name)
|
|
25
|
+
@event_loop.stop_timer(name.to_sym)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# True while the named timer is scheduled.
|
|
29
|
+
def running?(name)
|
|
30
|
+
@event_loop.timer_running?(name.to_sym)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Null is the default control for applications running outside a Runtime
|
|
34
|
+
# (unit specs, console): starts and stops are no-ops.
|
|
35
|
+
class Null
|
|
36
|
+
def start(name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def stop(name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def running?(name)
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
# Projectile is simple physics motion for games: a position advanced by a
|
|
5
|
+
# velocity, and a velocity advanced by an acceleration (typically gravity),
|
|
6
|
+
# one fixed time step per frame (semi-implicit Euler, matching
|
|
7
|
+
# charmbracelet/harmonica's projectile):
|
|
8
|
+
#
|
|
9
|
+
# ball = Charming::Projectile.new(
|
|
10
|
+
# delta_time: Charming.fps(60),
|
|
11
|
+
# position: Charming::Projectile::Point.new(x: 0.0, y: 0.0),
|
|
12
|
+
# velocity: Charming::Projectile::Vector.new(x: 20.0, y: 0.0),
|
|
13
|
+
# acceleration: Charming::Projectile::TERMINAL_GRAVITY
|
|
14
|
+
# )
|
|
15
|
+
# position = ball.update # each frame
|
|
16
|
+
class Projectile
|
|
17
|
+
Point = Data.define(:x, :y, :z) do
|
|
18
|
+
def initialize(x:, y:, z: 0.0)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Vector = Data.define(:x, :y, :z) do
|
|
24
|
+
def initialize(x:, y:, z: 0.0)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Gravity for a coordinate plane whose origin is bottom-left (y grows upward).
|
|
30
|
+
GRAVITY = Vector.new(x: 0.0, y: -9.81)
|
|
31
|
+
|
|
32
|
+
# Gravity for terminal coordinates, whose origin is top-left (y grows downward).
|
|
33
|
+
TERMINAL_GRAVITY = Vector.new(x: 0.0, y: 9.81)
|
|
34
|
+
|
|
35
|
+
attr_reader :position, :velocity, :acceleration
|
|
36
|
+
|
|
37
|
+
def initialize(delta_time:, position:, velocity: Vector.new(x: 0.0, y: 0.0), acceleration: Vector.new(x: 0.0, y: 0.0))
|
|
38
|
+
@delta_time = delta_time
|
|
39
|
+
@position = position
|
|
40
|
+
@velocity = velocity
|
|
41
|
+
@acceleration = acceleration
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Advances one frame and returns the new position. Position moves by the
|
|
45
|
+
# current velocity before the velocity accelerates — keep this order.
|
|
46
|
+
def update
|
|
47
|
+
@position = shift(position, velocity)
|
|
48
|
+
@velocity = shift(velocity, acceleration)
|
|
49
|
+
position
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
attr_reader :delta_time
|
|
55
|
+
|
|
56
|
+
def shift(value, rate)
|
|
57
|
+
value.with(
|
|
58
|
+
x: value.x + rate.x * delta_time,
|
|
59
|
+
y: value.y + rate.y * delta_time,
|
|
60
|
+
z: value.z + rate.z * delta_time
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/charming/runtime.rb
CHANGED
|
@@ -20,6 +20,10 @@ module Charming
|
|
|
20
20
|
@screen = backend_screen
|
|
21
21
|
@coalesce_input = @application.respond_to?(:coalesce_input?) && @application.coalesce_input?
|
|
22
22
|
@event_loop = build_event_loop
|
|
23
|
+
@application.timer_control = Internal::TimerControl.new(
|
|
24
|
+
event_loop: @event_loop,
|
|
25
|
+
bindings: -> { @route.controller_class.timer_bindings }
|
|
26
|
+
)
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
# Runs the event loop: enters alt-screen, dispatches incoming events
|
|
@@ -52,12 +56,18 @@ module Charming
|
|
|
52
56
|
backend: @backend,
|
|
53
57
|
clock: @clock,
|
|
54
58
|
task_queue: @task_queue,
|
|
55
|
-
timer_bindings:
|
|
59
|
+
timer_bindings: autostart_timer_bindings,
|
|
56
60
|
coalesce_input: @coalesce_input,
|
|
57
61
|
interrupted: -> { @interrupted }
|
|
58
62
|
)
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
# The current route's timers that run from boot. Timers declared with
|
|
66
|
+
# `autostart: false` (including `animate` ticks) wait for `start_timer`.
|
|
67
|
+
def autostart_timer_bindings
|
|
68
|
+
@route.controller_class.timer_bindings.values.select(&:autostart)
|
|
69
|
+
end
|
|
70
|
+
|
|
61
71
|
# The first frame's response — the root route's action, with errors caught. Out-of-band escape
|
|
62
72
|
# sequences registered while rendering are collected and attached to the response.
|
|
63
73
|
def initial_response
|
|
@@ -282,7 +292,7 @@ module Charming
|
|
|
282
292
|
return response unless response.navigate?
|
|
283
293
|
|
|
284
294
|
@route = resolve_route(response.path)
|
|
285
|
-
@event_loop.reset_timers(
|
|
295
|
+
@event_loop.reset_timers(autostart_timer_bindings)
|
|
286
296
|
dispatch(@route.action)
|
|
287
297
|
end
|
|
288
298
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ******************************************************************************
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2008-2012 Ryan Juckett
|
|
6
|
+
# http://www.ryanjuckett.com/
|
|
7
|
+
#
|
|
8
|
+
# This software is provided 'as-is', without any express or implied
|
|
9
|
+
# warranty. In no event will the authors be held liable for any damages
|
|
10
|
+
# arising from the use of this software.
|
|
11
|
+
#
|
|
12
|
+
# Permission is granted to anyone to use this software for any purpose,
|
|
13
|
+
# including commercial applications, and to alter it and redistribute it
|
|
14
|
+
# freely, subject to the following restrictions:
|
|
15
|
+
#
|
|
16
|
+
# 1. The origin of this software must not be misrepresented; you must not
|
|
17
|
+
# claim that you wrote the original software. If you use this software
|
|
18
|
+
# in a product, an acknowledgment in the product documentation would be
|
|
19
|
+
# appreciated but is not required.
|
|
20
|
+
#
|
|
21
|
+
# 2. Altered source versions must be plainly marked as such, and must not be
|
|
22
|
+
# misrepresented as being the original software.
|
|
23
|
+
#
|
|
24
|
+
# 3. This notice may not be removed or altered from any source
|
|
25
|
+
# distribution.
|
|
26
|
+
#
|
|
27
|
+
# ******************************************************************************
|
|
28
|
+
#
|
|
29
|
+
# Ported to Go by Charmbracelet, Inc. in 2021 (charmbracelet/harmonica, MIT).
|
|
30
|
+
# Ported to Ruby for Charming in 2026 from charmbracelet/harmonica.
|
|
31
|
+
#
|
|
32
|
+
# For background on the algorithm see:
|
|
33
|
+
# https://www.ryanjuckett.com/damped-springs/
|
|
34
|
+
|
|
35
|
+
module Charming
|
|
36
|
+
# Spring is a damped harmonic oscillator for physics-based animation. It
|
|
37
|
+
# precomputes motion coefficients for a fixed time step so each frame update
|
|
38
|
+
# is four multiply-adds. Instances are immutable; the caller owns position
|
|
39
|
+
# and velocity (typically as Float attributes on an ApplicationState) and
|
|
40
|
+
# feeds them back in every frame:
|
|
41
|
+
#
|
|
42
|
+
# SPRING = Charming::Spring.new(delta_time: Charming.fps(60))
|
|
43
|
+
# position, velocity = SPRING.update(position, velocity, target)
|
|
44
|
+
#
|
|
45
|
+
# The damping ratio shapes the motion: below 1 overshoots and oscillates,
|
|
46
|
+
# exactly 1 reaches the target as fast as possible without overshooting,
|
|
47
|
+
# above 1 approaches slower still. Angular frequency scales the speed.
|
|
48
|
+
class Spring
|
|
49
|
+
def initialize(delta_time:, angular_frequency: 6.0, damping_ratio: 0.5)
|
|
50
|
+
angular_frequency = [0.0, angular_frequency].max
|
|
51
|
+
damping_ratio = [0.0, damping_ratio].max
|
|
52
|
+
@pos_pos, @pos_vel, @vel_pos, @vel_vel =
|
|
53
|
+
coefficients(delta_time, angular_frequency, damping_ratio)
|
|
54
|
+
freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Advances one frame toward *target*, returning `[new_position, new_velocity]`.
|
|
58
|
+
def update(position, velocity, target)
|
|
59
|
+
relative = position - target
|
|
60
|
+
[relative * @pos_pos + velocity * @pos_vel + target,
|
|
61
|
+
relative * @vel_pos + velocity * @vel_vel]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# True once the motion has effectively come to rest at *target* — the guard
|
|
65
|
+
# for stopping an animation timer.
|
|
66
|
+
def settled?(position, velocity, target, epsilon: 0.01)
|
|
67
|
+
(position - target).abs < epsilon && velocity.abs < epsilon
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def coefficients(delta_time, frequency, ratio)
|
|
73
|
+
return [1.0, 0.0, 0.0, 1.0] if frequency < Float::EPSILON
|
|
74
|
+
|
|
75
|
+
if ratio > 1.0 + Float::EPSILON
|
|
76
|
+
over_damped(delta_time, frequency, ratio)
|
|
77
|
+
elsif ratio < 1.0 - Float::EPSILON
|
|
78
|
+
under_damped(delta_time, frequency, ratio)
|
|
79
|
+
else
|
|
80
|
+
critically_damped(delta_time, frequency)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def over_damped(delta_time, frequency, ratio)
|
|
85
|
+
za = -frequency * ratio
|
|
86
|
+
zb = frequency * Math.sqrt(ratio * ratio - 1.0)
|
|
87
|
+
z1 = za - zb
|
|
88
|
+
z2 = za + zb
|
|
89
|
+
e2 = Math.exp(z2 * delta_time)
|
|
90
|
+
e1_over_two_zb = Math.exp(z1 * delta_time) / (2.0 * zb)
|
|
91
|
+
e2_over_two_zb = e2 / (2.0 * zb)
|
|
92
|
+
z1e1_over_two_zb = z1 * e1_over_two_zb
|
|
93
|
+
z2e2_over_two_zb = z2 * e2_over_two_zb
|
|
94
|
+
|
|
95
|
+
[e1_over_two_zb * z2 - z2e2_over_two_zb + e2,
|
|
96
|
+
-e1_over_two_zb + e2_over_two_zb,
|
|
97
|
+
(z1e1_over_two_zb - z2e2_over_two_zb + e2) * z2,
|
|
98
|
+
-z1e1_over_two_zb + z2e2_over_two_zb]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def under_damped(delta_time, frequency, ratio)
|
|
102
|
+
omega_zeta = frequency * ratio
|
|
103
|
+
alpha = frequency * Math.sqrt(1.0 - ratio * ratio)
|
|
104
|
+
exp_term = Math.exp(-omega_zeta * delta_time)
|
|
105
|
+
exp_sin = exp_term * Math.sin(alpha * delta_time)
|
|
106
|
+
exp_cos = exp_term * Math.cos(alpha * delta_time)
|
|
107
|
+
exp_omega_zeta_sin_over_alpha = omega_zeta * exp_sin / alpha
|
|
108
|
+
|
|
109
|
+
[exp_cos + exp_omega_zeta_sin_over_alpha,
|
|
110
|
+
exp_sin / alpha,
|
|
111
|
+
-exp_sin * alpha - omega_zeta * exp_omega_zeta_sin_over_alpha,
|
|
112
|
+
exp_cos - exp_omega_zeta_sin_over_alpha]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def critically_damped(delta_time, frequency)
|
|
116
|
+
exp_term = Math.exp(-frequency * delta_time)
|
|
117
|
+
time_exp = delta_time * exp_term
|
|
118
|
+
time_exp_freq = time_exp * frequency
|
|
119
|
+
|
|
120
|
+
[time_exp_freq + exp_term,
|
|
121
|
+
time_exp,
|
|
122
|
+
-frequency * time_exp_freq,
|
|
123
|
+
-time_exp_freq + exp_term]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
data/lib/charming/version.rb
CHANGED
data/lib/charming.rb
CHANGED
|
@@ -43,6 +43,12 @@ module Charming
|
|
|
43
43
|
Runtime.new(application, backend: backend).run
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# Returns the seconds-per-frame time delta for a frame rate, for initializing
|
|
47
|
+
# physics primitives: `Charming::Spring.new(delta_time: Charming.fps(60))`.
|
|
48
|
+
def self.fps(frames_per_second)
|
|
49
|
+
1.0 / frames_per_second
|
|
50
|
+
end
|
|
51
|
+
|
|
46
52
|
# Returns the normalized key symbol for an event-like object — `event.key` when the object responds
|
|
47
53
|
# to it, otherwise `event.to_sym`. Lets components treat raw strings and KeyEvent objects uniformly.
|
|
48
54
|
def self.key_of(event)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: charming
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- pando
|
|
@@ -206,6 +206,7 @@ files:
|
|
|
206
206
|
- lib/charming/controller/session_state.rb
|
|
207
207
|
- lib/charming/controller/sidebar_navigation.rb
|
|
208
208
|
- lib/charming/controller/terminal.rb
|
|
209
|
+
- lib/charming/controller/timers.rb
|
|
209
210
|
- lib/charming/database/commands.rb
|
|
210
211
|
- lib/charming/escape.rb
|
|
211
212
|
- lib/charming/events/focus_event.rb
|
|
@@ -276,6 +277,7 @@ files:
|
|
|
276
277
|
- lib/charming/internal/terminal/modified_key_parser.rb
|
|
277
278
|
- lib/charming/internal/terminal/mouse_parser.rb
|
|
278
279
|
- lib/charming/internal/terminal/tty_backend.rb
|
|
280
|
+
- lib/charming/internal/timer_control.rb
|
|
279
281
|
- lib/charming/presentation/component.rb
|
|
280
282
|
- lib/charming/presentation/components/activity_indicator.rb
|
|
281
283
|
- lib/charming/presentation/components/audio.rb
|
|
@@ -366,10 +368,12 @@ files:
|
|
|
366
368
|
- lib/charming/presentation/ui/truncate.rb
|
|
367
369
|
- lib/charming/presentation/ui/width.rb
|
|
368
370
|
- lib/charming/presentation/view.rb
|
|
371
|
+
- lib/charming/projectile.rb
|
|
369
372
|
- lib/charming/response.rb
|
|
370
373
|
- lib/charming/router.rb
|
|
371
374
|
- lib/charming/runtime.rb
|
|
372
375
|
- lib/charming/screen.rb
|
|
376
|
+
- lib/charming/spring.rb
|
|
373
377
|
- lib/charming/tasks/cancelled.rb
|
|
374
378
|
- lib/charming/tasks/inline_executor.rb
|
|
375
379
|
- lib/charming/tasks/progress.rb
|