Jazzと読書の日々

iPadを筆記具として使う方法を模索します

Obsidian で neko を飼うためのスクリプト

変な蜘蛛に噛まれた。 明日スパイダーマンになってるかも。

oneko.js

ソースを読んでいたら Obsidian と関係ない部分があったので整理しました。 まだ削れるかもしれないけど、 現状の覚え書きとして残しておきます。

neko.md

Templater の Startup templates に登録してください。

<%*
// https://github.com/adryd325/oneko.js を参照 

var nekoGIF = "url('https://raw.githubusercontent.com/adryd325/oneko.js/refs/heads/main/oneko.gif')"

var nekoEl = document.createElement("div")
var nekoPosX = 32
var nekoPosY = 32
var mousePosX = 0
var mousePosY = 0
var frameCount = 0
var idleTime = 0
var idleAnimation = null
var idleAnimationFrame = 0
var direction
var nekoSpeed = 10
var spriteSets = {
 idle: [[-3, -3]],
 alert: [[-7, -3]],
 scratchSelf: [[-5, 0],[-6, 0],[-7, 0]],
 scratchWallN: [[0, 0],[0, -1]],
 scratchWallS: [[-7, -1],[-6, -2]],
 scratchWallE: [[-2, -2],[-2, -3]],
 scratchWallW: [[-4, 0],[-4, -1]],
 tired: [[-3, -2]],
 sleeping: [[-2, 0],[-2, -1]],
 N: [[-1, -2],[-1, -3]],
 NE: [[0, -2],[0, -3]],
 E: [[-3, 0],[-3, -1]],
 SE: [[-5, -1],[-5, -2]],
 S: [[-6, -3],[-7, -2]],
 SW: [[-5, -3],[-6, -1]],
 W: [[-4, -2],[-4, -3]],
 NW: [[-1, 0],[-1, -1]]
}

function init() {
 nekoEl.id = "oneko"
 nekoEl.ariaHidden = true
 nekoEl.style.width = "32px"
 nekoEl.style.height = "32px"
 nekoEl.style.position = "absolute"
 nekoEl.style.pointerEvents = "none"
 nekoEl.style.backgroundImage = nekoGIF
 nekoEl.style.imageRendering = "pixelated"
 nekoEl.style.left = nekoPosX - 16 + "px"
 nekoEl.style.top = nekoPosY - 16 + "px"
 nekoEl.style.zIndex = 99
 document.body.appendChild(nekoEl)

 document.onmousemove = (e) => {
  mousePosX = e.clientX
  mousePosY = e.clientY - 16
 }
 window.onekoInterval = setInterval(frame, 100)
}

function setSprite(name, frame) {
 var length = spriteSets[name].length
 var sprite = spriteSets[name][frame % length]
 nekoEl.style.backgroundPosition =
    sprite[0] * 32 + "px " + sprite[1] * 32 + "px"
}

function resetIdleAnimation() {
 idleAnimation = null
 idleAnimationFrame = 0
}

function idle() {
 idleTime = idleTime + 1

 // every ~ 20 seconds
 if(
    idleTime > 10 &&
    Math.floor(Math.random() * 200) == 0 &&
    idleAnimation == null
 ) {
    var avalibleIdleAnimations = ["sleeping", "scratchSelf"]
    if(nekoPosX < 32) {
      avalibleIdleAnimations.push("scratchWallW")
    }
    if(nekoPosY < 32) {
      avalibleIdleAnimations.push("scratchWallN")
    }
    if(nekoPosX > window.innerWidth - 32) {
      avalibleIdleAnimations.push("scratchWallE")
    }
    if(nekoPosY > window.innerHeight - 32) {
      avalibleIdleAnimations.push("scratchWallS")
    }
    idleAnimation =
      avalibleIdleAnimations[
         Math.floor(Math.random() * avalibleIdleAnimations.length)
      ]
 }

 switch(idleAnimation) {
    case "sleeping":
      if(idleAnimationFrame < 8) {
         setSprite("tired", 0)
         break
      }
      setSprite("sleeping", Math.floor(idleAnimationFrame / 4))
      if(idleAnimationFrame > 192) resetIdleAnimation()
      break
    case "scratchWallN":
    case "scratchWallS":
    case "scratchWallE":
    case "scratchWallW":
    case "scratchSelf":
      setSprite(idleAnimation, idleAnimationFrame)
      if(idleAnimationFrame > 9) resetIdleAnimation()
      break
    default:
      setSprite("idle", 0)
      return
 }
 idleAnimationFrame = idleAnimationFrame + 1
}

