テクノなまこ

科学の力

熱力学のマクスウェルの関係式を脳内で導出できるようにする

マクスウェルの関係式は使っているうちにやがては暗記するのだろうけど、確認方法を知っておいた方が記憶に自信が出ると思う。教科書や他のネット記事の式はやたら冗長かつ対称性の全貌が見えない書き方に見えるので、できるだけ短くすっきり書きたいと思い、書いた。難易度としては、三角関数の和と差の公式を導出するのと暗算コストは変わらないと思うので、いかに表面的な文字の多さにひるまず、短く書いて全貌を理解するかが肝であると思う。

記号一覧。記号はアトキンス物理化学10版に従う

  • H エンタルピー
  • A ヘルムホルツ自由エネルギー(Fで表すこともある)
  • G ギブズ自由エネルギー
  • U 内部エネルギー
  • S エントロピー
  • T 温度
  • p 圧力(大文字Pで表されることもある)
  • V 体積
  • dq 熱流入
  • dw 仕事(される仕事がプラスと考えるので膨張したとき負である)

1ハグ(HAGU)それぞれの定義

$$H = U + pV$$ $$A = U - TS$$ $$G = H - TS$$ $$U = H - pV$$

HAGUというのは私が覚えやすいように適当に読んでいる略語であり意味はない。

  • Hはエンタルピーであり、大気圧下で風船を温めるのような等圧過程の過熱膨張(や冷却収縮)で、膨張に使われたぶんのエネルギー(流動エネルギー)も含めた潜在的なエネルギーの量を考えるときに使う。$pV$が足されるのは、膨張に使われるエネルギーが堆積変化と外圧の積だからである。

  • Aはヘルムホルツ自由エネルギーであり、エントロピー変化を司るクラウジウスの不等式$dS \geq \frac{dq}{T}$を定積条件$dw = 0$で熱力学第一法則$dU = dq + dw$を使って変形すると$U - TdS \leq 0$という式が出てきて、これの左辺を文字で置くと定温定積条件の反応の解析に便利なので置いたものである。定温定積条件で、ヘルムホルツ自由エネルギー変化dAが負の時反応は自発的に進む。あまり使われない。

  • Gはギブズ自由エネルギーであり、エントロピー変化を司るクラウジウスの不等式$dS \geq \frac{dq}{T}$を定圧条件の仕事の定義$dw = -pdV$と熱力学第一法則$dU = dq + dw$とエンタルピーの定義$H = U + pV$を使って変形すると$H - TdS \leq 0$という式が出てきて、これの左辺を文字で置くと定圧定積条件の反応の解析に便利なので置いたものである。定圧定積条件で、ギブズ自由エネルギー変化dGが負の時反応は自発的に進む。自然界は定圧環境が多いのでよく使われる。吸熱反応が自発的に進む説明に使われる。

  • Uは内部エネルギーである。

2全微分形式

$$dH = TdS + Vdp$$ $$dA = -SdT - pdV$$ $$dG = -SdT + Vdp$$ $$dU = TdS - pdV$$

dS、dp、dT、dVは二文字で一つの変数であり、dが付くことで小さい値であることを示している。T、S、V、pは定数であり、変数についた係数である。それぞれの式全体は、二変数関数の全微分の形式であり、直観的な言葉で言えば、二種類の変数の方向の傾きを示す三次元の接平面の式である。

3マクスウェルの関係式

$$\left( \frac{\partial V}{\partial S}\right) _ p = \left( \frac{\partial T}{\partial p}\right) _ S$$ $$\left(\frac{\partial p}{\partial T}\right) _ V = \left(\frac{\partial S}{\partial V}\right) _ T$$ $$-\left(\frac{\partial V}{\partial T}\right) _ p = \left(\frac{\partial S}{\partial p}\right) _ T$$ $$-\left(\frac{\partial p}{\partial S}\right) _ V = \left(\frac{\partial T}{\partial V}\right) _ S$$

偏微分のかっこの右下についている文字は、その文字を定数とみなして微分することを表している

1から2を出す方法

Uの全微分形式$dU = TdS - pdV$をクラウジウスの不等式と仕事の定義と熱力学第一法則から作る

クラウジウスの不等式を変形し$TdS = q _ {rev}$を得る

エントロピー変化を定義するクラウジウスの不等式 $$dS \geqq \frac{q}{T}$$ の等号条件は可逆過程(圧力差0でエネルギー損失なくちょっとずつ進むピストンの過程)であるので $$dS = \frac{q _ {rev}}{T}$$ $$TdS = q _ {rev}$$

仕事の定義$dw = -pdV$を得る

外圧pが変化しないとき、仕事wの大きさは、体積変化dVと外圧pの積である。獲得型の符号で書くと、仕事をするとき系からエネルギーは失われるから符号は負である。 $$dw = -pdV$$

熱力学第一法則をベースに$TdS = q _ {rev}$と$dw = -pdV$を代入する

内部エネルギー変化は熱量が入るか仕事をされることで増加する。 $$dU =dq _ {rev} + dw$$ この式に上で求めた二つの式$TdS = q _ {rev}$と$dw = -pdV$を入れるとUの全微分形式 $$dU = TdS - pdV$$ が得られる

得られたUの全微分形式$dU = TdS - pdV$と、HAGUの各定義を使って、HAGUの各全微分形式を玉突き式に埋めていく

Hの全微分形式$dH = TdS + Vdp$をUの全微分形式$dU = TdS - pdV$とHの定義$H = U + pV$から得る

Hの定義 $$H = U + pV$$ を全微分すると積の微分公式から $$dH = dU + pdV + Vdp$$ となる。上で得られたUの全微分形式$dU = TdS - pdV$を代入し $$dH = (TdS - pdV) + pdV + Vdp$$ $$dH = TdS + Vdp$$

Aの全微分形式$A = U - TS$をUの全微分形式$dU = TdS - pdV$とAの定義$A = U - TS$から得る

Aの定義 $$A = U - TS$$ を全微分すると積の微分公式から $$dA = dU - TdS - SdT$$ となる。上で得られたUの全微分形式$dU = TdS - pdV$を代入し $$dA = (TdS - pdV) - TdS - SdT$$ $$dH = -pdV -SdT$$

Gの全微分形式$dG = -SdT + Vdp$をHの全微分形式$dH = TdS + Vdp$とGの定義$G = H - TS$から得る

Gの定義 $$G = H - TS$$ を全微分すると積の微分公式から $$dG = dH - TdS - SdT$$ となる。上で得られたHの全微分形式$dH = TdS + Vdp$を代入し $$dG = (TdS + Vdp) - TdS - SdT$$ $$dG = -SdT + Vdp$$

2から3を出す方法

二変数関数の全微分の係数の公式$\left( \frac{\partial b}{\partial x}\right) _ y = \left( \frac{\partial a}{\partial y}\right) _ x$を導出する

