このサイトは、只今WEB業界で活躍中のデザイナー、プログラマーの方々の情報を集めたweb統合情報サイトです。

Archives Details

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その3 egui で組む 3 パネル UI とバンクビュー

Rust

2026.09.26

どもです。

前回は CHR の 2BPP フォーマットとパレットという、バイト列の話でした。今回からいよいよ egui の話に入ります。

「egui って即時モードだけど、ちゃんとしたエディタっぽい UI が組めるの?」というのは自分も最初に気になったところでして。結論から言うと、パネルレイアウト・スクロール・テクスチャ表示・ドラッグ & ドロップあたりは全部素直に組めました。

ということで、今回は R-CHR の 3 パネルレイアウト、バンクビュー(CHR 全体表示)、情報パネル、キーボード操作の実装をまとめた形になります。

連載の目次

  1. 概要とアーキテクチャ
  2. NES の CHR フォーマット(2BPP)と iNES ヘッダ、パレット
  3. egui で組む 3 パネル UI とバンクビュー(この記事)
  4. ドットエディタの基礎とアンドゥ設計
  5. 図形ツール(Bresenham・楕円・塗りつぶし・スタンプ)
  6. PNG インポートと 3 つのマッピング戦略
  7. macOS ネイティブメニュー・多言語対応・GitHub Actions でのリリース

即時モード GUI とは

egui は 即時モード(immediate mode) の GUI ライブラリです。

普通の GUI フレームワーク(保持モード)では、ボタンやパネルといったウィジェットのオブジェクトを作ってツリーに登録し、イベントが来たらコールバックで状態を変える、という作りになります。

即時モードはそうではなく、毎フレーム update() が呼ばれて、その中で「今の状態」から UI を全部描き直すというスタイルです。ボタンは if ui.button("保存").clicked() { ... } のように、描画とイベント判定が同じ行で完結します。

impl eframe::App for RChrApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        // ここが毎フレーム呼ばれる。self の状態を見て UI を描く
    }
}

ウィジェットの状態を保持するオブジェクトが無いので、アプリの状態は全部自前の構造体(R-CHR なら RChrApp)に持つことになります。「状態はここに全部ある」というのがはっきりしているので、ドットエディタのように「データが変わったら画面も変わる」が当たり前のアプリだと、むしろ考えることが減って楽です。

エントリポイント

main.rs は短いです。

fn main() -> eframe::Result {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_title("R-CHR")
            .with_inner_size([1200.0, 720.0])
            .with_icon(load_icon()),
        ..Default::default()
    };

    eframe::run_native(
        "R-CHR",
        options,
        Box::new(|cc| {
            // macOS: NSApp が初期化された後(ここ)でネイティブメニューを構築し外観を設定する
            #[cfg(target_os = "macos")]
            {
                native_menu::init();
                native_menu::set_app_appearance(true); // デフォルトはダークモード
            }

            egui_extras::install_image_loaders(&cc.egui_ctx);
            editor::setup::setup_fonts(&cc.egui_ctx);
            Ok(Box::new(editor::app::RChrApp::default()))
        }),
    )
}

eframe::run_native() にウィンドウ設定とアプリ生成クロージャを渡すだけ。クロージャの中でやっているのは 3 つです。

  • macOS ネイティブメニューの初期化(最終回で解説)
  • egui_extras::install_image_loaders():ツールバーの SVG アイコンを egui::Image で読めるようにする
  • setup_fonts():日本語フォントの登録
日本語フォントは自分で埋め込む

egui のデフォルトフォントには CJK が入っていないので、日本語 UI にするならフォントを自分で登録する必要があります。R-CHR では Noto Sans JP を include_bytes! で埋め込んでいます。