function frame() {
 frameCount = frameCount + 1
 var diffX = nekoPosX - mousePosX
 var diffY = nekoPosY - mousePosY
 var distance = Math.sqrt(Math.pow(diffX, 2) + Math.pow(diffY, 2))

 if(distance < nekoSpeed || distance < 48) {
    idle()
    return
 }

 idleAnimation = null
 idleAnimationFrame = 0

 if(idleTime > 1) {
    setSprite("alert", 0)
    // count down after being alerted before moving
    idleTime = Math.min(idleTime, 7)
    idleTime = idleTime - 1
    return
 }

 direction = ""
 if(diffY / distance > 0.5) {
    direction = "N"
 } else if(diffY / distance < -0.5) {
    direction = "S"
 }
 if(diffX / distance > 0.5) {
    direction = direction + "W"
 } else if(diffX / distance < -0.5) {
    direction = direction + "E"
 }
 setSprite(direction, frameCount)

 if(distance > nekoSpeed) {
    nekoPosX = nekoPosX - (diffX / distance) * nekoSpeed
    nekoPosY = nekoPosY - (diffY / distance) * nekoSpeed
 } else {
    nekoPosX = mousePosX
    nekoPosY = mousePosY
 }

 nekoPosX = Math.min(
    Math.max(16, nekoPosX),
    document.body.clientWidth - 16
 )
 nekoPosY = Math.min(
    Math.max(16, nekoPosY),
    document.body.clientHeight - 16
 )

 nekoEl.style.left = nekoPosX - 16 + "px"
 nekoEl.style.top = nekoPosY - 16 + "px"
}

init()
%>

nekoGIF で画像の変更ができます。

エディタ画面でもカーソルを追いかけます。

まとめ

サッカーまであと3時間。 起きてられるかなあ。

キャンバスに neko を飼うと幸せになります

webneko からここまで来ました。

oneko.js

昨日、dataview で埋め込む方法を見つけ 「oneko.js を応用するわけか」と気づきました。 Gareth Stretton さんの記事です。

一応有料記事だけど、お試しとして無料のまま読ませてもらいました。 oneko.js を走らせるコードが書いてあります。 というか、oneko.js そのもの。 oneko.js 自体は改造自由のフリー素材。 Stretton さんは gif の呼び出しを改良しています。

dataview なので

```dataviewjs
コードの本体
```

となってます。

でもこれだと、 その dataview ファイルを開かないとネコさんが現れません。 ちょいと面倒ではありませんか。

それで、コードをそのまま拝借し

<%*
コードの本体
%>

と書き直して、Templater スクリプトに改造しました。

これを Startup templates に登録。 そして、再起動すると・・・。

ネコさん参上

はい、成功。

立ち上げたときから常駐する。 マウスを使うと追いかけてきます。 指で画面をタップしても反応します。 背景部分でも走ります。

カード上にカーソルがあるときは追いかけるけど、 ノート内のカーソルには反応しません。 やっぱりノートは別空間になってるのだろうか。 css でもノートはキャンバスの外になってそうだし。 ネコさんには見えないようです。

ソースを見ると画像を base64 で埋め込んでいるので、 オフラインでも大丈夫。 書き換えたら他のネコさんになるかもしれません。 イヌさんにもなるかもしれません。 アニメの得意な人ならカスタマイズできそう。

neko.md

それで思ったのが、自分で oneko.js を改良すること。

Obsidian で走るのは oneko-ie6.js ですね。

そのまま Templater スクリプトにし、 88行目をURLアドレスに書き換えます。

nekoEl.style.backgroundImage = "url('https://raw.githubusercontent.com/adryd325/oneko.js/refs/heads/main/oneko.gif')";

これで再起動すればネコさんと戯れることができます。 結構簡単。 上記のURLアドレスを書き換えれば、 他のネコさんやイヌさんも載せることができます。

幸せとは何か

うん、幸せ。

偶然見つけて、こちらにもモチベーションがあって、ピッタリくる。 外からやってくるものと内から湧いてくるものがタイミングよく出会う。 その感じが「幸せ」ですね。

することが合うこと。 それで「し・合わせ」。 日本語はそうできているように思いました。 コントロールするのではなく、 出会うもの。 出会って驚くこと。

まとめ

やっぱりネコさんはかわいい。 和みます。

追記

ギズモードの Obsidian レビューが面白かった。 今から使うと「Claude との連携」になるのかな。 最後の人の「キャンバスに base を埋め込み、タグで管理」も参考になりました。 あと仕事が同時並行で進んでいて、 それをどうやりくりするかは難しそう。