一般に$df = a dx + b dy$と書けるとき$\left( \frac{\partial b}{\partial x}\right) _ y = \left( \frac{\partial a}{\partial y}\right) _ x$が成り立つ。なぜなら

二変数関数$f(x,y)$の全微分$df$は $$df = \left( \frac{\partial f}{\partial x}\right) _ y dx + \left( \frac{\partial f}{\partial y}\right) _ x dy$$ 偏微分の係数の部分を文字a, bでおく $$df = a dx + b dy$$ 下の式を眺めて、aとbの定義に分解して成り立つことを納得する $$\left( \frac{\partial b}{\partial x}\right) _ y = \left( \frac{\partial ^ 2 f}{\partial x \partial y}\right) = \left( \frac{\partial a}{\partial y}\right) _ x$$

二変数関数の全微分の係数の公式$\left( \frac{\partial b}{\partial x}\right) _ y = \left( \frac{\partial a}{\partial y}\right) _ x$を、HAGU全微分形式に当てはめる

下の式を見て、既視感を感じ、二変数関数の全微分の係数の公式$\left( \frac{\partial b}{\partial x}\right) _ y = \left( \frac{\partial a}{\partial y}\right) _ x$を当てはめる $$dH = TdS + Vdp$$ $$dA = -SdT - pdV$$ $$dG = -SdT + Vdp$$ $$dU = TdS - pdV$$

付録 係数の比較により熱力学関数の偏微分による定義を得る

$$dH = TdS + Vdp$$ これは2変数関数H(S, p)の式である。H(S, p)の全微分は $$dH = \left( \frac{\partial H}{\partial S}\right) _ p dS+ \left( \frac{\partial H}{\partial p}\right) _ S dp$$ 係数の比較から $$T = \left( \frac{\partial H}{\partial S}\right) _ p$$ $$V = \left( \frac{\partial H}{\partial p}\right) _ S$$ が得られる。同様に繰り返して

$$dA = -SdT - pdV$$ から $$-S= \left( \frac{\partial A}{\partial T}\right) _ V$$ $$-p = \left( \frac{\partial A}{\partial V}\right) _ T$$ が得られ

$$dG = -SdT + Vdp$$ から $$-S = \left( \frac{\partial G}{\partial T}\right) _ p$$ $$V = \left( \frac{\partial G}{\partial p}\right) _ S$$ が得られ

$$dU = TdS - pdV$$ から $$T = \left( \frac{\partial U}{\partial S}\right) _ V$$ $$-p = \left( \frac{\partial U}{\partial V}\right) _ S$$ が得られる。

pythonで軌道シミュレーション

目次

画像書き出し

# Import Python Modules
import numpy as np # 数値計算ライブラリ
import matplotlib.pyplot as plt # 描画ライブラリ
from scipy.integrate import odeint # 常微分方程式を解くライブラリ

# 定数
# G = 6.67430 * 10 **(-20) [km^3 kg^-1 s^-2] 万有引力定数(km)
# M = 5.972 * 10 ** 24 # [kg] 地球質量
GM = 398600 # [km^3 s^-2] 地球の重力定数

# 二体問題の運動方程式
def func(x, t):
    r_E = np.linalg.norm([x[0],x[1],x[2]])
    dxdt = [x[3],
            x[4],
            x[5],
            -GM*x[0]/(r_E**3),
            -GM*x[1]/(r_E**3),
            -GM*x[2]/(r_E**3)]
    return dxdt 

# 微分方程式の初期条件
x0 = [6400, 0, 0, 0, 9, 0] # 位置(x,y,z)+速度(vx,vy,vz)
t  = np.linspace(0, 30000, 1000) # (開始、終了、ステップ数) 1日分 軌道伝播

# 微分方程式の数値計算
sol = odeint(func, x0, t)

# 二次元描画
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.set_xlabel('km')
ax.set_ylabel('km')

#地球をプロット
ax.plot(0,0,'.', c='black') #点をプロットする場合

ax.plot(sol[:, 0],sol[:, 1], 'black') #numpyのnparrayの記法で、各時間の0列目(x座標)、1列目(y座標)を抽出
ax.set_aspect('equal') # グラフのアスペクト比を揃える
plt.savefig('orbit.png') # 画像を保存
plt.show()

kmで計算しているので、万有引力定数が-11乗でなく-20乗になっている。万有引力万有引力定数Gと地球質量Mから計算すると誤差が大きくなるため、重力定数GMとしてハードコードしている

地球の大きさを描画する

# Import Python Modules
import numpy as np # 数値計算ライブラリ
import matplotlib.pyplot as plt # 描画ライブラリ
from scipy.integrate import odeint # 常微分方程式を解くライブラリ
from matplotlib import patches

# 定数
# G = 6.67430 * 10 **(-20) [km^3 kg^-1 s^-2] 万有引力定数(km)
# M = 5.972 * 10 ** 24 # [kg] 地球質量
GM = 398600 # [km^3 s^-2] 地球の重力定数
R = 6378
# 二体問題の運動方程式
def func(x, t):
    r_E = np.linalg.norm([x[0],x[1],x[2]])
    dxdt = [x[3],
            x[4],
            x[5],
            -GM*x[0]/(r_E**3),
            -GM*x[1]/(r_E**3),
            -GM*x[2]/(r_E**3)]
    return dxdt 

# 微分方程式の初期条件
x0 = [R+600, 0, 0, 0, 9, 0] # 位置(x,y,z)+速度(vx,vy,vz)
t  = np.linspace(0, 30000, 1000) # (開始、終了、ステップ数) 1日分 軌道伝播

# 微分方程式の数値計算
sol = odeint(func, x0, t)

# 二次元描画
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.set_xlabel('km')
ax.set_ylabel('km')

#地球をプロット
c = patches.Circle( (0,0), R)
ax.add_patch(c)
# ax.plot(0,0,'.') #点をプロットする場合

ax.plot(sol[:, 0],sol[:, 1], 'black') #numpyのnparrayの記法で、各時間の0列目(x座標)、1列目(y座標)を抽出
ax.set_aspect('equal') # グラフのアスペクト比を揃える
plt.savefig('orbit.png') # 画像を保存
plt.show()

マウスを使って三次元で観察する

# Import Python Modules
import numpy as np # 数値計算ライブラリ
import matplotlib.pyplot as plt # 描画ライブラリ
from scipy.integrate import odeint # 常微分方程式を解くライブラリ

# 定数
# G = 6.67430 * 10 **(-20) [km^3 kg^-1 s^-2] 万有引力定数(km)
# M = 5.972 * 10 ** 24 # [kg] 地球質量
GM = 398600 # [km^3 s^-2] 地球の重力定数

# 二体問題の運動方程式
def func(x, t):
    r_E = np.linalg.norm([x[0],x[1],x[2]])
    dxdt = [x[3],
            x[4],
            x[5],
            -GM*x[0]/(r_E**3),
            -GM*x[1]/(r_E**3),
            -GM*x[2]/(r_E**3)]
    return dxdt 