pub fn setup_fonts(ctx: &egui::Context) {
    let mut fonts = egui::FontDefinitions::default();

    // Noto Sans JP Regular — 本文フォント
    fonts.font_data.insert(
        "noto_regular".to_owned(),
        egui::FontData::from_static(include_bytes!(
            "../../assets/fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf"
        )).into(),
    );
    fonts.families.get_mut(&egui::FontFamily::Proportional).unwrap().insert(0, "noto_regular".to_owned());
    fonts.families.get_mut(&egui::FontFamily::Monospace).unwrap().push("noto_regular".to_owned());

    // Noto Sans JP Bold — bold_font named family
    fonts.font_data.insert(
        "noto_bold".to_owned(),
        egui::FontData::from_static(include_bytes!(
            "../../assets/fonts/Noto_Sans_JP/static/NotoSansJP-Bold.ttf"
        )).into(),
    );
    fonts.families.insert(
        egui::FontFamily::Name(theme::BOLD_FONT.into()),
        vec!["noto_bold".to_owned()],
    );

    ctx.set_fonts(fonts);
}

egui にはフォントウェイトの概念が無いので、太字は「bold_font という名前のフォントファミリーを別に登録して、そのファミリーを指定する」形で実現しています。theme.rs に font_label() や rich_label() のようなヘルパーを用意して、セクション見出しは常にそれを通すようにしました。

update() の流れ

update() の中でやっていることを上から順に並べるとこうなります。

update() 呼び出し(1フレーム)
│
├── Visuals 設定(dark / light)
├── [macOS] handle_native_menu()      … ネイティブメニューのイベント処理
├── テクスチャ再生成(texture_dirty フラグ)
├── ウィンドウ閉じるリクエスト処理     … 未保存なら確認ダイアログ
├── タイトルバー更新(未保存変更を * で表示)
├── [非 macOS] show_menu_bar()        … egui のメニューバー
├── 1px ボーダー(TopBottomPanel)
├── 右パネル: show_info_panel()
├── 中央パネル: show_dot_editor()      … Option を返す
├── バンクビュー: show_bank_view()
├── EditorAction 適用(apply_action)
├── PNG インポートダイアログ
├── ドラッグ & ドロップ検知
├── About ダイアログ(egui::Window)
└── handle_keyboard()

app.rs にはこの骨格だけを書いて、各パネルの中身は bank_view.rs / dot_editor.rs / info_panel.rs の impl RChrApp に分けています。

3 パネルレイアウト

egui のパネルは SidePanel / TopBottomPanel / CentralPanel の 3 種類で、先に宣言したパネルから順に領域を確保していき、最後に CentralPanel が残りを全部取るというルールです。

R-CHR は「右に情報パネル、その左にドットエディタ、残りがバンクビュー」なので、右から順に宣言します。

// ── 右パネル(情報・描画色・パレット - 245px固定)
egui::SidePanel::right("info_panel")
    .resizable(false)
    .exact_width(245.0)
    .frame(egui::Frame::side_top_panel(&ctx.style()).inner_margin(egui::Margin::symmetric(12, 8)).fill(theme::COL_PANEL_BG))
    .show(ctx, |ui| {
        self.show_info_panel(ui);
    });

// ── 中央パネル(ドットエディタ)
let mut editor_action: Option = None;
let dot_max_w = (ctx.screen_rect().width() - 245.0 - theme::BANK_VIEW_MIN_W).max(180.0);
egui::SidePanel::right("dot_editor_panel")
    .resizable(true)
    .default_width(420.0)
    .min_width(180.0)
    .max_width(dot_max_w)
    .frame(egui::Frame::side_top_panel(&ctx.style()).inner_margin(egui::Margin::ZERO))
    .show(ctx, |ui| {
        editor_action = self.show_dot_editor(ui);
    });

// ── バンクビュー(メイン)
egui::CentralPanel::default()
    .frame(egui::Frame::central_panel(&ctx.style()).inner_margin(egui::Margin::ZERO))
    .show(ctx, |ui| {
        // ...
        self.show_bank_view(ui);
    });

ドットエディタも SidePanel::right にして resizable(true) を付けているのがポイントで、これで境界をドラッグして幅を変えられます。max_width を「画面幅 − 情報パネル 245px − バンクビュー最低幅 410px」で計算しているのは、ドットエディタを広げすぎてバンクビューが潰れないようにするためです。

バンクビュー:CHR 全体をテクスチャで表示する

ここが今回のメインです。

