タブをtransparentタグに変換してもいいですが……それでもまだ見にくいと思う……
いいアイデアがあったらコメントください。
タブの補完はそれほど難しいことではないと思いますが……
import math
import sys
from datetime import datetime, timedelta
from decimal import ROUND_HALF_EVEN, ROUND_HALF_UP, Decimal
from random import randrange
from string import Template
from typing import Optional
class Blink:
"""
オプションを選択して実行すると点滅のタグが自動生成される。
オプションの詳細は公公式ドキュメントを参照して下さい。
点滅させる文字はcontentという名称で指定してください。
すべてのオプションに対応しているわけではありません。
動作例
>>> Blink(content="ゴリラ")
ゴリラ
>>> Blink(content="ゴリラ", inview=True)
ゴリラ
>>> Blink(content="ゴリラ", start_ratio=10, end_ratio=50, loop_count=1,total_time=1, inview=True, forwards=True)
ゴリラ
>>> Blink(content="必要なオプションだけ指定して下さい。", loop_count=1,total_time=1, inview=True, forwards=True)
必要なオプションだけ指定して下さい。
<TODO> speed が 1より大きいときの動作のテスト。
<TODO> オプションを一個も指定しないとき<<blink>>が理想だが、実際には<<blink:>>で始まってるので':'を取り除く。
<TODO> inview, forwards 以外のオプションへの対応
"""
# ハーメルンに貼ると内容が評価されてバグるので、bl_inkにした。
template = Template("《bl ink:${options}》${content}《/blink》")
"""
def __init__(
self,
content: str | int = "hello",
start_ratio: Optional[int] = None,
end_ratio: Optional[int] = None,
delay_time: Optional[float | int] = None,
loop_count: Optional[float | int] = None,
total_time: Optional[float | int] = None,
inview: Optional[bool] = False,
forwards: Optional[bool] = False,
ext: Optional[str] = None,
):
self.content = content
self.range_option = (
f"v{int(start_ratio)}-{int(end_ratio)}"
if (start_ratio or start_ratio == 0)
and start_ratio >= 0
and 100 >= end_ratio >= start_ratio
else None
)
self.delay_time = f"d{round(delay_time)}" if delay_time else None
self.loop_count = f"l{round(loop_count)}" if loop_count else None
self.time_option = f"t{round(total_time)}" if total_time else None
self.inview_option = "inview" if inview else None
self.forwards_option = "forwards" if forwards else None
self.ext = ext
def _merge_options(self):
base_options = [
self.range_option,
self.delay_time,
self.loop_count,
self.time_option,
self.inview_option,
self.forwards_option,
self.ext,
]
return ",".join(list(filter(lambda item: item is not None, base_options)))
def _substitude_template(self):
return self.template.substitute(
options=self._merge_options(), content=self.content
)
def __str__(self):
return self._substitude_template()
@classmethod
def supply(cls, args):
return cls(**args)._substitude_template()
class FadeInSpeech:
"""FadeInSpeech.
長い文章をフェードインさせるとき、上記のBlink機能を使って一文字づつ表示開始時間を計算させるのは人間技ではないのでまとめて計算する。
content (文字列+一時停止), speed (文字ごとの間隔), delay_time, is_inview, is_forwards(公式ドキュメント参照)を指定できる。
使用例
>>> FadeInSpeech(contents="ゴリラ")
ゴリラ
>>> FadeInSpeech(contents="hello/1.1/world",speed=0.1)
helloworld
一時停止について
一時停止は/{秒数}/を文字列に入れると動作する。
例えば"前/10/後"を文字列の中に入れると前を表示した後、10秒停止してから後が表示される。
/10/をコンテンツの中に入れると、10秒、そこで停止する。
なおspeed分の停止時間と一時停止の時間は加算されて処理される。
例えば『content='前/1/後', speed=1』の場合、「前」が表示されてから二秒後に「後」は表示される。
/1.11/などの少数も入力可能。
delay_timeについて
『content='前/1/後', speed=1, delay=2』と『content='/2/前/1/後', speed=1』は同義。
小数について
最終出力時にはすべてミリ秒まで四捨五入されるが、計算ロジックの中ではそれより先も保持しているので他の端数と合算しての繰り上がりなどが有効。
例えば『content='前/0.001/中/0.001/後',speed=0.001, delay=0.001』のような指定の場合、
「前」と「中」(内部的には0.003秒後)は0.00秒後に同時に表示されるが、「後」(内部的には0.005秒後)は0.01秒後に表示される。
contentsについて
list型を入れることもできる。
>>> FadeInSpeech(contents=["ゴリラ", "ラッパ"])
ゴリララッパ
ただし、listを入れた場合は、一時停止は正常に動作しない。
>>>FadeInSpeech(contents=["ゴリラ", "/10/", "ラッパ"])
ゴリラ/10/ラッパ
"""
def __init__(
self,
contents: str | int | list[str] = "hello worlds",
speed: int | float = 0.10,
delay_time: int | float = 0,
is_inview: bool = True,
is_forwards: bool = True,
ext: Optional[str] = None,
):
self.showing_time = len(contents) * speed
self.speed = speed
self.delay_time = delay_time
self.is_inview = is_inview
self.is_forwards = is_forwards
self.contents = contents
self.sleep_flag = False
self.ext = ext
def make_simple_blinks(self):
"""
簡単なロジックでブリンク郡を作る。
文字列が短いと正常に動作しないし、文字列が長すぎても正常に動作しない。
雑に実装するとこうなる。
抱えてる問題(解決方法はsimple_blinksを参照)
t(整数(>=1)、文字の表示にかかる時間の合計) を与えられると、tの時間を100等分にした上で一文字辺りの表示時間を計算し、各文字に開始時間を割り振る。
これでは、最小のtが1であるため細かな制御が効かない。例えば2文字だと、一文字の表示にかかる時間が0.5秒になりかなり遅い。体感、10文字以下でこの問題が顕著。
100等分なので文字が(t * 100)文字以上だと同時に表示させる必要のある文字が出てくる。(t * 100)文字以下であったとしても、文字が多めだと表現の幅が狭くなる。
"""
blinks = []
for index, content in enumerate(self.contents):
start_ratio = index * 100 // len(self.contents)
end_ratio = 100
total_time = self.showing_time
args = {
"content": content,
"start_ratio": start_ratio,
"end_ratio": end_ratio,
"delay_time": self.delay_time,
"loop_count": 1,
"total_time": total_time,
"inview": self.is_inview,
"forwards": self.is_forwards,
}
blinks.append(Blink.supply(args))
# print(blinks)
return blinks
def make_blinks(self):
"""
simple_blinksの問題を解消させたもの。
simple_blinksとの違い。
文字数が100文字を越えても表現の幅が狭まらないように、個別の文字に対して、全体の中の特定の文字、ではなく、別個に表示タイミングを計算している。
「100秒で完結する100個ロジックの10個目のブリンク」を「10個目のブリンクは10.00秒後に動作を開始するブリンク」に変換している。
そのため最短指定の秒数が一秒を切っている。(やろうと思ったら素数を活用したさらに細かな分割とかできるが、やりたくはない)
つまり「1秒で完結する2個のロジック(表示時間0.5秒)」しかできなかったのを「0.2秒毎に2文字をブリンクさせる」ができるようになっている。
あと、文の途中のスリープを有効にした。
"""
blinks = []
blink_work_time = math.ceil(self.speed)
sleep_count = []
sleep_delay: float = 0.00
is_sleeping = False
# contentsがlist[str]のときは、スリープ関係の動作が正常に動作しない。
for index, content in enumerate(self.contents):
# スリープ関係の処理に入るか
if content == "/" or is_sleeping:
# 開始、終了系か
if content == "/":
if not is_sleeping:
is_sleeping = True
else:
is_sleeping = False
sleep_delay += float("".join(sleep_count))
sleep_count = []
# スリープ時間の読み取り
elif is_sleeping:
try:
if content != ".":
float(content)
except ValueError:
print("多分、スリープのタグを閉じるのを忘れています")
sys.exit()
sleep_count.append(content)
continue
start_time = index * self.speed + sleep_delay
start_f, start_i = math.modf(start_time)
# <TODO> ここら辺りの処理、スピードが一秒より長いとバグりそう。
if start_f != 1:
start_ratio = round(start_f * 100)
blink_start_time = start_i
else:
start_ratio = 50
blink_work_time = start_i + 1
args = {
"content": content,
"start_ratio": start_ratio,
"end_ratio": 100,
"delay_time": self.delay_time + blink_start_time,
"loop_count": 1,
"total_time": blink_work_time,
"inview": self.is_inview,
"forwards": self.is_forwards,
"ext": self.ext,
}
blinks.append(Blink.supply(args))
return blinks
def __str__(self):
return "".join(self.make_blinks())