Last active
January 3, 2026 15:46
-
-
Save yelizariev/6a738322b614741c4e7f5d4c5fa88422 to your computer and use it in GitHub Desktop.
wartime.mydream42.com
| Title | Author | PORTAL | Sources | PowerPointMan | Slides | Doc | Next | Icon | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Ласкаво просимо до Києва! Іпотека для молодих сімей від 7,9% 💙💛 |
Ivan Yelizariev |
|
<showup> <target> <duration> <source> <filename> cut<cut> v<velocity> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html><!--head | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀--> <html lang="en"> <!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢲⣾⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⠆ --> <head> <!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⡿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⠏ --> <meta charset="UTF-8" /> <!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢘⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⡿⣿⠃ --> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠼⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⢳⢰⡇ --> <title>{{ markdown.yaml.Title }}</title> <!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣢⢹⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⣏⠲⣹⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣄⣤⡴⠖⠃ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠜⢧⢣⠎⠷⣄⠀⠀⠀⠀⠀⠀⠀⢧⢣⡙⢧⡀⠀⠀⠀⠀⠀⠀⠀⣠⣾⠛⣭⠶⠛⠉ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡽⢧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⣎⢣⠺⡽⣆⠀⠀⠀⠀⠀⢸⠇⡜⡩⣷⠀⠀⠀⠀⠀⠀⣴⢇⣺⡏⠁ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⡘⢳⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡷⡑⢦⣿⠀⠀⠀⠀⡴⡫⠜⡰⣧⠏⠀⠀⠀⠀⠀⢸⢋⠦⣽⠁ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠈⣿⣶⢨⡍⠛⠛⢻⣦⡄⠀⠀⠀⠀⣿⢣⠑⣦⠘⣦⠀⠀⣼⢱⠉⡎⣵⡏⠀⠀⠀⠀⠀⢠⣾⠉⡖⣿ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠶⢭⣜⣢⢃⠏⠻⣄⠀⠀⠀⣿⠎⡑⢆⠣⢍⠟⡭⠓⡌⠳⡘⢤⠳⣄⣀⢠⡴⢾⠛⠭⣘⣴⡏ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠱⣧⢊⡵⢊⠷⡴⣺⠹⢌⣙⣬⠵⠮⠖⠛⠛⠚⠓⠧⠮⣖⣩⢩⣉⢆⠧⣙⣲⡾⠋⠀⠀⠀⠀⠀⠀⠀⢀⣤⣤⣤⣤⣄⣀ --> <link rel="icon" type="image/png" href="{{ markdown.yaml.Icon | default: 'https://jesus.lamourism.com/favicon.ico' }}"/> <!-- | |
| ⠀⠀⠀⢢⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢳⣌⢲⠩⡜⢡⢆⡷⠋⠁⠀⠀⢀⣀⡤⡤⠤⣄⣀⠀⠀⠉⠙⠲⣎⡲⢱⣏⠀⠀⠀⠀⠀⠀⠀⣰⣞⢫⣳⡬⠖⠒⠚⠛⠛⠒ --> <meta property="og:image" content="https://gist.github.com/user-attachments/assets/c64786b8-a32f-4dde-ab08-26cd9fad963a"> <!-- | |
| ⠀⠀⠀⠀⠙⣿⣦⡀⠀⠀⠀⠀⣀⣠⣀⣄⣀⠀⠀⠀⠀⣿⠆⣇⠣⣥⠞⠀⢀⣠⢴⠺⢹⣈⠒⡍⡚⣌⡺⢩⠳⠦⣄⠀⠈⠱⢧⢊⠷⣠⣤⢴⠤⡤⢾⡑⢬⣶⠏ --> <meta property="og:title" content="Whatever you think it is, it's not" /> <!-- | |
| ⠀⠀⠀⠀⠀⠘⢶⣭⣓⠶⠶⠿⠿⣋⠼⡑⣊⠗⣦⣤⠞⢣⠚⣴⠋⠁⣠⡔⡏⢎⢆⡣⠗⠒⠛⠚⠋⠉⠙⠧⣏⡜⣈⠳⡄⠀⠈⢳⡘⠴⡠⢎⡒⢥⢒⣬⠟⠃ --> <meta property="og:image:type" content="image/png"> <!-- ☀️ --> </head> <!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠉⠓⠿⠬⠧⠶⠭⠶⣗⢍⡚⠴⣨⠙⡆⣿⠃⠀⣰⢋⠴⣩⠞⠁⠀⣠⣤⡤⠴⡴⣤⣄⡀⠈⠙⢆⡝⢜⢦⠀⠀⢻⡢⡑⣮⠼⠖⠛⠁ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⢽⣣⠜⡱⠰⡿⠀⠀⡏⡎⢼⡏⠀⢀⡾⢃⣶⠾⠷⢦⣑⠦⡙⣷⡀⠈⣏⡜⡸⣧⠀⠀⢷⣹⡇ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣇⠱⢳⡇⠀⢸⠧⣙⢼⡇⠀⢸⢳⣹⠃⠀⢀⠀⠈⢳⡜⣸⡇⠀⢸⡜⡡⢻⡄⠀⢸⢫⢓⡦⣤⡀⠀⠀⠀⢀⣀⣀⣤⣀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⢤⢤⣤⡞⢣⢍⢻⡇⠀⢸⡆⢡⢺⡇⠀⢸⡇⡜⢦⣤⣾⡇⠀⠀⣟⢼⡇⠀⢸⡧⡑⣻⡇⠀⣸⢃⠎⡴⢡⢛⠳⡶⡿⢟⡩⢩⣱⣎⡻⣦⣄ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡞⢥⠱⣈⠖⣢⠜⡥⢊⢼⡇⠀⠈⣇⢣⠎⣧⠀⠀⠻⣞⣰⣍⡿⠏⠀⢠⣟⣾⡇⠀⢸⠇⡥⣿⠀⠀⡏⣎⣼⣐⣣⠎⡱⣑⣴⡺⠞⠏⠉⠈⠉⠳⢭⣦⡀ | |
| ⠐⠒⠲⢴⣦⣤⣄⣀⣠⣴⠛⡥⢋⣶⠕⠷⢮⣴⣭⣴⡋⠼⣷⡀⠀⠙⣧⡙⢸⢧⣀⣀⠀⠀⠀⠀⣠⡴⠯⣱⡞⠀⢀⡾⣱⣸⠃⠀⣸⣙⣼⠁⠀⠈⠙⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠒ | |
| ⠀⠀⠀⠀⠈⠙⠿⠯⣭⣦⠽⠞⠋⠀⠀⠀⠀⠀⠀⠈⢿⡖⢤⠻⣦⡀⠈⠻⣆⠦⣩⢉⡍⢫⡝⣩⠱⣘⡵⠋⠀⢠⡞⡱⣱⠏⠀⢀⡷⠌⣷⡀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⡾⡘⢤⠓⡬⡕⢤⡀⠈⠙⠒⠧⠮⠥⠼⠴⠛⠉⠀⣠⠴⣫⠰⣣⠏⠀⢀⣾⡱⣉⠼⡹⣆ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡞⢫⠱⢨⡑⢎⡑⢦⠙⣌⠻⠶⣤⣄⣀⣠⣄⣀⣠⡤⠶⡛⢭⠚⣀⡷⠋⠀⣠⡿⠿⠶⣥⢢⠑⣌⠻⡖⠶⢶⢤⣀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡟⡌⢣⣓⡦⠾⠞⠋⠙⢷⣌⢣⠓⡔⢢⠆⡱⢘⣦⣽⣴⣥⢷⠼⠛⠉⠀⣠⢾⣯⠀⠀⠀⠈⠳⣽⣢⢵⣼⣑⣮⣘⣭⣷⡄ --> <body> <!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡞⢢⡙⡼⠉⠀⠀⠀⠀⠀⢀⡿⣄⢋⡔⢃⣎⣥⠿⣥⣀⠀⠀⠀⠀⠀⠀⡞⢭⠡⢾⠀⠀⠀⠀⠀⠀⠉⠉⠀⠀⠈⠉⠙⣾⣻⡆ --> <audio id="music" autoplay loop crossorigin="anonymous" style="display:none"> <!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⡞⣩⢣⡾⠃⠀⠀⠀⠀⠀⢠⡟⢣⠜⣢⣼⠟⠉⠘⡧⢡⢋⢻⣆⠀⠀⠀⠀⣿⣄⠳⡸⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠈⢿⣯⡄ --> <source src="https://moses.lamourism.com/radio/mishary-rashid-alafasy-130-muslimcentral.com.mp3" type="audio/mpeg"> <!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣿⡿⣟⢧⣛⡤⠟⠀⠀⠀⠀⠀⠀⠀⣿⢸⢃⢼⡼⠃⠀⠀⢠⡇⠇⡼⣸⡟⠀⠀⠀⠀⢻⣿⡄⢣⢛⢧⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣇ --> </audio> <!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⡡⢎⣾⠀⠀⠀⠀⢸⣏⠲⡹⣿⠁⠀⠀⠀⠀⠀⠑⠾⣧⡈⢦⠹⣳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠂ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣇⠎⣾⠄⠀⠀⠀⠠⠻⣖⠡⠻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠉⠓⣧⣘⣯ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣏⣾⡟⠀⠀⠀⠀⠀⠀⠹⣎⡕⢺⡧⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣟⣿⠄ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠶⣿⡮⠊⠀⠀⠀⠀⠀⠀⠀⠀⠘⣧⣻⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⡿ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡾⠿⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡼⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⡇ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠧ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⡿⠁ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⠋ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣞⢻⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠑⠒⠂⢏⠵⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡔⠁⠀⠤⠀⠀⠈⡏⠡⠵⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡴⠋⠀⠒⠒⠋⠉⠉⠉⠸⡐⠛⠋⠳⡀⠀⠀⠀⠀⢀⣔⣻⣦⡀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠊⠈⠀⠀⠀⠁⠀⢀⣀⣀⣀⣣⠡⠶⠤⠬⠦⡀⢀⡴⠣⣤⣉⣫⢻⣆⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⣠⣾⣦⡀⠀⠀⠀⣠⠊⠀⠀⠀⠀⠀⠀⠀⠐⢀⣀⠀⠀⠈⡆⠒⠒⠒⠒⠘⢿⡒⠒⣤⣄⣈⡖⣤⣑⣄⠀⠀⠀ | |
| ⠀⢀⣾⣿⡽⠿⣿⣦⣀⠞⠁⡀⠀⠀⢀⠈⠉⠉⠉⠉⠉⠉⠉⠁⠀⠸⡉⠩⠭⢥⠀⠀⠙⢶⠮⢤⣤⠸⡬⢭⡞⠳⡄⠀ | |
| ⠐⠛⠚⠓⠒⠓⠓⠓⠓⠒⠒⠒⠒⠒⠒⠒⠛⠛⠛⠛⠛⠛⠛⠛⠒⠒⠓⠛⠛⠛⠛⠛⠛⠒⠓⠒⠒⠒⠓⠒⠛⠛⠛⠂ | |
| --><script type="x-shader/x-fragment">#version 300 es | |
| /********* | |
| * Author: Matthias Hurrle (@atzedent) | |
| * Source: https://codepen.io/atzedent/pen/NWVYOMG | |
| */ | |
| precision highp float; | |
| out vec4 O; | |
| uniform float time; | |
| uniform vec2 resolution; | |
| #define FC gl_FragCoord.xy | |
| #define R resolution | |
| #define T time | |
| #define hue(a) (.6+.6*cos(6.3*(a)+vec3(0,83,21))) | |
| float rnd(float a) { | |
| vec2 p=fract(a*vec2(12.9898,78.233)); p+=dot(p,p*345.); | |
| return fract(p.x*p.y); | |
| } | |
| vec3 pattern(vec2 uv) { | |
| vec3 col=vec3(0); | |
| for (float i=.0; i++<20.;) { | |
| float a=rnd(i); | |
| vec2 n=vec2(a,fract(a*34.56)), p=sin(n*(T+7.)+T*.5); | |
| float d=dot(uv-p,uv-p); | |
| col+=.00125/d*hue(dot(uv,uv)+i*.125+T); | |
| } | |
| return col; | |
| } | |
| void main(void) { | |
| vec2 uv=(FC-.5*R)/min(R.x,R.y); | |
| vec3 col=vec3(0); | |
| float s=2.4, | |
| a=atan(uv.x,uv.y), | |
| b=length(uv); | |
| uv=vec2(a*5./6.28318,.05/tan(b)+T); | |
| uv=fract(uv)-.5; | |
| col+=pattern(uv*s); | |
| O=vec4(col,1); | |
| } | |
| </script> | |
| <script> | |
| window.onload = init | |
| function init() { | |
| let renderer, canvas | |
| const dpr = Math.max(1, .5*devicePixelRatio) | |
| const resize = () => { | |
| const { innerWidth: width, innerHeight: height } = window | |
| canvas.width = width * dpr | |
| canvas.height = height * dpr | |
| if (renderer) { | |
| renderer.updateScale(dpr) | |
| } | |
| } | |
| const source = document.querySelector("script[type='x-shader/x-fragment']").textContent | |
| canvas = document.createElement("canvas") | |
| //document.body.innerHTML = "" | |
| document.body.appendChild(canvas) | |
| document.body.style = "margin:0;touch-action:none;overflow:hidden" | |
| canvas.style.width = "100%" | |
| canvas.style.height = "auto" | |
| canvas.style.userSelect = "none" | |
| renderer = new Renderer(canvas, dpr) | |
| renderer.setup() | |
| renderer.init() | |
| resize() | |
| if (renderer.test(source) === null) { | |
| renderer.updateShader(source) | |
| } | |
| window.onresize = resize | |
| const loop = (now) => { | |
| renderer.render(now) | |
| requestAnimationFrame(loop) | |
| } | |
| loop(0); | |
| } | |
| class Renderer { | |
| #vertexSrc = "#version 300 es\nprecision highp float;\nin vec4 position;\nvoid main(){gl_Position=position;}" | |
| #fragmtSrc = "#version 300 es\nprecision highp float;\nout vec4 O;\nuniform float time;\nuniform vec2 resolution;\nvoid main() {\n\tvec2 uv=gl_FragCoord.xy/resolution;\n\tO=vec4(uv,sin(time)*.5+.5,1);\n}" | |
| #vertices = [-1, 1, -1, -1, 1, 1, 1, -1] | |
| constructor(canvas, scale) { | |
| this.canvas = canvas | |
| this.scale = scale | |
| this.gl = canvas.getContext("webgl2") | |
| this.gl.viewport(0, 0, canvas.width * scale, canvas.height * scale) | |
| this.shaderSource = this.#fragmtSrc | |
| this.mouseCoords = [0, 0] | |
| this.pointerCoords = [0, 0] | |
| this.nbrOfPointers = 0 | |
| } | |
| get defaultSource() { return this.#fragmtSrc } | |
| updateShader(source) { | |
| this.reset() | |
| this.shaderSource = source | |
| this.setup() | |
| this.init() | |
| } | |
| updateMouse(coords) { | |
| this.mouseCoords = coords | |
| } | |
| updatePointerCoords(coords) { | |
| this.pointerCoords = coords | |
| } | |
| updatePointerCount(nbr) { | |
| this.nbrOfPointers = nbr | |
| } | |
| updateScale(scale) { | |
| this.scale = scale | |
| this.gl.viewport(0, 0, this.canvas.width * scale, this.canvas.height * scale) | |
| } | |
| compile(shader, source) { | |
| const gl = this.gl | |
| gl.shaderSource(shader, source) | |
| gl.compileShader(shader) | |
| if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
| console.error(gl.getShaderInfoLog(shader)) | |
| this.canvas.dispatchEvent(new CustomEvent('shader-error', { detail: gl.getShaderInfoLog(shader) })) | |
| } | |
| } | |
| test(source) { | |
| let result = null | |
| const gl = this.gl | |
| const shader = gl.createShader(gl.FRAGMENT_SHADER) | |
| gl.shaderSource(shader, source) | |
| gl.compileShader(shader) | |
| if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
| result = gl.getShaderInfoLog(shader) | |
| } | |
| if (gl.getShaderParameter(shader, gl.DELETE_STATUS)) { | |
| gl.deleteShader(shader) | |
| } | |
| return result | |
| } | |
| reset() { | |
| const { gl, program, vs, fs } = this | |
| if (!program || gl.getProgramParameter(program, gl.DELETE_STATUS)) return | |
| if (gl.getShaderParameter(vs, gl.DELETE_STATUS)) { | |
| gl.detachShader(program, vs) | |
| gl.deleteShader(vs) | |
| } | |
| if (gl.getShaderParameter(fs, gl.DELETE_STATUS)) { | |
| gl.detachShader(program, fs) | |
| gl.deleteShader(fs) | |
| } | |
| gl.deleteProgram(program) | |
| } | |
| setup() { | |
| const gl = this.gl | |
| this.vs = gl.createShader(gl.VERTEX_SHADER) | |
| this.fs = gl.createShader(gl.FRAGMENT_SHADER) | |
| this.compile(this.vs, this.#vertexSrc) | |
| this.compile(this.fs, this.shaderSource) | |
| this.program = gl.createProgram() | |
| gl.attachShader(this.program, this.vs) | |
| gl.attachShader(this.program, this.fs) | |
| gl.linkProgram(this.program) | |
| if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { | |
| console.error(gl.getProgramInfoLog(this.program)) | |
| } | |
| } | |
| init() { | |
| const { gl, program } = this | |
| this.buffer = gl.createBuffer() | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer) | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.#vertices), gl.STATIC_DRAW) | |
| const position = gl.getAttribLocation(program, "position") | |
| gl.enableVertexAttribArray(position) | |
| gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0) | |
| program.resolution = gl.getUniformLocation(program, "resolution") | |
| program.time = gl.getUniformLocation(program, "time") | |
| program.touch = gl.getUniformLocation(program, "touch") | |
| program.pointerCount = gl.getUniformLocation(program, "pointerCount") | |
| program.pointers = gl.getUniformLocation(program, "pointers") | |
| } | |
| render(now = 0) { | |
| const { gl, program, buffer, canvas, mouseCoords, pointerCoords, nbrOfPointers } = this | |
| if (!program || gl.getProgramParameter(program, gl.DELETE_STATUS)) return | |
| gl.clearColor(0, 0, 0, 1) | |
| gl.clear(gl.COLOR_BUFFER_BIT) | |
| gl.useProgram(program) | |
| gl.bindBuffer(gl.ARRAY_BUFFER, buffer) | |
| gl.uniform2f(program.resolution, canvas.width, canvas.height) | |
| gl.uniform1f(program.time, now * 1e-3) | |
| gl.uniform2f(program.touch, ...mouseCoords) | |
| gl.uniform1i(program.pointerCount, nbrOfPointers) | |
| gl.uniform2fv(program.pointers, pointerCoords) | |
| gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) | |
| } | |
| } | |
| </script> | |
| <!-- Slides part. --> | |
| <style> | |
| /* ------------------------------------------------------------ | |
| Layout base | |
| ------------------------------------------------------------ */ | |
| html, body { height: 100%; } | |
| body{ | |
| margin:0; | |
| overflow:hidden; | |
| background:#000; | |
| color:#fff; | |
| cursor:pointer; | |
| font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; | |
| } | |
| #slides{ | |
| position:fixed; | |
| inset:0; | |
| pointer-events:none; | |
| z-index:10; | |
| } | |
| #preload{ | |
| position:fixed; | |
| left:-10000px; | |
| top:-10000px; | |
| width:1px; | |
| height:1px; | |
| overflow:hidden; | |
| opacity:0; | |
| pointer-events:none; | |
| z-index:-1; | |
| } | |
| :root{ | |
| --c-margin: 3vmin; /* center margin */ | |
| --lr-margin: 6vmin; /* left/right margin */ | |
| --c-max-w: 50vw; /* C: max-width 50% */ | |
| --lr-max-w: 40vw; /* L/R: 40% each */ | |
| --lr-max-h: 90vh; /* safety cap to avoid giant elements */ | |
| } | |
| /* ------------------------------------------------------------ | |
| Slide wrapper (effects only) | |
| ------------------------------------------------------------ */ | |
| #slides .slide{ | |
| position:absolute; | |
| opacity:0; | |
| transform-origin:50% 50%; | |
| will-change: transform, opacity; | |
| filter: drop-shadow(0 20px 60px rgba(0,0,0,0.45)); | |
| /* absolute appear/disappear time for *-CC/*-LL/*-RR */ | |
| --edge: 2s; | |
| --hold: max(0s, calc(var(--dur, 2s) - (var(--edge) * 2))); | |
| } | |
| /* oval defines real visible shape */ | |
| #slides .slide .oval{ | |
| display:inline-block; | |
| width:fit-content; | |
| height:fit-content; | |
| border-radius:50%; | |
| overflow:hidden; | |
| -webkit-mask-image: radial-gradient(circle, | |
| rgba(0,0,0,1) 60%, | |
| rgba(0,0,0,0.85) 68%, | |
| rgba(0,0,0,0.4) 75%, | |
| rgba(0,0,0,0.0) 82% | |
| ); | |
| mask-image: radial-gradient(circle, | |
| rgba(0,0,0,1) 60%, | |
| rgba(0,0,0,0.85) 68%, | |
| rgba(0,0,0,0.4) 75%, | |
| rgba(0,0,0,0.0) 82% | |
| ); | |
| } | |
| /* video keeps aspect ratio, does NOT crop */ | |
| #slides .slide .oval > video{ | |
| display:block; | |
| width:auto; | |
| height:auto; | |
| object-fit:contain; | |
| object-position:center; | |
| } | |
| /* ------------------------------------------------------------ | |
| Targets: L / C / R (regular) | |
| ------------------------------------------------------------ */ | |
| /* offsets from screen center to target anchor */ | |
| #slides .slide.t-L{ | |
| --from-x: 30vw; | |
| --from-y: 0; | |
| --to-x: 0; | |
| --to-y: 0; | |
| --exit-x: -25vw; | |
| --exit-y: 0; | |
| } | |
| #slides .slide.t-R{ | |
| --from-x: -30vw; | |
| --from-y: 0; | |
| --to-x: 0; | |
| --to-y: 0; | |
| --exit-x: 25vw; | |
| --exit-y: 0; | |
| } | |
| /* C: screen center, max-width 50% */ | |
| #slides .slide.t-C{ | |
| left:50%; | |
| top:50%; | |
| transform: translate(-50%, -50%) scale(0.05); | |
| } | |
| #slides .slide.t-C .oval > video{ | |
| max-width: min(var(--c-max-w), calc(100vw - var(--c-margin)*2)); | |
| max-height: calc(100vh - var(--c-margin)*2); | |
| } | |
| #slides .slide.t-C.play{ | |
| animation: popHoldOutCenter var(--dur, 2s) ease-in-out forwards; | |
| } | |
| /* L: left side, bottom aligned */ | |
| #slides .slide.t-L{ | |
| left: var(--lr-margin); | |
| bottom: var(--lr-margin); | |
| transform: translate(var(--from-x), var(--from-y)) scale(0.05); | |
| } | |
| #slides .slide.t-L .oval > video{ | |
| max-width: min(var(--lr-max-w), calc(100vw - var(--lr-margin)*2)); | |
| max-height: min(var(--lr-max-h), calc(100vh - var(--lr-margin)*2)); | |
| } | |
| #slides .slide.t-L.play{ | |
| animation: popHoldOutSide var(--dur, 2s) ease-in-out forwards; | |
| } | |
| /* R: right side, top aligned */ | |
| #slides .slide.t-R{ | |
| right: var(--lr-margin); | |
| top: var(--lr-margin); | |
| transform: translate(var(--from-x), var(--from-y)) scale(0.05); | |
| } | |
| #slides .slide.t-R .oval > video{ | |
| max-width: min(var(--lr-max-w), calc(100vw - var(--lr-margin)*2)); | |
| max-height: min(var(--lr-max-h), calc(100vh - var(--lr-margin)*2)); | |
| } | |
| #slides .slide.t-R.play{ | |
| animation: popHoldOutSide var(--dur, 2s) ease-in-out forwards; | |
| } | |
| /* ------------------------------------------------------------ | |
| Targets: LL / CC / RR (absolute 2s appear + 2s disappear) | |
| Total time is still controlled by --dur on element. | |
| ------------------------------------------------------------ */ | |
| /* same motion vars as L */ | |
| #slides .slide.t-LL{ | |
| --from-x: 30vw; | |
| --from-y: 0; | |
| --to-x: 0; | |
| --to-y: 0; | |
| --exit-x: -25vw; | |
| --exit-y: 0; | |
| } | |
| /* same motion vars as R */ | |
| #slides .slide.t-RR{ | |
| --from-x: -30vw; | |
| --from-y: 0; | |
| --to-x: 0; | |
| --to-y: 0; | |
| --exit-x: 25vw; | |
| --exit-y: 0; | |
| } | |
| /* CC: screen center, max-width 50% */ | |
| #slides .slide.t-CC{ | |
| left:50%; | |
| top:50%; | |
| transform: translate(-50%, -50%) scale(0.05); | |
| } | |
| #slides .slide.t-CC .oval > video{ | |
| max-width: min(var(--c-max-w), calc(100vw - var(--c-margin)*2)); | |
| max-height: calc(100vh - var(--c-margin)*2); | |
| } | |
| #slides .slide.t-CC.play{ | |
| animation: | |
| popInCenter var(--edge) ease-out forwards, | |
| holdCenter var(--hold) linear forwards var(--edge), | |
| popOutCenter var(--edge) ease-in forwards calc(var(--edge) + var(--hold)); | |
| } | |
| /* LL: left side, bottom aligned */ | |
| #slides .slide.t-LL{ | |
| left: var(--lr-margin); | |
| bottom: var(--lr-margin); | |
| transform: translate(var(--from-x), var(--from-y)) scale(0.05); | |
| } | |
| #slides .slide.t-LL .oval > video{ | |
| max-width: min(var(--lr-max-w), calc(100vw - var(--lr-margin)*2)); | |
| max-height: min(var(--lr-max-h), calc(100vh - var(--lr-margin)*2)); | |
| } | |
| #slides .slide.t-LL.play{ | |
| animation: | |
| popInSide var(--edge) ease-out forwards, | |
| holdSide var(--hold) linear forwards var(--edge), | |
| popOutSide var(--edge) ease-in forwards calc(var(--edge) + var(--hold)); | |
| } | |
| /* RR: right side, top aligned */ | |
| #slides .slide.t-RR{ | |
| right: var(--lr-margin); | |
| top: var(--lr-margin); | |
| transform: translate(var(--from-x), var(--from-y)) scale(0.05); | |
| } | |
| #slides .slide.t-RR .oval > video{ | |
| max-width: min(var(--lr-max-w), calc(100vw - var(--lr-margin)*2)); | |
| max-height: min(var(--lr-max-h), calc(100vh - var(--lr-margin)*2)); | |
| } | |
| #slides .slide.t-RR.play{ | |
| animation: | |
| popInSide var(--edge) ease-out forwards, | |
| holdSide var(--hold) linear forwards var(--edge), | |
| popOutSide var(--edge) ease-in forwards calc(var(--edge) + var(--hold)); | |
| } | |
| /* ------------------------------------------------------------ | |
| Animations (regular) | |
| ------------------------------------------------------------ */ | |
| @keyframes popHoldOutCenter{ | |
| 0% { opacity:0; transform: translate(-50%, -50%) scale(0.05); } | |
| 12% { opacity:0.95; transform: translate(-50%, -50%) scale(1.00); } | |
| 80% { opacity:0.85; transform: translate(-50%, -50%) scale(1.00); } | |
| 100% { opacity:0; transform: translate(-50%, -50%) scale(1.35); } | |
| } | |
| /* for L/R: no translate centering (anchored to corner-ish) */ | |
| @keyframes popHoldOutSide{ | |
| /* start: screen center */ | |
| 0%{ | |
| opacity:0; | |
| transform: translate(var(--from-x), var(--from-y)) scale(0.05); | |
| } | |
| /* arrive to side anchor */ | |
| 12%{ | |
| opacity:0.95; | |
| transform: translate(var(--to-x), var(--to-y)) scale(1); | |
| } | |
| /* main hold */ | |
| 80%{ | |
| opacity:0.95; | |
| transform: translate(var(--to-x), var(--to-y)) scale(1); | |
| } | |
| /* exit outward */ | |
| 100%{ | |
| opacity:0; | |
| transform: translate(var(--exit-x), var(--exit-y)) scale(1.35); | |
| } | |
| } | |
| /* ------------------------------------------------------------ | |
| Animations (absolute edges) | |
| Note: keyframes stay % based, but durations are absolute via --edge. | |
| ------------------------------------------------------------ */ | |
| @keyframes popInCenter{ | |
| from{ | |
| opacity:0; | |
| transform: translate(-50%, -50%) scale(0.05); | |
| } | |
| to{ | |
| opacity:0.95; | |
| transform: translate(-50%, -50%) scale(1.00); | |
| } | |
| } | |
| @keyframes holdCenter{ | |
| from, to{ | |
| opacity:0.85; | |
| transform: translate(-50%, -50%) scale(1.00); | |
| } | |
| } | |
| @keyframes popOutCenter{ | |
| to{ | |
| opacity:0; | |
| transform: translate(-50%, -50%) scale(1.35); | |
| } | |
| } | |
| @keyframes popInSide{ | |
| from{ | |
| opacity:0; | |
| transform: translate(var(--from-x), var(--from-y)) scale(0.05); | |
| } | |
| to{ | |
| opacity:0.95; | |
| transform: translate(var(--to-x), var(--to-y)) scale(1.00); | |
| } | |
| } | |
| @keyframes holdSide{ | |
| from, to{ | |
| opacity:0.95; | |
| transform: translate(var(--to-x), var(--to-y)) scale(1.00); | |
| } | |
| } | |
| @keyframes popOutSide{ | |
| to{ | |
| opacity:0; | |
| transform: translate(var(--exit-x), var(--exit-y)) scale(1.35); | |
| } | |
| } | |
| /* Reduce motion */ | |
| @media (prefers-reduced-motion: reduce){ | |
| #slides .slide{ | |
| animation:none !important; | |
| opacity:1; | |
| transform:none !important; | |
| } | |
| } | |
| </style> | |
| <div id="slides" aria-label="Slides"></div> | |
| <div id="preload" aria-hidden="true"></div> | |
| <script> | |
| // ------------------------------ | |
| // Core refs | |
| // ------------------------------ | |
| const audio = document.getElementById("music"); | |
| const slidesLayer = document.getElementById("slides"); | |
| const preloadLayer = document.getElementById("preload"); | |
| let happy = false; | |
| let slidesStarted = false; | |
| // ------------------------------ | |
| // Utils | |
| // ------------------------------ | |
| // 中文 ✨ 祈祷打印:按词数延迟输出 | |
| // ✨ FR : Impression “prière” avec temporisation proportionnelle | |
| console.pray = function (text) { | |
| const lines = String(text || "").split("\n"); | |
| let delay = 500; | |
| lines.forEach((line) => { | |
| const wc = line.trim() ? line.trim().split(/\s+/).length : 0; | |
| setTimeout(() => console.log(line), delay); | |
| delay += Math.max(1, wc) * 100; | |
| }); | |
| }; | |
| // ------------------------------ | |
| // Sources + GET overrides | |
| // ------------------------------ | |
| window.APP_CONFIG = { | |
| Sources: { | |
| {%- assign sources = markdown.yaml.Sources -%} | |
| {%- if sources -%} | |
| {%- for s in sources -%} | |
| "{{ s[0] }}": "{{ s[1] }}"{%- unless forloop.last -%},{%- endunless -%} | |
| {%- endfor -%} | |
| {%- endif -%} | |
| } | |
| }; | |
| function applySourceOverridesFromQuery(sources) { | |
| const params = new URLSearchParams(window.location.search); | |
| Object.keys(sources || {}).forEach((key) => { | |
| if (!params.has(key)) return; | |
| const value = params.get(key); | |
| if (!value) return; | |
| try { | |
| const u = new URL(value, window.location.href); | |
| sources[key] = u.href; | |
| console.log(`[override] ${key} -> ${u.href}`); | |
| } catch (e) { | |
| console.warn(`[override] invalid url for ${key}:`, value); | |
| } | |
| }); | |
| return sources; | |
| } | |
| applySourceOverridesFromQuery(window.APP_CONFIG.Sources); | |
| function buildVideoUrl(sourceKey, filename) { | |
| const sources = window.APP_CONFIG?.Sources || {}; | |
| const base = sources[sourceKey] || ""; | |
| if (!base) return String(filename || ""); | |
| const b = String(base); | |
| const f = String(filename || ""); | |
| if (b.endsWith("/") && f.startsWith("/")) return b + f.slice(1); | |
| if (!b.endsWith("/") && !f.startsWith("/")) return b + "/" + f; | |
| return b + f; | |
| } | |
| function normalizeTarget(t) { | |
| const x = String(t || "C").toUpperCase(); | |
| return ["C", "L", "R", "CC", "LL", "RR"].includes(x) ? x : "C"; | |
| } | |
| // ------------------------------ | |
| // Schedule | |
| // Example: | |
| // - 0 L 30 HolyWar 1.mp4 cut0 v1 | |
| // Doc: "<showup> <target> <duration> <source> <filename> cut<cut> v<velocity>" | |
| // ------------------------------ | |
| const SCHEDULE = [ | |
| {%- assign slides = markdown.yaml.Slides -%} | |
| {%- if slides -%} | |
| {%- for spec in slides -%} | |
| {%- assign s = spec | strip -%} | |
| {%- assign parts = s | split: ' ' -%} | |
| {%- assign showup_s = parts[0] | plus: 0 -%} | |
| {%- assign target = parts[1] -%} | |
| {%- assign dur_s = parts[2] | plus: 0 -%} | |
| {%- assign source_key = parts[3] -%} | |
| {%- assign filename = parts[4] -%} | |
| {%- assign cut_token = parts[5] | default: 'cut0' -%} | |
| {%- assign vel_token = parts[6] | default: 'v1' -%} | |
| {%- assign cut_s_str = cut_token | remove: 'cut' -%} | |
| {%- assign vel_str = vel_token | remove: 'v' -%} | |
| { | |
| showupMs: {{ showup_s | times: 1000 }}, | |
| durationMs: {{ dur_s | times: 1000 }}, | |
| sourceKey: "{{ source_key }}", | |
| filename: "{{ filename }}", | |
| cutMs: {{ cut_s_str | times: 1000 }}, | |
| speed: {{ vel_str | default: 1 }}, | |
| target: "{{ target }}" | |
| }{%- unless forloop.last -%},{%- endunless -%} | |
| {%- endfor -%} | |
| {%- endif -%} | |
| ] | |
| .map(x => ({ ...x, target: normalizeTarget(x.target) })) | |
| .sort((a, b) => a.showupMs - b.showupMs); | |
| // ------------------------------ | |
| // Preload + cloning strategy | |
| // Goals: | |
| // - preload once per URL (one hidden base element in DOM) | |
| // - for each slide instance, create a NEW video element (clone) so slides can overlap | |
| // - clones also start hidden/offscreen until attached to a slide | |
| // ------------------------------ | |
| const PRELOAD_LEAD_MS = 60000; | |
| // base (hidden) video per URL, used only for warming network/cache | |
| const baseVideoByUrl = new Map(); | |
| function hideVideoOffscreen(v) { | |
| v.style.position = "fixed"; | |
| v.style.left = "-10000px"; | |
| v.style.top = "-10000px"; | |
| v.style.width = "1px"; | |
| v.style.height = "1px"; | |
| v.style.opacity = "0"; | |
| v.style.pointerEvents = "none"; | |
| } | |
| function resetVideoInlineVisibility(v) { | |
| v.style.position = ""; | |
| v.style.left = ""; | |
| v.style.top = ""; | |
| v.style.width = ""; | |
| v.style.height = ""; | |
| v.style.opacity = ""; | |
| v.style.pointerEvents = ""; | |
| } | |
| function ensureBaseVideo(url, idx) { | |
| const key = idx + ":" + url; | |
| if (baseVideoByUrl.has(key)) return baseVideoByUrl.get(key); | |
| const v = document.createElement("video"); | |
| v.src = url; | |
| v.muted = true; | |
| v.playsInline = true; | |
| v.preload = "auto"; | |
| v.loop = false; | |
| v.autoplay = true; | |
| hideVideoOffscreen(v); | |
| preloadLayer.appendChild(v); | |
| try { v.load(); } catch (_) {} | |
| baseVideoByUrl.set(key, v); | |
| return v; | |
| } | |
| function preloadUrl(url, idx) { | |
| ensureBaseVideo(url, idx); | |
| } | |
| // Create a fresh playable instance (clone) | |
| // 中文 ✨ 每个 slide 都要独立 video 实例,支持并行播放 | |
| // ✨ FR : Chaque slide doit avoir sa propre instance vidéo (lecture en parallèle) | |
| function createVideoInstance(url, idx) { | |
| // ensure preload exists first (so network cache is warm) | |
| v = ensureBaseVideo(url, idx); | |
| v.src = url; | |
| v.muted = true; | |
| v.playsInline = true; | |
| v.preload = "auto"; | |
| v.loop = true; | |
| v.autoplay = false; | |
| // start hidden/offscreen until attached | |
| hideVideoOffscreen(v); | |
| preloadLayer.appendChild(v); | |
| try { v.load(); } catch (_) {} | |
| return v; | |
| } | |
| // ------------------------------ | |
| // Slide rendering (audio-driven, supports overlaps) | |
| // ------------------------------ | |
| // Track which slides are currently mounted (can be multiple) | |
| const activeSlides = new Map(); // idx -> { el, v, endMs } | |
| function isActiveAt(s, tMs) { | |
| return tMs >= s.showupMs && tMs < (s.showupMs + s.durationMs); | |
| } | |
| function startVideoFrom(v, { cutMs, speed }) { | |
| const cutS = Math.max(0, Number(cutMs || 0)) / 1000; | |
| const sp = Number(speed || 1); | |
| try { v.pause(); } catch (_) {} | |
| try { v.playbackRate = sp; } catch (_) {} | |
| try { v.currentTime = cutS; } catch (_) {} | |
| const onMeta = () => { | |
| try { v.playbackRate = sp; } catch (_) {} | |
| try { | |
| if (cutS > 0 && isFinite(v.duration)) { | |
| v.currentTime = Math.min(Math.max(0, cutS), Math.max(0, v.duration - 0.05)); | |
| } else { | |
| v.currentTime = cutS; | |
| } | |
| } catch (_) {} | |
| v.removeEventListener("loadedmetadata", onMeta); | |
| }; | |
| v.addEventListener("loadedmetadata", onMeta); | |
| Promise.resolve().then(() => v.play().catch(() => {})); | |
| } | |
| function mountSlide(idx) { | |
| const item = SCHEDULE[idx]; | |
| console.log("start slide", item); | |
| const url = buildVideoUrl(item.sourceKey, item.filename); | |
| // fresh instance for parallel playback | |
| const v = createVideoInstance(url, idx); | |
| // wrapper: new element => CSS animation restarts | |
| const el = document.createElement("div"); | |
| el.className = `slide t-${item.target}`; | |
| el.style.setProperty("--dur", (Math.max(300, item.durationMs) / 1000) + "s"); | |
| const inner = document.createElement("div"); | |
| inner.className = "oval"; | |
| inner.appendChild(v); | |
| el.appendChild(inner); | |
| // attach visible | |
| resetVideoInlineVisibility(v); | |
| slidesLayer.appendChild(el); | |
| // 1) Do NOT start animation yet. Wait for stable layout. | |
| // 中文 ✨ 等 metadata,避免视频尺寸后到导致 translate(-50%) 跳动 | |
| // ✨ FR : Attendre metadata pour stabiliser la taille avant l’animation | |
| const startVisual = () => { | |
| // 2) force layout now that dimensions exist | |
| void el.offsetWidth; | |
| // 3) next frame -> add .play to start CSS animation | |
| requestAnimationFrame(() => { | |
| // extra frame helps on Safari/iOS | |
| requestAnimationFrame(() => el.classList.add("play")); | |
| }); | |
| }; | |
| if (v.readyState >= 1) { | |
| startVisual(); | |
| } else { | |
| v.addEventListener("loadedmetadata", startVisual, { once: true }); | |
| } | |
| // start playback (ok to start now; visual animation waits for metadata anyway) | |
| startVideoFrom(v, { cutMs: item.cutMs, speed: item.speed }); | |
| activeSlides.set(idx, { el, v, endMs: item.showupMs + item.durationMs }); | |
| } | |
| function unmountSlide(idx) { | |
| const rec = activeSlides.get(idx); | |
| if (!rec) return; | |
| const { el, v } = rec; | |
| try { v.pause(); } catch (_) {} | |
| hideVideoOffscreen(v); | |
| preloadLayer.appendChild(v); | |
| el.remove(); | |
| activeSlides.delete(idx); | |
| } | |
| // ------------------------------------------- | |
| // GET ?start=<seconds> : start audio from there + ignore previous slides | |
| // 中文 ✨ 读取 start 参数(秒),从该位置开始播放,并忽略之前的幻灯片 | |
| // ✨ FR : Lire le paramètre start (secondes), démarrer l’audio à ce point et ignorer les slides précédents | |
| // ------------------------------------------- | |
| const START_AT_SEC = (() => { | |
| const p = new URLSearchParams(window.location.search); | |
| if (!p.has("start")) return null; | |
| const raw = p.get("start"); | |
| const n = Number(raw); | |
| return Number.isFinite(n) && n >= 0 ? n : null; | |
| })(); | |
| // 只渲染从这个时间点之后的 slides | |
| // ✨ Ne rendre que les slides après ce seuil temporel | |
| const START_AT_MS = START_AT_SEC != null ? START_AT_SEC * 1000 : null; | |
| function schedulePreloads() { | |
| SCHEDULE.forEach((item, idx) => { | |
| if (item.showupMs < START_AT_MS) | |
| return; | |
| const url = buildVideoUrl(item.sourceKey, item.filename); | |
| const whenMs = Math.max(0, item.showupMs - PRELOAD_LEAD_MS - START_AT_MS); | |
| setTimeout(() => preloadUrl(url, idx), whenMs); | |
| }); | |
| } | |
| // ------------------------------------------- | |
| // Helper: active check with "ignore previous slides" | |
| // ------------------------------------------- | |
| function isActiveAtWithStart(s, tMs) { | |
| if (START_AT_MS) { | |
| // ignore all slides that end before start | |
| if ((s.showupMs) <= START_AT_MS) return false; | |
| // also ignore any time before start | |
| if (tMs < START_AT_MS) return false; | |
| } | |
| return isActiveAt(s, tMs); | |
| } | |
| // Call this once, right after you set audio.src (and before play if possible) | |
| function applyStartOffsetToAudioOnce() { | |
| if (START_AT_SEC == null) return; | |
| let applied = false; | |
| const apply = () => { | |
| if (applied) return; | |
| applied = true; | |
| try { | |
| // clamp if duration is known | |
| if (isFinite(audio.duration) && audio.duration > 0) { | |
| audio.currentTime = Math.min(Math.max(0, START_AT_SEC), Math.max(0, audio.duration - 0.05)); | |
| } else { | |
| audio.currentTime = START_AT_SEC; | |
| } | |
| } catch (_) {} | |
| // If any slides were mounted before (shouldn't happen if you call early), clean them | |
| try { | |
| for (const idx of Array.from(activeSlides.keys())) unmountSlide(idx); | |
| } catch (_) {} | |
| }; | |
| // If metadata is already available, apply immediately; else wait. | |
| if (audio.readyState >= 1) apply(); | |
| else audio.addEventListener("loadedmetadata", apply, { once: true }); | |
| // Safety: in some browsers seeking is allowed only after canplay | |
| audio.addEventListener("canplay", apply, { once: true }); | |
| } | |
| function tickSlides() { | |
| if (!slidesStarted) return; | |
| const tSec = Number(audio.currentTime || 0); | |
| const tMs = tSec * 1000; | |
| // show time if admin mode | |
| updateAdminHud(tSec); | |
| // 1) mount any slides that should be active but aren't mounted | |
| for (let i = 0; i < SCHEDULE.length; i++) { | |
| const s = SCHEDULE[i]; | |
| if (isActiveAtWithStart(s, tMs) && !activeSlides.has(i)) { | |
| mountSlide(i); | |
| } | |
| } | |
| // 2) unmount slides that are no longer active | |
| for (const [idx, rec] of activeSlides.entries()) { | |
| const s = SCHEDULE[idx]; | |
| if (!isActiveAtWithStart(s, tMs)) unmountSlide(idx); | |
| } | |
| requestAnimationFrame(tickSlides); | |
| } | |
| function startSlidesByAudio() { | |
| if (slidesStarted) return; | |
| console.log("startSlidesByAudio"); | |
| slidesStarted = true; | |
| // if start param is present and audio is currently before it, don't show slides yet | |
| // (tickSlides already enforces that, but this avoids flash if anything was mounted) | |
| if (START_AT_MS != null) { | |
| for (const idx of Array.from(activeSlides.keys())) unmountSlide(idx); | |
| } | |
| requestAnimationFrame(tickSlides); | |
| } | |
| // admin mode: ?admin=admin -> show current seconds at bottom-center | |
| // 中文 ✨ admin=admin 时显示当前音频秒数(底部居中) | |
| // ✨ FR : si admin=admin, afficher les secondes courantes (bas-centre) | |
| const ADMIN_MODE = (() => { | |
| const p = new URLSearchParams(window.location.search); | |
| return p.get("admin") === "admin"; | |
| })(); | |
| let adminHudEl = null; | |
| function ensureAdminHud() { | |
| if (!ADMIN_MODE) return null; | |
| if (adminHudEl) return adminHudEl; | |
| adminHudEl = document.createElement("div"); | |
| adminHudEl.id = "admin-hud"; | |
| adminHudEl.style.position = "fixed"; | |
| adminHudEl.style.left = "50%"; | |
| adminHudEl.style.bottom = "12px"; | |
| adminHudEl.style.transform = "translateX(-50%)"; | |
| adminHudEl.style.zIndex = "9999"; | |
| adminHudEl.style.pointerEvents = "none"; | |
| adminHudEl.style.padding = "6px 10px"; | |
| adminHudEl.style.borderRadius = "10px"; | |
| adminHudEl.style.background = "rgba(0,0,0,0.55)"; | |
| adminHudEl.style.backdropFilter = "blur(6px)"; | |
| adminHudEl.style.webkitBackdropFilter = "blur(6px)"; | |
| adminHudEl.style.color = "#fff"; | |
| adminHudEl.style.font = "600 33px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif"; | |
| adminHudEl.style.letterSpacing = "0.02em"; | |
| document.body.appendChild(adminHudEl); | |
| return adminHudEl; | |
| } | |
| function updateAdminHud(tSec) { | |
| if (!ADMIN_MODE) return; | |
| const el = ensureAdminHud(); | |
| if (!el) return; | |
| el.textContent = `t = ${tSec.toFixed(2)}s`; | |
| } | |
| // ------------------------------ | |
| // Audio sequence | |
| // ------------------------------ | |
| const MAIN_URL = "{{ markdown.yaml.PowerPointMan }}"; | |
| const INTRO_SECONDS = 12; | |
| const startTime = Date.now(); | |
| function startAudioSequence() { | |
| const mainPreload = document.createElement("audio"); | |
| mainPreload.src = MAIN_URL; | |
| mainPreload.preload = "auto"; | |
| try { mainPreload.load(); } catch (_) {} | |
| let switched = false; | |
| function switchToMain() { | |
| if (switched) return; | |
| switched = true; | |
| audio.pause(); | |
| audio.src = MAIN_URL; | |
| audio.loop = false; | |
| applyStartOffsetToAudioOnce(); | |
| try { audio.currentTime = 0; } catch (_) {} | |
| audio.play().catch(() => {}); | |
| audio.addEventListener("ended", next, { once: true }); | |
| } | |
| try { audio.currentTime = 0; } catch (_) {} | |
| console.log("Start Intro"); | |
| ensureAdminHud(); | |
| const onTime = () => { | |
| if (!switched && (Date.now() - startTime) >= INTRO_SECONDS * 1000) { | |
| startSlidesByAudio(); | |
| switchToMain(); | |
| audio.removeEventListener("timeupdate", onTime); | |
| } | |
| }; | |
| audio.addEventListener("timeupdate", onTime); | |
| audio.addEventListener("ended", () => { | |
| if (!switched) { | |
| startSlidesByAudio(); | |
| switchToMain(); | |
| } | |
| }, { once: true }); | |
| audio.addEventListener("error", () => { | |
| if (!switched) { | |
| startSlidesByAudio(); | |
| switchToMain(); | |
| } | |
| }, { once: true }); | |
| audio.play().catch(err => console.log("Autoplay blocked:", err)); | |
| } | |
| // ------------------------------ | |
| // Click bootstrap | |
| // ------------------------------ | |
| document.addEventListener("click", function () { | |
| if (happy) return; | |
| schedulePreloads(); | |
| startAudioSequence(); | |
| console.pray(`{{ markdown.RAW | replace: "`", "\`" }}`); | |
| happy = true; | |
| }, { once: true }); | |
| </script> | |
| <script> | |
| function next() { | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const witch = urlParams.get("debug") || "{{ markdown.yaml.Next }}"; | |
| window.location.href = witch; | |
| } | |
| </script></body><!-- | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣄ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣭⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣹⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣤⠤⢤⣀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠴⠒⢋⣉⣀⣠⣄⣀⣈⡇ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⣾⣯⠴⠚⠉⠉⠀⠀⠀⠀⣤⠏⣿ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡿⡇⠁⠀⠀⠀⠀⡄⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⡿⠿⢛⠁⠁⣸⠀⠀⠀⠀⠀⣤⣾⠵⠚⠁ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⢦⡀⠀⣠⠀⡇⢧⠀⠀⢀⣠⡾⡇⠀⠀⠀⠀⠀⣠⣴⠿⠋⠁⠀⠀⠀⠀⠘⣿⠀⣀⡠⠞⠛⠁⠂⠁⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡈⣻⡦⣞⡿⣷⠸⣄⣡⢾⡿⠁⠀⠀⠀⣀⣴⠟⠋⠁⠀⠀⠀⠀⠐⠠⡤⣾⣙⣶⡶⠃⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣂⡷⠰⣔⣾⣖⣾⡷⢿⣐⣀⣀⣤⢾⣋⠁⠀⠀⠀⣀⢀⣀⣀⣀⣀⠀⢀⢿⠑⠃--></html><!-- | |
| ⠀⠀⠀⠀⠀⠀⠠⡦⠴⠴⠤⠦⠤⠤⠤⠤⠤⠴⠶⢾⣽⣙⠒⢺⣿⣿⣿⣿⢾⠶⣧⡼⢏⠑⠚⠋⠉⠉⡉⡉⠉⠉⠹⠈⠁⠉⠀⠨⢾⡂ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⠀⠀⠀⠂⠐⠀⠀⠀⠈⣇⡿⢯⢻⣟⣇⣷⣞⡛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣆⠀⠀⠀⠀⢠⡷⡛⣛⣼⣿⠟⠙⣧⠅⡄⠀⠀⠀⠀⠀⠀⠰⡆⠀⠀⠀⠀⢠⣾⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⢶⠏⠉⠀⠀⠀⠀⠀⠿⢠⣴⡟⡗⡾⡒⠖⠉⠏⠁⠀⠀⠀⠀⣀⢀⣠⣧⣀⣀⠀⠀⠀⠚⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⣠⢴⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⣠⣷⢿⠋⠁⣿⡏⠅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⣿⢭⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⢀⡴⢏⡵⠛⠀⠀⠀⠀⠀⠀⠀⣀⣴⠞⠛⠀⠀⠀⠀⢿⠀⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⢿⠘⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⣀⣼⠛⣲⡏⠁⠀⠀⠀⠀⠀⢀⣠⡾⠋⠉⠀⠀⠀⠀⠀⠀⢾⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⡴⠟⠀⢰⡯⠄⠀⠀⠀⠀⣠⢴⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⣹⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⡾⠁⠁⠀⠘⠧⠤⢤⣤⠶⠏⠙⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢾⡃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠘⣇⠂⢀⣀⣀⠤⠞⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠈⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠾⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢼⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | |
| ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛--> |
| Title | Author | PORTAL | FlooPowder |
|---|---|---|---|
Товарищ Горбачёв, срочное сообщение ТАСС: в СССР закончилась водка! |
Ivan Yelizariev |
Кто на лавочке сидел,
Кто на улицу глядел,
Толя пел,
Борис молчал,
Николай ногой качал.
Дело было вечером,
Делать было нечего.
Галка села на заборе,
Кот забрался на чердак.
Тут сказал ребятам Боря
Просто так:
— А у меня в кармане гвоздь!
А у вас?
— А у нас сегодня гость!
А у вас?
— А у нас сегодня кошка
Родила вчера котят.
Котята выросли немножко,
А есть из блюдца не хотят!
— А у нас в квартире газ!
А у вас?
— А у нас —
▐▓▐▐
▐██████▐ ▐███░░ ██
▐█▏▐▐▐█▐ ▐██▐ ██
▐█▏ █▐ ▐▐ ▓█ ▐▐ ▐▐ ▐▐ ▐▐ ██
▐█▏ █▐ ▐████▐ ▐█▐ ▐█▏ █▏████▐ ▐█████▐ ▓████▐ ▓████▐ ██
▐█▏ █▐ ▐██▐▐██▐ ▐█▐ ▐█▏ ███▐▐██▐ ▐░▐▐▐██▐ ▓█░▐▐▐▏ ▓█░▐▐▐▏ ██
▐█▏ █▐ ▓▎▐ ▐█▏ ▐█▐ ▐█▏ █▎▐ ▐█▏ ▐█▏ ▐█▏ ▐█▏ ██
▐█▏ █▐ █▎ █▒ ▐██▓▓██▏ █▎ █▒ ▐▓████▏ ▐█▏ ▐█▏ ██
▐█▏ █▐ ██ ██ ▐██████▏ ██ ██ ▐██░▐▐█▏ ▐█▐ ▐█▐ █▒
▐█▐ █▐ █▎ █▒ ▐█▐ ▐█▏ █▎ █▒ ▓█ ▐█▏ ▐█▏ ▐█▏ ▐▐
▓█▐ █▐ ▓▎▐ ▐█▏ ▐█▐ ▐█▏ ▓▎▐ ▐█▏ █▒ ▐█▏ ▐█▏ ▐█▏ ▐▐
▐███▓▓▓██▐ ▐██▐▐▓█▐ ▐█▐ ▐█▏ ▐██▐▐▓█▐ ██▐▐▐██▏ ███▐▐▓▏ ███▐▐▓▏ ██
▓████████▐ ▐████▐ ▐█▐ ▐█▏ ▐████▐ ▐████▐█▏ ▐████░ ▐████░ ██
▓█ █▐ ▐▐ ▐▐ ▐▐ ▐▐ ▐▐
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
