NII IZNANKAIZNANKA.TECH · VPN
JA

作者向けドキュメント · フォーマット 1 · クライアント 6.0+

テーマの作り方

新しいパレットから HTML と JS の独自シーンまで。すべてこの 1 ページに。

レベル
3
トークン
17
ウィンドウのラベル
24

00概要

テーマとは

テーマは、パソコン版 NII Iznanka クライアントの見た目と音を変えます。

  • バージョン 6.0 以降、テーマはクライアントに組み込まれていません
  • 各テーマはカタログに個別のパッケージとして置かれています .nftheme
  • クライアントはテーマが選ばれたときだけダウンロードします
  • ビルドに残っている基本テーマはひとつ:NII IZNANKA

テーマは誰でも作れます。パッケージは普通の zip で、manifest.json、画像、サウンド、必要なら独自の HTML・CSS・JS を入れます。スタジオでアップロードし、モデレーターが審査し、サーバーがバージョンに署名すると、全員のカタログにテーマが表示されます。

01パレット

manifest.json だけ:色、フォント、背景、サウンド、音楽、イントロ、独自の文言、ウィンドウサイズ。コード不要。

02スタイル

さらに theme.css をクライアントのウィンドウに重ねる:ボタン、パネル、ダイアログの形は自由、チャネルの状態に応じたアニメーションも。

03シーン

ウィンドウ全体を自作:HTML・CSS・JS のページ。クライアントから状態を受け取り、SDK で操作します。

レベルは重ねられます。シーンにもトークンと theme.css を持たせられ、シーンの上に開くクライアントのダイアログに色が付きます。

変更できるもの

項目方式レベル
ウィンドウとダイアログのすべての色tokensパレット
フォント:本文、見出し、等幅fontsパレット
背景:画像または動画、暗さ、ぼかしbackgroundパレット
接続、切断、エラー、クリックのサウンドsoundsパレット
BGMmusicパレット
テーマ選択時のイントロ(字幕付き)introパレット
文言:「接続」→「スフィアに入る」wordsパレット
ウィンドウのサイズ、角丸、形(ギザギザの縁まで)windowパレット
ウィンドウとトレイのアイコンiconパレット
ウィンドウのあらゆる要素:形、配置、アニメーションtheme.cssスタイル
ウィンドウ全体:独自のボタン、ノード一覧、画面sceneシーン
通常のウィンドウの背後に動く背景scene.mode: backdropシーン
クライアントの設定画面に独自のテーマ設定settingsすべて

テーマの流れ

  1. 01作者nftheme pack
  2. 02スタジオサイトへアップロード
  3. 03モデレーションテーマとコード
  4. 04署名スタジオの Ed25519
  5. 05カタログiznanka.tech
  6. 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 というファイルができます。

  • クライアントで確認:テーマ → 作者向け →「テーマフォルダを開く」
  • すべて動いたら、パッケージをスタジオにドラッグ
どこから始めるか
パレットから。トークン 5 つと背景だけで、印象的なテーマになります。シーンはウィンドウの動きを独自にしたいときに。

最小の manifest.json

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.json64 KB
▸ 動画
テーマで一番重い要素です。1280×720、H.264、2〜3 Mbit/s の動画は 1 分で約 20 MB。イントロが短いほどテーマのダウンロードが速くなります。

03リファレンス

manifest.json

  • 最初の 5 フィールドは必須、残りは任意です
  • テキストは文字列でも言語別でも指定できます:"name": { "ru": "НОЧЬ", "en": "NIGHT" }
  • クライアントはユーザーの言語、次にロシア語、次に英語の順で使います
