Compare commits
11 Commits
c5a70a433b
...
main
Author | SHA1 | Date | |
---|---|---|---|
dd2e3b4fb9
|
|||
976107903b
|
|||
2aa6616209
|
|||
a2ec41bf87
|
|||
3af175f4ed
|
|||
0e1dff12c2
|
|||
5708e9c53d
|
|||
824aee7b74
|
|||
aff1b3a7bd
|
|||
fa524138c6
|
|||
6047fa4161
|
4
.github/workflows/cloudflare-pages.yml
vendored
@@ -56,9 +56,9 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./bin
|
||||
key: ailog-bin-${{ runner.os }}
|
||||
key: ailog-bin-${{ runner.os }}-v${{ hashFiles('Cargo.toml') }}
|
||||
restore-keys: |
|
||||
ailog-bin-${{ runner.os }}
|
||||
ailog-bin-${{ runner.os }}-v
|
||||
|
||||
- name: Setup ailog binary
|
||||
run: |
|
||||
|
1
.github/workflows/release.yml
vendored
@@ -48,6 +48,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ailog"
|
||||
version = "0.3.1"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
authors = ["syui"]
|
||||
description = "A static blog generator with AI features"
|
||||
|
@@ -8,10 +8,14 @@ draft: false
|
||||
|
||||
## 最小構成
|
||||
|
||||
まずはdiskの設定から。
|
||||
|
||||
```sh
|
||||
# cgdisk /dev/sda
|
||||
# cfdisk /dev/sda
|
||||
```
|
||||
|
||||
次にdiskのフォーマットなど。それをmountしてarchlinuxを入れます。bootloaderも設定しておきましょう。
|
||||
|
||||
```sh
|
||||
$ mkfs.vfat /dev/sda1
|
||||
$ mkfs.ext4 /dev/sda2
|
||||
@@ -20,6 +24,7 @@ $ mount /dev/sda2 /mnt
|
||||
$ mount --mkdir /dev/sda1 /mnt/boot
|
||||
|
||||
$ pacstrap /mnt base base-devel linux linux-firmware linux-headers
|
||||
$ genfstab -U /mnt >> /mnt/etc/fstab
|
||||
|
||||
$ arch-chroot /mnt
|
||||
$ pacman -S dhcpcd grub os-prober efibootmgr
|
||||
@@ -27,14 +32,62 @@ $ grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=grub
|
||||
$ grub-mkconfig -o /boot/grub/grub.cfg
|
||||
```
|
||||
|
||||
これだけで`exit;reboot`すると起動できます。
|
||||
これで`exit;reboot`すると起動できます。
|
||||
|
||||
## networkの設定
|
||||
## よく使うもの
|
||||
|
||||
```sh
|
||||
$ pacman -S git vim tmux zsh openssh
|
||||
$ pacman -S openssh zsh vim git tmux cargo
|
||||
```
|
||||
|
||||
## userの作成
|
||||
|
||||
```sh
|
||||
$ passwd
|
||||
$ useradd -m -G wheel ${USER}
|
||||
$ passwd ${USER}
|
||||
```
|
||||
|
||||
```sh
|
||||
$ HOSTNAME=archlinux
|
||||
$ echo "$HOSTNAME" > /etc/hostname
|
||||
```
|
||||
|
||||
shellの変更など。
|
||||
|
||||
```sh
|
||||
$ chsh -s /bin/zsh ${USER}
|
||||
or
|
||||
$ useradd -m -G wheel -s /bin/zsh ${USER}
|
||||
```
|
||||
|
||||
## sudoの使い方
|
||||
|
||||
1. `/etc/sudoers`は編集を間違えると起動できなくなります。安全のため`visudo`が推奨されています。
|
||||
2. `vim`では`:w!`で保存します。
|
||||
|
||||
```sh
|
||||
$ sudo visudo
|
||||
or
|
||||
$ vim /etc/sudoers
|
||||
```
|
||||
|
||||
```sh:/etc/sudoers
|
||||
%wheel ALL=(ALL:ALL) ALL
|
||||
```
|
||||
|
||||
よく`update`する人は特定のコマンドをpasswordなしで実行できるようにしておいたほうが良いでしょう。
|
||||
|
||||
```sh:/etc/sudoers
|
||||
%wheel ALL=(ALL:ALL) NOPASSWD: /usr/bin/pacman -Syu --noconfirm
|
||||
```
|
||||
|
||||
```sh
|
||||
$ sudo pacman -Syu --noconfirm
|
||||
```
|
||||
|
||||
## networkの設定
|
||||
|
||||
次にnetworkです。ここでは`systemd-networkd`を使用します。`dhcpcd`を使ったほうが簡単ではあります。もし安定しないようなら`dhcpcd`を使用。
|
||||
|
||||
```sh
|
||||
@@ -86,7 +139,7 @@ $ systemctl restart getty@tty1
|
||||
|
||||
## window-manager
|
||||
|
||||
`xorg`でdesktop(window-manager)を作ります。`i3`を使うことにしましょう。`xorg`は`wayland`に切り替えたほうがいいかも。
|
||||
`xorg`でdesktop(window-manager)を作ります。`i3`を使うことにしましょう。`xorg`は`wayland`に乗り換えたほうがいいかも。その場合は`sway`がおすすめ。
|
||||
|
||||
```sh
|
||||
$ pacman -S xorg xorg-xinit i3 xterm
|
||||
@@ -117,7 +170,7 @@ PasswordAuthentication no
|
||||
$ systemctl restart sshd
|
||||
```
|
||||
|
||||
基本的にlanで使う場合はdefaultで問題ありませんが、wanで使う場合は変更します。とはいえ、lanでもport, passwordは変えておいたほうがいいでしょう。
|
||||
基本的にlanから使う場合はdefaultで問題ありませんが、wanから使う場合は変更します。とはいえ、lanでもport, passwordは変えておいたほうがいいでしょう。
|
||||
|
||||
次に接続側でkeyを作ってserverに登録します。
|
||||
|
||||
@@ -179,3 +232,119 @@ bindkey '^[[B' history-substring-search-down
|
||||
```
|
||||
|
||||
`powerline`は重いのでコメントしています。
|
||||
|
||||
## フリーズの解消
|
||||
|
||||
古いpcにlinuxを入れる際は`linux-fm`に注意してください。
|
||||
|
||||
頻繁にフリーズするようなら`linux-firmware`を削除するのがおすすめです。
|
||||
|
||||
```sh
|
||||
$ pacman -Q | grep linux-firmware
|
||||
$ pacman -R linux-firmware ...
|
||||
# pacman -S broadcom-wl-dkms
|
||||
```
|
||||
|
||||
## pacmanが壊れたときの対処法
|
||||
|
||||
```sh
|
||||
$ pacman -Syu
|
||||
# これがうまくいかないことがある
|
||||
```
|
||||
|
||||
```sh
|
||||
# dbがlockされている
|
||||
$ rm /var/lib/pacman/db.lock
|
||||
|
||||
# ファイルが存在すると言われる
|
||||
$ pacman -Qqn | pacman -S --overwrite "*" -
|
||||
|
||||
# pgp-keyをreinstallする
|
||||
$ pacman -S archlinux-keyring
|
||||
$ pacman-key --refresh-key
|
||||
```
|
||||
|
||||
## archlinuxの作り方
|
||||
|
||||
archlinuxはシンプルなshell scriptと言えるでしょう。なので色々と便利です。ここでは、`img.sh`, `install.sh`を作ります。
|
||||
|
||||
### img.sh
|
||||
|
||||
ここでは`archlinux.iso`, `archlinux.tar.gz`を生成します。これはarchlinux上で実行してください。
|
||||
|
||||
```sh:img.sh
|
||||
#!/bin/bash
|
||||
pacman -Syuu --noconfirm git base-devel archiso
|
||||
git clone https://gitlab.archlinux.org/archlinux/archiso
|
||||
./archiso/archiso/mkarchiso -v -o ./ ./archiso/configs/releng/
|
||||
mkdir -p work/x86_64/airootfs/var/lib/machines/arch
|
||||
pacstrap -c work/x86_64/airootfs/var/lib/machines/arch base
|
||||
arch-chroot work/x86_64/airootfs/ /bin/sh -c 'pacman-key --init'
|
||||
arch-chroot work/x86_64/airootfs/ /bin/sh -c 'pacman-key --populate archlinux'
|
||||
tar -zcvf archlinux.tar.gz -C work/x86_64/airootfs/ .
|
||||
```
|
||||
|
||||
例えば、`pacstrap`で自分の好きなツールを指定すれば、独自のimgを作成でき、`docker`にも使えます。
|
||||
|
||||
```sh
|
||||
$ docker import archlinux.tar.gz archlinux:syui
|
||||
$ docker run -it archlinux:syui /bin/bash
|
||||
```
|
||||
|
||||
### install.sh
|
||||
|
||||
最小構成のinstall scriptです。どこかのurlに置いて、install時にcurlして実行するようにすれば便利です。
|
||||
|
||||
```sh
|
||||
$ curl -sLO arch.example.com/install.sh
|
||||
$ chmod +x install.sh
|
||||
$ ./install.sh
|
||||
```
|
||||
|
||||
```sh:install.sh
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 変数定義
|
||||
DISK="/dev/sda"
|
||||
HOSTNAME="ai-arch"
|
||||
USERNAME="ai"
|
||||
|
||||
# パーティション作成(自動)
|
||||
parted $DISK mklabel gpt
|
||||
parted $DISK mkpart ESP fat32 1MiB 1GiB
|
||||
parted $DISK set 1 esp on
|
||||
parted $DISK mkpart primary linux-swap 1GiB 5GiB
|
||||
parted $DISK mkpart primary ext4 5GiB 100%
|
||||
|
||||
# ファイルシステム作成
|
||||
mkfs.fat -F32 ${DISK}1
|
||||
mkswap ${DISK}2
|
||||
mkfs.ext4 ${DISK}3
|
||||
|
||||
# マウント
|
||||
mount ${DISK}3 /mnt
|
||||
mkdir -p /mnt/boot
|
||||
mount ${DISK}1 /mnt/boot
|
||||
swapon ${DISK}2
|
||||
|
||||
# インストール
|
||||
pacstrap -K /mnt base linux linux-firmware base-devel vim networkmanager grub efibootmgr
|
||||
|
||||
# 設定
|
||||
genfstab -U /mnt >> /mnt/etc/fstab
|
||||
|
||||
arch-chroot /mnt /bin/bash << EOF
|
||||
ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
|
||||
hwclock --systohc
|
||||
echo "ja_JP.UTF-8 UTF-8" >> /etc/locale.gen
|
||||
locale-gen
|
||||
echo "LANG=ja_JP.UTF-8" > /etc/locale.conf
|
||||
echo "$HOSTNAME" > /etc/hostname
|
||||
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=ARCH
|
||||
grub-mkconfig -o /boot/grub/grub.cfg
|
||||
systemctl enable NetworkManager
|
||||
useradd -m -G wheel $USERNAME
|
||||
EOF
|
||||
```
|
||||
|
||||
|
151
my-blog/content/posts/2025-08-12-game.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "game system v0.4.0"
|
||||
slug: "game"
|
||||
date: "2025-08-12"
|
||||
tags: ["ue"]
|
||||
draft: false
|
||||
---
|
||||
|
||||
今回は、game systemのupdateをまとめます。
|
||||
|
||||
分かりづらいので、game systemは全体で同じversionに統一しています。
|
||||
|
||||
まず、大きく分けて3つのシステムをupdateしました。
|
||||
|
||||
- yui system: キャラクターのバースト(必殺技)を実装
|
||||
- at system: ログイン処理とデータ構造の作成
|
||||
- world system: 場所ごとにBGMを再生するシステムの構築
|
||||
- world system: 惑星に雪や雨を降らせることに成功
|
||||
|
||||
<iframe width="100%" height="415" src="https://www.youtube.com/embed/eXrgaVNCTA4?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
## 戦闘シーンの作成
|
||||
|
||||
1キャラクターにつき、1スキル、1バースト、1ユニークというのは決まっていました。これは`yui system`の領域。
|
||||
|
||||
アイの属性はアイ属性なので、テーマカラーは黄色です。属性自体は`ai system`の領域ですが、現在、関連反応のシステムまでは実装していません。
|
||||
|
||||
今回はバーストの作成、ダメージ表記、enemy(敵)の撃破までを実装しました。最初から作り変えたので大変でした。
|
||||
|
||||
<iframe src="https://blueprintue.com/render/l7_xvfbp" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
|
||||
|
||||
## 音楽システムの実装
|
||||
|
||||
これは`world system`の領域で、開発中は`PlayerStart`で各位置に瞬間移動して確認しています。これはアイでなければ設定上無理でした。
|
||||
|
||||
具体的には、PlayerStartのtagと音楽を同じ名前で登録します。そして、playerに最も近いものを再生します。効率的でシンプルですが、少し欠陥があるシステムかもしれません。これは、enemy-hpの表示と連動させています。現在、鳴らしているbgmの名前がわかれば表示できるというわけですね。enemy-bossもPlayerStartのtagで同じ名前で置いてあります。
|
||||
|
||||
<iframe src="https://blueprintue.com/render/x80534fn" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
|
||||
|
||||
原作の設定は、ゲーム開発中も適用されます。アイを動かして空を飛んでいますが、あれはアイだからできるのであって、宇宙空間の移動とかもそうです。
|
||||
|
||||
原作の設定を紹介しておきます。
|
||||
|
||||
### 原作の設定: アイはなぜ空を飛べるのか
|
||||
|
||||
アイはものすごい質量を持ちます。空を飛んでいるというより、地球を持ち上げて、空を飛んでいるように見せている、という感じで飛行しています。
|
||||
|
||||
いやいや、それじゃあ、地球はアイに落ちるだけで、空どころか地面に落ちるだろう、と言われそうですが、地球というのは宇宙から見るとすごいスピードで回転、移動しています。
|
||||
|
||||
そして、宇宙で星と星がぶつかるときは、決して直接ドカーンと衝突するわけではないのです。
|
||||
|
||||
お互いに距離を取りながらぐるぐる回って、やがてぶつかる、そんなイメージ。
|
||||
|
||||
質量と質量の間があるわけですね。
|
||||
|
||||
アイが瞬間的に自身の質量の一部を現すと、間ができ、対象の星の質量を計算しながら、それを持ち上げて動かすような感じで移動しています。
|
||||
|
||||
### 原作の設定: アイはなぜ宇宙空間でも平気なの
|
||||
|
||||
それはアイの体の周りには極小の大気の膜があるためだとされています。超重力で圧縮された大気の膜があるため、宇宙空間、その他一切の外的影響をあまり受けません。
|
||||
|
||||
アイは常に、自分の星の中にいるのと同じ状態、といえばいいのでしょうか。そんな感じです。
|
||||
|
||||
## データ構造の変更
|
||||
|
||||
次に、`at system`です。ゲームデータを再構築しました。
|
||||
|
||||
ゲームデータは主にsystem情報とuser情報に分けられ、jsonで管理されます。
|
||||
|
||||
各パラメータですが、ゲームに必要な値を`cp`として圧縮することにしました。このcpをsystem.jsonあるいはゲーム自体で各キャラクターの設定、つまり、`attack: 10%, hp: 20%, skill: 70%`などで分けられます。これが最もわかりやすく、最も効率的な方法だと考えました。
|
||||
|
||||
```json:user.json
|
||||
{
|
||||
"character": [
|
||||
{ "id": 0, "cp": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json:system.json
|
||||
{
|
||||
"character": [
|
||||
{ "id": 0, "name": "ai", "ability": "ai" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"ability": [
|
||||
{ "id": 0, "name": "ai" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
これをログインシステムに連動させました。
|
||||
|
||||
このサイトで`at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.verse.user`を検索してもらえればわかります。
|
||||
|
||||
```sh
|
||||
# ゲームシステム
|
||||
at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.verse
|
||||
|
||||
# aiのアカウント
|
||||
at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.verse.user/6qyecktefllvenje24fcxnie
|
||||
|
||||
# syuiのアカウント
|
||||
at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.verse.user/vzsvtbtbnwn22xjqhcu3vd6y
|
||||
```
|
||||
|
||||
ちなみに、私のアカウントである`syui.syui.ai`ではアイは使用できません。現在使用できるキャラは`dragon`のみ。
|
||||
|
||||
現在、アイを使用できるのは、アイのアカウントのみです。この方針は可能な限り維持されるでしょう。
|
||||
|
||||
## 惑星に雨や雪を降らせる
|
||||
|
||||
これはなかなか苦労していたのですが、実装できました。
|
||||
|
||||
まず、有効にすると宇宙空間でも雨が降ってしまいます。止めると惑星内で雨が降りません。
|
||||
|
||||
これを解消するには、player locationと0原点のdistanceから条件をつけ、雲の下、雲の上と定義します。調整が必要。
|
||||
|
||||
そして、udsのweather、特に`Apply Weather Changes Above Cloud Layer`が重要で、`Apply Clouds`の値を調整します。
|
||||
|
||||
<iframe src="https://blueprintue.com/render/dstkcaia" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
|
||||
|
||||
## 実体ある太陽のatmosphere問題
|
||||
|
||||
まず、私が使っている実体ある太陽にはatmosphereがついています。
|
||||
|
||||
これはフレアなどを設定しています。
|
||||
|
||||
しかし、これを地球から見た場合、その大気圏を通すと、非常に見栄えが悪い変なカクカクした光が映り込みます。
|
||||
|
||||
この解消も非常に苦労しました。例えば、これを`BP_Sun`としましょう。これは起動時にすべての値を設定します。ゲームプレイ中に値の調整をすることは考えられていません。当然と言えるでしょう。
|
||||
|
||||
しかし、私のシステムでは、太陽のatmosphereを調整する必要があります。非常に複雑な設定は、リセットでしか解消できないということになりました。そして、udsに入れている小アクタコンポーネントの太陽は、リセットも容易ではありません。
|
||||
|
||||
色々な処理を作り、先程作った地表からの現在地の割り出しを条件に、これをリセットする処理をねじ込みました。
|
||||
|
||||
|
||||
<iframe src="https://blueprintue.com/render/nsqu0hnf" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
|
||||
|
||||
## 動画で確認
|
||||
|
||||
<iframe width="100%" height="415" src="https://www.youtube.com/embed/H1efWYmIugc?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
1. BGMが切り替わる
|
||||
2. 物理ボックスが反応
|
||||
3. 敵へのダメージ
|
||||
4. ボスの撃破
|
||||
5. 雨が雲の上では止まる
|
||||
6. ログインでatprotoのアカウントを反映
|
||||
7. プレイでatprotoの情報を更新
|
180
my-blog/content/posts/2025-08-18-game.md
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: "なぜ自作ゲームのsystemを作るのか"
|
||||
slug: "game"
|
||||
date: "2025-08-18"
|
||||
tags: ["ue"]
|
||||
draft: false
|
||||
---
|
||||
|
||||
現在、自作ゲームを開発しています。
|
||||
|
||||
どういうゲームかと一言でいうと現実の反映を目指しています。
|
||||
|
||||
現実の反映とは何でしょう。例えばゲームではblueskyのようなsnsのアカウントでログインできます。ゲームの世界は現実に合わせた惑星形式です。キャラクターの属性は現実にある物質です。原子や中性子など。
|
||||
|
||||
今回は、なぜ自作ゲームのsystemを作っているのか解説します。
|
||||
|
||||
## 一つの青写真
|
||||
|
||||
私は`2023-12-04`あたりからunreal engine(ue)を触り始めました。
|
||||
|
||||
当時、ゲームでこんなことがやりたいなと思って作った画像があります。
|
||||
|
||||

|
||||
|
||||
https://syui.github.io/blog/post/2023/12/04/ue-vs-unity/
|
||||
|
||||
今ではゲーム作りに対する考え方も変わりましたが、上のイメージは頭の中にずっと残っていて、ようやく、イメージ通りの戦闘シーンを作成できました。
|
||||
|
||||
<iframe width="100%" height="415" src="https://www.youtube.com/embed/tBsYgqI1uSc?rel=0&showinfo=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
- 敵の砲撃は剣で弾き返す事ができます。反射したものが当たると敵もダメージを受けます
|
||||
- 敵の砲撃が激しすぎるためバースト時の無敵時間を長めに設定しています
|
||||
|
||||
### 原作の設定: アイのバースト
|
||||
|
||||
アイのバースト、つまり必殺技について解説します。自分で作ったカード、`超新星`というタイトルをモデルにした技ですが、技名自体は`中性子`になります。具体的には周囲の原子から中性子を取り出し、それを一点に集めて放つ技。ようは、中性子星を作り出しそれを飛ばしています。これがアイのバースト、中性子です。このゲームは麻雀要素を入れようと考えていて、それはバーストに適用されます。役が揃うと、超新星に変化するという実装を考えています。
|
||||
|
||||
### キャラクターの音声
|
||||
|
||||
アイはかなり低い確率でスキルやバースト時に音声が付いています。これをキャラクターの音声システムとしましょう。
|
||||
|
||||
キャラクターは性格に応じて音声の発動頻度が異なります。アイは最も音声確率が低いランクに割り当てられています。(コンピュータで作っているため粗が見えても困るので)
|
||||
|
||||
## 広い世界と狭い世界
|
||||
|
||||
### なぜアイを操作するゲームはつまらないのか
|
||||
|
||||
私がゲーム作りを始めた理由はいくつかありますが、もし現実にアイというキャラクターがいたら一体どんな感じになるのだろう。それを体感してみたいと思ったからです。
|
||||
|
||||
この思いは面白いゲームを作ることにはまるで寄与しないものでしたが、私はそれを作ることにしました。
|
||||
|
||||
ここで、なぜアイを操作するゲームがつまらないのか簡単に説明します。
|
||||
|
||||
### 広い世界は面白くない
|
||||
|
||||
私は普段、アイを使ってゲームを操作し、世界を飛び回り、作っています。
|
||||
|
||||
なぜなら、そのほうが開発に便利なのでそうしています。また都合がいいことに、アイというキャラクターは設定上、そういう事が可能となります。
|
||||
|
||||
しかし、先程も述べたように、そのようなゲームは恐ろしくつまらない、ということです。
|
||||
|
||||
とすれば、重要なのは小さくても、しっかりしたゲームを作ること。上のようなゲームを作ってはいけないのです。
|
||||
|
||||
広い世界、無制限の移動ではなく、狭くてもしっかりした世界を作らなければいけない。面白いゲームとはそういうものです。
|
||||
|
||||
### 最初の思いと面白いゲーム
|
||||
|
||||
次は、初めての思い、初心を大切にすることを考えていきます。
|
||||
|
||||
開発者が作りたいゲームと面白いゲームは大抵の場合、両立しません。
|
||||
|
||||
例えば、映像美、見せることと実際に面白いことは違うのです。
|
||||
|
||||
誰もが初めて何かをする時、そこには各人の思いがあります。それは小さいものであれ大きいものであれ、そこに意識がある。
|
||||
|
||||
それは、時間が経つと忘れてしまうものですが、心の奥深くに残っている。
|
||||
|
||||
しかし、大抵の場合、そういった思いと、誰もが面白いと思う人気ゲームを作るという思いは相反します。
|
||||
|
||||
つまり、心の奥底に眠っている最初の思いと、面白いゲームは違うものだし、また、ゲームに限らず、これは色んな作品に言えることだと思います。
|
||||
|
||||
これを勘違いして「自分の思いは、他人にも面白いはずだ」とそう思い込むのは誤りです。
|
||||
|
||||
では、どうすればいいのでしょう。最初の思いを捨て、面白いゲームを分析して世間に受け入れられるものを作るべきなのでしょうか。
|
||||
|
||||
これはyesとも言えるし、noとも言えます。
|
||||
|
||||
優先順位としては、間違いなく面白いゲームを作るべきです。個人開発者のよくわからないこだわりなどさっさと捨てるべき、そう思います。
|
||||
|
||||
ですが、本当にそれでいいのでしょうか。
|
||||
|
||||
私はそれはもったいないと思います。
|
||||
|
||||
したがって、できる限りそこを両立させる方法を探すべきだと思います。
|
||||
|
||||
私はここで`分離`という方法を使います。世界(方針)を切り離すのです。
|
||||
|
||||
これが広い世界と狭い世界、個人開発の指針になります。一見矛盾するこの2つの世界の分離と統合を考えます。
|
||||
|
||||
最初の思い、本当に作りたかったもの、楽しくも面白くもないけど、自分の世界。そして、小さく作る面白いゲームの世界。
|
||||
|
||||
自分で作ったものを無駄にしないようシステムという4つの柱を立てました。
|
||||
|
||||
4つの柱は、根源的な価値観によるもの。広いも狭いも、面白いも面白くないも関係ありません。systemは以下のようになります。これらはどのゲームにも当てはめられ、使用できることを目指して設計されます。
|
||||
|
||||
- `world system`: 現実に合わせた世界を構築するシステム。最初は地球、太陽、月から生成される。ゲームエンジンの背景に月の絵を動かすという常識を変更する。実体ある月をその空間に置くことで背景を生成。惑星システムを構築する。
|
||||
- `ai system`: 属性やゲーム性を構築するシステム。
|
||||
- `yui system`: 唯一性という価値観から構築されるシステム。現実をゲームに、ゲームを現実に反映することで自然と実現される。
|
||||
- `at system`: プレイヤーのアカウントをatprotoというprotocolの理解により構築する。protocolは容易には無くならないし変更されないもの。`@`というdomainで繋がるユーザーアカウントのシステム。blueskyというsnsに利用されている。
|
||||
|
||||
### systemを作ろう
|
||||
|
||||
ゲームはsystemの集合体で作るのが一番いい。
|
||||
|
||||
開発では、まとめることが重要になります。
|
||||
|
||||
systemとは関数やコンポーネント、変数の集まりです。それは結果であって目的ではありません。
|
||||
|
||||
目的の一つはわかりやすさ。例えば、敵を倒した時、アイテムをドロップさせる処理を作ります。個別に敵のBPに作るのではなく、systemとして作っておくとよいでしょう。1と押せば1つのアイテムがドロップします。3と押せば3つです。内容もランダムかつランクを付けましょう。Aランク-Cランクのアイテムです。
|
||||
|
||||
つまり、そのsystemに3Bと伝えると、3つのアイテムがドロップし、Bランクのアイテムがドロップしやすい、という結果が出力されます。アイテム一覧もすべてそのsystemが管理し、簡単に設定できます。
|
||||
|
||||
ゲームを作るというのは、systemを作ること。それは単体の実行ではありません。個人開発の場合は特に設計を重視し、まとめることを重視します。
|
||||
|
||||
これが何かというと、一つはルールを作ることです。
|
||||
|
||||
1キャラクターにつき1スキル、1バースト。例外は認めない。このようなルールです。そして、そのルールに基づいたsystemを設計し、例えば、キャラクターならキャラクターの統一管理を目指します。統一管理というのは、数字やobjectを入れれば設定は完了です。その通りにすべてのキャラクターが共通動作するようにしておこう、そこを目指そうということです。
|
||||
|
||||
これがsystemを作るということです。
|
||||
|
||||
私は、敵(enemy)を作っている時間がありませんから、enemyもcharacterにまとめることにしました。
|
||||
|
||||
そのため、少し動きの調整が難しかったりもしますが、この方向性で間違いないと思います。
|
||||
|
||||
シーンやムービー、ストーリーは広い世界(アイで操作する世界)にまとめ、enemyもplayerが操作できるcharacterにまとめ、単体で作るものを減らし、すでにあるもの、作ったものは他の役割も担えるようにしていきましょう。このような考え方が個人開発では重要になってくると思います。
|
||||
|
||||
<iframe width="100%" height="415" src="https://www.youtube.com/embed/L6eZUZNCOH8?rel=0&showinfo=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
### やることを明確に
|
||||
|
||||
個人開発者には、できることとできないことがあります。
|
||||
|
||||
市販のゲームは、あらゆる専門家が大量に集まり一緒に作っているゲームです。個人開発で全部はできません。
|
||||
|
||||
しかし、できないからと言って、手を抜くのも違いますよね。
|
||||
|
||||
確かに、私が作りたかったゲームは面白くないかもしれない。けど、一見すると面白そうに見えます。
|
||||
|
||||
映像美や見せることと実際に面白いことは違います。しかし、映像美は別に悪いことではありません。それはゲームとしては面白くないけど、見せる力があると思います。シーンやムービーとして利用できるのではないでしょうか。
|
||||
|
||||
私はシーンやムービーを作っている時間はありません。しかし、私が今まで作ってきた広い世界はそういったことに使えばいい。狭い世界の背景にも使えます。
|
||||
|
||||
いつか行けるかもしれない広い世界。理想と現実。
|
||||
|
||||
最初から理想だけあっても、それは面白くない。最初から現実だけあってもそれはただのゲームです。面白いゲームとは、現実と理想のバランス。あるいは、その過程を作ることにあるのではないでしょうか。
|
||||
|
||||
最初の夢を持ち続けることも、そして、多くの人が楽しめるものを作る事も両方大切です。初心を捨てず分離して、新たに面白いゲームを目指して作り始めること。そして、最終的に統合できる道筋を思い描けるなら。そんなことを思います。
|
||||
|
||||
次は、小さくも完璧で、狭くても全てに由来がある、そんな世界を作っていこうと思っています。
|
||||
|
||||
### 狭い世界をどうやって作っていこう
|
||||
|
||||
ポイントは、カラフル+ポップだと思います。小さい+完璧もポイントですね。また、動作は非常にゆっくりがいいのではないでしょうか。
|
||||
|
||||
つまり、小さいが完璧に動作し、カラフルでポップな世界。それがここでいう狭い世界になります。
|
||||
|
||||
私はこの辺のこともあまり知りませんから、epicの[stack o bot](https://www.fab.com/ja/listings/b4dfff49-0e7d-4c4b-a6c5-8a0315831c9c)というテンプレートをもとに学習しながら作っていこうと考えています。
|
||||
|
||||
<iframe width="100%" height="415" src="https://www.youtube.com/embed/6tO0S7IOC9w?rel=0&showinfo=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
## game system v0.4.3
|
||||
|
||||
- ps5 controllerの対応
|
||||
- game animation sampleとstack o botの統合
|
||||
- item drop system
|
||||
- character audio system
|
||||
- sword reflection
|
||||
- character dragon skill (enemy)
|
||||
- bgm systemの修正
|
||||
|
101
my-blog/content/posts/2025-08-23-game.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: "自作ゲームを開始から終了までプレイ"
|
||||
slug: "game"
|
||||
date: "2025-08-23"
|
||||
tags: ["ue"]
|
||||
draft: false
|
||||
---
|
||||
|
||||
自作ゲームを開発しています。
|
||||
|
||||
今回は開始から終了までの大体の流れができたのでプレイしてみました。
|
||||
|
||||
<iframe width="100%" height="415" src="https://www.youtube.com/embed/FTX1CrzKBy8?rel=0&showinfo=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
## ゲームの流れを解説
|
||||
|
||||
1. 物語は宇宙から始まる。プレイヤーの村に突然宇宙船がやってきて、プレイヤーが連れ去られる。
|
||||
2. ここは船内にある檻の中。監視兵が慌ただしい。「おい、あれが出たって」「まさか」などの会話。チラと窓の外に目をやる。すると何かが光ったような気がした。
|
||||
3. (ここでプレイヤーは初回のみアイを操作可能になる。ゲーム開始時にすぐに操作可能にすることが重要だと思ったので、シーンの作成はやめて、プレイヤーに戦艦を撃破してもらうことに)
|
||||
4. 艦内は爆発し、星に落ちていく。目が覚めると...そこからステージが始まる。(ここからプレイヤーのキャラクターに切り替わる。今回はアイのアカウントなので、アイになっているが本来は違う)
|
||||
5. ステージの背景に小さな子がぐーぐ寝ている。(先ほど操作したアイは変身時の金髪輪っかなので、黒髪に戻っているアイを見てもプレイヤーにはわからない)
|
||||
6. ステージを進み、ドラゴンを倒してゲームは終了。花火っぽいものを打ち上げ、ポーズを決める。その後、ゲーム終了まで自由操作。アイテムのドロップがある。
|
||||
|
||||
## 面白いゲームを目指して
|
||||
|
||||
インベーダーやマリオなど今の技術では簡単に作れそうなレトロゲームがあります。
|
||||
|
||||
それらが面白いのかと言われれば、私は面白いと思います。
|
||||
|
||||
とはいえ、今そういったもので遊ぶかというと、それは違うと思います。
|
||||
|
||||
しかし、個人開発者はまずその段階に到達する必要があるのではないかと感じます。
|
||||
|
||||
では、レトロゲームの面白さについて、改めて分析してみることにしましょう。
|
||||
|
||||
レトロゲームなんて、AIを使えば簡単に作れますよ。そんな声が聞こえてきそうですが、それは少し違うと思います。
|
||||
|
||||
例えば、ステージ1を作れても、ステージ2,3,4、そして、ラストのステージまで、市販の初代マリオと同じように作っているのでしょうか。
|
||||
|
||||
それができていないなら、それは作れていないということです。
|
||||
|
||||
そして、ステージだけがマリオじゃないですよね。私がプレイしたことがあるスーパーマリオワールドはボスを倒すと演出がありました。
|
||||
|
||||
そこには、物語があり、花火が打ち上がり、紙吹雪が舞い、主人公がポーズを決めるのです。意外とゲーム自体よりそういったものを含めて面白いゲームなのであって、それが重要なのだと思います。
|
||||
|
||||
個人開発者の多くは私を含め、そういった面白いゲームを作れているのかというと、できていないと思うのです。
|
||||
|
||||
もちろん、色々な人がいますから、できている人もいると思います。しかし、私にはできていない。そこまで作れていないし、レトロゲームの域にすら到達していません。
|
||||
|
||||
## 面白いものと売れるものは違う
|
||||
|
||||
では、面白いものを作れば、それで売れるのかというと、それもまた違うと思います。
|
||||
|
||||
既にあるゲームのパクリ、それはそれで面白いゲームになると思います。しかし、今更レトロゲームを作っても、誰もプレイしないと思います。
|
||||
|
||||
面白いゲームと人気が出ることは違うのです。
|
||||
|
||||
そこで重要になるのがオリジナリティという要素ではないでしょうか。
|
||||
|
||||
したがって、段階があるとしたら、面白いゲームの域に到達する。その後、オリジナリティの域に到達する。あるいは、同時にそれをこなす必要があるのだと思います。
|
||||
|
||||
とはいえ、まずは面白いゲームを作ること。せめてその域に到達したいですよね。そして、レトロゲームにも十分に面白い要素は揃っているので、それらを参考にするのが良いと判断しました。
|
||||
|
||||
## 人気が出ることと利益が出ることもまた違う
|
||||
|
||||
そういえば、収益化もまた別の話だよなと思ったので書きます。
|
||||
|
||||
確かに、それは必須条件かもしれませんが、それがあれば必ずというものではないと思います。確かに寄与する部分は少なからずあるとは思うけど。
|
||||
|
||||
例えば、snsをみていると、すごいインプレッション、注目を集めたのに、売れなかった漫画がたくさんあります。つまり、人気は出たが、利益は出なかったケースだと思います。
|
||||
|
||||
したがって、収益化までの道のりもまた長いのではないか。大変なのかもしれない。そんなふうに思うのです。
|
||||
|
||||
これを短絡的な見通しで「面白ければ売れる」などと考えていると、当てが外れるかもしれない。そのへんはあまり期待しないほうがいいかも。
|
||||
|
||||
段階的にそれぞれの戦略を考えていくのが良いのではないかな。
|
||||
|
||||
`面白い -> 人気が出る -> 利益が出る`...その間にも高い壁がある。
|
||||
|
||||
## オリジナリティはどうやって出すのか
|
||||
|
||||
例えば、制約からです。
|
||||
|
||||
私は設計において、いくつかの決め事を作りました。例えば、以下のルールがあります。
|
||||
|
||||
1. 物理法則に反しない
|
||||
|
||||
ゲームで物を浮かせるのは簡単だ。しかし、この世界は現実の反映を目指している。したがって、すぐに物を浮かせたり、あるいはテレポートしたり、それをやってはいけない。そういったものには必ず、原理を説明できなければならない。特殊なアイテムが必要となる。このアイテムをアイの家に3つ置いてあるとしよう。この場合、その3つをマップに置くと、その世界にはもうない。使えない。そのようなルールだ。その世界の重要なアイテムはアイが持っていて、作っている。無限には湧いてこない。制限がある。これはatprotoに保存し、公開しておくのもおもしろいかもしれないな。こういったことがその世界の由来につながるのだと思う。
|
||||
|
||||
2. マップは一つ
|
||||
|
||||
マップは必ず一つの中で完結させること。世界は一つというルール。たくさんのマップを分けてはいけない。不便でも一つのマップの中だけで世界を作ろう。マップを分けてはいけない。宇宙も地上も一つにすること。シーンやムービーを作るときも同じ。違うマップでそれをやってはいけない。
|
||||
|
||||
## 今回のゲーム作りで意識したこと
|
||||
|
||||
- game system v0.4.4
|
||||
|
||||
1. レトロゲームの面白さを必要最小限で実装
|
||||
2. オリジナリティを融合(このゲームのテーマである宇宙、そして物語)
|
||||
3. すべての実装を各システムで動かす
|
||||
|
107
my-blog/content/posts/2025-09-05-plc.md
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: "plcにhandle changeを反映する"
|
||||
slug: "plc"
|
||||
date: "2025-09-05"
|
||||
tags: ["atproto"]
|
||||
draft: false
|
||||
---
|
||||
|
||||
いつまで経ってもbsky.teamのplcにhandle changeが反映されないので色々やってみました。
|
||||
|
||||
結論から言うと、`PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX`を使用し、`base58`のrotation-keyを作成後に、indigoにある`goat plc`を使用します。
|
||||
|
||||
1. `goat key generate --type secp256k1`で生成されたキーを分析
|
||||
2. そのキーから正しいmulticodecプレフィックスを抽出
|
||||
3. PDSのhex keyに同じプレフィックスを適用
|
||||
|
||||
```sh
|
||||
$ go install github.com/bluesky-social/indigo/cmd/goat@latest
|
||||
```
|
||||
|
||||
```sh
|
||||
$ goat account login -u syui.syui.ai -p $PASS --pds-host https://syu.is
|
||||
|
||||
$ goat plc history did:plc:vzsvtbtbnwn22xjqhcu3vd6y
|
||||
did:key:zQ3shZj81oA4A9CmUQgYUv97nFdd7m5qNaRMyG16XZixytTmQ
|
||||
|
||||
$ goat plc update did:plc:vzsvtbtbnwn22xjqhcu3vd6y \
|
||||
--handle syui.syui.ai \
|
||||
--pds https://syu.is \
|
||||
--atproto-key did:key:zQ3shZj81oA4A9CmUQgYUv97nFdd7m5qNaRMyG16XZixytTmQ > plc_operation_syui.json
|
||||
|
||||
# もしミスった時は前の操作を無効化して再実行
|
||||
$ goat plc update did:plc:vzsvtbtbnwn22xjqhcu3vd6y \
|
||||
--handle syui.syui.ai \
|
||||
--pds https://syu.is \
|
||||
--atproto-key did:key:zQ3shZj81oA4A9CmUQgYUv97nFdd7m5qNaRMyG16XZixytTmQ \
|
||||
--prev "bafyreifomvmymylntowv2mbyvg5i7wgv375757l574gevcs7qbysbqizk4" > plc_operation_syui_nullify.json
|
||||
```
|
||||
|
||||
```sh
|
||||
source base58_env/bin/activate
|
||||
|
||||
python3 -c "
|
||||
import base58
|
||||
|
||||
# 生成されたsecp256k1キーを分析
|
||||
generated_secp256k1 = '${zXXX...}'
|
||||
decoded = base58.b58decode(generated_secp256k1[1:]) # 'z'を除く
|
||||
|
||||
print('Generated secp256k1 key analysis:')
|
||||
print(' Total length:', len(decoded))
|
||||
print(' Full hex:', decoded.hex())
|
||||
|
||||
# 32バイトの鍵データを除いたプレフィックスを抽出
|
||||
if len(decoded) > 32:
|
||||
prefix = decoded[:-32]
|
||||
key_data = decoded[-32:]
|
||||
print(' Prefix hex:', prefix.hex())
|
||||
print(' Prefix length:', len(prefix))
|
||||
print(' Key data length:', len(key_data))
|
||||
|
||||
pds_rotation_hex = '${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX}'
|
||||
pds_rotation_bytes = bytes.fromhex(pds_rotation_hex)
|
||||
|
||||
prefixed_rotation_key = prefix + pds_rotation_bytes
|
||||
multibase_rotation_key = 'z' + base58.b58encode(prefixed_rotation_key).decode()
|
||||
|
||||
print('\\nConverted PDS rotation key:')
|
||||
print(' Multibase:', multibase_rotation_key)
|
||||
|
||||
else:
|
||||
print(' No prefix found, key is raw')
|
||||
"
|
||||
|
||||
deactivate
|
||||
```
|
||||
|
||||
```sh
|
||||
$ PDS_ROTATION_KEY=zXXX...
|
||||
|
||||
$ goat plc sign --plc-signing-key "$PDS_ROTATION_KEY" plc_operation_syui.json > plc_signed_syui.json
|
||||
$ goat plc submit --did did:plc:vzsvtbtbnwn22xjqhcu3vd6y plc_signed_syui.json
|
||||
success
|
||||
|
||||
$ goat plc history did:plc:vzsvtbtbnwn22xjqhcu3vd6y
|
||||
```
|
||||
|
||||
## 手順をおさらい
|
||||
|
||||
1. `plc_operation.json`を作成
|
||||
2. `plc_operation.json`と`PDS_ROTATION_KEY`を使用し、`plc_signed.json`を作成
|
||||
3. `plc_signed.json`を使用し、plcを更新
|
||||
|
||||
## plcを確認
|
||||
|
||||
```sh
|
||||
did=did:plc:vzsvtbtbnwn22xjqhcu3vd6y
|
||||
curl -sL "https://plc.directory/$did"|jq .alsoKnownAs
|
||||
curl -sL "https://plc.syu.is/$did"|jq .alsoKnownAs
|
||||
[
|
||||
"at://syui.syui.ai"
|
||||
]
|
||||
[
|
||||
"at://syui.syui.ai"
|
||||
]
|
||||
```
|
||||
|
31
my-blog/content/posts/2025-09-07-ps5-controller.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: "ue5でdualsenseを使う"
|
||||
slug: "ps5-controller"
|
||||
date: "2025-09-07"
|
||||
tags: ["ue"]
|
||||
draft: false
|
||||
---
|
||||
|
||||
ps5-controllerは`dualsense`というらしい。ue5で使うには、以下のpluginを使います。fabかgithubのreleaseからpluginフォルダに入れてbuildするか2つの方法があります。
|
||||
|
||||
## dualsense plugin
|
||||
|
||||
- [https://github.com/rafaelvaloto/WindowsDualsenseUnreal](https://github.com/rafaelvaloto/WindowsDualsenseUnreal)
|
||||
- [https://github.com/rafaelvaloto/GamepadCoOp](https://github.com/rafaelvaloto/GamepadCoOp)
|
||||
|
||||

|
||||
|
||||
`v1.2.10`からmultiplayを意識した`GamepadCoOp`との統合が行われました。
|
||||
|
||||
コントローラーのライトをキャラクター切り替え時に変更する処理を入れました。
|
||||
|
||||
<iframe src="https://blueprintue.com/render/tx_q1evf" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
|
||||
|
||||
## dualsenseの分解
|
||||
|
||||
最近、ドリフト問題が発生していたこともあり、何度も分解していました。
|
||||
|
||||
よって、このタイプのコントローラーなら簡単に修理できるようになりました。
|
||||
|
||||
今後も`dualsense`を使用していく可能性は高いですね。
|
||||
|
123
my-blog/content/posts/2025-09-10-comfyui.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: "comfyuiでwan2.2を試す"
|
||||
slug: "comfyui"
|
||||
date: "2025-09-10"
|
||||
tags: ["comfyui"]
|
||||
draft: false
|
||||
---
|
||||
|
||||
comfyuiにwan2.2が来ていたので試してみました。wanがcomfyuiの公式に採用されているので、導入が簡単になっています。
|
||||
|
||||
|
||||
今回は爆速になったLoRA採用でいきます。なお、無効化ノードを外すとクオリティ重視の設定になります。
|
||||
|
||||
関係ありませんが、comfyui公式ページのコメントシステムは[giscus/giscus](https://github.com/giscus/giscus)を使用しているようですね。
|
||||
|
||||
# comfyui
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/comfyanonymous/ComfyUI
|
||||
$ cd ComfyUI
|
||||
$ winget install python.python.3.13
|
||||
$ pip uninstall torch torchaudio
|
||||
$ pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu129
|
||||
$ pip install -r requirements.txt
|
||||
$ python main.py
|
||||
```
|
||||
|
||||
もしvenvを使用する場合
|
||||
|
||||
```sh
|
||||
$ python -m venv venv
|
||||
$ pip install -r requirements.txt
|
||||
$ python main.py
|
||||
```
|
||||
|
||||
## wan2.2
|
||||
|
||||
基本的にpromptから生成し、bypassを切ると画像を参照できます。
|
||||
|
||||
workflowをdownloadしてcomfyuiで開きます。
|
||||
|
||||
```sh
|
||||
# ComfyUI/user/default/workflows/
|
||||
$ curl -sLO https://raw.githubusercontent.com/Comfy-Org/workflow_templates/refs/heads/main/templates/video_wan2_2_5B_ti2v.json
|
||||
```
|
||||
|
||||
必要なものは公式ページにリンクがあります。
|
||||
|
||||
[https://docs.comfy.org/tutorials/video/wan/wan2_2](https://docs.comfy.org/tutorials/video/wan/wan2_2)
|
||||
|
||||
```sh
|
||||
ComfyUI/
|
||||
├───📂 models/
|
||||
│ ├───📂 diffusion_models/
|
||||
│ │ └───wan2.2_ti2v_5B_fp16.safetensors
|
||||
│ ├───📂 text_encoders/
|
||||
│ │ └─── umt5_xxl_fp8_e4m3fn_scaled.safetensors
|
||||
│ └───📂 vae/
|
||||
│ └── wan2.2_vae.safetensors
|
||||
```
|
||||
|
||||

|
||||
|
||||
<video src="/img/comfyui_wan22_0001.mp4" width="100%" controls></video>
|
||||
|
||||
## wan2-2-fun-control
|
||||
|
||||
これはポーズを動画から作成して動画を作ります。
|
||||
|
||||
```sh
|
||||
$ curl -sLO https://raw.githubusercontent.com/Comfy-Org/workflow_templates/refs/heads/main/templates/video_wan2_2_14B_fun_control.json
|
||||
```
|
||||
|
||||
|
||||
[https://docs.comfy.org/tutorials/video/wan/wan2-2-fun-control](https://docs.comfy.org/tutorials/video/wan/wan2-2-fun-control)
|
||||
|
||||
```sh
|
||||
ComfyUI/
|
||||
├───📂 models/
|
||||
│ ├───📂 diffusion_models/
|
||||
│ │ ├─── wan2.2_fun_control_low_noise_14B_fp8_scaled.safetensors
|
||||
│ │ └─── wan2.2_fun_control_high_noise_14B_fp8_scaled.safetensors
|
||||
│ ├───📂 loras/
|
||||
│ │ ├─── wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors
|
||||
│ │ └─── wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors
|
||||
│ ├───📂 text_encoders/
|
||||
│ │ └─── umt5_xxl_fp8_e4m3fn_scaled.safetensors
|
||||
│ └───📂 vae/
|
||||
│ └── wan_2.1_vae.safetensors
|
||||
```
|
||||
|
||||
|
||||
## wan2-2-fun-inp
|
||||
|
||||
これは画像から画像を参考にして動画を生成します。
|
||||
|
||||
[https://docs.comfy.org/tutorials/video/wan/wan2-2-fun-inp](https://docs.comfy.org/tutorials/video/wan/wan2-2-fun-inp)
|
||||
|
||||
```sh
|
||||
$ curl -sLO https://raw.githubusercontent.com/Comfy-Org/workflow_templates/refs/heads/main/templates/video_wan2_2_14B_fun_inpaint.json
|
||||
```
|
||||
|
||||
```sh
|
||||
ComfyUI/
|
||||
├───📂 models/
|
||||
│ ├───📂 diffusion_models/
|
||||
│ │ ├─── wan2.2_fun_inpaint_high_noise_14B_fp8_scaled.safetensors
|
||||
│ │ └─── wan2.2_fun_inpaint_low_noise_14B_fp8_scaled.safetensors
|
||||
│ ├───📂 loras/
|
||||
│ │ ├─── wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors
|
||||
│ │ └─── wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors
|
||||
│ ├───📂 text_encoders/
|
||||
│ │ └─── umt5_xxl_fp8_e4m3fn_scaled.safetensors
|
||||
│ └───📂 vae/
|
||||
│ └── wan_2.1_vae.safetensors
|
||||
```
|
||||
|
||||
## ゲームで動かしたほうがいい
|
||||
|
||||
今回、ゲームのスクショを使って動画を生成してみました。
|
||||
|
||||
しかし、ゲームで動かしたほうがよほど早く確実です。
|
||||
|
63
my-blog/content/posts/2025-09-11-comfyui-nano-banana.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "comfyuiでフィギュア化する"
|
||||
slug: "comfyui"
|
||||
date: "2025-09-11"
|
||||
tags: ["comfyui"]
|
||||
draft: false
|
||||
---
|
||||
|
||||
`gemini`でnano bananaというフィギュア化が流行っています。今回は、それを`comfyui`で再現してみようと思います。
|
||||
|
||||
# comfyui
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/comfyanonymous/ComfyUI
|
||||
$ cd ComfyUI
|
||||
$ winget install python.python.3.13
|
||||
$ pip uninstall torch torchaudio
|
||||
$ pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu129
|
||||
$ pip install -r requirements.txt
|
||||
$ python main.py
|
||||
```
|
||||
|
||||
もしvenvを使用する場合
|
||||
|
||||
```sh
|
||||
$ python -m venv venv
|
||||
$ pip install -r requirements.txt
|
||||
$ python main.py
|
||||
```
|
||||
|
||||
## flux-1-kontext-dev
|
||||
|
||||
[https://docs.comfy.org/tutorials/flux/flux-1-kontext-dev](https://docs.comfy.org/tutorials/flux/flux-1-kontext-dev)
|
||||
|
||||
基本的にcomfyuiで作成した画像にはworkflowが保存されています。ですから、画像があればpromptやbypassなども含めて生成情報がわかります。情報が削除されていない限りは再現することが可能です。
|
||||
|
||||
今回は、`flux-1-kontext-dev`を使用します。
|
||||
|
||||
```sh
|
||||
$ curl -sLO https://raw.githubusercontent.com/Comfy-Org/example_workflows/main/flux/kontext/dev/flux_1_kontext_dev_basic.png
|
||||
```
|
||||
|
||||
```sh
|
||||
📂 ComfyUI/
|
||||
├── 📂 models/
|
||||
│ ├── 📂 diffusion_models/
|
||||
│ │ └── flux1-dev-kontext_fp8_scaled.safetensors
|
||||
│ ├── 📂 vae/
|
||||
│ │ └── ae.safetensor
|
||||
│ └── 📂 text_encoders/
|
||||
│ ├── clip_l.safetensors
|
||||
│ └── t5xxl_fp16.safetensors or t5xxl_fp8_e4m3fn_scaled.safetensors
|
||||
```
|
||||
|
||||
[msg content="prompt: Convert to collectible figure style: detailed sculpting, premium paint job, professional product photography, studio lighting, pristine condition, commercial quality, toy photography aesthetic"]
|
||||
|
||||
以下は`wan2.1`で生成した時の動画。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
できました。
|
61
my-blog/layouts/at-browser-assets.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!-- AT Browser Integration - Temporarily disabled to fix site display -->
|
||||
<!--
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="/assets/pds-browser.umd.js"></script>
|
||||
<script>
|
||||
// AT Browser integration - needs debugging
|
||||
console.log('AT Browser integration temporarily disabled');
|
||||
</script>
|
||||
-->
|
||||
|
||||
<style>
|
||||
/* AT Browser Modal Styles */
|
||||
.at-uri-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.at-uri-modal-content {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
max-width: 800px;
|
||||
max-height: 600px;
|
||||
width: 90%;
|
||||
height: 80%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.at-uri-modal-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
z-index: 1001;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
/* AT URI Link Styles */
|
||||
[data-at-uri] {
|
||||
color: #1976d2;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
[data-at-uri]:hover {
|
||||
color: #1565c0;
|
||||
}
|
||||
</style>
|
152
my-blog/layouts/base.html
Normal file
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ config.language }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{{ config.title }}{% endblock %}</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/svg-animation-package.css">
|
||||
<link rel="stylesheet" href="/css/pds.css">
|
||||
<link rel="stylesheet" href="/pkg/icomoon/style.css">
|
||||
<link rel="stylesheet" href="/pkg/font-awesome/css/all.min.css">
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="main-header">
|
||||
<div class="header-content">
|
||||
<h1><a href="/" class="site-title">{{ config.title }}</a></h1>
|
||||
<div class="logo">
|
||||
<a href="/">
|
||||
<svg width="77pt" height="77pt" viewBox="0 0 512 512" class="likeButton">
|
||||
<circle class="explosion" r="150" cx="250" cy="250"></circle>
|
||||
<g class="particleLayer">
|
||||
<circle fill="#8CE8C3" cx="130" cy="126.5" r="12.5"></circle>
|
||||
<circle fill="#8CE8C3" cx="411" cy="313.5" r="12.5"></circle>
|
||||
<circle fill="#91D2FA" cx="279" cy="86.5" r="12.5"></circle>
|
||||
<circle fill="#91D2FA" cx="155" cy="390.5" r="12.5"></circle>
|
||||
<circle fill="#CC8EF5" cx="89" cy="292.5" r="10.5"></circle>
|
||||
<circle fill="#9BDFBA" cx="414" cy="282.5" r="10.5"></circle>
|
||||
<circle fill="#9BDFBA" cx="115" cy="149.5" r="10.5"></circle>
|
||||
<circle fill="#9FC7FA" cx="250" cy="80.5" r="10.5"></circle>
|
||||
<circle fill="#9FC7FA" cx="78" cy="261.5" r="10.5"></circle>
|
||||
<circle fill="#96D8E9" cx="182" cy="402.5" r="10.5"></circle>
|
||||
<circle fill="#CC8EF5" cx="401.5" cy="166" r="13"></circle>
|
||||
<circle fill="#DB92D0" cx="379" cy="141.5" r="10.5"></circle>
|
||||
<circle fill="#DB92D0" cx="327" cy="397.5" r="10.5"></circle>
|
||||
<circle fill="#DD99B8" cx="296" cy="392.5" r="10.5"></circle>
|
||||
</g>
|
||||
<g transform="translate(0,512) scale(0.1,-0.1)" fill="#000000" class="icon_syui">
|
||||
<path class="syui" d="M3660 4460 c-11 -11 -33 -47 -48 -80 l-29 -60 -12 38 c-27 88 -58 92 -98 11 -35 -70 -73 -159 -73 -169 0 -6 -5 -10 -10 -10 -6 0 -15 -10 -21 -22 -33 -73 -52 -92 -47 -48 2 26 -1 35 -14 38 -16 3 -168 -121 -168 -138 0 -5 -13 -16 -28 -24 -24 -13 -35 -12 -87 0 -221 55 -231 56 -480 56 -219 1 -247 -1 -320 -22 -44 -12 -96 -26 -115 -30 -57 -13 -122 -39 -200 -82 -8 -4 -31 -14 -50 -23 -41 -17 -34 -13 -146 -90 -87 -59 -292 -252 -351 -330 -63 -83 -143 -209 -143 -225 0 -10 -7 -23 -15 -30 -8 -7 -15 -17 -15 -22 0 -5 -13 -37 -28 -71 -16 -34 -36 -93 -45 -132 -9 -38 -24 -104 -34 -145 -13 -60 -17 -121 -17 -300 1 -224 1 -225 36 -365 24 -94 53 -175 87 -247 28 -58 51 -108 51 -112 0 -3 13 -24 28 -48 42 -63 46 -79 22 -85 -11 -3 -20 -9 -20 -14 0 -5 -4 -9 -10 -9 -5 0 -22 -11 -37 -25 -16 -13 -75 -59 -133 -100 -58 -42 -113 -82 -123 -90 -9 -8 -22 -15 -27 -15 -6 0 -10 -6 -10 -13 0 -8 -11 -20 -25 -27 -34 -18 -34 -54 0 -48 14 3 25 2 25 -1 0 -3 -43 -31 -95 -61 -52 -30 -95 -58 -95 -62 0 -5 -5 -8 -11 -8 -19 0 -84 -33 -92 -47 -4 -7 -15 -13 -22 -13 -14 0 -17 -4 -19 -32 -1 -8 15 -15 37 -18 l38 -5 -47 -48 c-56 -59 -54 -81 9 -75 30 3 45 0 54 -11 9 -13 16 -14 43 -4 29 11 30 10 18 -5 -7 -9 -19 -23 -25 -30 -7 -7 -13 -20 -13 -29 0 -12 8 -14 38 -9 20 4 57 8 82 9 25 2 54 8 66 15 18 10 23 8 32 -13 17 -38 86 -35 152 6 27 17 50 34 50 38 0 16 62 30 85 19 33 -15 72 -2 89 30 8 15 31 43 51 62 35 34 38 35 118 35 77 0 85 2 126 33 24 17 52 32 61 32 9 0 42 18 73 40 30 22 61 40 69 40 21 0 88 -26 100 -38 7 -7 17 -12 24 -12 7 0 35 -11 62 -25 66 -33 263 -84 387 -101 189 -25 372 -12 574 41 106 27 130 37 261 97 41 20 80 37 85 39 6 2 51 31 100 64 166 111 405 372 489 534 10 20 22 43 27 51 5 8 12 22 15 30 3 8 17 40 31 70 54 115 95 313 108 520 13 200 -43 480 -134 672 -28 58 -51 108 -51 112 0 3 -13 24 -29 48 -15 24 -34 60 -40 80 -19 57 3 142 50 193 10 11 22 49 28 85 6 36 16 67 21 68 18 6 31 53 25 83 -4 18 -17 33 -36 41 -16 7 -29 15 -29 18 1 10 38 50 47 50 5 0 20 11 33 25 18 19 22 31 17 61 -3 20 -14 45 -23 55 -16 18 -16 20 6 44 15 16 21 32 18 49 -3 15 1 34 8 43 32 43 7 73 -46 55 l-30 -11 0 85 c0 74 -2 84 -18 84 -21 0 -53 -33 -103 -104 l-34 -48 -5 74 c-7 102 -35 133 -80 88z m-870 -740 c36 -7 75 -14 88 -16 21 -4 23 -9 16 -37 -3 -18 -14 -43 -24 -57 -10 -14 -20 -35 -24 -46 -4 -12 -16 -32 -27 -45 -12 -13 -37 -49 -56 -79 -20 -30 -52 -73 -72 -96 -53 -60 -114 -133 -156 -189 -21 -27 -44 -54 -52 -58 -7 -4 -13 -14 -13 -22 0 -7 -18 -33 -40 -57 -22 -23 -40 -46 -40 -50 0 -5 -19 -21 -42 -38 -47 -35 -85 -38 -188 -15 -115 25 -173 20 -264 -23 -45 -22 -106 -46 -136 -56 -48 -15 -77 -25 -140 -50 -70 -28 -100 -77 -51 -84 14 -2 34 -10 45 -17 12 -7 53 -16 91 -20 90 -9 131 -22 178 -57 20 -16 52 -35 70 -43 18 -7 40 -22 49 -32 16 -18 15 -22 -24 -88 -23 -39 -47 -74 -53 -80 -7 -5 -23 -26 -36 -45 -26 -39 -92 -113 -207 -232 -4 -4 -37 -36 -73 -71 l-66 -64 -20 41 c-58 119 -105 240 -115 301 -40 244 -35 409 20 595 8 30 21 66 28 80 7 14 24 54 38 89 15 35 35 75 46 89 11 13 20 31 20 38 0 8 3 14 8 14 4 0 16 16 27 36 24 45 221 245 278 281 23 15 44 30 47 33 20 20 138 78 250 123 61 24 167 50 250 61 60 7 302 -1 370 -14z m837 -661 c52 -101 102 -279 106 -379 2 -42 0 -45 -28 -51 -16 -4 -101 -7 -187 -8 -166 -1 -229 10 -271 49 -19 19 -19 19 14 49 22 21 44 31 65 31 41 0 84 34 84 66 0 30 12 55 56 112 19 25 37 65 44 95 11 51 53 111 74 104 6 -2 25 -32 43 -68z m-662 -810 c17 -10 40 -24 53 -30 12 -7 22 -16 22 -20 0 -4 17 -13 38 -19 20 -7 44 -18 52 -24 8 -7 33 -21 55 -31 22 -11 42 -23 45 -26 11 -14 109 -49 164 -58 62 -11 101 -7 126 14 15 14 38 18 78 16 39 -2 26 -41 -49 -146 -78 -109 -85 -118 -186 -219 -61 -61 -239 -189 -281 -203 -17 -5 -73 -29 -104 -44 -187 -92 -605 -103 -791 -21 -42 19 -47 24 -37 41 5 11 28 32 51 48 22 15 51 38 64 51 13 12 28 22 33 22 17 0 242 233 242 250 0 6 5 10 10 10 6 0 10 6 10 14 0 25 50 55 100 62 59 8 56 6 115 83 50 66 74 117 75 162 0 14 7 40 16 57 18 38 52 41 99 11z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<!-- User Handle Input Form -->
|
||||
<div class="pds-search-section">
|
||||
<form class="pds-search-form" onsubmit="searchUser(); return false;">
|
||||
<div class="form-group">
|
||||
<input type="text" id="handleInput" placeholder="at://syui.ai" value="syui.ai" />
|
||||
<button type="submit" id="searchButton" class="pds-btn">
|
||||
@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<button class="ask-ai-btn" onclick="toggleAskAI()" id="askAiButton">
|
||||
<span class="ai-icon icon-ai"></span>
|
||||
ai
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Ask AI Panel -->
|
||||
<div class="ask-ai-panel" id="askAiPanel" style="display: none;">
|
||||
<div class="ask-ai-content">
|
||||
<div id="authCheck" class="auth-check">
|
||||
<div class="loading-content">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chatForm" class="ask-ai-form" style="display: none;">
|
||||
<input type="text" id="aiQuestion" placeholder="What would you like to know?" />
|
||||
<button onclick="askQuestion()" id="askButton">Ask</button>
|
||||
</div>
|
||||
|
||||
<div id="chatHistory" class="chat-history" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="main-content">
|
||||
<!-- Pds Panel -->
|
||||
{% include "pds-header.html" %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% block sidebar %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer class="main-footer">
|
||||
<div class="footer-social">
|
||||
<a href="https://syu.is/syui" target="_blank"><i class="fab fa-bluesky"></i></a>
|
||||
<a href="https://git.syui.ai/ai" target="_blank"><span class="icon-ai"></span></a>
|
||||
<a href="https://github.com/syui" target="_blank"><i class="fab fa-github"></i></a>
|
||||
</div>
|
||||
<p>© {{ config.author }}</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Config variables from Hugo
|
||||
window.OAUTH_CONFIG = {
|
||||
{% if config.oauth.pds %}
|
||||
pds: "{{ config.oauth.pds }}",
|
||||
{% else %}
|
||||
pds: "syu.is",
|
||||
{% endif %}
|
||||
{% if config.oauth.admin %}
|
||||
admin: "{{ config.oauth.admin }}",
|
||||
{% else %}
|
||||
admin: "ai.syui.ai",
|
||||
{% endif %}
|
||||
{% if config.oauth.collection %}
|
||||
collection: "{{ config.oauth.collection }}"
|
||||
{% else %}
|
||||
collection: "ai.syui.log"
|
||||
{% endif %}
|
||||
};
|
||||
</script>
|
||||
<script src="/js/ask-ai.js"></script>
|
||||
<script src="/js/pds.js"></script>
|
||||
<script src="/js/theme.js"></script>
|
||||
<script src="/js/image-comparison.js"></script>
|
||||
|
||||
<!-- Mermaid support -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'neutral',
|
||||
securityLevel: 'loose',
|
||||
themeVariables: {
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
fontSize: '14px'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% include "oauth-assets.html" %}
|
||||
{% include "at-browser-assets.html" %}
|
||||
</body>
|
||||
</html>
|
135
my-blog/layouts/game.html
Normal file
@@ -0,0 +1,135 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Game - {{ config.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="gameContainer" class="game-container">
|
||||
<div id="gameAuth" class="game-auth-section">
|
||||
<h1>Login to Play</h1>
|
||||
<p>Please authenticate with your AT Protocol account to access the game.</p>
|
||||
<div id="authRoot"></div>
|
||||
</div>
|
||||
<div id="gameFrame" class="game-frame-container" style="display: none;">
|
||||
<iframe
|
||||
id="pixelStreamingFrame"
|
||||
src="https://verse.syui.ai/simple-noui.html"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
allow="microphone; camera; fullscreen; autoplay"
|
||||
class="pixel-streaming-iframe"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Game specific styles */
|
||||
.game-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-auth-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.game-auth-section h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.game-auth-section p {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 30px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.game-frame-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pixel-streaming-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Override auth button for game page */
|
||||
.game-auth-section .auth-section {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.game-auth-section .auth-button {
|
||||
font-size: 1.2em;
|
||||
padding: 12px 30px;
|
||||
}
|
||||
|
||||
/* Hide header and footer on game page */
|
||||
body:has(.game-container) header,
|
||||
body:has(.game-container) footer,
|
||||
body:has(.game-container) nav {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Remove any body padding/margin for full screen game */
|
||||
body:has(.game-container) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Wait for OAuth component to be loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check if user is already authenticated
|
||||
const checkAuthStatus = () => {
|
||||
// Check if OAuth components are available and user is authenticated
|
||||
if (window.currentUser && window.currentAgent) {
|
||||
showGame();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Show game iframe
|
||||
const showGame = () => {
|
||||
document.getElementById('gameAuth').style.display = 'none';
|
||||
document.getElementById('gameFrame').style.display = 'block';
|
||||
};
|
||||
|
||||
// Listen for OAuth success
|
||||
window.addEventListener('oauth-success', function(event) {
|
||||
console.log('OAuth success:', event.detail);
|
||||
showGame();
|
||||
});
|
||||
|
||||
// Check auth status on load
|
||||
if (!checkAuthStatus()) {
|
||||
// Check periodically if OAuth components are loaded
|
||||
const authCheckInterval = setInterval(() => {
|
||||
if (checkAuthStatus()) {
|
||||
clearInterval(authCheckInterval);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Include OAuth assets -->
|
||||
{% include "oauth-assets.html" %}
|
||||
{% endblock %}
|
45
my-blog/layouts/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="timeline-container">
|
||||
|
||||
<div class="timeline-feed">
|
||||
{% for post in posts %}
|
||||
<article class="timeline-post">
|
||||
<div class="post-header">
|
||||
<div class="post-meta">
|
||||
<time class="post-date">{{ post.date }}</time>
|
||||
{% if post.language %}
|
||||
<span class="post-lang">{{ post.language }}</span>
|
||||
{% endif %}
|
||||
{% if post.type == "ai" %}
|
||||
<span class="post-ai">
|
||||
<span class="ai-icon icon-ai"></span>
|
||||
ai
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post-content">
|
||||
<h3 class="post-title">
|
||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||
</h3>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- OAuth Comment System -->
|
||||
<section class="comment-section">
|
||||
<div id="comment-atproto"></div>
|
||||
</section>
|
||||
|
||||
{% if posts|length == 0 %}
|
||||
<div class="empty-state">
|
||||
<p>No posts yet. Start writing!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
3
my-blog/layouts/oauth-assets.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<!-- OAuth Comment System - Load globally for session management -->
|
||||
<script type="module" crossorigin src="/assets/comment-atproto-93YR1Hl3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-rDV6HevJ.css">
|
71
my-blog/layouts/partials/oauth-widget.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!-- OAuth authentication widget for ailog -->
|
||||
<div id="oauth-widget">
|
||||
<div id="status" class="status">
|
||||
Login with your Bluesky account
|
||||
</div>
|
||||
|
||||
<!-- Login form -->
|
||||
<div id="login-form">
|
||||
<input type="text" id="handle-input" placeholder="Enter your handle (e.g., user.bsky.social)" style="width: 300px; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px;">
|
||||
<br>
|
||||
<button id="login-btn">🦋 Login with Bluesky</button>
|
||||
</div>
|
||||
|
||||
<!-- Authenticated state -->
|
||||
<div id="authenticated-state" style="display: none;">
|
||||
<div id="user-info"></div>
|
||||
<button id="logout-btn">Logout</button>
|
||||
<button id="test-profile-btn">Get Profile</button>
|
||||
</div>
|
||||
|
||||
<div id="console-log" class="log"></div>
|
||||
</div>
|
||||
|
||||
<script src="/oauth-widget-simple.js"></script>
|
||||
|
||||
<style>
|
||||
.status {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.user-info {
|
||||
background: #e8f5e8;
|
||||
border: 1px solid #4caf50;
|
||||
}
|
||||
.error {
|
||||
background: #ffeaea;
|
||||
border: 1px solid #f44336;
|
||||
color: #d32f2f;
|
||||
}
|
||||
#oauth-widget button {
|
||||
background: #1185fe;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
}
|
||||
#oauth-widget button:hover {
|
||||
background: #0d6efd;
|
||||
}
|
||||
#oauth-widget button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.log {
|
||||
text-align: left;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
48
my-blog/layouts/pds-header.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<div class="pds-container">
|
||||
<div class="pds-header">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Current User DID -->
|
||||
<div id="userDidSection" class="user-did-section" style="display: none;">
|
||||
<div class="pds-display">
|
||||
<strong>PDS:</strong> <span id="userPdsText"></span>
|
||||
</div>
|
||||
<div class="handle-display">
|
||||
<strong>Handle:</strong> <span id="userHandleText"></span>
|
||||
</div>
|
||||
<div class="did-display">
|
||||
<span id="userDidText"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collection List -->
|
||||
<div id="collectionsSection" class="collections-section" style="display: none;">
|
||||
<div class="collections-header">
|
||||
<button id="collectionsToggle" class="collections-toggle" onclick="toggleCollections()">[+] Collections</button>
|
||||
</div>
|
||||
<div id="collectionsList" class="collections-list" style="display: none;">
|
||||
<!-- Collections will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AT URI Records -->
|
||||
<div id="recordsSection" class="records-section" style="display: none;">
|
||||
<div id="recordsList" class="records-list">
|
||||
<!-- Records will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- AT URI Modal -->
|
||||
<div id="atUriModal" class="at-uri-modal-overlay" style="display: none;" onclick="closeAtUriModal(event)">
|
||||
<div class="at-uri-modal-content">
|
||||
<button class="at-uri-modal-close" onclick="closeAtUriModal()">×</button>
|
||||
<div id="atUriContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
6
my-blog/layouts/pds.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}at-uri browser - {{ config.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
373
my-blog/layouts/post-complex.html
Normal file
@@ -0,0 +1,373 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ post.title }} - {{ config.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="article-container">
|
||||
<article class="article-content">
|
||||
<header class="article-header">
|
||||
<h1 class="article-title">{{ post.title }}</h1>
|
||||
<div class="article-meta">
|
||||
<time class="article-date">{{ post.date }}</time>
|
||||
{% if post.language %}
|
||||
<span class="article-lang">{{ post.language }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="article-actions">
|
||||
{% if post.markdown_url %}
|
||||
<a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown">
|
||||
.md
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if post.translation_url %}
|
||||
<a href="{{ post.translation_url }}" class="action-btn translation-btn" title="View Translation">
|
||||
🌐 {% if post.language == 'ja' %}English{% else %}日本語{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="article-body">
|
||||
{{ post.content | safe }}
|
||||
</div>
|
||||
|
||||
<!-- Comment Section -->
|
||||
<section class="comment-section">
|
||||
<div class="comment-container">
|
||||
<h3>Comments</h3>
|
||||
|
||||
<!-- ATProto Auth Widget Container -->
|
||||
<div id="atproto-auth-widget" class="comment-auth"></div>
|
||||
|
||||
<div id="commentForm" class="comment-form" style="display: none;">
|
||||
<textarea id="commentText" placeholder="Share your thoughts..." rows="4"></textarea>
|
||||
<button onclick="submitComment()" class="submit-btn">Post Comment</button>
|
||||
</div>
|
||||
|
||||
<div id="commentsList" class="comments-list">
|
||||
<!-- Comments will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<aside class="article-sidebar">
|
||||
<nav class="toc">
|
||||
<h3>Contents</h3>
|
||||
<div id="toc-content">
|
||||
<!-- TOC will be generated by JavaScript -->
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<!-- Include ATProto Libraries via script tags (more reliable than dynamic imports) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@atproto/oauth-client-browser@latest/dist/index.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@atproto/api@latest/dist/index.js"></script>
|
||||
|
||||
<!-- Fallback: Try multiple CDNs -->
|
||||
<script>
|
||||
console.log('Checking ATProto library availability...');
|
||||
|
||||
// Check if libraries loaded successfully
|
||||
if (typeof ATProto === 'undefined' && typeof window.ATProto === 'undefined') {
|
||||
console.log('Primary CDN failed, trying fallback...');
|
||||
|
||||
// Create fallback script elements
|
||||
const fallbackScripts = [
|
||||
'https://unpkg.com/@atproto/oauth-client-browser@latest/dist/index.js',
|
||||
'https://esm.sh/@atproto/oauth-client-browser',
|
||||
'https://cdn.skypack.dev/@atproto/oauth-client-browser'
|
||||
];
|
||||
|
||||
// Load fallback scripts sequentially
|
||||
let scriptIndex = 0;
|
||||
function loadNextScript() {
|
||||
if (scriptIndex < fallbackScripts.length) {
|
||||
const script = document.createElement('script');
|
||||
script.src = fallbackScripts[scriptIndex];
|
||||
script.onload = () => {
|
||||
console.log(`Loaded from fallback CDN: ${fallbackScripts[scriptIndex]}`);
|
||||
window.atprotoLibrariesReady = true;
|
||||
};
|
||||
script.onerror = () => {
|
||||
console.log(`Failed to load from: ${fallbackScripts[scriptIndex]}`);
|
||||
scriptIndex++;
|
||||
loadNextScript();
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
console.error('All CDN fallbacks failed');
|
||||
window.atprotoLibrariesReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
loadNextScript();
|
||||
} else {
|
||||
console.log('✅ ATProto libraries loaded from primary CDN');
|
||||
window.atprotoLibrariesReady = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Simple ATProto Widget (no external dependency) -->
|
||||
<link rel="stylesheet" href="/atproto-auth-widget/dist/atproto-auth.min.css">
|
||||
|
||||
<script>
|
||||
// Initialize auth widget
|
||||
let authWidget = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
generateTableOfContents();
|
||||
initializeAuthWidget();
|
||||
loadComments();
|
||||
});
|
||||
|
||||
function generateTableOfContents() {
|
||||
const tocContainer = document.getElementById('toc-content');
|
||||
const headings = document.querySelectorAll('.article-body h1, .article-body h2, .article-body h3, .article-body h4, .article-body h5, .article-body h6');
|
||||
|
||||
if (headings.length === 0) {
|
||||
tocContainer.innerHTML = '<p class="no-toc">No headings found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const tocList = document.createElement('ul');
|
||||
tocList.className = 'toc-list';
|
||||
|
||||
headings.forEach((heading, index) => {
|
||||
const id = `heading-${index}`;
|
||||
heading.id = id;
|
||||
|
||||
const listItem = document.createElement('li');
|
||||
listItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = `#${id}`;
|
||||
link.textContent = heading.textContent;
|
||||
link.className = 'toc-link';
|
||||
|
||||
// Smooth scroll behavior
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
heading.scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
|
||||
listItem.appendChild(link);
|
||||
tocList.appendChild(listItem);
|
||||
});
|
||||
|
||||
tocContainer.appendChild(tocList);
|
||||
}
|
||||
|
||||
// Initialize ATProto Auth Widget
|
||||
async function initializeAuthWidget() {
|
||||
try {
|
||||
// Check WebCrypto API availability
|
||||
console.log('WebCrypto check:', {
|
||||
available: !!window.crypto && !!window.crypto.subtle,
|
||||
secureContext: window.isSecureContext,
|
||||
protocol: window.location.protocol,
|
||||
hostname: window.location.hostname
|
||||
});
|
||||
|
||||
if (!window.crypto || !window.crypto.subtle) {
|
||||
throw new Error('WebCrypto API is not available. This requires HTTPS or localhost.');
|
||||
}
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
console.warn('Not in secure context - WebCrypto may not work properly');
|
||||
}
|
||||
|
||||
// Simplified approach: Show manual OAuth form
|
||||
console.log('Using simplified OAuth approach...');
|
||||
showSimpleOAuthForm();
|
||||
// Fallback to widget initialization
|
||||
authWidget = await window.initATProtoWidget('#atproto-auth-widget', {
|
||||
clientId: clientId,
|
||||
onLogin: (session) => {
|
||||
console.log('User logged in:', session.handle);
|
||||
document.getElementById('commentForm').style.display = 'block';
|
||||
},
|
||||
onLogout: () => {
|
||||
console.log('User logged out');
|
||||
document.getElementById('commentForm').style.display = 'none';
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('ATProto Auth Error:', error);
|
||||
// Show user-friendly error message
|
||||
const authContainer = document.getElementById('atproto-auth-widget');
|
||||
if (authContainer) {
|
||||
let errorMessage = 'Authentication service is temporarily unavailable.';
|
||||
let suggestion = 'Please try refreshing the page.';
|
||||
|
||||
if (error.message && error.message.includes('WebCrypto')) {
|
||||
errorMessage = 'This feature requires a secure HTTPS connection.';
|
||||
suggestion = 'Please ensure you are accessing via https://log.syui.ai';
|
||||
}
|
||||
|
||||
authContainer.innerHTML = `
|
||||
<div class="atproto-auth__fallback">
|
||||
<p>${errorMessage}</p>
|
||||
<p>${suggestion}</p>
|
||||
<details style="margin-top: 10px; font-size: 0.8em; color: #666;">
|
||||
<summary>Technical details</summary>
|
||||
<pre>${error.message || 'Unknown error'}</pre>
|
||||
</details>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
theme: 'default'
|
||||
});
|
||||
} else if (typeof window.ATProtoAuthWidget === 'function') {
|
||||
// Fallback to direct widget initialization
|
||||
authWidget = new window.ATProtoAuthWidget({
|
||||
containerSelector: '#atproto-auth-widget',
|
||||
clientId: clientId,
|
||||
onLogin: (session) => {
|
||||
console.log('User logged in:', session.handle);
|
||||
document.getElementById('commentForm').style.display = 'block';
|
||||
},
|
||||
onLogout: () => {
|
||||
console.log('User logged out');
|
||||
document.getElementById('commentForm').style.display = 'none';
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('ATProto Auth Error:', error);
|
||||
const authContainer = document.getElementById('atproto-auth-widget');
|
||||
if (authContainer) {
|
||||
authContainer.innerHTML = `
|
||||
<div class="atproto-auth__fallback">
|
||||
<p>Authentication service is temporarily unavailable.</p>
|
||||
<p>Please try refreshing the page.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
theme: 'default'
|
||||
});
|
||||
await authWidget.init();
|
||||
} else {
|
||||
throw new Error('ATProto widget not available');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize auth widget:', error);
|
||||
// Show fallback UI
|
||||
const authContainer = document.getElementById('atproto-auth-widget');
|
||||
if (authContainer) {
|
||||
authContainer.innerHTML = `
|
||||
<div class="atproto-auth__fallback">
|
||||
<p>Authentication widget failed to load.</p>
|
||||
<p>Please check your internet connection and refresh the page.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
const commentText = document.getElementById('commentText').value.trim();
|
||||
if (!commentText || !authWidget.isLoggedIn()) {
|
||||
alert('Please login and enter a comment');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const postSlug = '{{ post.slug }}';
|
||||
const postUrl = window.location.href;
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
// Create comment record using the auth widget
|
||||
const response = await authWidget.createRecord('ai.log.comment', {
|
||||
$type: 'ai.log.comment',
|
||||
text: commentText,
|
||||
post_slug: postSlug,
|
||||
post_url: postUrl,
|
||||
createdAt: createdAt
|
||||
});
|
||||
|
||||
console.log('Comment posted:', response);
|
||||
document.getElementById('commentText').value = '';
|
||||
loadComments();
|
||||
} catch (error) {
|
||||
console.error('Comment submission failed:', error);
|
||||
alert('Failed to post comment: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function showAuthenticatedState(session) {
|
||||
const authContainer = document.getElementById('atproto-auth-widget');
|
||||
const agent = new window.ATProtoAgent(session);
|
||||
|
||||
authContainer.innerHTML = `
|
||||
<div class="atproto-auth__authenticated">
|
||||
<p>✅ Authenticated as: <strong>${session.did}</strong></p>
|
||||
<button id="logout-btn" class="atproto-auth__button">Logout</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('logout-btn').onclick = async () => {
|
||||
await session.signOut();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
// Show comment form
|
||||
document.getElementById('commentForm').style.display = 'block';
|
||||
window.currentSession = session;
|
||||
window.currentAgent = agent;
|
||||
}
|
||||
|
||||
function showLoginForm(oauthClient) {
|
||||
const authContainer = document.getElementById('atproto-auth-widget');
|
||||
|
||||
authContainer.innerHTML = `
|
||||
<div class="atproto-auth__login">
|
||||
<h4>Login with ATProto</h4>
|
||||
<input type="text" id="handle-input" placeholder="user.bsky.social" />
|
||||
<button id="login-btn" class="atproto-auth__button">Connect</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('login-btn').onclick = async () => {
|
||||
const handle = document.getElementById('handle-input').value.trim();
|
||||
if (!handle) {
|
||||
alert('Please enter your handle');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = await oauthClient.authorize(handle);
|
||||
window.open(url, '_self', 'noopener');
|
||||
} catch (error) {
|
||||
console.error('OAuth authorization failed:', error);
|
||||
alert('Authentication failed: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Enter key support
|
||||
document.getElementById('handle-input').onkeypress = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
document.getElementById('login-btn').click();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function loadComments() {
|
||||
try {
|
||||
const commentsList = document.getElementById('commentsList');
|
||||
commentsList.innerHTML = '<p class="loading">Loading comments from ATProto network...</p>';
|
||||
|
||||
// In a real implementation, you would query an aggregation service
|
||||
// For demo, show empty state
|
||||
setTimeout(() => {
|
||||
commentsList.innerHTML = '<p class="no-comments">Comments will appear here when posted via ATProto.</p>';
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Failed to load comments:', error);
|
||||
document.getElementById('commentsList').innerHTML = '<p class="error">Failed to load comments</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
196
my-blog/layouts/post-simple.html
Normal file
@@ -0,0 +1,196 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ post.title }} - {{ config.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="article-container">
|
||||
<article class="article-content">
|
||||
<header class="article-header">
|
||||
<h1 class="article-title">{{ post.title }}</h1>
|
||||
<div class="article-meta">
|
||||
<time class="article-date">{{ post.date }}</time>
|
||||
{% if post.language %}
|
||||
<span class="article-lang">{{ post.language }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="article-actions">
|
||||
{% if post.markdown_url %}
|
||||
<a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown">
|
||||
.md
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if post.translation_url %}
|
||||
<a href="{{ post.translation_url }}" class="action-btn translation-btn" title="View Translation">
|
||||
🌐 {% if post.language == 'ja' %}English{% else %}日本語{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="article-body">
|
||||
{{ post.content | safe }}
|
||||
</div>
|
||||
|
||||
<!-- Simple Comment Section -->
|
||||
<section class="comment-section">
|
||||
<div class="comment-container">
|
||||
<h3>Comments</h3>
|
||||
|
||||
<!-- Simple OAuth Button -->
|
||||
<div class="simple-oauth">
|
||||
<p>📝 To comment, authenticate with Bluesky:</p>
|
||||
<button id="bluesky-auth" class="oauth-button">
|
||||
🦋 Login with Bluesky
|
||||
</button>
|
||||
<p class="oauth-note">
|
||||
<small>After authentication, you can post comments that will be stored in your ATProto PDS.</small>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="comments-list" class="comments-list">
|
||||
<p class="no-comments">Comments will appear here when posted via ATProto.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<aside class="article-sidebar">
|
||||
<nav class="toc">
|
||||
<h3>Contents</h3>
|
||||
<div id="toc-content">
|
||||
<!-- TOC will be generated by JavaScript -->
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
generateTableOfContents();
|
||||
initializeSimpleAuth();
|
||||
});
|
||||
|
||||
function generateTableOfContents() {
|
||||
const tocContainer = document.getElementById('toc-content');
|
||||
const headings = document.querySelectorAll('.article-body h1, .article-body h2, .article-body h3, .article-body h4, .article-body h5, .article-body h6');
|
||||
|
||||
if (headings.length === 0) {
|
||||
tocContainer.innerHTML = '<p class="no-toc">No headings found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const tocList = document.createElement('ul');
|
||||
tocList.className = 'toc-list';
|
||||
|
||||
headings.forEach((heading, index) => {
|
||||
const id = `heading-${index}`;
|
||||
heading.id = id;
|
||||
|
||||
const listItem = document.createElement('li');
|
||||
listItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = `#${id}`;
|
||||
link.textContent = heading.textContent;
|
||||
link.className = 'toc-link';
|
||||
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
heading.scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
|
||||
listItem.appendChild(link);
|
||||
tocList.appendChild(listItem);
|
||||
});
|
||||
|
||||
tocContainer.appendChild(tocList);
|
||||
}
|
||||
|
||||
function initializeSimpleAuth() {
|
||||
const authButton = document.getElementById('bluesky-auth');
|
||||
|
||||
authButton.addEventListener('click', function() {
|
||||
// Simple approach: Direct redirect to Bluesky OAuth
|
||||
const isProduction = window.location.hostname === 'log.syui.ai';
|
||||
const clientId = isProduction
|
||||
? 'https://log.syui.ai/client-metadata.json'
|
||||
: window.location.origin + '/client-metadata.json';
|
||||
|
||||
const authUrl = `https://bsky.social/oauth/authorize?` +
|
||||
`client_id=${encodeURIComponent(clientId)}&` +
|
||||
`redirect_uri=${encodeURIComponent(window.location.href)}&` +
|
||||
`response_type=code&` +
|
||||
`scope=atproto%20transition:generic&` +
|
||||
`state=demo-state`;
|
||||
|
||||
console.log('Redirecting to:', authUrl);
|
||||
|
||||
// Open in new tab for now (safer for testing)
|
||||
window.open(authUrl, '_blank');
|
||||
|
||||
// Show status message
|
||||
authButton.innerHTML = '✅ Check the new tab for authentication';
|
||||
authButton.disabled = true;
|
||||
});
|
||||
|
||||
// Check if we're returning from OAuth
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('code')) {
|
||||
console.log('OAuth callback detected:', urlParams.get('code'));
|
||||
document.querySelector('.simple-oauth').innerHTML = `
|
||||
<div class="oauth-success">
|
||||
✅ OAuth callback received!<br>
|
||||
<small>Code: ${urlParams.get('code')}</small><br>
|
||||
<small>In a full implementation, this would exchange the code for tokens.</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.simple-oauth {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.oauth-button {
|
||||
background: #1185fe;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.oauth-button:hover {
|
||||
background: #0d6efd;
|
||||
}
|
||||
|
||||
.oauth-button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.oauth-note {
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.oauth-success {
|
||||
background: #d1edff;
|
||||
border: 1px solid #b6d7ff;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
color: #0c5460;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
106
my-blog/layouts/post.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ post.title }} - {{ config.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="article-container">
|
||||
<article class="article-content">
|
||||
<header class="article-header">
|
||||
<h1 class="article-title">{{ post.title }}</h1>
|
||||
<div class="article-meta">
|
||||
<time class="article-date">{{ post.date }}</time>
|
||||
{% if post.language %}
|
||||
<span class="article-lang">{{ post.language }}</span>
|
||||
{% endif %}
|
||||
{% if post.extra.type == "ai" %}
|
||||
<span class="article-ai">
|
||||
<span class="ai-icon icon-ai"></span>
|
||||
ai
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not post.extra.type or post.extra.type != "ai" %}
|
||||
<div class="article-actions">
|
||||
{% if post.markdown_url %}
|
||||
<a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown">
|
||||
.md
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if post.translation_url %}
|
||||
<a href="{{ post.translation_url }}" class="action-btn translation-btn" title="View Translation">
|
||||
🌐 {% if post.language == 'ja' %}English{% else %}日本語{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if not post.extra.type or post.extra.type != "ai" %}
|
||||
<nav class="toc">
|
||||
<h3>Contents</h3>
|
||||
<div id="toc-content">
|
||||
<!-- TOC will be generated by JavaScript -->
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="article-body">
|
||||
{{ post.content | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="comment-atproto"></div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// Generate table of contents
|
||||
function generateTableOfContents() {
|
||||
const tocContainer = document.getElementById('toc-content');
|
||||
if (!tocContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headings = document.querySelectorAll('.article-body h1, .article-body h2, .article-body h3, .article-body h4, .article-body h5, .article-body h6');
|
||||
|
||||
if (headings.length === 0) {
|
||||
tocContainer.innerHTML = '<p class="no-toc">No headings found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const tocList = document.createElement('ul');
|
||||
tocList.className = 'toc-list';
|
||||
|
||||
headings.forEach((heading, index) => {
|
||||
const id = `heading-${index}`;
|
||||
heading.id = id;
|
||||
|
||||
const listItem = document.createElement('li');
|
||||
listItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = `#${id}`;
|
||||
link.textContent = heading.textContent;
|
||||
link.className = 'toc-link';
|
||||
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
heading.scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
|
||||
listItem.appendChild(link);
|
||||
tocList.appendChild(listItem);
|
||||
});
|
||||
|
||||
tocContainer.appendChild(tocList);
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
generateTableOfContents();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 260 KiB |
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 252 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 46 KiB |
BIN
my-blog/static/img/comfyui_flex1_nano_banana_0001.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
my-blog/static/img/comfyui_wan21_0001.webp
Normal file
After Width: | Height: | Size: 2.2 MiB |
BIN
my-blog/static/img/comfyui_wan22_0001.mp4
Normal file
BIN
my-blog/static/img/comfyui_wan22_0001.png
Normal file
After Width: | Height: | Size: 520 KiB |
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 723 KiB After Width: | Height: | Size: 723 KiB |
BIN
my-blog/static/img/ue_ps5_controller_v0100.jpg
Normal file
After Width: | Height: | Size: 2.2 MiB |
@@ -12,6 +12,12 @@
|
||||
{% if post.language %}
|
||||
<span class="post-lang">{{ post.language }}</span>
|
||||
{% endif %}
|
||||
{% if post.extra and post.extra.type == "ai" %}
|
||||
<span class="post-ai">
|
||||
<span class="ai-icon icon-ai"></span>
|
||||
ai
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -12,8 +12,14 @@
|
||||
{% if post.language %}
|
||||
<span class="article-lang">{{ post.language }}</span>
|
||||
{% endif %}
|
||||
{% if post.extra and post.extra.type == "ai" %}
|
||||
<span class="article-ai">
|
||||
<span class="ai-icon icon-ai"></span>
|
||||
ai
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not post.extra.type or post.extra.type != "ai" %}
|
||||
{% if not post.extra or not post.extra.type or post.extra.type != "ai" %}
|
||||
<div class="article-actions">
|
||||
{% if post.markdown_url %}
|
||||
<a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown">
|
||||
@@ -29,7 +35,7 @@
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if not post.extra.type or post.extra.type != "ai" %}
|
||||
{% if not post.extra or not post.extra.type or post.extra.type != "ai" %}
|
||||
<nav class="toc">
|
||||
<h3>Contents</h3>
|
||||
<div id="toc-content">
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ailog-oauth",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
@@ -125,18 +125,6 @@ export default function RecordList({ title, records, apiConfig, showTitle = true
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="record-meta">
|
||||
{record.value.post?.url && (
|
||||
<a
|
||||
href={record.value.post.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="record-url"
|
||||
>
|
||||
{record.value.post.url}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expandedRecords.has(i) && (
|
||||
<div className="json-display">
|
||||
|
@@ -26,7 +26,9 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
||||
}
|
||||
})
|
||||
|
||||
const [activeTab, setActiveTab] = useState(isAiPost ? 'collection' : 'profiles')
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
isAiPost ? 'collection' : (pageContext.isTopPage ? 'profiles' : 'users')
|
||||
)
|
||||
|
||||
// Fetch page-specific chat records for individual article pages
|
||||
useEffect(() => {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pds-browser",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.4",
|
||||
"description": "AT Protocol browser for ai.log",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
40
scpt/run.zsh
@@ -20,9 +20,49 @@ function _env() {
|
||||
function _deploy_ailog() {
|
||||
}
|
||||
|
||||
function _sync_versions() {
|
||||
# Get version from Cargo.toml
|
||||
local version=$(grep '^version = ' "$d/Cargo.toml" | cut -d'"' -f2)
|
||||
if [[ -z "$version" ]]; then
|
||||
echo "⚠️ Could not find version in Cargo.toml"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "ℹ️ Syncing versions to $version"
|
||||
|
||||
# Update oauth/package.json
|
||||
if [[ -f "$d/oauth/package.json" ]]; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
local temp_file=$(mktemp)
|
||||
jq --arg version "$version" '.version = $version' "$d/oauth/package.json" > "$temp_file"
|
||||
mv "$temp_file" "$d/oauth/package.json"
|
||||
echo "✅ Updated oauth/package.json to $version"
|
||||
else
|
||||
sed -i.bak "s/\"version\":[[:space:]]*\"[^\"]*\"/\"version\": \"$version\"/" "$d/oauth/package.json"
|
||||
rm -f "$d/oauth/package.json.bak"
|
||||
echo "✅ Updated oauth/package.json to $version (sed)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update pds/package.json
|
||||
if [[ -f "$d/pds/package.json" ]]; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
local temp_file=$(mktemp)
|
||||
jq --arg version "$version" '.version = $version' "$d/pds/package.json" > "$temp_file"
|
||||
mv "$temp_file" "$d/pds/package.json"
|
||||
echo "✅ Updated pds/package.json to $version"
|
||||
else
|
||||
sed -i.bak "s/\"version\":[[:space:]]*\"[^\"]*\"/\"version\": \"$version\"/" "$d/pds/package.json"
|
||||
rm -f "$d/pds/package.json.bak"
|
||||
echo "✅ Updated pds/package.json to $version (sed)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function _server() {
|
||||
lsof -ti:$port | xargs kill -9 2>/dev/null || true
|
||||
cd $d/my-blog
|
||||
_sync_versions
|
||||
cargo build --release
|
||||
cp -rf $ailog $CARGO_HOME/bin/
|
||||
$ailog build
|
||||
|
@@ -302,7 +302,8 @@ impl Generator {
|
||||
"excerpt": excerpt,
|
||||
"markdown_url": markdown_url,
|
||||
"translation_url": translation_url,
|
||||
"language": self.config.site.language
|
||||
"language": self.config.site.language,
|
||||
"extra": post.extra
|
||||
})
|
||||
}).collect();
|
||||
|
||||
|
@@ -70,9 +70,13 @@ async fn communicate_with_claude_mcp(
|
||||
// Claude Code MCPプロセスを起動
|
||||
// Use the full path to avoid shell function and don't use --continue
|
||||
let claude_executable = if claude_code_path == "claude" {
|
||||
"/Users/syui/.claude/local/claude"
|
||||
// Use dirs crate for cross-platform home directory detection
|
||||
match dirs::home_dir() {
|
||||
Some(home) => home.join(".claude/local/claude").to_string_lossy().to_string(),
|
||||
None => "/Users/syui/.claude/local/claude".to_string(), // fallback
|
||||
}
|
||||
} else {
|
||||
claude_code_path
|
||||
claude_code_path.to_string()
|
||||
};
|
||||
|
||||
let mut child = tokio::process::Command::new(claude_executable)
|
||||
|