前回の render_full_image() で CHR 全体を幅 128px の RGBA 画像にしました。これを毎フレーム作り直すのはさすがに無駄なので、GPU テクスチャとしてキャッシュして、変更があったフレームだけ再生成します。

if self.texture_dirty {
    if let Some(rom) = &self.rom {
        if !rom.chr_data().is_empty() {
            let image = render_full_image(
                rom.chr_data(),
                &self.dat_palette,
                self.selected_palette_set,
                &self.master_palette,
            );
            self.bank_texture = Some(ctx.load_texture(
                "bank_view",
                image,
                egui::TextureOptions::NEAREST,
            ));
        }
    }
    self.texture_dirty = false;
}

texture_dirty フラグは、ドットを塗った・パレットを変えた・ファイルを開いた、といったタイミングで true にします。あとは update() の冒頭でこのブロックが拾ってくれる、という形です。

TextureOptions::NEAREST は必須です。デフォルトの線形補間だと拡大したときにドットがぼやけてしまって、ドット絵エディタとしては話にならないので。

整数倍スケール

表示倍率はウィンドウ幅に合わせて自動で決めますが、必ず整数倍にしています。

let available_w = ui.available_width();
let scale = (available_w / 128.0).floor().max(1.0);
let tile_px = 8.0 * scale; // 1 タイルの表示サイズ(px)

128px 幅の画像を「利用可能幅 ÷ 128 の切り捨て」倍で表示する。中途半端な倍率だとドットの境界が滲むので、ここも NEAREST と同じ理由です。

ScrollArea の中にテクスチャを置く

スクロールは egui::ScrollArea::vertical() にお任せです。その中で画像サイズぶんの領域を allocate_exact_size() で確保し、painter.image() でテクスチャを貼ります。

let mut scroll_area = egui::ScrollArea::vertical()
    .id_salt("bank_scroll")
    .auto_shrink([false, false]);

if let Some(addr) = self.pending_scroll_addr.take() {
    let row = addr / 0x100;
    scroll_area = scroll_area.vertical_scroll_offset(row as f32 * tile_px);
}

scroll_area.show(ui, |ui| {
    let (rect, response) = ui.allocate_exact_size(
        egui::vec2(display_w, display_h),
        egui::Sense::click(),
    );

    // テクスチャ描画
    let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
    ui.painter().image(texture_id, rect, uv, egui::Color32::WHITE);
    // ...
});

pending_scroll_addr は「次のフレームでここまでスクロールしてほしい」という予約で、アドレスジャンプや矢印キー移動で Some(アドレス) をセットしておくと、次の show_bank_view() で vertical_scroll_offset() に変換されて消費されます。「1 行 = 0x100 バイト」なので addr / 0x100 が行番号、それに tile_px を掛ければピクセル位置です。

グリッド線と選択ハイライト

テクスチャの上に、painter でグリッドと選択枠を重ねて描きます。

  • タイル単位の細いグリッド(白の 10% くらいの半透明)
  • フォーカスサイズ単位のやや濃いグリッド(16px 以上のとき)
  • 選択ブロックの赤い枠線
// 選択ブロックのハイライト
if let Some(tile_idx) = selected_tile_snap {
    let t_row = tile_idx / 16;
    let t_col = tile_idx % 16;
    let bx = rect.left() + t_col as f32 * tile_px;
    let by_ = rect.top()  + t_row as f32 * tile_px;
    let bs  = tile_px * n as f32;
    let hl  = egui::Rect::from_min_size(egui::pos2(bx, by_), egui::vec2(bs, bs));
    painter.rect_stroke(
        hl, 0.0,
        egui::Stroke::new(2.0, egui::Color32::from_rgb(255, 80, 80)),
        egui::StrokeKind::Outside,
    );
}

即時モードなので、こういう「重ね描き」は本当にただ順番に描くだけです。テクスチャを貼って、その上に線を引く。以上。

クリックでタイルを選択

allocate_exact_size() に Sense::click() を渡しているので、返ってきた response でクリック判定ができます。クリック位置から rect の左上を引いて、tile_px で割れば行・列が出ます。