フィールド型説明
format数値常に 1
id文字列3〜40 文字:半角英小文字、数字、ハイフン。永久にあなたのものになります。スタジオのテーマ名は使用済み
name40 文字までのテキストカタログとクライアントでの名前
version文字列1.0.0。新しいアップロードごとに番号を上げる
author40 文字までの文字列パッケージ内の作者名。カタログにはスタジオのニックネームが表示されます
description400 文字までのテキストカタログでの説明
tags6 個までの文字列任意。クライアントとカタログでは表示されなくなりました
accent#rrggbbカタログでのカードの色
minClient文字列クライアントの最低バージョン(6.0.0 以上)
previewパスプレビュー。デフォルトは preview.webp、.png、または .jpg
screenshots8 個までのパステーマページ用のスクリーンショット
iconpng へのパスウィンドウとトレイのアイコン
license60 文字までの文字列ライセンス。例: CC-BY-4.0
credits12 個までの文字列クレジット:アート、音楽、フォントの作者
homepagehttps://…テーマページに表示する作者のリンク
windowオブジェクトウィンドウのサイズと形、節 07
tokensオブジェクト色とサイズ、節 04
fontsオブジェクトフォント、節 04
backgroundオブジェクト背景、節 05
soundsオブジェクトイベントのサウンド、節 05
musicパスまたはオブジェクトBGM、節 05
introパスまたはオブジェクトイントロ、節 05
wordsオブジェクト独自の文言、節 06
cssパススタイルシート、節 08
sceneパスまたはオブジェクトシーン、節 09
settings配列テーマの設定、節 11
manifest.json:全フィールド
{
  "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ウィンドウ中央のハイライト
panelrgba(12,10,22,.78)パネル、メニュー、ダイアログの背景
line#23232e枠線と区切り線
text#e9e9f2本文
muted#6f6f80キャプションと補助テキスト
accent#a855f7メインカラー:接続ボタン、選択中のノード、見出し
accent2#7c3aedセカンドアクセント:ホバー、グラデーション
ok#4ade80チャネル接続中、「保護中」
warn#f97316メンテナンス、警告
danger#ef4444エラー、チャネル切断
glowrgba(168,85,247,.45)アクセントの周りのグロー
shadowrgba(0,0,0,.6)ダイアログの影
overlayrgba(0,0,0,.72)ダイアログの下の暗幕
radius0pxボタンとパネルの角丸
border1px枠線の太さ
gap14pxパネル間の余白

試してみる

右側で色を調整してください。下に manifest.json 用のコードが出ます。

NII IZNANKA VPN
ヘルプテーマサーバー設定
オランダ · 41 msリトアニア · 33 msイタリア · 58 ms接続
STATUS保護中受信 1.3 GB

    

フォント

役割は 3 つ。クライアントがファイルを読み込み、--nf-font-body、--nf-font-display、--nf-font-mono を設定します。

  • body:すべてのテキスト
  • display:大きな文字とボタン
  • mono:数字、ログ、アドレス
manifest.json
"fonts": {
  "body": "assets/fonts/grotesk.woff2",
  "display": "assets/fonts/pixel.woff2",
  "mono": "assets/fonts/mono.woff2"
}
▸ 言語
クライアントは 4 言語に対応しています。フォントにキリル文字や漢字がない場合はシステムフォントで代替されます。各言語でテーマを確認してください。フォントは埋め込みが許可されたライセンスのものを。OFL なら問題ありません。

05パレット

背景、サウンド、イントロ

背景

フィールド項目デフォルト
image背景画像なし
video背景動画:無音、ループ、画像の上に重ねるなし
color画像の下の色なし
fitcover、contain、または fillcover
positionCSS と同じ:center、top、 left 30%center
dim暗さ 0…10
blurぼかし 0…40 px0

イベントのサウンド

イベントの音はクライアントが再生します。オン・オフと音量はテーマごとにクライアントの設定にあります。

イベント再生タイミング
connectチャネルが接続された
disconnectチャネルが切断された
error接続に失敗した、またはチャネルが切れた
switch接続中のノード切り替え
clickクライアントのウィンドウでボタンを押した
hoverボタンにホバーした
open, closeダイアログが開いた・閉じた
notify通知
manifest.json
"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:イントロはテーマを初めて選んだときだけ
manifest.json
"intro": { "video": "assets/intro.mp4", "subtitles": "assets/intro.vtt", "volume": 0.85, "once": false }
assets/intro.vtt
WEBVTT

00:00.500 --> 00:03.200
接続確立。ようこそ。

00:03.400 --> 00:06.000
チャネル開通。

06パレット

独自の文言

クライアントはテーマの言葉で話せます。BERSERK ではサーバーが「土地」に、設定が「装備」になりました。今はどのテーマでもできます。

  • 文言は言語ごとに指定、40 文字まで
  • 言語を指定しなければ、クライアントの通常の文言のまま
manifest.json
"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
manifest.json:BERSERK のようなギザギザの縁
"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
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クライアントのロゴと名前
markbrand 内のリングロゴ
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アドレス、通信量、セッション時間
telegramTelegram ボタン
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-statusoff, connecting, on, disconnecting, switching, error
data-nf-themeあなたのテーマの id
data-nf-langru, en, zh, ja
data-nf-full全画面モードで付与
出口を隠さない
テーマ切り替えボタンとウィンドウのボタンは見える状態にしてください。テーマからは常に抜けられる必要があります。Ctrl+Shift+Backspace で基本テーマに戻せますが、そうしたテーマはモデレーターが通しません。

09シーン

ウィンドウを丸ごと自作

シーンはパッケージ内の HTML ページです。クライアントはそれをウィンドウ全体にサンドボックスで開き、SDK で状態を渡します。中ではブラウザにできることがすべて使えます:canvas、WebGL、CSS アニメーション、動画、サウンド、独自フォント。

manifest.json
"scene": { "entry": "scene/index.html", "mode": "full" }
mode動作
fullシーンがウィンドウ全体を占め、クライアントのメイン画面は隠れます。クライアントのダイアログ(設定、サーバー、ヘルプ)はシーンの上に開き、トークンと theme.css で色付けされます
backdropシーンは通常のクライアントウィンドウの背後で動く背景になります。マウスとキーボードの入力は受け取りません

ウィンドウのレイヤー

  1. イントロintro、最前面
  2. クライアントのダイアログtokens + theme.css
  3. シーン fullscene/index.html
  4. クライアントのウィンドウtokens + theme.css
  5. シーン backdrop動く背景
  6. 背景background

シーンの骨組み

scene/index.html
<!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>
scene/scene.js
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 秒待ちます
GPU なしの場合
仮想マシンやリモートデスクトップでは canvas は CPU で描画されます。フレームループ内の 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.previewbooleantrue プレビュー時(サイトまたは nftheme preview)、クライアントでは false
nf.scale数値次の呼び出し後のフレーム倍率: nf.fit()
nf.version文字列SDK のバージョン

イベント

イベント受け取るデータタイミング
initすべてクライアントがデータを渡したときに 1 回。ここから描画を始めます
statenf.stateチャネルの接続・切断、ノード・言語・サウンド・全画面の変更
statsnf.statsチャネル接続中は毎秒
nodesnf.nodes新しいノード一覧や応答時間の測定結果が届いた(F5、nf.ping())
settingsnf.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 オブジェクト

nf.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 }
}
▸ state にないもの
キー、ノードのアドレス、ユーザーの IP。シーンには不要です。