# 微分方程式の初期条件
x0 = [6000, 0, 0, 0, 8, 1] # 位置(x,y,z)+速度(vx,vy,vz)
t  = np.linspace(0, 30000, 1000) # (開始、終了、ステップ数) 1日分 軌道伝播

# 微分方程式の数値計算
sol = odeint(func, x0, t)

# 三次元描画
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection='3d')
ax.plot(0,0,0,'.')#地球をプロット
ax.plot(sol[:, 0], sol[:, 1], sol[:, 2],linewidth = 0.5)
ax.set_aspect('equal') # グラフのアスペクト比を揃える
plt.show()

初速のz成分を0km/sから1km/sに変更し、三次元でプロットした

gif動画を書き出す

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from scipy.integrate import odeint # 常微分方程式を解くライブラリ


GM_E = 398600 # km3/s2 地球の重力定数
L_E = [0,0,0]

def func(x, t):
    r_E = np.linalg.norm([x[0]-L_E[0],x[1]-L_E[1],x[2]-L_E[2]])
    dxdt = [x[3],
            x[4],
            x[5],
            -GM_E*x[0]/(r_E**3),
            -GM_E*x[1]/(r_E**3),
            -GM_E*x[2]/(r_E**3)]
    return dxdt 

def update(i, x, y):

    plt.plot(x[0:i], y[0:i],c="blue")

def draw():
    # 微分方程式の初期条件
    r0 = [6500, 0, 0, 0, 9, 0] # 位置(x,y,z)+速度(vx,vy,vz)
    t  = np.linspace(0, 10000, 1000) # 1日分 軌道伝播
    sol = odeint(func, r0, t)
    print("keisan owari")

    # 抽出して二次元化
    x1, y1, z1 = sol[:, 0], sol[:, 1], sol[:, 2]
    x = x1[::10]
    y = y1[::10]

    fig = plt.figure() #figure objectを取得
    plt.plot(0,0,'.',c='blue')# 地球をプロット
    plt.xlim(min(x),max(x))
    plt.ylim(min(y),max(y))
    plt.gca().set_aspect('equal')

    ani = animation.FuncAnimation(fig, update, fargs = (x, y), frames = len(x),interval = 10)
    ani.save("orbit.gif", writer = 'pillow',fps=30)
draw()

mp4を書き出す

mp4を書き出す場合は、writerをpillowから次のように変える

    ani = animation.FuncAnimation(fig, update, fargs = (x, y), frames = len(x),interval = 10)
    writer = animation.writers['ffmpeg'](fps=60, metadata=dict(artist='mika'), bitrate=200)
    ani.save("orbit.mp4", writer = writer)

月と、太陽の摂動と、太陽地球回転フレーム

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from scipy.integrate import odeint # 常微分方程式を解くライブラリ
import glob

# G = 6.67430 * 10 **(-11) [m3 kg-1 s-2] 万有引力定数(mksa)
# G = 6.67430 * 10 **(-20) [km3 kg-1 s-2] 万有引力定数(km)

# ME = 5.972 * 10 ** 24 # [kg] 地球質量
# MM = 7.345 * 10 ** 22 # [kg] 月質量
MS = 1.989 * 10 ** 30 # [kg] 太陽質量

GM_E = 398600.4354360959 # [km3 s-2] 地球の重力定数
GM_M = 4902.8072 # [km3/s2] 月の重力定数
GM_S = 13.27 * 10 ** 10 # [km3 s-2] 太陽の重力定数

L_E = [0,0,0] # 地球の座標[km]

# R_SE = 1.496 * 10 ** 8 # [km] 地球公転半径
R_EM = 3.8 * 10 ** 5 # [km] 月公転半径

# A = 5.927 * 10 ** -6 # [km s-2] 地球付近での太陽重力の加速度
# GM_S/R_SE
W = 2*np.pi/(86400*365)
w = 2*np.pi/(86400*30)
gradA = 17.74 * 10 ** (-14) # GM_S/R_SE ** 3 # [km s-2] 地球付近での太陽重力と遠心力による加速度の傾斜  GM_S/R_SE**2 + W**2

def L_M(t):
    # 月の運動
    offset = 170/360 * 2*np.pi
    return [-384400*np.cos(w*t+offset),-384400*np.sin(w*t+offset),0]

def F(r, t):
    r_E = np.linalg.norm([r[0]-L_E[0],r[1]-L_E[1],r[2]-L_E[2]])
    r_M = np.linalg.norm([r[0]-L_M(t)[0],r[1]-L_M(t)[1],r[2]-L_M(t)[2]])
    LM = L_M(t)

    dxdt = [r[3],
            r[4],
            r[5],
            GM_E*(L_E[0]-r[0])/(r_E**3) + GM_M*(LM[0]-r[0])/(r_M**3) + gradA * r[0] * np.cos(W*t),
            GM_E*(L_E[1]-r[1])/(r_E**3) + GM_M*(LM[1]-r[1])/(r_M**3) + gradA * r[1] * np.sin(W*t),
            GM_E*(L_E[2]-r[2])/(r_E**3) + GM_M*(LM[2]-r[2])/(r_M**3)]
    return dxdt

def update(i, x, y, unittime,framew):
    t = i*unittime
    LM = L_M(t)
    plt.cla() #現在描写されているグラフを消去
    plt.plot(0,0,0,'.',c='blue')# 地球をプロット
    plt.plot(LM[0]*np.cos(framew*t)+LM[1]*np.sin(framew*t),
             -LM[0]*np.sin(framew*t)+LM[1]*np.cos(framew*t),
             '.',c='red')# 月をプロット
    plt.xlim(-2000000,1500000)
    plt.ylim(-1500000,1500000)

    plt.plot(x[0:i], y[0:i],c="blue") #plot
    plt.text(-1500000,-1000000,f'day = {int(t/86400)}', fontsize=10)

def draw():
    # 微分方程式の初期条件r0
    v0 = 11.48964 #初速12.5922 12.592
    h0 = 6000 # 初期高度
    th0 = np.pi*1 # 初期位置の経度(太陽側が0、反時計回り)
    r0 = [-h0*np.cos(th0), -h0*np.sin(th0), 0, v0*np.sin(th0), -v0*np.cos(th0), 0] # 位置[km](x,y,z)+速度[km/s](vx,vy,vz)

    # 微分方程式の計算設定
    timeRange = int(86400 * 320) #時間範囲[s] (参考 1d=86400s)
    calcStep = 40 #時間ステップ[s]
    t  = np.linspace(0, timeRange, int(timeRange/calcStep)) # 時間範囲(開始[s],終了[s],分割数)
    
    # 微分方程式を解く
    sol = odeint(F, r0, t) # [x, y, z, u, v, w]

    # print(sol[5])
    x1 = []
    y1 = []
    z1 = []

    framew = W

    for i, raw in enumerate(sol):
        t = i*calcStep
        x1.append(raw[0]*np.cos(framew*t) + raw[1]*np.sin(framew*t))
        y1.append(-raw[0]*np.sin(framew*t) + raw[1]*np.cos(framew*t))
        z1.append(raw[2])

    # 動画保存用にフレームを抜き出す
    skiprate = int(1440/calcStep*15) #144skiprateで、60fps 10秒step計算で1秒が一日になる
    x = x1[::skiprate]
    y = y1[::skiprate]

    dic = calcStep*skiprate

    fig = plt.figure() #figure objectを取得
    plt.gca().set_aspect('equal')

    ani = animation.FuncAnimation(fig, update, fargs = (x, y,dic,framew), frames = len(x),interval = 10)

    # savemp4(ani,'ww.mp4')
    savegif(ani,'16.gif')
    # savemp4(ani,getname())

