Support for dynamically generated SVG images

Currently the only way to create svg Handle is either a file or static bytes.
This doesn’t allow creating svg that is generated on the fly from tools like svgbob

I want to create a PR that supports this feature. Would the members of this community approve of such PR?

1 Like

Use Handle::from_memory to create svgs from data generated at runtime: iced/core/src/svg.rs at master · iced-rs/iced · GitHub

Yes, I’m aware of that function, but it needs the bytes to have a 'static lifetime, which is not possible with Vec<u8>.

That’s not quite right, you can generate svgs at runtime as the comment in the source code alludes to.

The following assumes you’ve installed iced with iced = { version = "0.13.1", features = ["async-std", "svg"] } (or tokio in lieu of async-std, for the rotation animation)

use iced::widget::{button, center, column, container, horizontal_space, row, slider, svg, text};
use iced::{Element, Fill, Length, Subscription};
use std::f64::consts::PI;
use std::time::Duration;

pub fn main() -> iced::Result {
    iced::application("iced svg polygon", State::update, State::view)
        .subscription(State::subscription)
        .run()
}

#[derive(Debug)]
struct State {
    edges: u32,
    hue: f32,
    saturation: f32,
    brightness: f32,
    rotation: f32,
    playing: bool,
}

impl Default for State {
    fn default() -> Self {
        Self {
            edges: 5,
            hue: 0.0,
            saturation: 100.0,
            brightness: 100.0,
            rotation: 0.0,
            playing: false,
        }
    }
}

#[derive(Debug, Clone)]
pub enum Message {
    EdgesChanged(u32),
    Hue(f32),
    Saturation(f32),
    Brightness(f32),
    Tick,
    PlayPause,
}

impl State {
    fn update(&mut self, message: Message) {
        match message {
            Message::PlayPause => {
                self.playing = !self.playing;
            }
            Message::EdgesChanged(edges) => {
                self.edges = edges.max(3); // Minimum 3 edges for a polygon
            }
            Message::Hue(hue) => {
                self.hue = hue;
            }
            Message::Saturation(saturation) => {
                self.saturation = saturation;
            }
            Message::Brightness(brightness) => {
                self.brightness = brightness;
            }
            Message::Tick => {
                self.rotation = (self.rotation + 0.5) % 360.0;
            }
        }
    }

    fn generate_polygon_svg(&self) -> String {
        let radius = 100.0;
        let center_x = 150.0;
        let center_y = 150.0;
        let (fill_color, stroke_color) = self.get_colors();

        // Generate points for the polygon
        let points: Vec<(f64, f64)> = (0..self.edges)
            .map(|i| {
                let angle = (i as f64 * 2.0 * PI) / self.edges as f64;
                let x = center_x + radius * angle.cos();
                let y = center_y + radius * angle.sin();
                (x, y)
            })
            .collect();

        // Create SVG path
        let path_data = points
            .iter()
            .enumerate()
            .map(|(i, (x, y))| {
                if i == 0 {
                    format!("M {:.1} {:.1}", x, y)
                } else {
                    format!("L {:.1} {:.1}", x, y)
                }
            })
            .collect::<Vec<_>>()
            .join(" ");

        // Complete SVG with viewBox, path, and rotation transform
        format!(
            r#"<?xml version="1.0" encoding="UTF-8"?>
            <svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
                <g transform="rotate({} 150 150)">
                    <path d="{} Z" 
                          fill="{}" 
                          stroke="{}" 
                          stroke-width="2"/>
                </g>
            </svg>"#,
            self.rotation, path_data, fill_color, stroke_color
        )
    }