let new_tile = if response.clicked() {
    response.interact_pointer_pos().and_then(|pos| {
        let rel_x = pos.x - rect.left();
        let rel_y = pos.y - rect.top();
        if rel_x < 0.0 || rel_y < 0.0 { return None; }
        let col = (rel_x / tile_px) as usize;
        let row = (rel_y / tile_px) as usize;
        if col >= 16 { return None; }
        let global_tile = row * 16 + col;
        (global_tile < total_tiles).then_some(global_tile)
    })
} else {
    None
};

選択されたタイルのグローバルインデックスを self.selected_tile に入れておくと、ドットエディタ側がそれを読んでブロックを表示する、という連携です。

スクロール位置を状態に書き戻す

ScrollArea::show() の戻り値からはスクロールオフセットや表示領域の高さが取れるので、これを self に書き戻しておきます。

let scroll_y = scroll_out.state.offset.y;
self.scroll_addr = (scroll_y / tile_px) as usize * 0x100;
// 矢印キーのスクロール判定用にビューポート情報を保存
self.scroll_top_row = (scroll_y / tile_px) as usize;
self.visible_tile_rows = (scroll_out.inner_rect.height() / tile_px).floor() as usize;

scroll_addr は情報パネルの「現在アドレス」表示に、scroll_top_row / visible_tile_rows は矢印キーで選択タイルが画面外に出たときの自動スクロール判定に使います。

フォーカスサイズとアドレスジャンプ

バンクビュー上部のツールバーには、アドレス入力欄と 8 / 16 / 32 / 64 / 128 のフォーカスサイズ切り替えボタンがあります。

フォーカスサイズは enum で、値そのものをピクセル数にしてあります。

#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum FocusSize {
    S8   = 8,
    S16  = 16,
    S32  = 32,
    S64  = 64,
    S128 = 128,
}

impl FocusSize {
    /// 1 辺のタイル数(例: S32 → 4)
    pub(super) fn tile_count(self) -> usize { self as usize / 8 }
}

アドレスジャンプは 16 進文字列をパースして、対応するタイルにスクロール & フォーカスします。このとき、フォーカスサイズのグリッドにスナップさせているのがちょっとしたこだわりです。

pub(super) fn jump_to_address(&mut self) {
    let raw = self.address_input.trim()
        .trim_start_matches("0x")
        .trim_start_matches("0X");
    if let Ok(addr) = usize::from_str_radix(raw, 16) {
        let total_tiles = self.rom.as_ref().map_or(0, |r| r.chr_data().len() / 16);
        if total_tiles > 0 {
            let tile_idx = (addr / 16).min(total_tiles.saturating_sub(1));
            let n = self.focus_size.tile_count();
            let snap_col = (tile_idx % 16 / n) * n;
            let snap_row = (tile_idx / 16 / n) * n;
            let snapped  = snap_row * 16 + snap_col;

            self.selected_tile       = Some(snapped);
            self.pending_scroll_addr = Some(snap_row * 0x100);
            self.address_input       = format!("{:06X}", snapped * 16);
            return;
        }
    }
    // パース失敗・範囲外の場合は現在値に戻す
    // ...
}

例えばフォーカスが 32px(4×4 タイル)のときに中途半端なアドレスを入れても、4 タイル境界に揃った位置が選ばれます。

情報パネル:描画色とパレット編集

右の情報パネルには、現在アドレス、選択タイル番号、描画色 4 つ、パレット 4 セット、そして NES の 64 色一覧が並んでいます。

パレットの色を変える流れはこうです。

  1. パレットセットの色スウォッチをクリック → editing_palette_cell = Some((set, color)) にする
  2. 下の NES 64 色グリッド(8×8)から色をクリック → dat_palette.sets[set][color] にその NES インデックスを書き込む
  3. texture_dirty = true にしてバンクビューを再描画
if let (Some(idx), Some((set_idx, color_idx))) = (selected_nes_idx, self.editing_palette_cell) {
    self.dat_palette.sets[set_idx][color_idx] = idx;
    self.texture_dirty = true;
    self.editing_palette_cell = None;
}

