1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

整数からRGB空間の色を生成する(その1)

Last updated at Posted at 2024-04-28

動機

2024/04/25開催の春和の候!若手エンジニアふんわりLT Night!に参加した折、ひなさんのLT「友達にコード送ったら1行にされた」をお聞きしました。

LTではHSL空間を使ってエレガントに一行にされていたのですが、あえて RGB のままコードをどこまで短縮できるか試してみました。

この記事のコード辺は効率やメンテナンス性を無視しているので、本番で使ってはいけません。

大まかに色コードを指定できればよさそうだったので、元コードと境界値条件が合っているかは確認していません。間違いがあれば編集リクエストをお願いします。

これが元コードです。(https://speakerdeck.com/mi111025/0425?slide=4 より引用)

まずは今回のゴルフ対象になる関数を次のように決めます。

base code
const getColor = (range) => {
    let r, g, b;
    if (range >= 0 && range <= 17) {
        r = 255;
        g = range * 15;
        b = 0;
    } else if (range >= 18 && range <= 33) {
        r = 255 - (range - 17) * 15;
        g = 255;
        b = 0;
    } else if (range >= 34 && range <= 50) {
        r = 0;
        g = 255;
        b = (range - 34) * 15;
    } else if (range >= 51 && range <= 67) {
        r = 0;
        g = 255 - (range - 50) * 15;
        b = 255;
    } else if (range >= 68 && range <= 83) {
        r = (range - 68) * 15;
        g = 0;
        b = 255;
    } else if (range >= 84 && range <= 100) {
        r = 255;
        g = 0;
        b = 255 - (range - 84) * 15;
    }
    return `rgb(${r}, ${g}, ${b})`;
};

案1

pattern 1
let x=17,c=v=>`rgb(${[[x,v,0],[x-v%x,x,0],[0,x,v%x],[0,x-v%x,x],[v%x,0,x],[x,0,x-v%x]][v/x|0].map(c=>c*15).join()})`

// 116 characters
// c(40) -> 'rgb(0,255,90)'

元コードのロジックを愚直に書き出したパターンです。順にみていきましょう。

RGBの各値を配列で持つ

r, g, b の変数宣言をしないで、[r, g, b] の配列で持つようにします。
返り値のカンマ区切りの値は、配列を join() すれば生成できます。

この時点でのコード
const getColor = (range) => {
    let result;
    if (range >= 0 && range <= 17) {
        result = [255, range * 15, 0];
    } else if (range >= 18 && range <= 33) {
        result = [255 - (range - 17) * 15, 255, 0];
    } else if (range >= 34 && range <= 50) {
        result = [0, 255, (range - 34) * 15];
    } else if (range >= 51 && range <= 67) {
        result = [0, 255 - (range - 50) * 15, 255];
    } else if (range >= 68 && range <= 83) {
        result = [(range - 68) * 15, 0, 255];
    } else if (range >= 84 && range <= 100) {
        result = [255, 0, 255 - (range - 84) * 15];
    }
    return `rgb(${result.join()})`;
};

整数剰余を使う

元コードの if 分岐部分を書き換えます。

15 にかけられている range の範囲を、[0, 17) に収めたいようです。
こういう時は割り算のあまりを計算する % が使えます。

合わせて、境界値を17単位に変更します(次の項目に効いてきます)。
※ どこの分岐に入るかが変わりますが、出来上がる result は変わりません。

この時点でのコード
const getColor = (range) => {
    let result;
    if (range >= 0 && range < 17) {
        result = [255, range * 15, 0];
    } else if (range >= 17 && range < 34) {
        result = [255 - (range % 17) * 15, 255, 0];
    } else if (range >= 34 && range < 51) {
        result = [0, 255, (range % 17) * 15];
    } else if (range >= 51 && range < 68) {
        result = [0, 255 - (range % 17) * 15, 255];
    } else if (range >= 68 && range < 84) {
        result = [(range % 17) * 15, 0, 255];
    } else if (range >= 84 && range <= 100) {
        result = [255, 0, 255 - (range % 17) * 15];
    }
    return `rgb(${result.join()})`;
};

条件をまとめる

if 文の条件には range の範囲が指定されていますが、range を17で割った商の整数部分を見ることで、&& なしで同じ判定ができます。
これをやるために、前項で境界値を 17 単位にいじっておいたのです。

Math.trunc は、引数の整数部分を返す関数です。

この時点でのコード
const getColor = (range) => {
    let result;
    switch (Math.trunc(range / 17)) {
        case 0:
            result = [255, range * 15, 0];
            break;
        case 1:
            result = [255 - (range % 17) * 15, 255, 0];
            break;
        case 2:
            result = [0, 255, (range % 17) * 15];
            break;
        case 3:
            result = [0, 255 - (range % 17) * 15, 255];
            break;
        case 4:
            result = [(range % 17) * 15, 0, 255];
            break;
        case 5:
            result = [255, 0, 255 - (range % 17) * 15];
            break;
    }
    return `rgb(${result.join()})`;
};

result を直接得る

case は整数だけなので、配列で置き換えます。かなり完成形に近づいてきました。

この辺りから、製品コードでは使えないテクニックになってきます。

この時点でのコード
const getColor = (range) => {
    const result = [
        [255, range * 15, 0],
        [255 - (range % 17) * 15, 255, 0],
        [0, 255, (range % 17) * 15],
        [0, 255 - (range % 17) * 15, 255],
        [(range % 17) * 15, 0, 255],
        [255, 0, 255 - (range % 17) * 15],
    ][Math.trunc(range / 17)];
    return `rgb(${result.join()})`;
};

全体を15で割る

プログラム中に 255 がたくさん出てきています。
255 = 17 * 15 なので、result 全体を 15 で割ると短縮できそうです。
もちろんそのままだと値が壊れるので、最後に15倍して戻してあげます。

さらに、不要な括弧を取り除いて短縮します。

この時点でのコード
const getColor = (range) => {
    const result = [
        [17, range, 0],
        [17 - range % 17, 17, 0],
        [0, 17, range % 17],
        [0, 17 - range % 17, 17],
        [range % 17, 0, 17],
        [17, 0, 17 - range % 17],
    ][Math.trunc(range / 17)];
    return `rgb(${result.map(c => c * 15).join()})`;
};

Math.trunc を置き換える

先ほど追加した整数化関数ですが、長いのでイディオムに変えます。

整数化には何通りかやり方がありますが、最短は次の2つです。
どちらも JS のビット演算が整数部のみを対象にすることを利用します。

  • x|0
    • ビットOR演算
  • ~~x
    • ビットNOT演算を2回

もちろん、プロダクションコードで整数化のために使うべきではありません。

演算子の優先順位上、今回は前者を採用します(後者では ~~range / 17 と書くと (~~range) / 17 になってしまう)。

この時点でのコード
const getColor = (range) => {
    const result = [
        [17, range, 0],
        [17 - range % 17, 17, 0],
        [0, 17, range % 17],
        [0, 17 - range % 17, 17],
        [range % 17, 0, 17],
        [17, 0, 17 - range % 17],
    ][range / 17 | 0];
    return `rgb(${result.map(c => c * 15).join()})`;
};

マジックナンバーを置換する

今度は 17 がたくさん出てきているので、変数で持ちます。
1箇所あたり1文字短縮できます。

この時点でのコード
const x=17,getColor = (range) => {
    const result = [
        [x, range, 0],
        [x - range % x, x, 0],
        [0, x, range % x],
        [0, x - range % x, x],
        [range % x, 0, x],
        [x, 0, x - range % x],
    ][range / x | 0];
    return `rgb(${result.map(c => c * 15).join()})`;
};

スコープを汚染していますが気にしません。

文字列を直接返す

短い記法のアロー関数を使えるように、const result を削ります。

この時点でのコード
const x=17,getColor = (range) => `rgb(${[
        [x, range, 0],
        [x - range % x, x, 0],
        [0, x, range % x],
        [0, x - range % x, x],
        [range % x, 0, x],
        [x, 0, x - range % x],
    ][range / x | 0].map(c => c * 15).join()})`;

変数名を置換

変数名を1文字にします。

この時点でのコード
const x=17,c = v => `rgb(${[
        [x, v, 0],
        [x - v % x, x, 0],
        [0, x, v % x],
        [0, x - v % x, x],
        [v % x, 0, x],
        [x, 0, x - v % x],
    ][v / x | 0].map(c => c * 15).join()})`;

ここから不要なホワイトスペース文字を削ると、最初のコードを得られます!

終わりに

頭の体操としてのコードゴルフは楽しいですね。
まだまだ初心者なので、もっと短くできる方法があれば知りたいです。

この記事を書いている間に案2を思いついたので、改めて投稿します。

1
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?