From 852e8e090e524188efcfe485c16cf7a2e275488a Mon Sep 17 00:00:00 2001 From: Gergely Polonkai Date: Mon, 23 May 2022 18:03:10 +0200 Subject: [PATCH] [Refactor] Move the gen_svg function to the svg_clock module --- src/main.rs | 361 +------------------------------------------- src/svg_clock.rs | 383 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 373 insertions(+), 371 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1393d3c..0f8126f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,378 +3,19 @@ extern crate smithay_client_toolkit as sctk; use std::fs; use calloop::{timer::Timer, EventLoop}; -use chrono::prelude::{Local, Utc}; -use chrono::TimeZone; -use chrono::Timelike; use sctk::reexports::client::protocol::{wl_shm, wl_surface}; use sctk::shm::AutoMemPool; use sctk::window::{Event as WEvent, FallbackFrame}; use svg::node::element::path::Data as PathData; -use svg::node::element::{Circle, Group, Line, Rectangle, Style, Text}; -use svg::node::Text as TextNode; -use svg::Document; mod config; mod svg_clock; use config::{CompleteConfig, Config}; -use svg_clock::{ - cache_hour_name_paths, get_moon_path, get_range_path, hour_marker, seconds_to_degrees, - svg_to_usvg, HOUR_NAMES, HOUR_NAME_FONT_SIZE, IMAGE_WIDTH, OUTER_R, RING_WIDTH, - UTC_HOUR_FONT_SIZE, -}; +use svg_clock::{cache_hour_name_paths, gen_svg, svg_to_usvg}; sctk::default_environment!(SeasonalClock, desktop); -fn gen_svg(config: &Option, hour_name_path_cache: &[(PathData, PathData); 24]) -> Document { - let local_timestamp = Local::now(); - let utc_hour = local_timestamp.with_timezone(&Utc).time().hour(); - let local_hour = local_timestamp.time().hour(); - let local_minute = local_timestamp.time().minute(); - let local_second = local_timestamp.time().second(); - - let utc_offset = local_timestamp.offset().local_minus_utc(); - let local_time = local_timestamp.time().num_seconds_from_midnight() as i32; - let utc_rotation = seconds_to_degrees(utc_offset); - - // Calculate the Moon phase - let unixtime = suncalc::Timestamp(local_timestamp.timestamp_millis()); - let moon_illumination = suncalc::moon_illumination(unixtime); - let moon_radius = IMAGE_WIDTH as f32 * 0.071428571; - let moon_phase = moon_illumination.phase * 28.0; - - let local_hour_font_size = IMAGE_WIDTH as f32 * 0.02357; - let sun_radius = IMAGE_WIDTH as f32 * 0.0142871; - let marker_radius = OUTER_R - RING_WIDTH - 2.0 * UTC_HOUR_FONT_SIZE; - - let border = Rectangle::new() - .set("x", 0i32) - .set("y", 0i32) - .set("width", 700i32) - .set("height", 700i32) - .set("id", "border"); - let stylesheet = Style::new( - "\ - #border {stroke: none; fill: rgb(19, 17, 30); } - .hour path.hour-outline {stroke: rgb(0, 0, 0); stroke-width: 2px;} - .hour path.hour-name {stroke: none; fill: rgb(238, 187, 85);} - .hour path.utc {stroke: none; fill: rgb(91, 68, 38);} - .winter path {fill: rgb(70, 62, 108);} - .active.winter path.hour-outline {fill: rgb(100, 92, 138);} - .spring path {fill: rgb(55, 87, 55);} - .active.spring path.hour-outline {fill: rgb(85, 117, 85);} - .summer path {fill: rgb(113, 92, 43);} - .active.summer path.hour-outline {fill: rgb(143, 122, 73);} - .autumn path {fill: rgb(108, 68, 44);} - .active.autumn.hour-outline path {fill: rgb(138, 98, 74);} - .local-hour {stroke: none; fill: rgb(238, 187, 85);} - .night-time {stroke: none; fill: rgb(19, 17, 30);} - .blue-hour {stroke: none; fill: rgb(9, 1, 119);} - .golden-hour {stroke: none; fill: rgb(170, 132, 44);} - .day-time {stroke: none; fill: rgb(125, 197, 240);} - .marker {stroke: rgb(19, 17, 30); stroke-width: 2px; fill: none;} - .moon-background {stroke: rgb(170, 170, 170); stroke-width: 2px; fill: rgb(19, 17, 30);} - .moon {stroke: none; fill: rgb(170, 170, 170);} - .sun {stroke: none; fill: rgb(238, 187, 85);} - .mid-marker {stroke: red;} - .dial {stroke-width: 2px; stroke: rgb(238, 187, 85);} - #current-hour rect {stroke: none; fill: rgba(255, 255, 255, 0.5);} - #current-hour-name {font-weight: bold;}", - ); - - let mut local_clock = Group::new().set("id", "local-clock"); - - for hour in 0i32..24 { - let hour_str = match hour { - 0 => "Midnight".to_string(), - 12 => "Noon".to_string(), - _ => hour.to_string(), - }; - let rotation = hour * 15; - let hour_name = TextNode::new(hour_str); - let hour_node = Text::new() - .set("class", "local-hour") - .set("transform", format!("rotate({}, 350, 350)", 180 + rotation)) - .set("x", (IMAGE_WIDTH as f32) / 2.0) - .set( - "y", - (IMAGE_WIDTH as f32) / 2.0 - OUTER_R - local_hour_font_size / 2.0, - ) - .set("text-anchor", "middle") - .set("font-size", local_hour_font_size as f32) - .add(hour_name); - local_clock = local_clock.add(hour_node); - } - - let mut seasonal_clock = Group::new().set( - "transform", - format!( - "rotate({}, {}, {})", - utc_rotation, - IMAGE_WIDTH / 2, - IMAGE_WIDTH / 2 - ), - ); - - for hour in 0i32..24 { - seasonal_clock = seasonal_clock.add(hour_marker( - hour, - &hour_name_path_cache[hour as usize], - hour == utc_hour as i32, - )); - } - - let daytime_circle = Circle::new() - .set("class", "day-time") - .set("cx", IMAGE_WIDTH / 2) - .set("cy", IMAGE_WIDTH / 2) - .set("r", marker_radius); - - let day_parts_group: Option = if config.is_some() { - let sun_times = suncalc::get_times( - unixtime, - config.unwrap().latitude, - config.unwrap().longitude, - None, - ); - let noon = Utc - .timestamp_millis(sun_times.solar_noon.0) - .time() - .num_seconds_from_midnight() as i32; - let midnight = Utc - .timestamp_millis(sun_times.nadir.0) - .time() - .num_seconds_from_midnight() as i32; - let morning_golden_end = Utc - .timestamp_millis(sun_times.golden_hour_end.0) - .time() - .num_seconds_from_midnight() as i32; - let evening_golden_start = Utc - .timestamp_millis(sun_times.golden_hour.0) - .time() - .num_seconds_from_midnight() as i32; - let sunrise = Utc - .timestamp_millis(sun_times.sunrise.0) - .time() - .num_seconds_from_midnight() as i32; - let sunset = Utc - .timestamp_millis(sun_times.sunset.0) - .time() - .num_seconds_from_midnight() as i32; - let dawn = Utc - .timestamp_millis(sun_times.dawn.0) - .time() - .num_seconds_from_midnight() as i32; - let dusk = Utc - .timestamp_millis(sun_times.dusk.0) - .time() - .num_seconds_from_midnight() as i32; - - let golden_hour_path = get_range_path( - marker_radius, - "golden-hour", - morning_golden_end, - evening_golden_start, - ); - - let sun_disc = Circle::new() - .set("class", "sun") - .set("cx", IMAGE_WIDTH / 2) - .set("cy", IMAGE_WIDTH as f32 / 2.0 + OUTER_R / 2.0 + sun_radius) - .set("r", sun_radius) - .set( - "transform", - format!( - "rotate({}, {}, {})", - seconds_to_degrees(local_time) - utc_rotation, - IMAGE_WIDTH / 2, - IMAGE_WIDTH / 2 - ), - ); - - let blue_hour_path = get_range_path( - marker_radius, - "blue-hour", - sunrise, // morning_blue_end - sunset, // evening_blue_start - ); - - let nighttime_path = get_range_path(marker_radius, "night-time", dawn, dusk); - - let marker_circle = Circle::new() - .set("class", "marker") - .set("cx", IMAGE_WIDTH / 2) - .set("cy", IMAGE_WIDTH / 2) - .set("r", marker_radius); - - let noon_marker = Line::new() - .set("class", "mid-marker") - .set( - "transform", - format!( - "rotate({}, {}, {})", - seconds_to_degrees(noon), - IMAGE_WIDTH / 2, - IMAGE_WIDTH / 2 - ), - ) - .set("x1", IMAGE_WIDTH / 2) - .set( - "y1", - IMAGE_WIDTH as f32 / 2.0 + marker_radius - sun_radius as f32, - ) - .set("x2", IMAGE_WIDTH / 2) - .set("y2", IMAGE_WIDTH as f32 / 2.0 + marker_radius); - - let midnight_marker = Line::new() - .set("class", "mid-marker") - .set( - "transform", - format!( - "rotate({}, {}, {})", - seconds_to_degrees(midnight), - IMAGE_WIDTH / 2, - IMAGE_WIDTH / 2 - ), - ) - .set("x1", IMAGE_WIDTH / 2) - .set( - "y1", - IMAGE_WIDTH as f32 / 2.0 + marker_radius - sun_radius as f32, - ) - .set("x2", IMAGE_WIDTH / 2) - .set("y2", IMAGE_WIDTH as f32 / 2.0 + marker_radius); - - Some( - Group::new() - .set("id", "day-parts") - .set( - "transform", - format!( - "rotate({}, {}, {})", - utc_rotation, - IMAGE_WIDTH / 2, - IMAGE_WIDTH / 2 - ), - ) - .add(daytime_circle) - .add(golden_hour_path) - .add(sun_disc) - .add(blue_hour_path) - .add(nighttime_path) - .add(marker_circle) - .add(noon_marker) - .add(midnight_marker), - ) - } else { - None - }; - - let moon_circle = Circle::new() - .set("class", "moon-background") - .set("cx", IMAGE_WIDTH / 2) - .set("cy", IMAGE_WIDTH / 2) - .set("r", moon_radius); - - let moon_group = Group::new() - .set("id", "moon-container") - .add(moon_circle) - .add(get_moon_path(moon_radius, moon_phase)); - - let dial = Line::new() - .set("id", "dial") - .set("class", "dial") - .set( - "transform", - format!( - "rotate({}, {}, {})", - seconds_to_degrees(local_time), - IMAGE_WIDTH / 2, - IMAGE_WIDTH / 2 - ), - ) - .set("x1", IMAGE_WIDTH / 2) - .set("y1", IMAGE_WIDTH as f32 / 2.0 + OUTER_R * 0.5) - .set("x2", IMAGE_WIDTH / 2) - .set( - "y2", - IMAGE_WIDTH as f32 / 2.0 + OUTER_R - RING_WIDTH + HOUR_NAME_FONT_SIZE, - ); - - let current_box_width = (200f32 / 700f32) * IMAGE_WIDTH as f32; - let current_box_height = (100f32 / 700f32) * IMAGE_WIDTH as f32; - let current_hour_name_font_size = (40f32 / 700f32) * IMAGE_WIDTH as f32; - let current_time_font_size = (30f32 / 700f32) * IMAGE_WIDTH as f32; - - let current_hour_rect = Rectangle::new() - .set("x1", 0) - .set("y1", 0) - .set("width", current_box_width) - .set("height", current_box_height); - - let current_hour_name = Text::new() - .set("id", "current-hour-name") - .set("font-size", current_hour_name_font_size) - .set("text-anchor", "middle") - .set("dominant-baseline", "mathematical") - .set("x", current_box_width / 2.0) - .set( - "y", - (current_box_height / 5.0) + (current_hour_name_font_size / 2.0), - ) - .add(TextNode::new(HOUR_NAMES[utc_hour as usize])); - - let current_time_text = Text::new() - .set("font-size", current_time_font_size) - .set("text-anchor", "middle") - .set("dominant-baseline", "mathematical") - .set("x", current_box_width / 2.0) - .set("y", current_box_height - (current_time_font_size / 2.0)) - .add(TextNode::new(format!( - "{:02}:{:02}:{:02}", - local_hour, local_minute, local_second - ))); - - let top_pos = if (6..=18).contains(&local_hour) { - moon_radius * 1.5 // under the moon - } else { - 0.0 - moon_radius * 1.5 - current_box_height // above the moon - }; - - let current_hour_group = Group::new() - .set("id", "current-hour") - .set( - "transform", - format!( - "translate({}, {})", - IMAGE_WIDTH as f32 / 2.0 - current_box_width / 2.0, - IMAGE_WIDTH as f32 / 2.0 + top_pos - ), - ) - .add(current_hour_rect) - .add(current_hour_name) - .add(current_time_text); - - let mut document = Document::new() - .set("viewBox", (0i32, 0i32, 700i32, 700i32)) - .set("width", 700i32) - .set("height", 700i32) - .set("xmlns:xlink", "http://www.w3.org/1999/xlink") - .add(stylesheet) - .add(border) - .add(local_clock); - - if let Some(..) = day_parts_group { - document = document.add(day_parts_group.unwrap()); - } - - document - .add(seasonal_clock) - .add(moon_group) - .add(dial) - .add(current_hour_group) -} - fn main() { let xdg_dirs = xdg::BaseDirectories::new().unwrap(); let config_path = xdg_dirs diff --git a/src/svg_clock.rs b/src/svg_clock.rs index 4c2a264..7d308e5 100644 --- a/src/svg_clock.rs +++ b/src/svg_clock.rs @@ -1,25 +1,34 @@ use std::fmt; +use chrono::{ + prelude::{Local, Utc}, + TimeZone, Timelike, +}; use rctree::Node; use svg::{ node::{ - element::{path::Data as PathData, Definitions, Group, Path, Text, TextPath}, + element::{ + path::Data as PathData, Circle, Definitions, Group, Line, Path, Rectangle, Style, Text, + TextPath, + }, Text as TextNode, }, Document, }; use usvg::Tree; -pub const HOUR_NAMES: [&str; 24] = [ +use crate::config::Config; + +const HOUR_NAMES: [&str; 24] = [ "Candle", "Ice", "Comet", "Thimble", "Root", "Mist", "Sprout", "Rainbow", "Worm", "Bud", "Blossom", "Ladybug", "Geese", "Dust", "Peach", "Fog", "Acorn", "Gourd", "Soup", "Crow", "Mushroom", "Thunder", "Frost", "Lantern", ]; -pub const IMAGE_WIDTH: u32 = 700; -pub const HOUR_NAME_FONT_SIZE: f32 = IMAGE_WIDTH as f32 * 0.019109; -pub const OUTER_R: f32 = (IMAGE_WIDTH as f32) / 2.0 - 3.0 * HOUR_NAME_FONT_SIZE; -pub const RING_WIDTH: f32 = HOUR_NAME_FONT_SIZE * 3.0; -pub const UTC_HOUR_FONT_SIZE: f32 = IMAGE_WIDTH as f32 * 0.021462; +const IMAGE_WIDTH: u32 = 700; +const HOUR_NAME_FONT_SIZE: f32 = IMAGE_WIDTH as f32 * 0.019109; +const OUTER_R: f32 = (IMAGE_WIDTH as f32) / 2.0 - 3.0 * HOUR_NAME_FONT_SIZE; +const RING_WIDTH: f32 = HOUR_NAME_FONT_SIZE * 3.0; +const UTC_HOUR_FONT_SIZE: f32 = IMAGE_WIDTH as f32 * 0.021462; enum Season { Spring, @@ -43,7 +52,7 @@ impl fmt::Display for Season { } } -pub fn seconds_to_degrees(seconds: i32) -> f32 { +fn seconds_to_degrees(seconds: i32) -> f32 { seconds as f32 * 360.0 / 86400.0 } @@ -188,7 +197,7 @@ pub fn cache_hour_name_paths() -> [(PathData, PathData); 24] { ] } -pub fn hour_marker( +fn hour_marker( hour: i32, hour_name_path_data: &(PathData, PathData), is_current_hour: bool, @@ -257,7 +266,7 @@ pub fn hour_marker( .add(utc_hour_path) } -pub fn get_range_path(radius: f32, range_name: &str, start_time: i32, end_time: i32) -> Path { +fn get_range_path(radius: f32, range_name: &str, start_time: i32, end_time: i32) -> Path { let start_deg = seconds_to_degrees(start_time); let end_deg = seconds_to_degrees(end_time); let deg_diff = end_deg - start_deg; @@ -287,7 +296,7 @@ pub fn get_range_path(radius: f32, range_name: &str, start_time: i32, end_time: Path::new().set("class", range_name).set("d", path_data) } -pub fn get_moon_path(radius: f32, moon_phase: f64) -> Path { +fn get_moon_path(radius: f32, moon_phase: f64) -> Path { let handle_x_pos = radius as f64 * 1.34; let handle_y_pos = radius * 0.88; let min_x = IMAGE_WIDTH as f64 / 2.0 - handle_x_pos; @@ -326,3 +335,355 @@ pub fn get_moon_path(radius: f32, moon_phase: f64) -> Path { )); Path::new().set("class", "moon").set("d", path_data) } + +pub fn gen_svg( + config: &Option, + hour_name_path_cache: &[(PathData, PathData); 24], +) -> Document { + let local_timestamp = Local::now(); + let utc_hour = local_timestamp.with_timezone(&Utc).time().hour(); + let local_hour = local_timestamp.time().hour(); + let local_minute = local_timestamp.time().minute(); + let local_second = local_timestamp.time().second(); + + let utc_offset = local_timestamp.offset().local_minus_utc(); + let local_time = local_timestamp.time().num_seconds_from_midnight() as i32; + let utc_rotation = seconds_to_degrees(utc_offset); + + // Calculate the Moon phase + let unixtime = suncalc::Timestamp(local_timestamp.timestamp_millis()); + let moon_illumination = suncalc::moon_illumination(unixtime); + let moon_radius = IMAGE_WIDTH as f32 * 0.071428571; + let moon_phase = moon_illumination.phase * 28.0; + + let local_hour_font_size = IMAGE_WIDTH as f32 * 0.02357; + let sun_radius = IMAGE_WIDTH as f32 * 0.0142871; + let marker_radius = OUTER_R - RING_WIDTH - 2.0 * UTC_HOUR_FONT_SIZE; + + let border = Rectangle::new() + .set("x", 0i32) + .set("y", 0i32) + .set("width", 700i32) + .set("height", 700i32) + .set("id", "border"); + let stylesheet = Style::new( + "\ + #border {stroke: none; fill: rgb(19, 17, 30); } + .hour path.hour-outline {stroke: rgb(0, 0, 0); stroke-width: 2px;} + .hour path.hour-name {stroke: none; fill: rgb(238, 187, 85);} + .hour path.utc {stroke: none; fill: rgb(91, 68, 38);} + .winter path {fill: rgb(70, 62, 108);} + .active.winter path.hour-outline {fill: rgb(100, 92, 138);} + .spring path {fill: rgb(55, 87, 55);} + .active.spring path.hour-outline {fill: rgb(85, 117, 85);} + .summer path {fill: rgb(113, 92, 43);} + .active.summer path.hour-outline {fill: rgb(143, 122, 73);} + .autumn path {fill: rgb(108, 68, 44);} + .active.autumn.hour-outline path {fill: rgb(138, 98, 74);} + .local-hour {stroke: none; fill: rgb(238, 187, 85);} + .night-time {stroke: none; fill: rgb(19, 17, 30);} + .blue-hour {stroke: none; fill: rgb(9, 1, 119);} + .golden-hour {stroke: none; fill: rgb(170, 132, 44);} + .day-time {stroke: none; fill: rgb(125, 197, 240);} + .marker {stroke: rgb(19, 17, 30); stroke-width: 2px; fill: none;} + .moon-background {stroke: rgb(170, 170, 170); stroke-width: 2px; fill: rgb(19, 17, 30);} + .moon {stroke: none; fill: rgb(170, 170, 170);} + .sun {stroke: none; fill: rgb(238, 187, 85);} + .mid-marker {stroke: red;} + .dial {stroke-width: 2px; stroke: rgb(238, 187, 85);} + #current-hour rect {stroke: none; fill: rgba(255, 255, 255, 0.5);} + #current-hour-name {font-weight: bold;}", + ); + + let mut local_clock = Group::new().set("id", "local-clock"); + + for hour in 0i32..24 { + let hour_str = match hour { + 0 => "Midnight".to_string(), + 12 => "Noon".to_string(), + _ => hour.to_string(), + }; + let rotation = hour * 15; + let hour_name = TextNode::new(hour_str); + let hour_node = Text::new() + .set("class", "local-hour") + .set("transform", format!("rotate({}, 350, 350)", 180 + rotation)) + .set("x", (IMAGE_WIDTH as f32) / 2.0) + .set( + "y", + (IMAGE_WIDTH as f32) / 2.0 - OUTER_R - local_hour_font_size / 2.0, + ) + .set("text-anchor", "middle") + .set("font-size", local_hour_font_size as f32) + .add(hour_name); + local_clock = local_clock.add(hour_node); + } + + let mut seasonal_clock = Group::new().set( + "transform", + format!( + "rotate({}, {}, {})", + utc_rotation, + IMAGE_WIDTH / 2, + IMAGE_WIDTH / 2 + ), + ); + + for hour in 0i32..24 { + seasonal_clock = seasonal_clock.add(hour_marker( + hour, + &hour_name_path_cache[hour as usize], + hour == utc_hour as i32, + )); + } + + let daytime_circle = Circle::new() + .set("class", "day-time") + .set("cx", IMAGE_WIDTH / 2) + .set("cy", IMAGE_WIDTH / 2) + .set("r", marker_radius); + + let day_parts_group: Option = if config.is_some() { + let sun_times = suncalc::get_times( + unixtime, + config.unwrap().latitude, + config.unwrap().longitude, + None, + ); + let noon = Utc + .timestamp_millis(sun_times.solar_noon.0) + .time() + .num_seconds_from_midnight() as i32; + let midnight = Utc + .timestamp_millis(sun_times.nadir.0) + .time() + .num_seconds_from_midnight() as i32; + let morning_golden_end = Utc + .timestamp_millis(sun_times.golden_hour_end.0) + .time() + .num_seconds_from_midnight() as i32; + let evening_golden_start = Utc + .timestamp_millis(sun_times.golden_hour.0) + .time() + .num_seconds_from_midnight() as i32; + let sunrise = Utc + .timestamp_millis(sun_times.sunrise.0) + .time() + .num_seconds_from_midnight() as i32; + let sunset = Utc + .timestamp_millis(sun_times.sunset.0) + .time() + .num_seconds_from_midnight() as i32; + let dawn = Utc + .timestamp_millis(sun_times.dawn.0) + .time() + .num_seconds_from_midnight() as i32; + let dusk = Utc + .timestamp_millis(sun_times.dusk.0) + .time() + .num_seconds_from_midnight() as i32; + + let golden_hour_path = get_range_path( + marker_radius, + "golden-hour", + morning_golden_end, + evening_golden_start, + ); + + let sun_disc = Circle::new() + .set("class", "sun") + .set("cx", IMAGE_WIDTH / 2) + .set("cy", IMAGE_WIDTH as f32 / 2.0 + OUTER_R / 2.0 + sun_radius) + .set("r", sun_radius) + .set( + "transform", + format!( + "rotate({}, {}, {})", + seconds_to_degrees(local_time) - utc_rotation, + IMAGE_WIDTH / 2, + IMAGE_WIDTH / 2 + ), + ); + + let blue_hour_path = get_range_path( + marker_radius, + "blue-hour", + sunrise, // morning_blue_end + sunset, // evening_blue_start + ); + + let nighttime_path = get_range_path(marker_radius, "night-time", dawn, dusk); + + let marker_circle = Circle::new() + .set("class", "marker") + .set("cx", IMAGE_WIDTH / 2) + .set("cy", IMAGE_WIDTH / 2) + .set("r", marker_radius); + + let noon_marker = Line::new() + .set("class", "mid-marker") + .set( + "transform", + format!( + "rotate({}, {}, {})", + seconds_to_degrees(noon), + IMAGE_WIDTH / 2, + IMAGE_WIDTH / 2 + ), + ) + .set("x1", IMAGE_WIDTH / 2) + .set( + "y1", + IMAGE_WIDTH as f32 / 2.0 + marker_radius - sun_radius as f32, + ) + .set("x2", IMAGE_WIDTH / 2) + .set("y2", IMAGE_WIDTH as f32 / 2.0 + marker_radius); + + let midnight_marker = Line::new() + .set("class", "mid-marker") + .set( + "transform", + format!( + "rotate({}, {}, {})", + seconds_to_degrees(midnight), + IMAGE_WIDTH / 2, + IMAGE_WIDTH / 2 + ), + ) + .set("x1", IMAGE_WIDTH / 2) + .set( + "y1", + IMAGE_WIDTH as f32 / 2.0 + marker_radius - sun_radius as f32, + ) + .set("x2", IMAGE_WIDTH / 2) + .set("y2", IMAGE_WIDTH as f32 / 2.0 + marker_radius); + + Some( + Group::new() + .set("id", "day-parts") + .set( + "transform", + format!( + "rotate({}, {}, {})", + utc_rotation, + IMAGE_WIDTH / 2, + IMAGE_WIDTH / 2 + ), + ) + .add(daytime_circle) + .add(golden_hour_path) + .add(sun_disc) + .add(blue_hour_path) + .add(nighttime_path) + .add(marker_circle) + .add(noon_marker) + .add(midnight_marker), + ) + } else { + None + }; + + let moon_circle = Circle::new() + .set("class", "moon-background") + .set("cx", IMAGE_WIDTH / 2) + .set("cy", IMAGE_WIDTH / 2) + .set("r", moon_radius); + + let moon_group = Group::new() + .set("id", "moon-container") + .add(moon_circle) + .add(get_moon_path(moon_radius, moon_phase)); + + let dial = Line::new() + .set("id", "dial") + .set("class", "dial") + .set( + "transform", + format!( + "rotate({}, {}, {})", + seconds_to_degrees(local_time), + IMAGE_WIDTH / 2, + IMAGE_WIDTH / 2 + ), + ) + .set("x1", IMAGE_WIDTH / 2) + .set("y1", IMAGE_WIDTH as f32 / 2.0 + OUTER_R * 0.5) + .set("x2", IMAGE_WIDTH / 2) + .set( + "y2", + IMAGE_WIDTH as f32 / 2.0 + OUTER_R - RING_WIDTH + HOUR_NAME_FONT_SIZE, + ); + + let current_box_width = (200f32 / 700f32) * IMAGE_WIDTH as f32; + let current_box_height = (100f32 / 700f32) * IMAGE_WIDTH as f32; + let current_hour_name_font_size = (40f32 / 700f32) * IMAGE_WIDTH as f32; + let current_time_font_size = (30f32 / 700f32) * IMAGE_WIDTH as f32; + + let current_hour_rect = Rectangle::new() + .set("x1", 0) + .set("y1", 0) + .set("width", current_box_width) + .set("height", current_box_height); + + let current_hour_name = Text::new() + .set("id", "current-hour-name") + .set("font-size", current_hour_name_font_size) + .set("text-anchor", "middle") + .set("dominant-baseline", "mathematical") + .set("x", current_box_width / 2.0) + .set( + "y", + (current_box_height / 5.0) + (current_hour_name_font_size / 2.0), + ) + .add(TextNode::new(HOUR_NAMES[utc_hour as usize])); + + let current_time_text = Text::new() + .set("font-size", current_time_font_size) + .set("text-anchor", "middle") + .set("dominant-baseline", "mathematical") + .set("x", current_box_width / 2.0) + .set("y", current_box_height - (current_time_font_size / 2.0)) + .add(TextNode::new(format!( + "{:02}:{:02}:{:02}", + local_hour, local_minute, local_second + ))); + + let top_pos = if (6..=18).contains(&local_hour) { + moon_radius * 1.5 // under the moon + } else { + 0.0 - moon_radius * 1.5 - current_box_height // above the moon + }; + + let current_hour_group = Group::new() + .set("id", "current-hour") + .set( + "transform", + format!( + "translate({}, {})", + IMAGE_WIDTH as f32 / 2.0 - current_box_width / 2.0, + IMAGE_WIDTH as f32 / 2.0 + top_pos + ), + ) + .add(current_hour_rect) + .add(current_hour_name) + .add(current_time_text); + + let mut document = Document::new() + .set("viewBox", (0i32, 0i32, 700i32, 700i32)) + .set("width", 700i32) + .set("height", 700i32) + .set("xmlns:xlink", "http://www.w3.org/1999/xlink") + .add(stylesheet) + .add(border) + .add(local_clock); + + if let Some(..) = day_parts_group { + document = document.add(day_parts_group.unwrap()); + } + + document + .add(seasonal_clock) + .add(moon_group) + .add(dial) + .add(current_hour_group) +}