スウォッチ類は全部 allocate_exact_size() + painter.rect_filled() で自前描画しています。egui に色ボタンウィジェットが無いわけではないのですが、ドット絵エディタっぽい見た目にしたかったので、24px の四角を並べる形にしました。ホバーすると 0x1A のように NES インデックスがツールチップで出ます。

キーボード操作

キー入力は ctx.input() の中でまとめて拾って、フラグに落としてから処理します。ctx.input() のクロージャ内で self を借用すると面倒なことになるので、いったん変数に逃がすのがコツです。

ctx.input(|i| {
    let cmd = i.modifiers.ctrl || i.modifiers.mac_cmd;

    if cmd && i.key_pressed(egui::Key::Z) {
        do_undo = true;
    } else if i.key_pressed(egui::Key::Z) {
        new_palette_set = Some(0);
    }
    // ...
    if cmd && i.key_pressed(egui::Key::S) {
        if i.modifiers.shift { do_save_as = true; } else { do_save = true; }
    }

    // 矢印キー(フォーカスブロック単位で移動)
    if i.key_pressed(egui::Key::ArrowRight) { d_col += 1; }
    if i.key_pressed(egui::Key::ArrowLeft)  { d_col -= 1; }
    if i.key_pressed(egui::Key::ArrowDown)  { d_row += 1; }
    if i.key_pressed(egui::Key::ArrowUp)    { d_row -= 1; }
});

Z / X / C / V 単独でパレットセット 0〜3 の切り替え、Cmd 付きだと Undo / Copy / Paste。左手のホームポジション付近だけでパレットを切り替えられるので、右手はマウスに置いたままで済みます。

矢印キーで選択タイルを動かしたときは、画面外に出たときだけスクロールさせます。

// 選択タイルが可視範囲外に出た場合のみスクロール
let visible_end = self.scroll_top_row + self.visible_tile_rows.max(1);
if new_row < self.scroll_top_row {
    // 上に出た → 選択行を先頭に
    self.pending_scroll_addr = Some(new_row * 0x100);
} else if new_row >= visible_end {
    // 下に出た → 選択行が末尾に来るよう調整
    let start_row = new_row + 1 - self.visible_tile_rows.max(1);
    self.pending_scroll_addr = Some(start_row * 0x100);
}
// 可視範囲内なら scroll しない(チラツキ防止)

最初は毎回スクロールさせていたのですが、画面内で 1 タイル動かすたびにビューがガタガタ動いてしまって、これがなかなか不快でして。バンクビューから書き戻した scroll_top_row / visible_tile_rows を使って判定するようにしたら落ち着きました。

ドラッグ & ドロップ

ファイルの D&D は egui が標準で拾ってくれます。ctx.input() の raw.dropped_files にパスが入ってくるので、拡張子で振り分けるだけです。

let dropped = ctx.input(|i| {
    i.raw.dropped_files.iter().find_map(|f| {
        let path = f.path.as_ref()?;
        let ext = path.extension()?.to_str()?.to_ascii_lowercase();
        Some((path.clone(), ext))
    })
});
if let Some((path, ext)) = dropped {
    match ext.as_str() {
        "nes" | "bin" | "zip" => self.open_file_from_path(&path),
        "png" | "bmp"         => self.open_png_import_from_path(&path),
        _                     => {}
    }
}

.nes を放り込めば開く、.png を放り込めばインポートダイアログが出る。これだけで体感がだいぶ変わります。

未保存の変更とウィンドウを閉じる処理

タイトルバーの * 表示と、閉じるときの確認ダイアログもここで。

// ── タイトルバー更新(未保存変更を * で表示)
let title = if self.is_modified {
    format!("*{}", self.file_name.as_deref().unwrap_or(""))
} else {
    format!("{}", self.file_name.as_deref().unwrap_or(""))
};
ctx.send_viewport_cmd(egui::ViewportCommand::Title(title));

ウィンドウを閉じようとしたときは close_requested() で検知して、未保存ならいったん CancelClose を送ってダイアログを出し、「保存して閉じる」なら保存後に改めて Close を送ります。

if ctx.input(|i| i.viewport().close_requested()) {
    if self.is_modified {
        ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
        // ... ダイアログを出して選択に応じて処理 ...
        //   保存して閉じる → save_file() → Ok なら ViewportCommand::Close
        //   保存せず閉じる → is_modified = false; ViewportCommand::Close
        //   キャンセル     → 何もしない
    }
}