11すべてのレベル

テーマの設定

  • 設定をマニフェストに書けば、クライアントが設定画面のあなたのテーマ欄に表示します
  • 値はクライアントが保存します。シーンは nf.settings と次のイベントで受け取ります: settings
  • 設定は 16 個まで
typeフィールド値
toggledefault:true または falseboolean
rangemin, max, step, default数値
selectoptions:{ value, label } の選択肢 2〜12 個、 default文字列
colordefault: #rrggbb#rrggbb 形式の文字列
manifest.json
"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": "ボタンの周りのリングの色" }
]
scene/scene.js
nf.on('settings', (values) => {
  document.body.classList.toggle('glitch', values.glitch);
  rings.speed = values.speed;
});

テーマの音は別途記述する必要はありません。テーマに sounds、music、intro があれば、クライアントがオン・オフと音量を自動で追加します。

12セキュリティ

サンドボックス

テーマは、ユーザーの VPN キーがあるアプリの中で動く他人のコードです。そのためクライアントは、審査済みであってもどのテーマも信用しません。

シーン · サンドボックスHTML、CSS、JS、canvas、動画nftheme://id/:自分のファイルのみ
NII Iznanka クライアントキー、設定、アドレストンネル、ファイル、ネットワーク

ネットワークキーファイルウィンドウ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 呼び出しログ
  • 保存のたびにファイルを再読み込み