Obsidian に webneko を飼って幸せになろう

98用の常駐ソフト neko.com を探してたどり着いたのが・・・。

webneko

自分の記事でした。

そっか、お前は幸せになりたかったのか。 うん、ネコはいいですね、飼いたいです。 それだったら Obsidian で webneko と暮らしましょう。

neko.html

html を埋め込むには HTML Docs プラグインを使います。

<script>
NekoType = "classic"
</script>
<script src =  "https://webneko.net/n20171213.js">
</script>

これを neko.html とします。

次に文中に下記スクリプトを埋め込みます。

![[neko.html|600 x 80]]

すると、そこにネコさんが現れます。

これがカーソルを追いかける。 そして手放せなくなります。 アニマルセラピーだね。

キャンバスでも

「保管庫からノートを追加」で neko.html を読み込む。

ドッグランならぬキャットランを、 広げれば広げるほど楽しそうに走り回るので、 ほぼ全面を覆い尽くす。

これ、壁紙を走らせる方法はないだろうか。

まとめ

NekoType でいろんな種類のネコを飼うことができます。 black とか kina とか。 でもシンプルな classic が一番好きかな。

追記

「脅されれば愛情が育まれる」というロジックがどうにも理解できない。 家族への愛が脅迫によって生まれるだろうか。 国への愛も同じではないか。

追記

dataviewで動かしている人がいました。 ということは templater で起動時に埋め込めるということだな。 もうちょっと研究してみよう。

ほら、壁紙をネコさんが走ってる。

デイリー・キャンバスで意味が変わってきた

今週は「合体」だった。 台風も合体するし、ノートも合体する。

キャンバス・ノート・システム

初めはカードを分割したり結合したりするだけでしたが、 キャンバスとデイリーノートが合体することで使い方に変化が起こりました。

デイリーノートは「外部」です。 外から情報が流れ込む窓になっている。

ブックマーク

例えば Safari があるじゃないですか。 共有から Obsidain を呼び出す。 そのとき Today を選択できます。

Today とはデイリーノートへの追記なので、 Webサイトのリンクがキャンバスから確認できるようになります。 コピペもできる。 するとそれが骨格になるのです。

「書くこと」の受信箱。 それがデイリー・ノートです。 思いついたアイデアを書き留めるのもそうだし、 読んでいる本の一文を書き写すのもインプットです。 それらが共有経由でデイリー・ノートに集まってくる。

折りたたみ

キャンバスになって、 折りたたみを多用するようになりました。 キャンバスはノートを切り替えるごとに、 その先頭の飛ばされてしまう仕様になっています。 カーソル位置を覚えているわけではありません。 ここが使いにくい。

この問題を回避するには、 関係ないセクションを折りたためばいい、と気づきました。 少し前に考えていた「ページング」ですね。 「見出し+内容」を一つの単位とみなす。 それを扱うには折りたたみが有効です。 いま考えたいところだけ開いておく。

この書き方だとカーソルを先頭に飛ばされることがありません。 書きたいところが開いている。 少しズレることはあります。 でも同じページ内なので修正しやすい。

段落づくり

ページは「問い+答え」でもあります。 問いを立てそれを仮見出しにし、 その後に「答え」を書いていく。 記事にするときは、 その見出しを削ることになります。 「段落」はページがいくつか集まったもの。 ページ自体が「段落」なのではありません。

ページは書くための単位ですね。 組立て用のパーツであり、並び順は後でいい。 とにかく「答え」に専念する。 そのための装置です。

右クリックの「新規タブに開く」でノートが開きます。 エディタ画面にすれば「アウトライン」の並べ替えができます。 テキストにフォーカスを当てることができる。 さあ、ここから最後のラストスパート。

段落は人に読んでもらうものなので、 「問い」は隠れて構いません。 仮見出しを削っていき、 「答え」だけ並んでも意味が通るようにします。 デッサンの補助線に消しゴムをかける。 自問自答のリズムは残るけれど隠し味にする。

それが「段落づくり」になります。

まとめ

iPadは複数のノートを同時に開きながら編集するのが難しかったけど、 このシステムだとハードルがクリアされます。 デイリー・ノートは単体で運用するものではなく、 他のノートに転写して息を吹き返す。 だから、マルチウィンドウが意味をなす。

反対に記事を書くときは、 他の情報を見なくて済むようにする。 これは「新規タブに開く」で解決する。 フォーカシングになります。

液状化と結晶化の合体。 今までやってきたことが「形」になってきました。

Obsidian:MP3プレーヤーを載せよう

暮らしにBGMを。

MP3ファイル