ダイアログ自体は macOS では NSAlert、それ以外は rfd::MessageDialog を使い分けています。この辺りも最終回で。

まとめ

  • egui は即時モード。毎フレーム update() で状態から UI を全部描き直す
  • パネルは宣言順に領域を取るので、右から SidePanel::right を 2 つ並べて残りを CentralPanel に
  • CHR 全体は render_full_image() で 1 枚の画像にして GPU テクスチャにキャッシュ。texture_dirty で必要なときだけ再生成
  • TextureOptions::NEAREST と整数倍スケールでドットを滲ませない
  • ScrollArea のオフセットを状態に書き戻して、アドレス表示や矢印キーの自動スクロールに使う
  • 日本語表示には Noto Sans JP を埋め込んで登録。太字は別ファミリーとして登録

即時モードは「状態 → 描画」が一方通行なので、バンクビューのように「データが変わったらテクスチャを作り直す」という設計に自然に落ち着きます。保持モードでやりがちな「どのウィジェットを更新すべきか」を考えなくて良いのは楽ですね。

次回は中央のドットエディタです。クリックした位置からドットの座標を割り出して、CHR に書き込んで、アンドゥできるようにする、というエディタの心臓部の話になります。

ではではぁ。

またまたぁ。

Comment

Related Article

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その3 egui で組む 3 パネル UI とバンクビュー

2026.09.26

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その2 CHR の 2BPP フォーマットと iNES ヘッダ、パレット

2026.09.24

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その1 概要とアーキテクチャ

2026.09.22

ゲーム開発に必要な基本数学入門

2025.08.15

Godot 4 & Rust 始め方

2025.06.19

Rustでつくる ふっかつのじゅもん

2025.04.29

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その5

2025.04.14

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その4

2025.04.10

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その3

2025.04.08

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その2

2025.04.07

CATEGORY LIST

LATEST NEWS

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その3 egui で組む 3 パネル UI とバンクビュー

Rust

2026.09.26

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その2 CHR の 2BPP フォーマットと iNES ヘッダ、パレット

Rust

2026.09.24

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その1 概要とアーキテクチャ

Rust

2026.09.22

M5Stack Stopwatchで、オリジナル LT(Lightning Talks)タイマーを作成した。

電子工作

2026.09.21

【Railway】GitHub Actions で定期的にサーバーをリスタートさせる

運用

2026.08.30

ラズパイが高騰しているのでストレス解消のためAliExpressで購入した電子パーツ達

電子工作

2026.08.02

【2026】【VMware Fusion】MacでWindowsを無料で扱う。

windows

2026.07.19

Raspberry Pi 5 でマインクラフトサーバーを立てる(Java版 × 統合版クロスプレイ対応)

RaspberryPi

2026.06.24

ラズパイが高い。

RaspberryPi

2026.05.26

【Claude Code】フル稼働。ToDo Appを様々なGUIフレームワーク用いて作らせる。

AI・Bot・algorithm

2026.05.24

Macで歩く「たのしいバイナリの歩き方」うさみみハリケーンの代わりに、Cheat Engine / Bit slicerを使用する

アセンブラ

2026.04.12

Macで歩く「たのしいバイナリの歩き方」

アセンブラ

2026.04.10

RANKING

Follow

SPONSOR

現在、掲載募集中です。



Links

About Us

WEBデザイナーの、WEBデザイナーによる、WEBデザイナーの為のサイト。「みんなで書こう!」と仲間を募ってみたが、結局書くのは自分だけとなってしまいました。日々のメモを綴っていきます。

Entry Profile

Graphical FrontEnd Engineer
- Daisuke Takayama

MAD CITY 北九州市で生まれ育つ。20代はバンド活動に明け暮れ、ふと「webデザイナーになりたい。」と思い、デジタルハリウッド福岡校入学。卒業後、数々の賞を受賞、web業界をざわつかせる。
現在、主に、ゲーム制作中心に港区六本木界隈で活動中。

FOLLOW US