▸ スタブ
スタブはシーンと SDK を正確に再現します。通常のクライアントウィンドウは似ていますが完全に同じではありません。theme.css の最終結果はクライアントで確認してください。

クライアントで

  1. テーマ → 作者向け →「テーマフォルダを開く」:フォルダからテーマを読み込み、保存のたびに再読み込みします
  2. 「.nftheme ファイルを開く」:ビルド済みパッケージの確認
  3. 「開発者ツール」:シーンのコンソール、レイアウト、ネットワークを表示する DevTools。ブロックされたものも確認できます

フォルダやファイルから読み込んだテーマには未検証と表示され、カタログのテーマと同じサンドボックスで動きます。

よくあるエラー

メッセージ対処法
ファイル「…」がパッケージにありませんマニフェストのパスがファイルと一致しません。大文字・小文字も区別されます
使用できない名前ファイル名を半角英字にし、スペースを除く
中身が .mp3 に見えません別形式のファイルです。名前の変更では直らないので、変換し直してください
シーンが SDK を読み込んでいません<head> に <script src="/__nf/sdk.js"> を追加
id「…」は使用済みです他の作者かスタジオのテーマの id です。自分の id を選んでください
公開済みより新しいバージョンではありません上げる: version
ウィンドウが動かないシーンに次がない: nf.window.dragRegions()
インターネット上の画像が読み込まれない仕様です。パッケージに入れてください

14公開

審査とカタログ

  1. マイページからスタジオにログイン:メール、またはボットのリンクで
  2. パッケージをドラッグ。サイトが nftheme check と同じコードで検証し、キューに入れます
  3. モデレーターがサンドボックスとクライアントでテーマを開き、コードを確認します。結果は Telegram に届き、却下時は理由も添えられます
  4. 承認されたバージョンにサーバーが署名し、カタログに掲載されます。すでにインストールしている人のクライアントは自動で更新します

モデレーターが確認すること

通る通らない
テーマが動作する:接続、ノード選択、ダイアログ接続ボタンが見つからない、またはわからない
プレビュー:テーマそのものの正直なスクリーンショット別のテーマのプレビューや、注目を集めるための他人のアート
読みやすいコード。既知のライブラリはバージョンとライセンス付きでソースのない難読化・圧縮コード、eval、文字列からのコード実行
アート、音楽、フォントは自作か使用許可のあるもの、出典は次に記載: credits許可のない他人の作品。権利者から申し立てがあればテーマは削除されます
テーマから抜けられる:テーマ切り替え、最小化、閉じる隠されたテーマボタン、罠、予告なしの全画面点滅
音は sounds か nf.sound 経由で、設定を尊重設定を無視した音、大音量の不意打ち
アイドル時はほとんど CPU を使わない最小化中にファンが全開になる
偽のキー入力画面や決済画面、広告、リンク、紹介プログラム、18 禁、扇動や嫌がらせ

バージョン、非表示、取り消し

  • 新しいバージョンは毎回審査を受けます。審査中は以前のバージョンがカタログに残ります
  • 新しい申請を出すと、未審査の以前の申請は取り下げられます
  • 却下されたバージョンは同じ番号で再提出できます
  • 非表示:テーマはカタログから外されますが、インストール済みの人はそのまま使えます
  • 取り消し:危険なテーマのため、クライアントが無効化・削除します
  • アップロードは 1 日 12 回まで

15レシピ

すぐ使えるテクニック

接続ボタンを画像に

theme.css
[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); }

切断時の点滅

theme.css
[data-nf-status='error'] [data-nf='screen'] { animation: lost .12s steps(2) 6; }
@keyframes lost { 50% { filter: invert(1) hue-rotate(160deg); } }

通常のウィンドウの背後に動く背景

manifest.json
"tokens": { "bg": "transparent", "panel": "rgba(0,0,0,.55)" },
"scene": { "entry": "stars/index.html", "mode": "backdrop" }
stars/stars.js
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); } });
})();

セッションの記録

scene/scene.js
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 + ' 分');
  }
});

接続時の演出画面

scene/scene.js
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 フィールドはスタジオ署名付きのパッケージでのみ使えます。

準備はいい?

自分のテーマを公開