def getname():   
    filelist = [int(filename.split('.')[0].replace('test','')) for filename in glob.glob('*.mp4')]
    return f'test{max(filelist)+1}.mp4'

def savemp4(ani,filename):
    writer = animation.writers['ffmpeg'](fps=60, metadata=dict(artist='mika'), bitrate=200)
    ani.save(filename, writer = writer)

def savegif(ani,filename):
    ani.save(filename, writer = 'pillow', fps=30)

draw()

UnrealEngine5で万有引力

参考

[UE5] Simulating Gravity in Unreal Engine 5 - YouTube 物体同士の万有引力
Unreal Engine 5 Planet Gravity System - Physics Objects - YouTube 特定の惑星への万有引力

地球-衛星系の万有引力

下準備

  • UE5.3をBlank,StarterKitなし,BluePrintありで起動する。
  • File>New level>Basicから新しいレベル(一つの独立した場面、Unityでのシーンに相当)を作る

地球の設定

  • Window>Content Broeser>Content Browser1から、Content Browserを表示する
  • Content Browser上で右クリック>BluePrintClass>Actorから、アクター(参照して動的に動かせるオブジェクトの母体)を作成する
  • Content Browser上でアクターの名前をEarthにする
  • Content Browser上でアクターをダブルクリックして開きエディターウィンドウ(正式名称を知らない)を表示する
  • エディターウィンドウ上でComponents>+Add>Sphereから、球体を作成する
  • エディターウィンドウのComponents上で、SphereをDefaultSceneRootの上にドラッグし、DefaultSceneRootをSphereで置き換える

地球に落とすものの設定

  • メインのウィンドウに戻り、+ボタン>Shapes>Sphereから衛星に相当する新しい球体を作成する
  • 衛星を地球から離す。
  • 衛星を選択した状態で
  • 地球を選択し、Details>Physics上で、Simulate Physicsにチェックを入れ、Massにチェックを入れ、Mass(kg)の値を1.0等にし、Enable Gravity(下方向の重力を適用する)のチェックを外す
  • Content Browser上で右クリック>BluePrint Class>GameMode baseから、ゲームモードベースのブループリントを作成する
  • Content Browser上ゲームモードベースのブループリントの名前をMain_Gamemode等にリネームする
  • Content Browser上でMain_Gamemodeをダブルクリックしてエディターウィンドウで開く
  • Main_Gamemodeを開いて表示されたエディターウィンドウのEvent Graphタブを表示する 下の画像のBluePrintを作る
  • 再生ボタンで球体が地球に落ちたら成功

付録 多対多の万有引力

参考の一つ目の動画では複数の天体の多対多の万有引力を実装している。多対多の万有引力のBluePrintは下のようになる。球体同士が衝突して一体となると回転し始める癖がある。

Unityで三人称視点移動+ズーム

Unityで3Dゲームを作るとき、ゲーム内でもblenderで操作するような3Dの視点移動ができてほしいと思ったので、調べて書いた。

参考

コード

視点の中心となるオブジェクトが「player」という名前だとする。カメラは、対象となるオブジェクトの子だとする。CamCtrl.csというC#スクリプトを作成して、中身を下コードに書き換えてカメラにアタッチすると、マウスのホイールでズームが、左クリックのドラッグで視点移動ができる。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CamCtrl : MonoBehaviour
{
    private Camera cam;
    private Vector3 startPos;
    private Vector3 startAngle;
    public GameObject player = null;

    //初めに一度実行される部分
    void Start()
    {
        cam = this.GetComponent<Camera>(); //アタッチ先のオブジェクト(カメラ)から、カメラコンポーネントを呼び出してくる
        player = GameObject.Find("player"); //視点の中心となる"player"という名前のゲームオブジェクトを探してくる
    }

    //毎フレーム実行される部分
    void Update()
    {
        float sensitiveZoom =
            2.0f * ((player.transform.position - cam.transform.position).magnitude + 0.1f);
            //ズーム感度を視点の中心となるオブジェクトとの距離から計算する。
            //0.1を足しているのは、距離0でも感度が0にならずズームアウト可能にするため
        float sensitiveRotate = 5.0f;//三人称の視点回転感度
        
        if (Input.GetMouseButton(0)) //left click
        {
            Vector3 angle = new Vector3(
                Input.GetAxis("Mouse X") * sensitiveRotate,
                -Input.GetAxis("Mouse Y") * sensitiveRotate,
                0
            );

            cam.transform.RotateAround(player.transform.position, Vector3.up, angle.x);
            cam.transform.RotateAround(player.transform.position, transform.right, angle.y);
        }

        // zoom
        float moveZ = Input.GetAxis("Mouse ScrollWheel") * sensitiveZoom;
        cam.transform.position += cam.transform.forward * moveZ;
    }
}

Input.GetMouseButton(0)の代わりに

Input.GetMouseButton(1)

と書くと、右クリックのドラッグで視点移動できる。

このコードで使った関数の他に、カメラの移動には

cam.transform.localPosition -= new Vector3(moveX, moveY, 0.0f);
cam.transform.Rotate(rotateY, rotateX, 0.0f);

というふうに使える関数もある

Unityで万有引力

Unityでは、rigidbodyコンポーネントをオブジェクトにつけることで物理演算を適用させられる。rigidbodyコンポーネントにはgravityというチェックボックスがあり、チェックするとその物体は重力で下に落ちるようになる。しかし、画面中央に表示した星に物が落ちていく状況など、重力が下方向に働いてほしくない場面もある。この記事では簡単に万有引力を実装するコンポーネントC#コード例を載せる。

参考にしたもの

