From 6b9af5c87cb8e8577a62caf6ca247f3c3fea9e47 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Fri, 17 Nov 2023 15:06:38 -0600 Subject: [PATCH] Merged in gosu_notifications --- lib/cyberarm_engine/notification.rb | 83 +++++++ lib/cyberarm_engine/notification_manager.rb | 242 ++++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 lib/cyberarm_engine/notification.rb create mode 100644 lib/cyberarm_engine/notification_manager.rb diff --git a/lib/cyberarm_engine/notification.rb b/lib/cyberarm_engine/notification.rb new file mode 100644 index 0000000..d7a5c3f --- /dev/null +++ b/lib/cyberarm_engine/notification.rb @@ -0,0 +1,83 @@ +module CyberarmEngine + class Notification + WIDTH = 500 + HEIGHT = 64 + EDGE_WIDTH = 8 + TRANSITION_DURATION = 750 + PADDING = 8 + + TTL_LONG = 5_000 + TTL_MEDIUM = 3_250 + TTL_SHORT = 1_500 + TIME_TO_LIVE = TTL_MEDIUM + + BACKGROUND_COLOR = Gosu::Color.new(0xaa313533) + EDGE_COLOR = Gosu::Color.new(0xaa010101) + ICON_COLOR = Gosu::Color.new(0xddffffff) + TITLE_COLOR = Gosu::Color.new(0xddffffff) + TAGLINE_COLOR = Gosu::Color.new(0xddaaaaaa) + + TITLE_SIZE = 28 + TAGLINE_SIZE = 18 + ICON_SIZE = HEIGHT - PADDING * 2 + + TITLE_FONT = Gosu::Font.new(TITLE_SIZE, bold: true) + TAGLINE_FONT = Gosu::Font.new(TAGLINE_SIZE) + + PRIORITY_HIGH = 1.0 + PRIORITY_MEDIUM = 0.5 + PRIORITY_LOW = 0.0 + + LINEAR_TRANSITION = :linear + EASE_IN_OUT_TRANSITION = :ease_in_out + + attr_reader :priority, :title, :tagline, :icon, :time_to_live, :transition_duration, :transition_type + def initialize( + host:, priority:, title:, title_color: TITLE_COLOR, tagline: "", tagline_color: TAGLINE_COLOR, icon: nil, icon_color: ICON_COLOR, + edge_color: EDGE_COLOR, background_color: BACKGROUND_COLOR, time_to_live: TIME_TO_LIVE, transition_duration: TRANSITION_DURATION, + transition_type: EASE_IN_OUT_TRANSITION + ) + @host = host + + @priority = priority + @title = title + @title_color = title_color + @tagline = tagline + @tagline_color = tagline_color + @icon = icon + @icon_color = icon_color + @edge_color = edge_color + @background_color = background_color + @time_to_live = time_to_live + @transition_duration = transition_duration + @transition_type = transition_type + + @icon_scale = ICON_SIZE.to_f / @icon.width if @icon + end + + def draw + Gosu.draw_rect(0, 0, WIDTH, HEIGHT, @background_color) + + if @host.edge == :top + Gosu.draw_rect(0, HEIGHT - EDGE_WIDTH, WIDTH, EDGE_WIDTH, @edge_color) + @icon.draw(EDGE_WIDTH + PADDING, PADDING, 0, @icon_scale, @icon_scale, @icon_color) if @icon + + elsif @host.edge == :bottom + Gosu.draw_rect(0, 0, WIDTH, EDGE_WIDTH, @edge_color) + @icon.draw(EDGE_WIDTH + PADDING, PADDING, 0, @icon_scale, @icon_scale, @icon_color) if @icon + + elsif @host.edge == :right + Gosu.draw_rect(0, 0, EDGE_WIDTH, HEIGHT, @edge_color) + @icon.draw(EDGE_WIDTH + PADDING, PADDING, 0, @icon_scale, @icon_scale, @icon_color) if @icon + + else + Gosu.draw_rect(WIDTH - EDGE_WIDTH, 0, EDGE_WIDTH, HEIGHT, @edge_color) + @icon.draw(PADDING, PADDING, 0, @icon_scale, @icon_scale, @icon_color) if @icon + end + + icon_space = @icon ? ICON_SIZE + PADDING : 0 + TITLE_FONT.draw_text(@title, PADDING + EDGE_WIDTH + icon_space, PADDING, 0, 1, 1, @title_color) + TAGLINE_FONT.draw_text(@tagline, PADDING + EDGE_WIDTH + icon_space, PADDING + TITLE_FONT.height, 0, 1, 1, @tagline_color) + end + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/notification_manager.rb b/lib/cyberarm_engine/notification_manager.rb new file mode 100644 index 0000000..bbd1637 --- /dev/null +++ b/lib/cyberarm_engine/notification_manager.rb @@ -0,0 +1,242 @@ +module CyberarmEngine + class NotificationManager + EDGE_TOP = :top + EDGE_BOTTOM = :bottom + EDGE_RIGHT = :right + EDGE_LEFT = :left + + MODE_DEFAULT = :slide + MODE_CIRCLE = :circle + + attr_reader :edge, :mode, :max_visible, :notifications + def initialize(edge: EDGE_RIGHT, mode: MODE_DEFAULT, window:, max_visible: 1) + @edge = edge + @mode = mode + @window = window + @max_visible = max_visible + + @notifications = [] + @drivers = [] + @slots = Array.new(max_visible, nil) + end + + def draw + @drivers.each do |driver| + case @edge + when :left, :right + x = @edge == :right ? @window.width + driver.x : -Notification::WIDTH + driver.x + y = driver.y + Notification::HEIGHT / 2 + + Gosu.translate(x, y + (Notification::HEIGHT + Notification::PADDING) * driver.slot) do + driver.draw + end + + when :top, :bottom + x = @window.width / 2 - Notification::WIDTH / 2 + y = @edge == :top ? driver.y - Notification::HEIGHT : @window.height + driver.y + slot_position = (Notification::HEIGHT + Notification::PADDING) * driver.slot + slot_position *= -1 if @edge == :bottom + + Gosu.translate(x, y + slot_position) do + driver.draw + end + end + end + end + + def update + show_next_notification if @drivers.size < @max_visible + @drivers.each do |driver| + if driver.done? + @slots[driver.slot] = nil + @drivers.delete(driver) + end + end + + @drivers.each(&:update) + end + + def show_next_notification + notification = @notifications.sort { |n| n.priority }.reverse.shift + return unless notification + return if available_slot_index < lowest_used_slot + @notifications.delete(notification) + + @drivers << Driver.new(edge: @edge, mode: @mode, notification: notification, slot: available_slot_index) + slot = @slots[available_slot_index] = @drivers.last + end + + def available_slot_index + @slots.each_with_index do |slot, i| + return i unless slot + end + + return -1 + end + + def lowest_used_slot + @slots.each_with_index do |slot, i| + return i if slot + end + + return -1 + end + + def highest_used_slot + _slot = -1 + @slots.each_with_index do |slot, i| + _slot = i if slot + end + + return _slot + end + + def create_notification(**args) + notification = Notification.new(host: self, **args) + @notifications << notification + end + + class Driver + attr_reader :x, :y, :notification, :slot + def initialize(edge:, mode:, notification:, slot:) + @edge = edge + @mode = mode + @notification = notification + @slot = slot + + @x, @y = 0, 0 + @delta = Gosu.milliseconds + @accumulator = 0.0 + + @born_at = Gosu.milliseconds + @duration_completed_at = Float::INFINITY + @transition_completed_at = Float::INFINITY + end + + def transition_in_complete? + Gosu.milliseconds - @born_at >= @notification.transition_duration + end + + def duration_completed? + Gosu.milliseconds - @transition_completed_at >= @notification.time_to_live + end + + def done? + Gosu.milliseconds - @duration_completed_at >= @notification.transition_duration + end + + def draw + ratio = 0.0 + + if not transition_in_complete? + ratio = animation_ratio + elsif transition_in_complete? and not duration_completed? + ratio = 1.0 + elsif duration_completed? + ratio = 1.0 - animation_ratio + end + + case @mode + when MODE_DEFAULT + Gosu.clip_to(0, 0, Notification::WIDTH, Notification::HEIGHT * ratio) do + @notification.draw + end + when MODE_CIRCLE + half = Notification::WIDTH / 2 + + Gosu.clip_to(half - (half * ratio), 0, Notification::WIDTH * ratio, Notification::HEIGHT) do + @notification.draw + end + end + end + + def update + case @mode + when MODE_DEFAULT + update_default + when MODE_CIRCLE + update_circle + end + + @accumulator += Gosu.milliseconds - @delta + @delta = Gosu.milliseconds + end + + + def update_default + case @edge + when :left, :right + if not transition_in_complete? # Slide In + @x = @edge == :right ? -x_offset : x_offset + elsif transition_in_complete? and not duration_completed? + @x = @edge == :right ? -Notification::WIDTH : Notification::WIDTH if @x.abs != Notification::WIDTH + @transition_completed_at = Gosu.milliseconds if @transition_completed_at == Float::INFINITY + @accumulator = 0.0 + elsif duration_completed? # Slide Out + @x = @edge == :right ? x_offset - Notification::WIDTH : Notification::WIDTH - x_offset + @x = 0 if @edge == :left and @x <= 0 + @x = 0 if @edge == :right and @x >= 0 + @duration_completed_at = Gosu.milliseconds if @duration_completed_at == Float::INFINITY + end + + when :top, :bottom + if not transition_in_complete? # Slide In + @y = @edge == :top ? y_offset : -y_offset + elsif transition_in_complete? and not duration_completed? + @y = @edge == :top ? Notification::HEIGHT : -Notification::HEIGHT if @x.abs != Notification::HEIGHT + @transition_completed_at = Gosu.milliseconds if @transition_completed_at == Float::INFINITY + @accumulator = 0.0 + elsif duration_completed? # Slide Out + @y = @edge == :top ? Notification::HEIGHT - y_offset : y_offset - Notification::HEIGHT + @y = 0 if @edge == :top and @y <= 0 + @y = 0 if @edge == :bottom and @y >= 0 + @duration_completed_at = Gosu.milliseconds if @duration_completed_at == Float::INFINITY + end + end + end + + def update_circle + case @edge + when :top, :bottom + @y = @edge == :top ? Notification::HEIGHT : -Notification::HEIGHT + when :left, :right + @x = @edge == :right ? -Notification::WIDTH : Notification::WIDTH + end + + if transition_in_complete? and not duration_completed? + @transition_completed_at = Gosu.milliseconds if @transition_completed_at == Float::INFINITY + @accumulator = 0.0 + elsif duration_completed? + @duration_completed_at = Gosu.milliseconds if @duration_completed_at == Float::INFINITY + end + end + + def animation_ratio + x = (@accumulator / @notification.transition_duration) + + case @notification.transition_type + when Notification::LINEAR_TRANSITION + x.clamp(0.0, 1.0) + when Notification::EASE_IN_OUT_TRANSITION # https://easings.net/#easeInOutQuint + (x < 0.5 ? 16 * x * x * x * x * x : 1 - ((-2 * x + 2) ** 5) / 2).clamp(0.0, 1.0) + end + end + + def x_offset + if not transition_in_complete? or duration_completed? + Notification::WIDTH * animation_ratio + else + 0 + end + end + + def y_offset + if not transition_in_complete? or duration_completed? + Notification::HEIGHT * animation_ratio + else + 0 + end + end + end + end +end \ No newline at end of file