色相環のグラデーション画像を出力するコードのメモ

アナログ時計の文字盤周りの装飾を追加するために画像を用意した。時計については以下に書いていた。

RustのBevyクレートでアナログ時計を作る
RustのBevyクレートを使ってアナログ時計を作ったので記録を残したい。 基本的にはドキュメントを参考に BevyはECSというパラダイム、概念を使っている。まずはこれを理解する。これについては公式のチュートリアルを見ればすぐわかる: 非...

色相環のグラデーション

文字盤の周りにリングを追加した。

上記リンクで使っているbevyではmaterialに対してグラデーションをかけるのはちょっと面倒そうに感じてしまったので、そんな感じの画像を用意した。このような感じ:

生成画像のサンプル

rustでの実装例

透過度というかアルファチャンネルを変えてアンチエイリアスを利かせている。

imageクレートでサクッと作れて大変ありがたい。

コードは以下:

use std::f32::consts::PI;

use image::{ImageBuffer, Rgba};

fn main() {
    println!("widthとantialias_powを変えるとフレーバーが変わる");
    let mut imgbuf = ImageBuffer::new(512, 512);

    let (cx, cy) = (256f32, 256f32);
    let radius = 240f32;
    let width = 10;
    let antialias_pow = 1;
    let r_min = radius - width as f32 / 2.0;
    let r_max = radius + width as f32 / 2.0;

    for (x, y, p) in imgbuf.enumerate_pixels_mut() {
        let (px, py) = (x as f32 - cx, y as f32 - cy);
        let from_o = (px.powi(2) + py.powi(2)) as f32;
        let from_o = from_o.sqrt();
        if r_min < from_o && from_o < r_max {
            // hue求めてセット
            let hue = px.atan2(-py);
            let hue = if hue < 0.0 { hue + 2.0 * PI } else { hue };
            let out_ratio = 1.0 - (radius - from_o).abs() / (width as f32 / 2.0);
            let alpha = out_ratio.powi(antialias_pow);
            *p = Rgba(hsl_to_rgb(hue.to_degrees(), 1.0, 0.5, alpha).into());
        } else {
            // 他は透明がいい
            *p = Rgba([0, 0, 0, 0]);
        }
    }
    // 適当なファイルパスを決めて出力
    let filepath = format!("z://a_w{}_a{}.png", width, antialias_pow);
    imgbuf.save(filepath).unwrap();
}

/// ref: https://stackoverflow.com/a/9493060
///
/// 上記をほぼそのまま移植したもの
///
/// hue range is `[0.0, 360.0]`.
/// sat and lig range are `[0.0, 1.0]`.
fn hsl_to_rgb(h: f32, s: f32, l: f32, a: f32) -> (u8, u8, u8, u8) {
    let h = h / 360.0;
    let (r, g, b): (f32, f32, f32);
    if s == 0.0 {
        (r, g, b) = (l, l, l);
    } else {
        let q = if l < 0.5 {
            l * (1.0 + s)
        } else {
            l + s - l * s
        };
        let p = 2.0 * l - q;
        r = hue_to_rgb(p, q, h + 1.0 / 3.0);
        g = hue_to_rgb(p, q, h);
        b = hue_to_rgb(p, q, h - 1.0 / 3.0);
    }
    (
        (r * 255f32) as u8,
        (g * 255f32) as u8,
        (b * 255f32) as u8,
        (a * 255f32) as u8,
    )
}

/// こっちはそのまま移植
///
/// return value range is [0,1]
fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
    if t < 0.0 {
        t += 1.0;
    }
    if t > 1.0 {
        t -= 1.0;
    }
    if t < 1.0 / 6.0 {
        return p + (q - p) * 6.0 * t;
    }
    if t < 1.0 / 2.0 {
        return q;
    }
    if t < 2.0 / 3.0 {
        return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
    }
    p
}

#[cfg(test)]
mod test {
    use crate::hsl_to_rgb;

    #[test]
    fn test_hsl_to_rgb1() {
        let res = hsl_to_rgb(0.0, 1.0, 0.5, 1.0);
        assert_eq!(res, (255, 0, 0, 255));
    }
    #[test]
    fn test_hsl_to_rgb2() {
        let res = hsl_to_rgb(168.0, 0.56, 0.36, 1.0);
        assert_eq!(res, (40, 143, 122, 255));
    }
}

おわり

使っている画像ビューアソフトで透過がおかしくなっていて、あれれーおかしいぞーとなっていた。 しかし、VSCodeで見るとちゃんと透過されていたので、画像ファイルの形式がおかしいか、ビューアソフトが対応していない形式かのどちらかだろうと思った。

test_hsl_to_rgb2でテストしているhslの値は、w3cのツールで見ると、rgbのg(だったかな)が1だけずれているが気にしないことにした。

以上です。

タイトルとURLをコピーしました