コード例

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Gravity : MonoBehaviour {
    //万有引力
    public static float G = 6.67259f * Mathf.Pow(10,-11); //万有引力定数6.67259×10^-11
    //public GameObject satellite = null;の行は、このスクリプトが衛星にアペンドされるものであり、thisで参照できるため必要ない
    Rigidbody satelliteRig = null;//惑星を回る人工衛星のゲームオブジェクトを初期化
    public GameObject earth = null; //重力源のゲームオブジェクトを初期化
    Rigidbody earthRig = null; //重力源のゲームオブジェクトのRigidboodyコンポーネントを初期化

    //初速
    public Vector3 v0 = new Vector3(0, 0, 0);//空のベクトルを作成

    //最初に一回だけ実行されるスクリプト
    void Start () {
        //万有引力
        satelliteRig = this.GetComponent<Rigidbody>();//アペンドされたオブジェクトからRigidbodyコンポーネントを呼び出してくる
        earthRig = earth.GetComponent<Rigidbody>();//重力源からRigidbodyコンポーネントを呼び出してくる

        //初速
        satelliteRig.AddForce(v0, ForceMode.VelocityChange);//衛星に初速を与える
    }

    //フレーム毎に実行されるスクリプト
    void Update () {
        Vector3 vec_direction = earth.transform.position - this.transform.position; //衛星から見た地球の方向ベクトルを計算
        Vector3 Univ_gravity = G * vec_direction.normalized * (earthRig.mass * satelliteRig.mass) / (vec_direction.sqrMagnitude); //万有引力を計算
        satelliteRig.AddForce(Univ_gravity); //衛星に万有引力をかける
    }
}

使い方

  • 重力源となるオブジェクトを作り、オブジェクトを選択した状態でInspector viewのadd componentからrigidbodyコンポーネントを作成する。重力源は固定したいため、rigidbodyコンポーネントの設定から、Use Gravity(下に落ちるようにする)のチェックを外し、Is Kinematic(物理演算を無視し、操作がない限り空間に固定)のチェックを入れる。
  • 重力を感じさせたいオブジェクトを作り、オブジェクトを選択した状態でInspector viewのadd componentからrigidbodyコンポーネントを作成する。衛星は動いてほしいので、rigidbodyコンポーネントの設定から、Use Gravity(下に落ちるようにする)のチェックを外し、Is Kinematicのチェックも外しておく。(kinematic(運動学的)にチェックを入れると物理演算を無視するのは直観に反する……ゲーム内で指定したルートで動かしたいから物理演算を無視するという意味らしい)
  • 画面下Project viewのファイルブラウザで、Assets内に右クリック>Create>C# Scriptから”Gravity.cs”という名前で新しいC#スクリプトファイルを作る。ファイル名はclass名と同一である必要があるので、必ず”Gravity.cs”という名前にする。”Gravity.cs”の中身に、上のコード例を張り付け保存する。
  • Project view上のコードのアイコンをScene view上の重力を感じせたい物体にドラッグし、スクリプトを物体にアペンドする。重力を感じさせたい物体それぞれについて、オブジェクトを選択し、Inspecter ViewのGravity(Script)のEarthで重力源となるオブジェクトを指定する。v0の項目から初速を設定する。

キーボード操作可能にする

using UnityEngine;

public class PlayerScript : MonoBehaviour
{
    Vector3 velocity; // オブジェクトの速度

    float speed = 0.01f;

    void Update()
    {
        // キー入力に基づいて速度ベクトルを変更
        if (Input.GetKey(KeyCode.W))
        {
            velocity += transform.up * speed * Time.deltaTime;
        }

        if (Input.GetKey(KeyCode.S))
        {
            velocity -= transform.up * speed * Time.deltaTime;
        }

        if (Input.GetKey(KeyCode.D))
        {
            velocity += transform.right * speed * Time.deltaTime;
        }

        if (Input.GetKey(KeyCode.A))
        {
            velocity -= transform.right * speed * Time.deltaTime;
        }

        // 速度ベクトルを使って位置を更新
        transform.position += velocity;
    }
}

使い方

  • PlayerScript.csを新たに作成して上コードを張り付け操作したいオブジェクトにアペンドする

軌跡を表示する

軌跡を表示させたいオブジェクトを選択し、inspector viewのadd componentからtrail rendererを追加する

完成図

追記

UpdateじゃなくてFixedUpdateを使うとゲーム内時間で更新されるためラグっても軌道が乱れない

html, JavaScriptで、一つの関数で複数の音楽の再生/停止を切り替えられて、audioタグを使わないボタンのコード例

コード
’1.mp3’, ’2.mp3’, ’3.mp3’という音声ファイルを再生したいとする

<script>
const audioElements = {};
function btn(filePath, button) {
  if (!audioElements[filePath]) {
      // 新しい音声ファイルを再生
      audioElements[filePath] = new Audio(filePath);
      audioElements[filePath].play();
      button.classList.add('playing'); // ボタンのスタイルを変更

      // 音声再生終了時のイベントハンドラを設定
      audioElements[filePath].addEventListener('ended', function() {
          button.classList.remove('playing'); // ボタンのスタイルを元に戻す
          delete audioElements[filePath]; // オブジェクトから削除
      });} else {
      // 既存の音声ファイルを停止
      audioElements[filePath].pause();
      audioElements[filePath].currentTime = 0; // 音声を最初から再生できるようにリセット
      delete audioElements[filePath]; // オブジェクトから削除
      button.classList.remove('playing'); // ボタンのスタイルを元に戻す
  }}
</script>
<style>

button{
  background-color: #47848b;
  color: white;
  border-radius: 1em;border-width: 0;
  &:hover {
    background-color: #4d9fa9;
  }
  &.playing {
    background-color: #14656e; /* 再生中のボタンの背景色を変更 */
  }
}

</style>
<button onclick="btn('1.mp3', this)">ボタン1</button>
<button onclick="btn('2.mp3', this)">ボタン2</button>
<button onclick="btn('3.mp3', this)">ボタン3</button>

機能

  • ひとつの関数で複数ファイルの音声の再生状態が管理できるので、html側で
<button onclick="btn('4.mp3', this)">ボタン1</button>

などとパスだけを異なるbuttonタグを足すだけで異なるボタンを増やせる。ネット上の他の実装のようにclass名を書いてjs側でaddEeventListener()をボタン毎に書く必要がなく、一つの関数を多数の音声ファイルに使いまわせ、html側で音声ファイルパスを指定できる。

  • マウスホバーで色が変化する。
  • 再生中は色が変わる。
  • 再生中にボタンを押すと再生が止まる。

背景

私のサイトでは何年か前から「さなボタン」に触発されて、「ミクボタン」という、押すと初音ミクの声が聴けるだけのボタンを設置しているが、ホームページをマークダウン化するにあたって、マークダウン中のhtmlでの入力量が最小になるように書き換えたかった。chatGPTに5回程度書き直させるとほとんど完璧な回答が出てきて、ネット上の実装よりもずっといいように思えたので公開した。

GitHub Pagesのmarkdownに自作テーマを適用し、ダークテーマや、tex数式や、シンタックスハイライトと言語名表示つきのコードブロックを使えるようにする

完成例

参考用

github pagesのmarkdownに自作テーマを適用し、ダークテーマや、tex数式や、シンタックスハイライトと言語名表示つきのコードブロックを使えるようにした。この記事ではその全コードを掲示し簡単な解説をする。本当は最小構成を掲示するべきだろうけれど面倒なのでやめた。

