伊地知ニジカ放送局だぬ゛ん゛. https://www.youtube.com/@deerjika
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test.py 13 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. from __future__ import annotations
  2. import math
  3. import sys
  4. from datetime import datetime, timedelta
  5. from enum import Enum, auto
  6. from typing import Callable, TypedDict
  7. import cv2
  8. import pygame
  9. from cv2 import VideoCapture
  10. from ephem import Moon, Observer, Sun # type: ignore
  11. from pygame import Rect, Surface
  12. from pygame.font import Font
  13. from pygame.mixer import Sound
  14. from pygame.time import Clock
  15. from common_module import CommonModule
  16. pygame.init ()
  17. FPS = 30
  18. SYSTEM_FONT = pygame.font.SysFont ('notosanscjkjp', 24, bold = True)
  19. USER_FONT = pygame.font.SysFont ('notosanscjkjp', 32, italic = True)
  20. DEERJIKA_FONT = pygame.font.SysFont ('07nikumarufont', 50)
  21. def main (
  22. ) -> None:
  23. game = Game ()
  24. bg = Bg (game)
  25. KitaSun (game)
  26. deerjika = Deerjika (game, DeerjikaPattern.RELAXED,
  27. x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  28. balloon = Balloon (game)
  29. CurrentTime (game, SYSTEM_FONT)
  30. try:
  31. Sound ('assets/bgm.mp3').play (loops = -1)
  32. except Exception:
  33. pass
  34. while True:
  35. for event in pygame.event.get ():
  36. if event.type == pygame.QUIT:
  37. pygame.quit ()
  38. sys.exit ()
  39. game.redraw ()
  40. class DeerjikaPattern (Enum):
  41. """
  42. ニジカの状態
  43. Members:
  44. NORMAL: 通常
  45. RELAXED: 足パタパタ
  46. SLEEPING: 寝ニジカ
  47. """
  48. NORMAL = auto ()
  49. RELAXED = auto ()
  50. SLEEPING = auto ()
  51. class Direction (Enum):
  52. """
  53. クリーチャの向き
  54. Members:
  55. LEFT: 左向き
  56. RIGHT: 右向き
  57. """
  58. LEFT = auto ()
  59. RIGHT = auto ()
  60. class Game:
  61. """
  62. ゲーム・クラス
  63. Attributes:
  64. clock (Clock): Clock オブゼクト
  65. frame (int): フレーム・カウンタ
  66. redrawers (list[Redrawer]): 再描画するクラスのリスト
  67. screen (Surface): 基底スクリーン
  68. """
  69. clock: Clock
  70. frame: int
  71. redrawers: list[Redrawer]
  72. screen: Surface
  73. sky: Sky
  74. def __init__ (
  75. self,
  76. ):
  77. self.screen = pygame.display.set_mode ((CWindow.WIDTH, CWindow.HEIGHT))
  78. self.clock = Clock ()
  79. self.frame = 0
  80. self.redrawers = []
  81. self._create_sky ()
  82. def redraw (
  83. self,
  84. ) -> None:
  85. for redrawer in sorted (self.redrawers, key = lambda x: x['layer']):
  86. if redrawer['obj'].enabled:
  87. redrawer['obj'].redraw ()
  88. pygame.display.update ()
  89. self.clock.tick (FPS)
  90. def _create_sky (
  91. self,
  92. ) -> None:
  93. self.sky = Sky ()
  94. self.sky.observer = Observer ()
  95. self.sky.observer.lat = '35'
  96. self.sky.observer.lon = '139'
  97. class GameObject:
  98. """
  99. 各ゲーム・オブゼクトの基底クラス
  100. Attributes:
  101. arg (float): 回転角度 (rad)
  102. ax (float): X 軸に対する加速度 (px/frame^2)
  103. ay (float): y 軸に対する加速度 (px/frame^2)
  104. enabled (bool): オブゼクトの表示可否
  105. frame (int): フレーム・カウンタ
  106. game (Game): ゲーム基盤
  107. height (int): 高さ (px)
  108. vx (float): x 軸に対する速度 (px/frame)
  109. vy (float): y 軸に対する速度 (px/frame)
  110. width (int): 幅 (px)
  111. x (float): X 座標 (px)
  112. y (float): Y 座標 (px)
  113. """
  114. arg: float = 0
  115. ax: float = 0
  116. ay: float = 0
  117. enabled: bool = True
  118. frame: int
  119. game: Game
  120. height: int
  121. vx: float = 0
  122. vy: float = 0
  123. width: int
  124. x: float
  125. y: float
  126. def __init__ (
  127. self,
  128. game: Game,
  129. layer: int | None = None,
  130. enabled: bool = True,
  131. x: float = 0,
  132. y: float = 0,
  133. ):
  134. self.game = game
  135. self.enabled = enabled
  136. self.frame = 0
  137. if layer is None:
  138. if self.game.redrawers:
  139. layer = max (r['layer'] for r in self.game.redrawers) + 10
  140. else:
  141. layer = 0
  142. self.game.redrawers.append ({ 'layer': layer, 'obj': self })
  143. self.x = x
  144. self.y = y
  145. def redraw (
  146. self,
  147. ) -> None:
  148. self.x += self.vx
  149. self.y += self.vy
  150. self.vx += self.ax
  151. self.vy += self.ay
  152. self.frame += 1
  153. class Bg (GameObject):
  154. """
  155. 背景
  156. Attributes:
  157. surface (Surface): 背景 Surface
  158. """
  159. surface: Surface
  160. def __init__ (
  161. self,
  162. game: Game,
  163. ):
  164. super ().__init__ (game)
  165. self.surface = pygame.image.load ('assets/bg.jpg')
  166. self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
  167. def redraw (
  168. self,
  169. ) -> None:
  170. self.game.screen.blit (self.surface, (self.x, self.y))
  171. super ().redraw ()
  172. class Deerjika (GameObject):
  173. """
  174. 伊地知ニジカ
  175. Attributes:
  176. height (int): 高さ (px)
  177. scale (float): 拡大率
  178. surfaces (list[Surface]): ニジカの各フレームを Surface にしたリスト
  179. width (int): 幅 (px)
  180. """
  181. height: int
  182. scale: float = .8
  183. surfaces: list[Surface]
  184. width: int
  185. def __init__ (
  186. self,
  187. game: Game,
  188. pattern: DeerjikaPattern = DeerjikaPattern.NORMAL,
  189. direction: Direction = Direction.LEFT,
  190. layer: int | None = None,
  191. x: float = 0,
  192. y: float = 0,
  193. ):
  194. super ().__init__ (game, layer, x = x, y = y)
  195. self.pattern = pattern
  196. self.direction = direction
  197. match pattern:
  198. case DeerjikaPattern.NORMAL:
  199. ...
  200. case DeerjikaPattern.RELAXED:
  201. match direction:
  202. case Direction.LEFT:
  203. self.width = 1280
  204. self.height = 720
  205. surface = pygame.image.load ('assets/deerjika_relax_left.png')
  206. self.surfaces = []
  207. for x in range (0, surface.get_width (), self.width):
  208. self.surfaces.append (
  209. surface.subsurface (x, 0, self.width, self.height))
  210. case Direction.RIGHT:
  211. ...
  212. def redraw (
  213. self,
  214. ) -> None:
  215. surface = pygame.transform.scale (self.surfaces[self.frame % len (self.surfaces)],
  216. (self.width * self.scale, self.height * self.scale))
  217. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  218. super ().redraw ()
  219. class CurrentTime (GameObject):
  220. """
  221. 現在日時表示
  222. Attributes:
  223. font (Font): フォント
  224. """
  225. font: Font
  226. def __init__ (
  227. self,
  228. game: Game,
  229. font: Font,
  230. ):
  231. super ().__init__ (game)
  232. self.font = font
  233. def redraw (
  234. self,
  235. ) -> None:
  236. for i in range (4):
  237. self.game.screen.blit (
  238. self.font.render (str (datetime.now ()), True, (0, 0, 0)),
  239. (i % 2, i // 2 * 2))
  240. super ().redraw ()
  241. class Balloon (GameObject):
  242. """
  243. 吹出し
  244. Attributes:
  245. answer (str): 回答テキスト
  246. image_url (str, None): 画像 URL
  247. length (int): 表示する時間 (frame)
  248. query (str): 質問テキスト
  249. surface (Surface): 吹出し Surface
  250. x_flip (bool): 左右反転フラグ
  251. y_flip (bool): 上下反転フラグ
  252. """
  253. answer: str = ''
  254. image_url: str | None = None
  255. length: int = 300
  256. query: str = ''
  257. surface: Surface
  258. x_flip: bool = False
  259. y_flip: bool = False
  260. def __init__ (
  261. self,
  262. game: Game,
  263. x_flip: bool = False,
  264. y_flip: bool = False,
  265. ):
  266. super ().__init__ (game, enabled = False)
  267. self.x_flip = x_flip
  268. self.y_flip = y_flip
  269. self.surface = pygame.transform.scale (pygame.image.load ('assets/balloon.png'),
  270. (CWindow.WIDTH, CWindow.HEIGHT / 2))
  271. self.surface = pygame.transform.flip (self.surface, self.x_flip, self.y_flip)
  272. def redraw (
  273. self,
  274. ) -> None:
  275. if self.frame >= self.length:
  276. self.enabled = False
  277. return
  278. query = self.query
  279. if CommonModule.len_by_full (query) > 21:
  280. query = CommonModule.mid_by_full (query, 0, 19.5) + '...'
  281. answer = Surface ((800, ((CommonModule.len_by_full (self.answer) - 1) // 16 + 1) * 50),
  282. pygame.SRCALPHA)
  283. for i in range (int (CommonModule.len_by_full (self.answer) - 1) // 16 + 1):
  284. answer.blit (DEERJIKA_FONT.render (
  285. CommonModule.mid_by_full (self.answer, 16 * i, 16), True, (192, 0, 0)),
  286. (0, 50 * i))
  287. surface = self.surface.copy ()
  288. surface.blit (USER_FONT.render ('>' + query, True, (0, 0, 0)), (120, 70))
  289. y: int
  290. if self.frame < 30:
  291. y = 0
  292. elif self.frame >= self.length - 90:
  293. y = answer.get_height () - 100
  294. else:
  295. y = int ((answer.get_height () - 100) * (self.frame - 30) / (self.length - 120))
  296. surface.blit (answer, (100, 150), Rect (0, y, 800, 100))
  297. self.game.screen.blit (surface, (0, 0))
  298. super ().redraw ()
  299. def talk (
  300. self,
  301. query: str,
  302. answer: str,
  303. image_url: str | None = None,
  304. length: int = 300,
  305. ) -> None:
  306. self.query = query
  307. self.answer = answer
  308. self.image_url = image_url
  309. self.length = length
  310. self.frame = 0
  311. self.enabled = True
  312. class KitaSun (GameObject):
  313. """
  314. き太く陽
  315. Attributes:
  316. sun (Sun): ephem の太陽オブゼクト
  317. surface (Surface): き太く陽 Surface
  318. """
  319. alt: float
  320. az: float
  321. sun: Sun
  322. surface: Surface
  323. def __init__ (
  324. self,
  325. game: Game,
  326. ):
  327. super ().__init__ (game)
  328. self.surface = pygame.transform.scale (pygame.image.load ('assets/sun.png'), (200, 200))
  329. self.sun = Sun ()
  330. def redraw (
  331. self,
  332. ) -> None:
  333. surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
  334. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  335. super ().redraw ()
  336. self.game.sky.observer.date = datetime.now () - timedelta (hours = 9)
  337. self.sun.compute (self.game.sky.observer)
  338. self.alt = self.sun.alt
  339. self.az = self.sun.az
  340. if abs (self.new_arg - self.arg) > math.radians (15):
  341. self.arg = self.new_arg
  342. self.x = self.new_x
  343. self.y = self.new_y
  344. @property
  345. def new_x (
  346. self,
  347. ) -> float:
  348. return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
  349. @property
  350. def new_y (
  351. self,
  352. ) -> float:
  353. return ((CWindow.HEIGHT / 2)
  354. - ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
  355. / math.sin (math.radians (60))))
  356. @property
  357. def new_arg (
  358. self,
  359. ) -> float:
  360. return math.atan2 (self.new_y - self.y, self.new_x - self.x)
  361. class Jojoko (GameObject):
  362. """
  363. 大月ヨヨコ
  364. Attributes:
  365. phase (float): 月齢
  366. """
  367. phase: float
  368. def __init__ (
  369. self,
  370. ):
  371. ...
  372. def redraw (
  373. self,
  374. ) -> None:
  375. ...
  376. class Sky:
  377. """
  378. 天体に関する情報を保持するクラス
  379. Attributes:
  380. observer (Observer): 観測値
  381. """
  382. observer: Observer
  383. class CWindow:
  384. """
  385. ウィンドゥに関する定数クラス
  386. Attributes:
  387. WIDTH (int): ウィンドゥ幅
  388. HEIGHT (int): ウィンドゥ高さ
  389. """
  390. WIDTH = 1024
  391. HEIGHT = 768
  392. class Redrawer (TypedDict):
  393. """
  394. 再描画処理を行ふゲーム・オブゼクトとその優先順位のペア
  395. Attributes:
  396. layer (int): レイア
  397. obj (GameObject): ゲーム・オブゼクト
  398. """
  399. layer: int
  400. obj: GameObject
  401. def get_surfaces_from_video (
  402. video_path: str,
  403. ) -> list[Surface]:
  404. cap = VideoCapture (video_path)
  405. if not cap.isOpened ():
  406. return []
  407. fps = cap.get (cv2.CAP_PROP_FPS)
  408. surfaces: list[Surface] = []
  409. while cap.isOpened ():
  410. (ret, frame) = cap.read ()
  411. if not ret:
  412. break
  413. frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB)
  414. frame_surface = pygame.surfarray.make_surface (frame)
  415. frame_surface = pygame.transform.rotate (frame_surface, -90)
  416. surfaces.append (pygame.transform.flip (frame_surface, True, False))
  417. cap.release ()
  418. return surfaces
  419. if __name__ == '__main__':
  420. main ()