    fn get_colors(&self) -> (String, String) {
        let (r, g, b) = hsb_to_rgb(self.hue, self.saturation, self.brightness);
        let (stroke_r, stroke_g, stroke_b) = hsb_to_rgb(
            self.hue,
            self.saturation,
            self.brightness * 0.7, // Darker stroke
        );

        let fill = format!(
            "rgba({},{},{},0.8)", // 80% opacity for fill
            (r * 255.0) as u8,
            (g * 255.0) as u8,
            (b * 255.0) as u8
        );

        let stroke = format!(
            "rgb({},{},{})",
            (stroke_r * 255.0) as u8,
            (stroke_g * 255.0) as u8,
            (stroke_b * 255.0) as u8
        );

        (fill, stroke)
    }

    fn view(&self) -> Element<Message> {
        // Generate SVG data
        let svg_data = self.generate_polygon_svg();

        // Create handle from memory
        let handle = svg::Handle::from_memory(svg_data.into_bytes());

        let svg = container(
            svg(handle)
                .width(Length::Fixed(300.0))
                .height(Length::Fixed(300.0)),
        )
        .style(container::rounded_box)
        .center(Fill);

        let edges = slider(3..=12, self.edges, Message::EdgesChanged);

        let edge_controls = container(
            column![
                row![
                    text("Number of edges:").size(20),
                    horizontal_space(),
                    button(text(if !self.playing { "Play" } else { "Pause" }))
                        .on_press(Message::PlayPause),
                ]
                .spacing(5),
                edges,
            ]
            .spacing(10),
        )
        .center_x(Fill);

        let hsb_controls = container(
            column![
                row![text("Color:").size(20),],
                row![
                    text(format!("Hue: {:.1}", self.hue)).size(15).width(Fill),
                    text(format!("Saturation: {:.1}", self.saturation))
                        .size(15)
                        .width(Fill),
                    text(format!("Brightness: {:.1}", self.brightness))
                        .size(15)
                        .width(Fill)
                ]
                .spacing(5),
                row![
                    slider(0.0..=360.0, self.hue, Message::Hue),
                    slider(0.0..=100.0, self.saturation, Message::Saturation),
                    slider(0.0..=100.0, self.brightness, Message::Brightness)
                ]
                .spacing(5)
            ]
            .spacing(10),
        );

        center(
            column![edge_controls, svg, hsb_controls]
                .spacing(20)
                .height(Fill),
        )
        .padding(20)
        .into()
    }

    fn subscription(&self) -> Subscription<Message> {
        if self.playing {
            iced::time::every(Duration::from_millis(10)).map(|_| Message::Tick)
        } else {
            Subscription::none()
        }
    }
}

fn hsb_to_rgb(h: f32, s: f32, b: f32) -> (f32, f32, f32) {
    let h = h / 360.0;
    let s = s / 100.0;
    let b = b / 100.0;

    let i = (h * 6.0).floor();
    let f = h * 6.0 - i;
    let p = b * (1.0 - s);
    let q = b * (1.0 - f * s);
    let t = b * (1.0 - (1.0 - f) * s);

    match (i % 6.0) as i32 {
        0 => (b, t, p),
        1 => (q, b, p),
        2 => (p, b, t),
        3 => (p, q, b),
        4 => (t, p, b),
        _ => (b, p, q),
    }
}

Oh nice, I didn’t realize that was possible. Thanks @airstrike!

This comes from my misunderstanding with Cow<'static, [u8]>.
I thought it meant a fixed array of u8 or a borrowed array of u8 with a 'static lifetime (embedded in the binary).

My first attempt was using:

let svg_handle = svg::Handle::from_memory(&data);

which of course would error into:

the trait `From<&std::string::String>` is not implemented for `Cow<'static, [u8]>`, which is required by `&std::string::String: Into<Cow<'static, [u8]>>`

I didn’t bother converting it into Vec<u8> via:

let svg_handle = svg::Handle::from_memory(data.into_bytes());

which would have done it, due to my misunderstanding with aCow.

1 Like

My pleasure, happy to help! My understanding of lifetimes is fuzzy at best, but I once came across this resource which you might also find worth reading:

1 Like