Obsidian は音楽データの埋め込みに対応しています。

![[音楽.mp3]]

でも一曲再生するだけ。 ループもしないしプレイリストも組めない。 まさか世界の Obsidian ユーザがこの状況を許しはしないだろう。

ということで探したらありました。

Music Player

サイドバーが音楽プレーヤーになります。

Import Obsidian: Music Player

コマンドバレットから「Music Player: Open music player」で起動します。

設定

mp3 のあるフォルダを指定してください。

music にしてみました。

使い方

音符ボタンで mp3 ファイルが表示されます。 タップで再生。

レコードの下の方にループやシャッフルがあります。

画像は、同じフォルダ内の cover.jpgmp3と同じ名前.jpg が表示されます。

プレイリストの作り方

mp3 が増えてきたらプレイリストに分けることができます。

All から、登録したい mp3 のリスト・ボタンを押します。

選択した mp3 がプレイリストに追加されます。

プレイリストの再生ボタンで、そのグループだけ再生します。

上伊那ぼたん

よくある日常系ギャグアニメと思っていたら、 7話あたりからかなあ、 切ない心の濃淡が描かれ物悲しくなってきました。 そして、あと一話で終わろうとしている。

慌てて abema.tv の全話無料でチェックし直したけど、 やっぱり初めのほうはギャグでした。 お酒と映画のウンチクでストーリーが進んでいく。 大人になろうと背伸びしている子どもたちの物語です。 まんきつ暮らしの大学生版? と思っていた。

でも寮長のイブキ視点で見ると、 お酒を他の人とは呑めないトラウマがテーマだった。 登山の途中で不思議なお婆さんに出会う。 そのお婆さんから、耳を澄ませ山の音を聞くことを教わる。 山の音が聞こえたとき、お婆さんの姿は消え、 イブキは代わりにペットボトルを運ぶ決意をする。 これはギャグではなく、象徴的な神話の世界です。

登場人物それぞれ心に傷がある。 その傷が癒えるわけではなく乗り越えるでもなく、 次の道を探し歩き続ける。 「恋愛」を被せてはいるけど、 それを安易な解決にしないところがいいなあ。 それとぼたんも、1回生なのにお酒を飲むのは二浪してたからでした。 その傷もところどころで疼いている。 取り残されることを恐れている。

あと一話では終われないでしょう。

まとめ

Hover Editor で表示するのが気に入ったけど、 手順がややこしいので省略します。

思考は歩行のリズムを刻む。 散歩してると考えが前に進みます。 だから座って考えるのは悪手なのですよね。 ADHDであることが「ヒト」の基盤であり、 学校はそこがわかっていない。 静かに座ってたら眠いじゃないですか。

座ってなら音楽が「歩行」になります。 歩行ペースにあったBGMを見つけましょう。

Obsidian:キャンバスで一週間を振り返る

いやあ、台風だよ。 まだ来てないのに大雨だよ。

デイリー・キャンバス

キャンパスをデイリーノートにする。 積み上げたなら、棚卸しもしたい。

それでマンダラに並べてみたら

今まで見たことのない風景になりました。 9日間のキャンバスを見渡せる。

weekly.md

Templater スクリプトです。

<%*
FOLDER = "canvas/"
DATE = "YYYY-MM-DD"

data = []
for(i=0; i < 9; i++){
 a = {}
 a.id = i
 a.type = "file"
 a.file = `${FOLDER}${tp.date.now(DATE, i-8)}.canvas`
 a.x = (i%3) * 420
 a.y = parseInt(i/3) * 320
 a.width = 400
 a.height = 300
 data.push(JSON.stringify(a))
}
s = `{"nodes":[${data.join(",")}]}`
file = `${FOLDER}weekly.canvas`
path = app.vault.getAbstractFileByPath(file)
if(path) {
  await app.vault.modify(path, s)
} else {
    path = await app.vault.create(file, s)
}
app.workspace.activeLeaf.openFile(path)
%>

FOLDER にデイリー・キャンバスのフォルダを設定してください。

Better Embedded Canvas

キャンバスの表示に下記プラグインを使っています。

Import Obsidian: Better Embedded Canvas

これを使わないと中身が表示されません。

オシャレな抽象画になってしまいます。

Better Embedded Canvas を使うと、 埋め込みのままテキストを編集したり、 YouTube を再生したりできます。 各キャンバスの右肩に「Open」ボタンが付いて、 それをクリックすることで開くこともできます。

これは曼荼羅っぽいな。

まとめ