最終構造
Mikanixonable.github.io
├── _layouts
│ └──default.html
├── md
│ └──style.css
├── _config.yml
└── index.md


やったこと(全6工程)(スクロール量とても長い)

_config.ymlを作成しリポジトリに置く

markdown: kramdown
plugins:
 - jekyll-sitemap
highlighter: rouge
kramdown:
  hard_wrap: true

markdown -> html変換をkramdownで行う」
xml形式のサイトマップを作ってgoogleにクロールされやすくし、検索に乗りやすくする」
シンタックスハイライトをrougeで行う」
「通常のmarkdownでは無視される改行を無視しない」
という意味のことが書いてある

github pagesに使用しているリポジトリに_layoutsというフォルダを作り、その中にdefault.htmlを作成する

(必ず_layouts/フォルダ下である必要がある。defaut.htmlの名前は変えてもいいが、その場合後述のmarkdownのlayout: defaultの部分を別の名前に書き換える必要がある)

default.htmlの中身を書く

default.htmlの中身に、以下をコピーする

<!doctype html>
<html lang="{{ site.lang | default: " ja" }}">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="color-scheme" content="light dark" />
  <!-- IE互換性のためのおまじない -->
  <link rel="stylesheet" href="./md/style.css">
  <script
    type="text/x-mathjax-config">MathJax.Hub.Config({tex2jax:{inlineMath:[['\$','\$'],['\\(','\\)']],processEscapes:true},CommonHTML: {matchFontHeight:false}});</script>
  <script type="text/javascript" async
    src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-MML-AM_CHTML"></script>

  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  {% seo %}
</head>
<body>
  <div class="wrapper">
      {{ content }}
  </div>
</body>
</html>
<script>
  let sheets = document.styleSheets
  let sheet = sheets[sheets.length - 1];
  let codeElements = document.querySelectorAll("code");
  for (let i = 0; i < codeElements.length; i++) {
    let codeElement = codeElements[i]
    codeElement.parentElement.classList.add("pre" + i)

    // <div class="language-shell highlighter-rouge">
    // <div class="highlight">
    // <pre class="highlight">
    // <code>
    // から「python」を抽出
    clsName = codeElement
    .parentElement
      .parentElement
      .parentElement
      .className
    langName = codeElement
      .parentElement
      .parentElement
      .parentElement
      .className
      .match(/language-(\S+)\s/)[1]

    sheet.insertRule(
      `.pre${i}:before { content: "${langName}";}`,
      sheet.cssRules.length);
  }
</script>

github pagesに使用しているリポジトリにmdというフォルダを作り、その中にstyle.cssを作成する。

(フォルダ名を「md」以外にしてもいいが、その場合上に載せたdefault.htmlのの「md」の箇所を別のフォルダ名に書き換える必要がある。また、フォルダ名を_layoutsにすると読み込まれなくなる。おそらく「_」で始まると読み込まれなくなると思う(未確認))

4. style.cssの中身を書く

style.cssの中身に、以下をコピーする

:root{
  --b1: #181921;
  --b2: #1a2638;
  --b3: #323e52;
  --b4: #323e52;
  --b4: #1a2a2c;
}

body {
  padding:0 0;
  margin: 0 0;
  font:14px/1.5 "OpenSansRegular", "Helvetica Neue", Helvetica, Arial, sans-serif;
  color:#ddd9ca;
  font-weight: normal;
  background: #181921;
  display: flex;
  align-items: center;
  flex-direction: column;
  /* background-attachment: fixed !important; */
}

.linkcard {
  border-radius: 5em;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.linkcard img{
  width: 8em;
  height: 12em;
  object-fit: cover;
  border-radius: 5em 5em 5em 5em   
}
.row {
  display: flex;
  flex-direction: row;
  align-items: center;
}
.row a{
  margin: 1em;
}
.columun {
  display: flex;
  flex-direction: column;
  align-items: center;
}

article{
  background-color: #1a2638;
  margin: 1em 0em;
  padding: 1em;
  border-radius: 1em;
}
article article {
  margin: 1em -0.5em;
  padding: 1em 0.5em;
  background-color: #323e52;
}

.wrapper {
  margin: 0 auto;
  padding: 30px;
  max-width:660px;
  position:relative;
}

svg {
  max-width: 660px;
  max-height: 400px;
}

img{
  max-height: 80vh;
  max-width: 80vw;
}

section img {
  max-width: 100%;
}

button {
  background-color: slategrey;
  color: white;
  border-radius: 1em;
  border-width: 0;
}
button:hover {
  filter: hue-rotate(20deg) saturate(90%) brightness(90%)
}
button.playing {
  filter: hue-rotate(20deg) saturate(200%)
}
.mikuButton button {
  background-color: #47848b;
}
.musicButton button {
  background-color: #8b4755;
}

h1, h2, h3, h4, h5, h6 {
  margin:0px;
  color: white;
  font-family: 'OpenSansRegular', "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: normal;
}

p, ul, ol, table, pre, dl {
  margin:0 0 20px;
}

h1, h2, h3 {
  line-height:1.1;
}

h1 {font-size:28px;}

h2 {font-size: 24px;}

h3 {
  font-size: 18px;
  line-height: 24px;
  font-family: 'OpenSansRegular', "Helvetica Neue", Helvetica, Arial, sans-serif !important;
  font-weight: normal;
}

a {
  color:#f5abb5;
  font-weight:400;
  text-decoration:none;
}
a:hover {
  color: #f69eaa;
  text-decoration: none;
}

a small {
  font-size:11px;
  color:#666;
  margin-top:-0.6em;
  display:block;
}


ul{
  list-style-image:url('../images/bullet.png');
}

strong {
  font-family: 'OpenSansBold', "Helvetica Neue", Helvetica, Arial, sans-serif !important;
  font-weight: normal;
}

blockquote {
  border-left:3px solid cadetblue;
  font-style: italic;
  color: #c8dfdf;
  margin:0;
  padding:0 0 0 20px;
  
}

.codepre:before {
  display: block;
  /* position: absolute; */
  inset-block-start: 0em;
  inset-inline-start: 0;
  /* content: attr(class); */
  padding-block: .5em;
  padding-inline: 1em;
  inline-size: fit-content;
  background-color: var(--b3);
  color: white;
  font-family: Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, monospace;
  border-radius: .5em .5em 0 0;
  /* content: "python"; */
}

.code {
  display: block;
  position: relative;
  padding: 1em 1em;
  background-color: var(--b2);
  border-radius: 0 .5em .5em .5em;
  font-family: Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, monospace;
  color:#efefef;
  font-size:13px;
  overflow: auto;
  /* overflow-y: hidden; */
}

.mermaidpre {
  /* background-color: #5292c7; */
  display: flex;
  justify-content: center;
}
/* .language-mermaid {
  background-color: #51e1bd;
  margin: 0 auto;
} */

table {
  width:100%;
  border-collapse:collapse;
}

th {
  text-align:left;
  padding:5px 10px;
  border-bottom:1px solid #434343;
  color: #b6b6b6;
  font-family: 'OpenSansSemibold', "Helvetica Neue", Helvetica, Arial, sans-serif !important;
  font-weight: normal;
}

td {
  text-align:left;
  padding:5px 10px;
  border-bottom:1px solid #434343;
}

hr {
  border: 0;
  outline: none;
  height: 3px;
  background: transparent url('../images/hr.gif') center center repeat-x;
  margin: 0 0 20px;
}

dt {
  color:#F0E7D5;
  font-family: 'OpenSansSemibold', "Helvetica Neue", Helvetica, Arial, sans-serif !important;
  font-weight: normal;
}


/* コードブロック */

@media print, screen and (max-width: 720px) {

  #title {
    .credits {
      display: block;
      width: 100%;
      line-height: 30px;
      text-align: center;

      .left {
        float: none;
        display: block;
      }

      .right {
        float: none;
        display: block;
      }
    }
  }
}

