00概要
テーマとは
テーマは、パソコン版 NII Iznanka クライアントの見た目と音を変えます。
- バージョン 6.0 以降、テーマはクライアントに組み込まれていません
- 各テーマはカタログに個別のパッケージとして置かれています
.nftheme - クライアントはテーマが選ばれたときだけダウンロードします
- ビルドに残っている基本テーマはひとつ:NII IZNANKA
テーマは誰でも作れます。パッケージは普通の zip で、manifest.json、画像、サウンド、必要なら独自の HTML・CSS・JS を入れます。スタジオでアップロードし、モデレーターが審査し、サーバーがバージョンに署名すると、全員のカタログにテーマが表示されます。
manifest.json だけ:色、フォント、背景、サウンド、音楽、イントロ、独自の文言、ウィンドウサイズ。コード不要。
さらに theme.css をクライアントのウィンドウに重ねる:ボタン、パネル、ダイアログの形は自由、チャネルの状態に応じたアニメーションも。
ウィンドウ全体を自作:HTML・CSS・JS のページ。クライアントから状態を受け取り、SDK で操作します。
レベルは重ねられます。シーンにもトークンと theme.css を持たせられ、シーンの上に開くクライアントのダイアログに色が付きます。
変更できるもの
| 項目 | 方式 | レベル |
|---|---|---|
| ウィンドウとダイアログのすべての色 | tokens | パレット |
| フォント:本文、見出し、等幅 | fonts | パレット |
| 背景:画像または動画、暗さ、ぼかし | background | パレット |
| 接続、切断、エラー、クリックのサウンド | sounds | パレット |
| BGM | music | パレット |
| テーマ選択時のイントロ(字幕付き) | intro | パレット |
| 文言:「接続」→「スフィアに入る」 | words | パレット |
| ウィンドウのサイズ、角丸、形(ギザギザの縁まで) | window | パレット |
| ウィンドウとトレイのアイコン | icon | パレット |
| ウィンドウのあらゆる要素:形、配置、アニメーション | theme.css | スタイル |
| ウィンドウ全体:独自のボタン、ノード一覧、画面 | scene | シーン |
| 通常のウィンドウの背後に動く背景 | scene.mode: backdrop | シーン |
| クライアントの設定画面に独自のテーマ設定 | settings | すべて |
テーマの流れ
- 01作者nftheme pack
- 02スタジオサイトへアップロード
- 03モデレーションテーマとコード
- 04署名スタジオの Ed25519
- 05カタログiznanka.tech
- 06クライアントカタログタブ
01クイックスタート
10 分でテーマ
必要なもの:
- Node.js 18 以降
- 作者キット nftheme-kit.zip:
nfthemeツール、SDK、3 つのテンプレート、プレビュー
アーカイブを展開するだけ。インストール不要、依存関係もありません。
# 1. テンプレート:palette、style、scene(デフォルト) node nftheme-kit/bin/nftheme.js init my-theme --level scene --name "MY THEME" # 2. ブラウザでプレビュー、変更は自動で反映 node nftheme-kit/bin/nftheme.js preview my-theme # 3. カタログと同じ検証 node nftheme-kit/bin/nftheme.js check my-theme # 4. アップロード用パッケージ node nftheme-kit/bin/nftheme.js pack my-theme
my-theme-1.0.0.nftheme というファイルができます。
- クライアントで確認:テーマ → 作者向け →「テーマフォルダを開く」
- すべて動いたら、パッケージをスタジオにドラッグ
最小の manifest.json
{
"format": 1,
"id": "night-owl",
"name": "NIGHT OWL",
"version": "1.0.0",
"author": "your_nick",
"tokens": { "accent": "#ffd23f", "bg": "#07060a" }
}隣に preview.webp(または .png、.jpg)を置きます。16:9 のテーマのスクリーンショットで、1280×720 が理想です。これだけで検証に通ります。
02パッケージ
パッケージ .nftheme
パッケージは zip アーカイブです。ルートに manifest.json を置き、残りは自由に配置します。マニフェストと CSS のパスはパッケージのルートからの相対パスです。
- フォルダごと圧縮した?外側のフォルダは自動で取り除かれます
- システムの不要ファイル(
__MACOSX、.DS_Store、Thumbs.db)は無視されます
my-theme.nftheme ├── manifest.json 必須 ├── preview.webp 必須:カタログのカード、16:9 ├── icon.png ウィンドウとトレイのアイコン、256×256 ├── theme.css 「スタイル」レベル ├── scene/index.html 「シーン」レベル ├── scene/scene.js └── assets/ 画像、サウンド、動画、フォント
ファイル名と種類
- ファイル名:半角英字、数字、ドット、ハイフン、アンダースコア。スペースとキリル文字は不可
- 大文字と小文字だけが違う 2 つのファイルは通りません。Windows では同じファイルになるためです
__nf/フォルダはクライアントが使用します- 種類は中身で判定されます。.mp3 に名前を変えた画像は通りません
| 種類 | 拡張子 |
|---|---|
| テキストとコード | json css js mjs html txt md vtt、UTF-8 |
| 画像 | png jpg jpeg webp gif avif svg |
| フォント | woff2 woff ttf otf |
| サウンド | mp3 ogg opus m4a wav |
| 動画 | mp4 webm |
上限
| 項目 | 上限 |
|---|---|
| パッケージ全体 | 64 MB |
| 展開後のパッケージ | 160 MB |
| 1 ファイル | 48 MB |
| パッケージ内のファイル数 | 600 |
| テキストファイル | 2 MB |
| 画像 | 8 MB、プレビューは 4 MB、アイコンは 1 MB まで |
| ファイルパス | 160 文字、8 階層まで |
| manifest.json | 64 KB |
03リファレンス
manifest.json
- 最初の 5 フィールドは必須、残りは任意です
- テキストは文字列でも言語別でも指定できます:
"name": { "ru": "НОЧЬ", "en": "NIGHT" } - クライアントはユーザーの言語、次にロシア語、次に英語の順で使います
| フィールド | 型 | 説明 |
|---|---|---|
format | 数値 | 常に 1 |
id | 文字列 | 3〜40 文字:半角英小文字、数字、ハイフン。永久にあなたのものになります。スタジオのテーマ名は使用済み |
name | 40 文字までのテキスト | カタログとクライアントでの名前 |
version | 文字列 | 1.0.0。新しいアップロードごとに番号を上げる |
author | 40 文字までの文字列 | パッケージ内の作者名。カタログにはスタジオのニックネームが表示されます |
description | 400 文字までのテキスト | カタログでの説明 |
tags | 6 個までの文字列 | 任意。クライアントとカタログでは表示されなくなりました |
accent | #rrggbb | カタログでのカードの色 |
minClient | 文字列 | クライアントの最低バージョン(6.0.0 以上) |
preview | パス | プレビュー。デフォルトは preview.webp、.png、または .jpg |
screenshots | 8 個までのパス | テーマページ用のスクリーンショット |
icon | png へのパス | ウィンドウとトレイのアイコン |
license | 60 文字までの文字列 | ライセンス。例: CC-BY-4.0 |
credits | 12 個までの文字列 | クレジット:アート、音楽、フォントの作者 |
homepage | https://… | テーマページに表示する作者のリンク |
window | オブジェクト | ウィンドウのサイズと形、節 07 |
tokens | オブジェクト | 色とサイズ、節 04 |
fonts | オブジェクト | フォント、節 04 |
background | オブジェクト | 背景、節 05 |
sounds | オブジェクト | イベントのサウンド、節 05 |
music | パスまたはオブジェクト | BGM、節 05 |
intro | パスまたはオブジェクト | イントロ、節 05 |
words | オブジェクト | 独自の文言、節 06 |
css | パス | スタイルシート、節 08 |
scene | パスまたはオブジェクト | シーン、節 09 |
settings | 配列 | テーマの設定、節 11 |
{
"format": 1,
"id": "night-signal",
"name": { "ru": "НОЧНОЙ СИГНАЛ", "en": "NIGHT SIGNAL" },
"version": "1.2.0",
"author": "iznanka",
"description": { "ru": "Серая панель, сигнал вместо кнопки, голос на подъём канала." },
"accent": "#c42b47",
"license": "CC-BY-4.0",
"credits": ["art and sound: fawn", "font Overused Grotesk: OFL"],
"screenshots": ["shots/on.webp", "shots/servers.webp"],
"icon": "icon.png",
"window": { "width": 1280, "height": 720 },
"tokens": { "bg": "#000", "panel": "#a0a0a0", "text": "#fff", "accent": "#c42b47" },
"fonts": { "body": "assets/grotesk.woff2", "display": "assets/pixel.woff2" },
"background": { "image": "assets/field.webp", "dim": 0.2 },
"sounds": { "connect": "assets/up.mp3", "disconnect": "assets/down.mp3", "click": "assets/tick.mp3" },
"music": { "src": "assets/menu.ogg", "volume": 0.35 },
"intro": { "video": "assets/briefing.mp4", "subtitles": "assets/briefing.vtt" },
"words": { "ru": { "connect": "ВЫСАДКА", "disconnect": "ЭВАКУАЦИЯ", "servers": "СФЕРЫ" } },
"css": "theme.css",
"scene": { "entry": "scene/index.html", "mode": "full" },
"settings": [
{ "id": "glitch", "type": "toggle", "label": "Glitch", "default": true }
]
}04パレット
色とフォント
トークンは、クライアントがウィンドウとすべてのダイアログに色を付けるための変数です。
- 色:
#hex、rgb()、rgba()、hsl()、transparent - サイズ:
px、em、rem、% - CSS とシーンでは
var(--nf-名前)として使えます
| トークン | 基本テーマでの値 | 色を付ける対象 |
|---|---|---|
bg | #000000 | すべての下にあるウィンドウの背景 |
surface | #0b0b12 | ウィンドウ中央のハイライト |
panel | rgba(12,10,22,.78) | パネル、メニュー、ダイアログの背景 |
line | #23232e | 枠線と区切り線 |
text | #e9e9f2 | 本文 |
muted | #6f6f80 | キャプションと補助テキスト |
accent | #a855f7 | メインカラー:接続ボタン、選択中のノード、見出し |
accent2 | #7c3aed | セカンドアクセント:ホバー、グラデーション |
ok | #4ade80 | チャネル接続中、「保護中」 |
warn | #f97316 | メンテナンス、警告 |
danger | #ef4444 | エラー、チャネル切断 |
glow | rgba(168,85,247,.45) | アクセントの周りのグロー |
shadow | rgba(0,0,0,.6) | ダイアログの影 |
overlay | rgba(0,0,0,.72) | ダイアログの下の暗幕 |
radius | 0px | ボタンとパネルの角丸 |
border | 1px | 枠線の太さ |
gap | 14px | パネル間の余白 |
試してみる
右側で色を調整してください。下に manifest.json 用のコードが出ます。
フォント
役割は 3 つ。クライアントがファイルを読み込み、--nf-font-body、--nf-font-display、--nf-font-mono を設定します。
body:すべてのテキストdisplay:大きな文字とボタンmono:数字、ログ、アドレス
"fonts": {
"body": "assets/fonts/grotesk.woff2",
"display": "assets/fonts/pixel.woff2",
"mono": "assets/fonts/mono.woff2"
}05パレット
背景、サウンド、イントロ
背景
| フィールド | 項目 | デフォルト |
|---|---|---|
image | 背景画像 | なし |
video | 背景動画:無音、ループ、画像の上に重ねる | なし |
color | 画像の下の色 | なし |
fit | cover、contain、または fill | cover |
position | CSS と同じ:center、top、 left 30% | center |
dim | 暗さ 0…1 | 0 |
blur | ぼかし 0…40 px | 0 |
イベントのサウンド
イベントの音はクライアントが再生します。オン・オフと音量はテーマごとにクライアントの設定にあります。
| イベント | 再生タイミング |
|---|---|
connect | チャネルが接続された |
disconnect | チャネルが切断された |
error | 接続に失敗した、またはチャネルが切れた |
switch | 接続中のノード切り替え |
click | クライアントのウィンドウでボタンを押した |
hover | ボタンにホバーした |
open, close | ダイアログが開いた・閉じた |
notify | 通知 |
"sounds": { "connect": "assets/sfx/up.mp3", "disconnect": "assets/sfx/down.mp3", "hover": "assets/sfx/tick.mp3" },
"music": { "src": "assets/music/menu.ogg", "volume": 0.35 }イントロ
- テーマを選ぶと動画が全画面で再生されます
- Esc でスキップ
- 字幕は WebVTT 形式
"once": true:イントロはテーマを初めて選んだときだけ
"intro": { "video": "assets/intro.mp4", "subtitles": "assets/intro.vtt", "volume": 0.85, "once": false }WEBVTT 00:00.500 --> 00:03.200 接続確立。ようこそ。 00:03.400 --> 00:06.000 チャネル開通。
06パレット
独自の文言
クライアントはテーマの言葉で話せます。BERSERK ではサーバーが「土地」に、設定が「装備」になりました。今はどのテーマでもできます。
- 文言は言語ごとに指定、40 文字まで
- 言語を指定しなければ、クライアントの通常の文言のまま
"words": {
"ru": { "connect": "ПРИНЯТЬ КЛЕЙМО", "disconnect": "СНЯТЬ КЛЕЙМО", "servers": "ЗЕМЛИ", "settings": "СНАРЯЖЕНИЕ" },
"en": { "connect": "TAKE THE BRAND", "servers": "LANDS" }
}| キー | ロシア語 | In English |
|---|---|---|
connect | ПОДКЛЮЧИТЬ | CONNECT |
disconnect | ОТКЛЮЧИТЬ | DISCONNECT |
connecting | ПОДКЛЮЧАЮ… | CONNECTING… |
disconnecting | ОТКЛЮЧАЮ… | DISCONNECTING… |
switching | ПЕРЕКЛЮЧАЮ… | SWITCHING… |
connected | ЗАЩИЩЕНО | PROTECTED |
disconnected | НЕ ЗАЩИЩЕНО | NOT PROTECTED |
servers | СЕРВЕРЫ | SERVERS |
settings | НАСТРОЙКИ | SETTINGS |
about | О ПРОГРАММЕ | ABOUT |
help | ПОМОЩЬ | HELP |
themes | ТЕМЫ | THEMES |
catalog | КАТАЛОГ ТЕМ | THEME CATALOG |
split | РАЗДЕЛЬНЫЙ ТОННЕЛЬ | SPLIT TUNNEL |
update | ОБНОВЛЕНИЕ | UPDATE |
auto | АВТОВЫБОР | AUTO |
ping | ОТКЛИК | PING |
received | ПРИНЯТО | RECEIVED |
sent | ОТДАНО | SENT |
session | СЕССИЯ | SESSION |
node | УЗЕЛ | NODE |
favorites | ИЗБРАННОЕ | FAVORITES |
maintenance | ТЕХ. РАБОТЫ | MAINTENANCE |
menu | МЕНЮ | MENU |
close | ЗАКРЫТЬ | CLOSE |
back | НАЗАД | BACK |
07パレット
ウィンドウ
| フィールド | 項目 | 上限 |
|---|---|---|
width, height | ウィンドウサイズ(ピクセル) | 800〜1920 × 560〜1080、デフォルトは 1000×700 |
radius | ウィンドウの角丸 | 0-48 |
transparent | 透明ウィンドウ:何も描かれていない部分からデスクトップが見える | true または false |
clip | ウィンドウの形:[x, y] の点(パーセント)による多角形 | 3〜96 点 |
- ウィンドウはマウスで伸縮できません。デザインは 1 つのサイズで作ります
- 全画面(F11)ではクライアントがウィンドウ全体を拡大し、余白を次の色で塗ります:
bg
"window": {
"width": 1280, "height": 720,
"clip": [[0,4],[6,0],[38,2],[61,0],[100,3],[98,40],[100,71],[97,100],[55,97],[30,100],[2,98],[0,60]]
}clip を使うとウィンドウは透明になります。ウィンドウのボタンが形の内側に残っているか確認してください。外にあると押せません。08スタイル
theme.css
theme.css はクライアントのスタイルの上に適用されます。
data-nfラベルを使いましょう。クライアントのバージョン間で変わりません- ウィンドウ内のクラス(
.screen、.rowsなど)は内部用で、予告なく名前が変わります url()のパスの基準はtheme.css
/* 接続ボタン:ネオンリング */
[data-nf='connect'] {
border-radius: 999px;
box-shadow: 0 0 32px var(--nf-glow);
}
/* チャネル接続中のパルス */
[data-nf-status='connecting'] [data-nf='connect'] { animation: pulse 1s ease-in-out infinite; }
@keyframes pulse { 50% { box-shadow: 0 0 0 8px var(--nf-glow); } }
/* メンテナンス中のノードは淡く */
[data-nf='server'][data-nf-maint] { opacity: .5; }
/* 独自のカーソル */
body { cursor: url('assets/cursor.png') 4 0, default; }ウィンドウのラベル
| data-nf | 説明 |
|---|---|
screen | 基本テーマのメインウィンドウ全体 |
map | パネルの背後のドット世界地図 |
brand | クライアントのロゴと名前 |
mark | brand 内のリングロゴ |
menu | メニュー列 |
menu-item | メニュー項目。どれかは data-nf-item でわかります:help、themes、servers、settings、about |
servers | ノード一覧とボタンのパネル |
server | ノードの行。選択中:aria-selected="true"、メンテナンス中:data-nf-maint、お気に入り: data-nf-fav |
mode-switch | 「自動選択/詳細」切り替え |
connect | 接続ボタン |
status | トンネル状態パネル |
status-text | 「保護中/未保護」の表示 |
progress | チャネル接続の進行バー |
log | ログウィンドウ |
meta | アドレス、通信量、セッション時間 |
telegram | Telegram ボタン |
version | バージョン表示 |
update-chip | 旧アップデートボタン。6.0.1 以降、アップデートは画面下中央のクライアント共通バナーで表示され、テーマはそれを隠したり動かしたりしません |
window-controls | 「最小化」「全画面」「閉じる」ボタン |
dialog | すべてのダイアログ。どれかは data-nf-dialog でわかります:servers、settings、about、help、themes、catalog、split、update、sound |
dialog-title | ダイアログのタイトル |
button | ダイアログ内の通常ボタン |
toast | ポップアップ通知 |
key-screen | キー入力画面。色の変更のみ可能で、シーンはその上に描画されません |
状態:<body> の属性
| 属性 | 値 |
|---|---|
data-nf-status | off, connecting, on, disconnecting, switching, error |
data-nf-theme | あなたのテーマの id |
data-nf-lang | ru, en, zh, ja |
data-nf-full | 全画面モードで付与 |
Ctrl+Shift+Backspace で基本テーマに戻せますが、そうしたテーマはモデレーターが通しません。09シーン
ウィンドウを丸ごと自作
シーンはパッケージ内の HTML ページです。クライアントはそれをウィンドウ全体にサンドボックスで開き、SDK で状態を渡します。中ではブラウザにできることがすべて使えます:canvas、WebGL、CSS アニメーション、動画、サウンド、独自フォント。
"scene": { "entry": "scene/index.html", "mode": "full" }| mode | 動作 |
|---|---|
full | シーンがウィンドウ全体を占め、クライアントのメイン画面は隠れます。クライアントのダイアログ(設定、サーバー、ヘルプ)はシーンの上に開き、トークンと theme.css で色付けされます |
backdrop | シーンは通常のクライアントウィンドウの背後で動く背景になります。マウスとキーボードの入力は受け取りません |
ウィンドウのレイヤー
- イントロintro、最前面
- クライアントのダイアログtokens + theme.css
- シーン fullscene/index.html
- クライアントのウィンドウtokens + theme.css
- シーン backdrop動く背景
- 背景background
シーンの骨組み
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="scene.css">
<script src="/__nf/sdk.js"></script>
</head>
<body>
<div id="frame">
<button id="core">-</button>
<div id="nodes"></div>
</div>
<script src="scene.js"></script>
</body>
</html>const frame = document.getElementById('frame');
nf.fit(frame, 1280, 720); // 1280×720 のフレーム
nf.window.dragRegions([{ x: 0, y: 0, w: 1180, h: 48 }]); // 上部をつかんでウィンドウを移動できる
const core = document.getElementById('core');
core.addEventListener('click', () => nf.toggle());
function render() {
const s = nf.state;
core.textContent = s.connected ? nf.t('disconnect') : nf.t('connect');
document.body.dataset.status = s.status;
}
nf.on('init', render);
nf.on('state', render);
nf.on('nodes', (nodes) => {
document.getElementById('nodes').replaceChildren(...nodes.map((node) => {
const row = document.createElement('button');
row.textContent = node.name + ' · ' + (node.ping || '-') + ' ms';
row.onclick = () => nf.select(node.id);
return row;
}));
});知っておくべきこと
- SDK は常に
/__nf/sdk.jsにあります。パッケージに入れる必要はありません - サイズ。ウィンドウは全画面になることもあります。固定サイズのフレームで描画し、次で収めてください:
nf.fit() - ドラッグ。ウィンドウには枠がありません。
nf.window.dragRegions()で場所を指定しないとウィンドウを動かせません - ウィンドウのボタン。デフォルト(
"hostChrome": true)では、クライアントがシーンの右上にテーマ・最小化・全画面・閉じるのブロックを置きます。nf.window.*で独自のボタンを描くなら"hostChrome": falseに。テンプレートはそうなっています - クライアントのショートカットキー(F1〜F12、Esc)はシーン内でも動きます。シーン自身でキーを使いたい場合は
event.preventDefault()を呼べば、クライアントには渡りません - 終了。テーマが変わるとシーンは
leaveを受け取ります。Promise を返せば、クライアントは最大 6 秒待ちます
shadowBlur や filter は、そうした環境を毎秒 2 フレームまで落とします。グローは drawImage で既製のスプライトとして描きましょう。ウィンドウが最小化されたら requestAnimationFrame を止めてください。state に visible: false が届きます。10シーン
SDK:オブジェクト nf
プロパティ
| プロパティ | 型 | 項目 |
|---|---|---|
nf.state | オブジェクト | チャネルの状態(詳細は後述)。init イベントの前は次の値: null |
nf.stats | オブジェクト | { rx, tx, rxRate, txRate, ping, at }:セッションのバイト数、速度(バイト/秒)、応答時間(ミリ秒)。チャネル接続中は毎秒更新 |
nf.nodes | 配列 | キーで使えるノード:{ id, name, country, city, ping, load, maintenance, favorite, selected }。ノードのアドレスはシーンに渡されません |
nf.settings | オブジェクト | テーマ設定の値(出典: manifest.settings |
nf.theme | オブジェクト | { id, version, name, base })。base はパッケージのルートのアドレスで、「/」で終わります |
nf.lang | 文字列 | クライアントの言語:ru、en、zh、 ja |
nf.preview | boolean | true プレビュー時(サイトまたは nftheme preview)、クライアントでは false |
nf.scale | 数値 | 次の呼び出し後のフレーム倍率: nf.fit() |
nf.version | 文字列 | SDK のバージョン |
イベント
| イベント | 受け取るデータ | タイミング |
|---|---|---|
init | すべて | クライアントがデータを渡したときに 1 回。ここから描画を始めます |
state | nf.state | チャネルの接続・切断、ノード・言語・サウンド・全画面の変更 |
stats | nf.stats | チャネル接続中は毎秒 |
nodes | nf.nodes | 新しいノード一覧や応答時間の測定結果が届いた(F5、nf.ping()) |
settings | nf.settings | ユーザーがクライアントでテーマ設定を変更した |
lang | 文字列 | クライアントの言語が変わった |
leave | 新しいテーマの id | テーマが変更される。Promise を返せば、シーンが終了演出を終えるまでクライアントは最大 6 秒待ちます |
メソッド
| メソッド | 動作 |
|---|---|
nf.on(event, fn) | 購読。購読解除の関数を返す |
nf.off(event, fn) | 購読解除 |
nf.ready() | シーンの描画が完了し、クライアントがカーテンを外します。呼ばなくても load 後に自動で外れます。長く保つには読み込み前に nf.manualReady = true |
nf.connect() | チャネルを接続 → Promise |
nf.disconnect() | チャネルを切断 → Promise |
nf.toggle() | 接続または切断 → Promise |
nf.select(id) | ノードを選択。接続中ならクライアントが自動で切り替えます |
nf.auto() | ノードの自動選択に戻す |
nf.ping() | 全ノードの応答時間を測定(F5 と同じ) |
nf.favorite(id) | ノードをお気に入りに追加・削除 |
nf.open(screen) | クライアントの画面を開く:servers、settings、about、help、themes、catalog、split、update |
nf.toast(text) | ウィンドウ下部の通知(140 文字まで) |
nf.window.minimize() | ウィンドウを最小化 |
nf.window.close() | トレイに格納 |
nf.window.fullscreen() | 全画面の切り替え |
nf.window.dragRegions(rects) | ウィンドウをドラッグできるシーン内の領域:[{ x, y, w, h }](シーンのピクセル単位、16 個まで) |
nf.t(key) | 現在の言語でのクライアントの文言(次を反映): manifest.words |
nf.asset(path) | パッケージ内ファイルの完全なアドレス |
nf.storage.get(key) | テーマのストレージから読み込む → Promise |
nf.storage.set(key, value) | 任意の JSON を書き込む(テーマごとに合計 64 KB まで)→ Promise |
nf.storage.remove(key) | 削除 → Promise |
nf.sound.play(path, { volume, loop }) | テーマのオン・オフと音量を反映してパッケージのファイルを再生 |
nf.sound.enabled / nf.sound.volume | テーマの音のオン・オフと全体の音量 0..1 |
nf.fit(el, w, h, mode) | w×h のフレームをウィンドウに収める。mode:contain または cover |
nf.format.bytes(n) / rate(n) / duration(ms) | «1.3 GB», «350 KB/s», «03:46:32» |
クライアントへの呼び出しはすべて Promise を返します。クライアントが拒否した場合(キーが未有効化など)、Promise はわかりやすいエラーで reject されます。
state オブジェクト
{
status: 'on', // off | connecting | on | disconnecting | switching | error
connected: true,
busy: false, // connecting、disconnecting、または switching
node: { id: 'nl-1', name: 'オランダ · アムステルダム', country: 'NL', city: 'アムステルダム',
ping: 41, load: 23, maintenance: false, favorite: true, selected: true },
auto: true, // ノードはクライアントが選ぶ
since: 1791232800000, // チャネルが接続された時刻(ミリ秒)、それ以外は null
error: null, // status === 'error' のときのエラー内容
lang: 'ru',
words: { connect: '接続', servers: 'サーバー', … }, // manifest.words を反映
sound: { enabled: true, volume: 0.8 },
visible: true, // ウィンドウが画面上にある:最小化もトレイ格納もされていない
fullscreen: false,
reduceMotion: false, // システムで「視差効果を減らす」が有効
platform: 'windows', // windows | linux
version: '6.0.0',
sub: { plan: 'NII IZNANKA', daysLeft: 27 },
update: { available: false, version: null }
}11すべてのレベル
テーマの設定
- 設定をマニフェストに書けば、クライアントが設定画面のあなたのテーマ欄に表示します
- 値はクライアントが保存します。シーンは
nf.settingsと次のイベントで受け取ります:settings - 設定は 16 個まで
| type | フィールド | 値 |
|---|---|---|
toggle | default:true または false | boolean |
range | min, max, step, default | 数値 |
select | options:{ value, label } の選択肢 2〜12 個、 default | 文字列 |
color | default: #rrggbb | #rrggbb 形式の文字列 |
"settings": [
{ "id": "glitch", "type": "toggle", "label": { "ru": "Помехи", "en": "Glitches" }, "default": true },
{ "id": "speed", "type": "range", "label": "リングの速度", "min": 0, "max": 2, "step": 0.1, "default": 1 },
{ "id": "scene", "type": "select", "label": "シーン", "default": "grid",
"options": [ { "value": "grid", "label": "グリッド" }, { "value": "signal", "label": "シグナル" } ] },
{ "id": "tint", "type": "color", "label": "色", "default": "#c42b47", "hint": "ボタンの周りのリングの色" }
]nf.on('settings', (values) => {
document.body.classList.toggle('glitch', values.glitch);
rings.speed = values.speed;
});テーマの音は別途記述する必要はありません。テーマに sounds、music、intro があれば、クライアントがオン・オフと音量を自動で追加します。
12セキュリティ
サンドボックス
テーマは、ユーザーの VPN キーがあるアプリの中で動く他人のコードです。そのためクライアントは、審査済みであってもどのテーマも信用しません。
nftheme://id/:自分のファイルのみネットワークキーファイルウィンドウcookie
- ネットワークは完全に遮断。
fetch、XMLHttpRequest、WebSocket、外部アドレスの画像やフォント、WebRTC はすべてクライアントがブロックします。テーマは自分のパッケージ内のファイルだけで動きます - シーンは隔離されています。クライアントのウィンドウとオリジンを共有しない iframe で、クライアントの DOM、Cookie、
localStorageにはアクセスできません。独自データには次があります:nf.storage - 秘密情報はなし。キーもノードのアドレスも IP もありません。参照: state
- 新しいウィンドウも遷移もなし。シーンは新しいウィンドウを開けず、ページを移動させず、ブラウザでリンクも開けません
- theme.css もネットワークなし。
url()はパッケージ内のファイルにのみ使えます - 署名。カタログからはスタジオの署名付きパッケージのみインストールされます。ファイルが差し替えられると sha256 が一致せず、署名検証に失敗します
- 取り消し。公開済みテーマに有害なものが見つかれば取り消されます。次回のカタログ確認時に、クライアントが自動で無効化・削除します
緊急脱出
Ctrl+Shift+Backspace:いつでも基本テーマに戻す。このキーはシーンより先にクライアントが受け取るため、テーマは横取りできません--safe-themeオプションで起動すると、最初から基本テーマで開きます
13デバッグ
確認方法
ブラウザで
nftheme preview my-theme は 127.0.0.1:4790 でクライアントのスタブを起動します:
data-nfラベル付きのウィンドウ、背景、トークン、theme.css- サンドボックス内のシーン
- 右側のパネル:チャネル状態、言語、サウンド、設定、検証エラー、SDK 呼び出しログ
- 保存のたびにファイルを再読み込み
クライアントで
- テーマ → 作者向け →「テーマフォルダを開く」:フォルダからテーマを読み込み、保存のたびに再読み込みします
- 「.nftheme ファイルを開く」:ビルド済みパッケージの確認
- 「開発者ツール」:シーンのコンソール、レイアウト、ネットワークを表示する DevTools。ブロックされたものも確認できます
フォルダやファイルから読み込んだテーマには未検証と表示され、カタログのテーマと同じサンドボックスで動きます。
よくあるエラー
| メッセージ | 対処法 |
|---|---|
ファイル「…」がパッケージにありません | マニフェストのパスがファイルと一致しません。大文字・小文字も区別されます |
使用できない名前 | ファイル名を半角英字にし、スペースを除く |
中身が .mp3 に見えません | 別形式のファイルです。名前の変更では直らないので、変換し直してください |
シーンが SDK を読み込んでいません | <head> に <script src="/__nf/sdk.js"> を追加 |
id「…」は使用済みです | 他の作者かスタジオのテーマの id です。自分の id を選んでください |
公開済みより新しいバージョンではありません | 上げる: version |
| ウィンドウが動かない | シーンに次がない: nf.window.dragRegions() |
| インターネット上の画像が読み込まれない | 仕様です。パッケージに入れてください |
14公開
審査とカタログ
- マイページからスタジオにログイン:メール、またはボットのリンクで
- パッケージをドラッグ。サイトが
nftheme checkと同じコードで検証し、キューに入れます - モデレーターがサンドボックスとクライアントでテーマを開き、コードを確認します。結果は Telegram に届き、却下時は理由も添えられます
- 承認されたバージョンにサーバーが署名し、カタログに掲載されます。すでにインストールしている人のクライアントは自動で更新します
モデレーターが確認すること
| 通る | 通らない |
|---|---|
| テーマが動作する:接続、ノード選択、ダイアログ | 接続ボタンが見つからない、またはわからない |
| プレビュー:テーマそのものの正直なスクリーンショット | 別のテーマのプレビューや、注目を集めるための他人のアート |
| 読みやすいコード。既知のライブラリはバージョンとライセンス付きで | ソースのない難読化・圧縮コード、eval、文字列からのコード実行 |
アート、音楽、フォントは自作か使用許可のあるもの、出典は次に記載: credits | 許可のない他人の作品。権利者から申し立てがあればテーマは削除されます |
| テーマから抜けられる:テーマ切り替え、最小化、閉じる | 隠されたテーマボタン、罠、予告なしの全画面点滅 |
音は sounds か nf.sound 経由で、設定を尊重 | 設定を無視した音、大音量の不意打ち |
| アイドル時はほとんど CPU を使わない | 最小化中にファンが全開になる |
| 偽のキー入力画面や決済画面、広告、リンク、紹介プログラム、18 禁、扇動や嫌がらせ |
バージョン、非表示、取り消し
- 新しいバージョンは毎回審査を受けます。審査中は以前のバージョンがカタログに残ります
- 新しい申請を出すと、未審査の以前の申請は取り下げられます
- 却下されたバージョンは同じ番号で再提出できます
- 非表示:テーマはカタログから外されますが、インストール済みの人はそのまま使えます
- 取り消し:危険なテーマのため、クライアントが無効化・削除します
- アップロードは 1 日 12 回まで
15レシピ
すぐ使えるテクニック
接続ボタンを画像に
[data-nf='connect'] {
width: 220px; height: 220px; padding: 0;
border: 0; border-radius: 50%;
background: url('assets/sigil-off.png') center / contain no-repeat;
color: transparent; /* 文字は隠すが、スクリーンリーダーは読み上げる */
filter: grayscale(1) brightness(.7);
transition: filter .4s, transform .2s;
}
[data-nf-status='on'] [data-nf='connect'] {
background-image: url('assets/sigil-on.png');
filter: drop-shadow(0 0 24px var(--nf-glow));
}
[data-nf='connect']:hover { transform: scale(1.04); }切断時の点滅
[data-nf-status='error'] [data-nf='screen'] { animation: lost .12s steps(2) 6; }
@keyframes lost { 50% { filter: invert(1) hue-rotate(160deg); } }通常のウィンドウの背後に動く背景
"tokens": { "bg": "transparent", "panel": "rgba(0,0,0,.55)" },
"scene": { "entry": "stars/index.html", "mode": "backdrop" }const c = document.querySelector('canvas');
const ctx = c.getContext('2d');
const stars = Array.from({ length: 220 }, () => ({ x: Math.random(), y: Math.random(), z: Math.random() }));
let speed = 0.0004;
nf.on('state', (s) => { speed = s.connected ? 0.003 : s.busy ? 0.008 : 0.0004; });
(function frame() {
c.width = innerWidth; c.height = innerHeight;
for (const s of stars) {
s.z -= speed; if (s.z <= 0) s.z = 1;
const k = 0.5 / s.z;
ctx.fillStyle = 'rgba(255,255,255,' + (1 - s.z) + ')';
ctx.fillRect((s.x - 0.5) * k * c.width + c.width / 2, (s.y - 0.5) * k * c.height + c.height / 2, 2, 2);
}
if (!nf.state || nf.state.visible !== false) requestAnimationFrame(frame);
else nf.on('state', function wake(s) { if (s.visible) { nf.off('state', wake); requestAnimationFrame(frame); } });
})();セッションの記録
nf.on('state', async (s) => {
if (s.status !== 'off' || !nf.state.since) return;
const minutes = Math.round((Date.now() - nf.state.since) / 60000);
const best = (await nf.storage.get('best')) || 0;
if (minutes > best) {
await nf.storage.set('best', minutes);
nf.toast('新記録:' + minutes + ' 分');
}
});接続時の演出画面
let was = null;
nf.on('state', (s) => {
if (was !== 'on' && s.status === 'on') {
deploy.querySelector('b').textContent = s.node ? s.node.name : '';
deploy.hidden = false;
nf.sound.play('assets/deploy.mp3');
setTimeout(() => { deploy.hidden = true; }, 2200);
}
was = s.status;
});16質問
要点まとめ
Android でもテーマは使える?
はい、同じフォーマットで対応予定です。パレット、文言、サウンドはそちらでも動き、シーンは WebView で動作します。1000×700 のウィンドウでも動くか確認してください。スマホの画面は縦長です。
ゲームやアニメのテーマを作れる?
アートとサウンドの権利を持っているか、権利者が許可していれば可能です。出典は credits に記載してください。権利者から申し立てがあればテーマは削除されます。
three.js、PixiJS、GSAP は使える?
はい。ライブラリのファイルをパッケージに入れ(ネットワークが遮断されているので CDN からは読み込めません)、改変せず、バージョンとライセンスを credits に記載してください。モデレーターが公式のファイルと照合します。
テーマは設定や記録をどこに保存する?
manifest.settings の設定はクライアントが保存します。独自データは nf.storage に 64 KB まで保存できます。テーマを削除すると一緒に消えます。
審査にはどれくらいかかる?
通常 1 日以内です。結果は Telegram に届きます。
スタジオのテーマも同じ仕組み?
ほぼ同じです。BERSERK、CYBERPUNK などはスタジオ署名付きの個別パッケージとしてカタログにあります。メインテーマ NII IZNANKA はさらにクライアントに内蔵されています。スタジオのテーマはウィンドウにより深く組み込めます。host フィールドはスタジオ署名付きのパッケージでのみ使えます。
準備はいい?