キャンバスはテキストだけでなく、 空間的な情報も担っているからだろうか。 中身を読まなくても配置から汲み取れるものがありますね。 「その日の自分」を呼び出すトリガーになる。

9面のトリガーを浴びることで「今週」と向き合う。 意識的には「いろいろあった」なんですけどね。 VZを動かして、90年代の電脳情報をネットで集めたりして。

そのとき「からだ」は何を体験していたのだろう。

Obsidian:mermaid で四象限分析を描く

二項対立の分析によく使うし。

四象限

四象限を使って現象を俯瞰する。 そうした技法に頼ることがあります。 二項対立だと死角ができやすい。 そこを可視化する方法としています。

たぶん、自分の「終極のボキャブラリー」なのだと思う。 ルート・メタファーか。 根っこにあるのは陰陽道の五行説なんだろうなあ。 現象は流動的である。 本質もなければゴールもない。 それをパターンとして認識するにはどうしたらいいか。

でもそのたび描画アプリで描くのは面倒で。 そこあたり mermaid で済ませたい。

quadrantChart

mermaid には四象限用のフォーマットがあります。

```mermaid
quadrantChart

x-axis  "無為" --> "努力"
y-axis "開放系" ---> "閉鎖系"

quadrant-1 "サルの時代"
quadrant-2 "サカナの時代"
quadrant-3 "ネコの時代"
quadrant-4 "ヤギの時代"
```

これだと下図のような感じ。

quadrantChart
x-axis  "無為" --> "努力"
y-axis "開放系" ---> "閉鎖系"
quadrant-1 "サルの時代"
quadrant-2 "サカナの時代"
quadrant-3 "ネコの時代"
quadrant-4 "ヤギの時代"

手軽ですね。 でも静的に見えてしまいます。

それぞれの時代は安定的なものでなく、 プロセスのフェーズのようなもので自壊していく。 自壊しながら次の時代を生成する。 いずれも常住ではない。

あ、いま気づいたけど 「個人の歴史」と見てもいいなあ。 ネコのときはネコとして暮らし、 ヤギになったらヤギに徹する。 その「時」に合わせて自ら変容する。 そういうマップとしても読み取れます。

これが四つのエリアに分割するとその感覚が出てきません。

block

もう一つ使えそうなのが block 構文。

```mermaid
block-beta

columns 7
space:3
x1("閉鎖系"):1
space:4
a(["サカナの時代"]):2
space:1
b(["サルの時代"]):2
space:1
y1("無為"):1
space:5
y2("努力"):1
space:1
c(["ネコの時代"]):2
space:1
d(["ヤギの時代"]):2
space:4
x2("開放系"):1

y1 <---> y2
x1 <---> x2
a --> c
c --> d
d --> b
b --> a

```

これだと下図のようになります。

block-beta

columns 7
space:3
x1("閉鎖系"):1
space:4
a(["サカナの時代"]):2
space:1
b(["サルの時代"]):2
space:1
y1("無為"):1
space:5
y2("努力"):1
space:1
c(["ネコの時代"]):2
space:1
d(["ヤギの時代"]):2
space:4
x2("開放系"):1

y1 <---> y2
x1 <---> x2

a --> c
c --> d
d --> b
b --> a

図として悪くない。

努力は「偶然」をノイズとして排除するので閉鎖系を生み出してしまう。 閉鎖系になるとシステムから流動性が失われる。 流動性が失われると不活状態になり無為に陥る。 その無為が他者に開かれると再び「偶然」を活かせるフェーズになる。 たぶん、この分析の「分母」は「偶然との距離感」なのだと思います。

ただ、それぞれのブロックが同じサイズなので、 強調点がどこにあるのか捉えづらい。 「時代」の縦サイズを増やしたいけど、 増やすと軸も同じサイズに間延びする。 親切なようで不親切な仕様をしています。

キャンバス・ノート・システム

今回重宝したのがキャンバス。 カードに mermaid を書いて実験しやすい。

ソースを書き換えるときは、 右クリックで「編集」を選びます。

すぐにはソース表示にならないので気づかなかったけど、 同じカードで「編集」を 2 回選んだときソースが表示されます。 書き換えて、エラーが出ないかすぐわかる。

そして、完成版ができたら右クリックの「Copy card content」。 これを本文に貼り付ければ記事が組み上がります。 パーツを外で作れるのは効率がいい。

こういうところでは効率厨だったりする。

まとめ

で、四象限を quadrant で描くか block で行くかですが、 こういうときは手軽さで選びますね。 quadrantChart の方が書き換えやすいです。

なので今後 mermaid で書くと思います。 動的なんだな、と見守ってください。