@media print, screen and (max-width: 480px) {

  #header {
    margin-top: -20px;
  }

  section {
    margin-top: 40px;
  }
  nav {
    display: none;
  }
}
/*
   generated by rouge http://rouge.jneen.net/
   original base16 by Chris Kempson (https://github.com/chriskempson/base16)
*/

.highlight table td { padding: 5px; }
.highlight table pre { margin: 0; }
.highlight, .highlight .w {
  color: #d0d0d0;
}
.highlight .err {
  color: #151515;
  background-color: #ac4142;
}

.highlight .c, .highlight .cd, .highlight .cm, .highlight .c1, .highlight .cs {
  color: #888;
}
.highlight .cp {
  color: #f4bf75;
}
.highlight .nt {
  color: #f4bf75;
}
.highlight .o, .highlight .ow {
  color: #adcace;
}
.highlight .pi {
  color: #d0d0d0;
}
.highlight .p {
  color: #dbd700;
}
.highlight .gi {
  color: #90a959;
}
.highlight .gd {
  color: #ac4142;
}
.highlight .gh {
  color: #aa759f;
  font-weight: bold;
}
.highlight .k, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kv {
  color: #5292c7;
}
.highlight .s {
  color: #ea8e85;
}

.highlight .kc {
  color: #d28445;
}
.highlight .kt {
  color: #d28445;
}
.highlight .kd {
  color: #d28445;
}
.highlight .sb, .highlight .sc, .highlight .sd, .highlight .s2, .highlight .sh, .highlight .sx, .highlight .s1 {
  color: #90a959;
}
.highlight .sr {
  color: #75b5aa;
}
.highlight .si {
  color: #8f5536;
}
.highlight .se {
  color: #8f5536;
}
.highlight .nn {
  color: #51e1bd;
}

.highlight .nc {
  color: #f4bf75;
}
.highlight .no {
  color: #f4bf75;
}
.highlight .na {
  color: #6a9fb5;
}
.highlight .nf, .highlight .nb {
  color: #dcdcaa;
}
.highlight .m, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .il, .highlight .mo, .highlight .mb, .highlight .mx {
  color: #ce916a;
}
.highlight .ss {
  color: #90a959;
}

markdownを用意する

github pagesに使用しているリポジトリに、1.mdなど、拡張子がmdのファイルを用意して、それに以下のようなmarkdownを書きこみ、15分くらい待つ

---
layout: default
title: 月面植物園
---
# 月面植物園
みかぶるのホームページ

> テクノなまこ、科学の力

SNS
- [bluesky](https://bsky.app/profile/mikanixonable.bsky.social)
- [misskey](https://misskey.io/@Mikanixonable)
- [twitter](https://twitter.com/Mikanixonable)

### テイラー展開
$$
f(x) = \sum_{n=0}^{\infty} \frac{f^{(n)}(a)}{n!}(x-a)^n
$$

~~~python
from matplotlib import pyplot as plt
import cv2 as cv2
import numpy as np

img = cv2.imread("1.png")
img = cv2.resize(img,(64,64))

print(img)
r = img[:,:,2]
g = img[:,:,1]
b = img[:,:,0]

rgb = np.dstack((r, g, b))
rgb_flat = rgb.reshape((rgb.shape[0]*rgb.shape[1], 3))
def rgb_to_hex(rgb):
    return '#{:02x}{:02x}{:02x}'.format(*rgb)
cCodes = np.apply_along_axis(rgb_to_hex, 1, rgb_flat)

# print(cCodes)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection='3d')
ax.scatter(b, g, r, c=cCodes,alpha=1)
plt.show()
~~~

背景

 最初に、無テーマのgithub pagesのmarkdown機能を試した。github pagesはjekyllという静的サイトビルダーを標準装備しており、github pagesの設定を済ませたリポジトリに拡張子がmdのファイルを作りmarkdownを書きこむと15分くらい待てばhtmlに変換して表示してくれる。公式サイトによればレンダリングエンジンをGFMにすれば(_config.ymlにmarkdown: GFMと書けば)githubのreadme.mdに使えるGFMのmarkdownと同等のものが使えるらしいが、readme.mdでは表示できるtex数式をgithub pagesでは表示できないし、readme.mdのようにダークテーマにはならないし、強制的にリポジトリ名をサイト上部に表示されるので、無テーマのmarkdownビルド機能にはたくさん不満があった。
 次に、標準テーマを試してみた。github pagesはいくつかのjekyllテーマを標準で使える。(_config.ymlにtheme: Caymanと書くとCaymanになる)標準テーマではコードブロックのシンタックスハイライトに対応していて、midnightやhackerではダークテーマに対応しており見た目が少しきれいになる。しかし不満もあり、すべてのページの末尾にテーマ作者へのクレジットリンクが挿入されるようになる。作者への敬意を忘れたくはないが、自宅に等しい個人サイトで毎ページテーマ作者へのリンクと宣伝が挟まるのは心理的に実用外に思えた。見た目を変える以上のことをするものをテーマと呼んではいけないと思う。何かオプトアウト設定でクレジットを外せるようになっているかもしれないが、もうすべて自分の制御下にあるものしか使う気がなくなってしまった。
 他にgithub pages公式のサポート外のjekyllテーマも使える(_config.ymlにremote_theme: chrisrhymes/bulma-clean-themeとリポジトリ名を書くとbulma-clean-themeになる)が、もうテーマを自作する気分になっていた。

経緯と解説

上のコード例が出来上がるまでの経緯を追い、ついでにコードの意味を解説する
参考github pages入門 - Qiita
 リポジトリの_layoutsフォルダ以下にdefault.htmlを置くと、html中の {{ content }}の部分にhtml化されたmarkdownが埋め込まれて表示されるようになる。default.htmlにいろいろ入れることで望みのページを作ることができる。私の場合は、midnightテーマをもとに改造したstyle.cssを読み込んで独自テーマを適用し、scriptタグを使ってmathjaxのcdnを読込みtex数式を使えるようにし、scriptタグにjsを書きこんでcssの疑似要素を書きこむことでコードブロックをzenn風に改造した。

(1/5)default.htmlを_layouts以下に置き、改造する

 私は、cc0で公開されている[https://github.com/pages-themes/midnight:title=midnight]をもとに改造して自作テーマを作った。まず、リポジトリに行き、_layouts以下にあるdefaulr.htmlをダウンロードしてきて自分のリポジトリの_layoutsに置く。次に、使用しない行を削除する。私が削除したのは、jqueryやstylesheetやrespond.jsへのリンク行と、

までのヘッダー(リポジトリ名を挿入する)と、
から
までのタイトル・クレジットを挿入する行だった。これらを削除した私にとってのdefault.htmlの最小構成は下のようになった。

<!doctype html>
<html lang="{{ site.lang | default: "ja" }}">
  <head>
    <meta charset="utf-8">
   {% seo %}
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
 <meta name="color-scheme" content="light dark" />
  </head>
  <body>
  <div class="wrapper">
    <section>
    {{ content }}
    </section>
  </div>
  </body>
</html>

{% seo %}というのはjekyll-seo-tagと呼ばれるもので、サイトの内容を分析して自動的にメタデータのタグを生成し、google検索に引っ掛かりやすく(SEO対策)してくれる。SEOは嫌いだが、サイトのメタデータが参照可能になるのはいいことだと思うので、残した。github pagesのSEO、OGPについて参考になるページ:https://takezoe.hatenablog.com/entry/2016/07/03/104536
はスクロールバーがダークモード仕様にし、読込中の背景を黒にする。

は、あとのcssでいじるために用意してあるもので、サイトの横幅の上限を設定することでデスクトップで開いたときにサイトの要素が左端に寄りすぎないようにする

(2/4)_layouts/dafault.htmlからmd/style.cssを読込み自作テーマを適用する

dafault.htmlのhead要素以下にの行を追加する。
midnightリポジトリの_sass内にあるmidnightテーマのcss1のjekyll-theme-midnight.scssとシンタックスハイライト用のrouge-base16-dark.scssをダウンロードしてきて、片方をstyle.cssに改名して、もう片方の内容をコピーしてきて付け足す。style.cssを自分のリポジトリのmd以下に置く。
style.css内の
@import "normalize";
@import "fonts";
@import "rouge-base16-dark.scss";
の行を削除する。
するとmidnightテーマとシンタックスハイライトをローカルで適用することができる。style.cssをいじれば背景色やその他を自分好みに変更できる。

(3/5)数式を使えるようにする

default.htmlのhead以下に

  <script type="text/x-mathjax-config">MathJax.Hub.Config({tex2jax:{inlineMath:[['\$','\$'],['\\(','\\)']],processEscapes:true},CommonHTML: {matchFontHeight:false}});</script>
  <script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-MML-AM_CHTML"></script>

を足すとtex数式が使えるようになる。数式は$で囲むと使うことができる。例えば$\sqrt{2}$でルート2を表示できる。\$\sqrt{2}\$でインラインでルート2を表示できる。

(4/5)コードブロックをzenn.dev風に改造する


zenn.devのこういうファイル名が左上に出ているコードブロックが好きだったので、再現しようとした。ツイッターで再現したい旨つぶやいていると、レさん(@wartemeinnightさん)が再現して送ってくれた!

jekyllの生成するコードブロックは次のような要素で包まれている

<div class="language-python highlighter-rouge">
          <div class="highlight">
            <pre class="highlight"><code>

このclass名から言語名を取り出してzenn.dev風にコードブロックの左上に表示できないだろうかと考えた。レさんのCSSでは言語名を疑似要素beforeを使って再現してあり、jsで疑似要素は操作できないため、jsで疑似要素を書きこむことにした。レさんにもらったCSSでは、コードブロックに疑似要素を付けて言語名を表示していたが、これではoverflow: auto;で行が長いときの横スクロールを実装すると疑似要素が消えてしまうため(https://teratail.com/questions/c9nkgcddbeb0wv
CSSでoverflow-x: scrollをつけたらbefore擬似クラスがが消える)、code要素の親のpreに疑似要素をつけるようにした。style.cssとdefault.htmlの追加箇所は以下のようになる。

pre:before {
  display: block;
  /* position: absolute; */
  inset-block-start: 0em;
  inset-inline-start: 0;
  /* content: attr(class); */
  padding-block: .5em;
  padding-inline: 1em;
  inline-size: fit-content;
  background-color: #323e52;
  color: white;
  border-radius: .5em .5em 0 0;
  /* content: "python"; */
  
}

code {

  display: block;
  position: relative;
  padding: 1em 1em;
  background-color: #1a2638;
  border-radius: 0 .5em .5em .5em;
  font-family: Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, monospace;
  color:#efefef;
  font-size:13px;
  overflow: auto;
  /* overflow-y: hidden; */
}

<script>
  let sheets = document.styleSheets
  let sheet = sheets[sheets.length - 1];
  let codeElements = document.querySelectorAll("code");
  for (let i = 0; i < codeElements.length; i++) {
    let codeElement = codeElements[i]
    codeElement.parentElement.classList.add("pre" + i)

    // <div class="language-shell highlighter-rouge">
    // <div class="highlight">
    // <pre class="highlight">
    // <code>
    // から「python」を抽出
    clsName = codeElement
    .parentElement
      .parentElement
      .parentElement
      .className
    langName = codeElement
      .parentElement
      .parentElement
      .parentElement
      .className

      .match(/language-(\S+)\s/)[1]
    sheet.insertRule(
      `.pre${i}:before { content: "${langName}";}`,
      sheet.cssRules.length);
  }
</script>

(5/5)mermaidを使えるようにする

mermaidはツリー図を書いたりできるマークダウン
参考GitHub Pages で Mermaid を使う - みちのぶのねぐら
default.html中のbodyタグをbody onload="initializeMermaid()"にする
default.html中(最後とか)に以下の記述を足す

<script src="https://unpkg.com/mermaid@10.5.1/dist/mermaid.min.js"></script>
<script>
  function initializeMermaid() {
   mermaid.initialize({
  "theme": "base",
  "fontFamily": "TimesNewRoman Times Serif",
  "themeVariables": {
    "lineColor": "aliceblue",
    "primaryColor": "#323e52",
    "secondaryColor": "#397",
    "tertiaryColor": "#1a2638",
    "primaryTextColor": "white",
    "secondaryTextColor": "white",
    "tertiaryTextColor": "white",
    "primaryBorderColor": "aliceblue",
    "secondaryBorderColor": "aliceblue",
    "tertiaryBorderColor": "slategray"
  }
   });
   window.mermaid.init(
      undefined,
      document.querySelectorAll('.language-mermaid'),
   );
}
</script>