about: add zoomer-friendly reading mode
This commit is contained in:
parent
bf7a48a36c
commit
9528d54af8
6 changed files with 205 additions and 0 deletions
|
@ -94,6 +94,9 @@ importers:
|
||||||
'@fontsource/ibm-plex-mono':
|
'@fontsource/ibm-plex-mono':
|
||||||
specifier: ^5.0.13
|
specifier: ^5.0.13
|
||||||
version: 5.0.13
|
version: 5.0.13
|
||||||
|
'@fontsource/luckiest-guy':
|
||||||
|
specifier: ^5.1.0
|
||||||
|
version: 5.1.0
|
||||||
'@fontsource/redaction-10':
|
'@fontsource/redaction-10':
|
||||||
specifier: ^5.0.2
|
specifier: ^5.0.2
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
|
@ -506,6 +509,9 @@ packages:
|
||||||
'@fontsource/ibm-plex-mono@5.0.13':
|
'@fontsource/ibm-plex-mono@5.0.13':
|
||||||
resolution: {integrity: sha512-gtlMmvk//2AgDEZDFsoL5z9mgW3ZZg/9SC7pIfDwNKp5DtZpApgqd1Fua3HhPwYRIHrT76IQ1tMTzQKLEGtJGQ==}
|
resolution: {integrity: sha512-gtlMmvk//2AgDEZDFsoL5z9mgW3ZZg/9SC7pIfDwNKp5DtZpApgqd1Fua3HhPwYRIHrT76IQ1tMTzQKLEGtJGQ==}
|
||||||
|
|
||||||
|
'@fontsource/luckiest-guy@5.1.0':
|
||||||
|
resolution: {integrity: sha512-F1V0LnFW7lSdmkv5zHfeBZ9aI7JLfwa0g+FCv+16sw/yvABH/oqdiaZgc3sri5QKaaEaZwHeoVUuXcn2DGbuxQ==}
|
||||||
|
|
||||||
'@fontsource/redaction-10@5.0.2':
|
'@fontsource/redaction-10@5.0.2':
|
||||||
resolution: {integrity: sha512-PODxYvb06YrNxdUBGcygiMibpgcZihzmvkmlX/TQAA2F7BUU/anfSKQi/VnLdJ/8LIK81/bUY+i7L/GP27FkVw==}
|
resolution: {integrity: sha512-PODxYvb06YrNxdUBGcygiMibpgcZihzmvkmlX/TQAA2F7BUU/anfSKQi/VnLdJ/8LIK81/bUY+i7L/GP27FkVw==}
|
||||||
|
|
||||||
|
@ -2470,6 +2476,8 @@ snapshots:
|
||||||
|
|
||||||
'@fontsource/ibm-plex-mono@5.0.13': {}
|
'@fontsource/ibm-plex-mono@5.0.13': {}
|
||||||
|
|
||||||
|
'@fontsource/luckiest-guy@5.1.0': {}
|
||||||
|
|
||||||
'@fontsource/redaction-10@5.0.2': {}
|
'@fontsource/redaction-10@5.0.2': {}
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.11.14':
|
'@humanwhocodes/config-array@0.11.14':
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"@eslint/js": "^9.5.0",
|
"@eslint/js": "^9.5.0",
|
||||||
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
||||||
"@fontsource/ibm-plex-mono": "^5.0.13",
|
"@fontsource/ibm-plex-mono": "^5.0.13",
|
||||||
|
"@fontsource/luckiest-guy": "^5.1.0",
|
||||||
"@fontsource/redaction-10": "^5.0.2",
|
"@fontsource/redaction-10": "^5.0.2",
|
||||||
"@imput/libav.js-remux-cli": "^5.5.6",
|
"@imput/libav.js-remux-cli": "^5.5.6",
|
||||||
"@imput/version-info": "workspace:^",
|
"@imput/version-info": "workspace:^",
|
||||||
|
|
192
web/src/components/misc/Brainrot.svelte
Normal file
192
web/src/components/misc/Brainrot.svelte
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import IconSpray from '@tabler/icons-svelte/IconSpray.svelte';
|
||||||
|
import '@fontsource/luckiest-guy';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let renderVideo = false;
|
||||||
|
let showVideo = false;
|
||||||
|
let videoElement: HTMLVideoElement | undefined;
|
||||||
|
let bounce = false;
|
||||||
|
let context: AudioContext | undefined;
|
||||||
|
let textDisplayed = "";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
context = new AudioContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
const prepareVideo = () => {
|
||||||
|
renderVideo = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const animate = (text: string) => {
|
||||||
|
bounce = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
bounce = true;
|
||||||
|
textDisplayed = text;
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
const readOutLoud = (text: string) => {
|
||||||
|
function fromBinary(encoded: string) {
|
||||||
|
const binary = atob(encoded);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
fetch('https://countik.com/api/text/speech', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text,
|
||||||
|
voice: 'en_us_006'
|
||||||
|
})
|
||||||
|
}).then(a => a.json()).then(x => {
|
||||||
|
const data = fromBinary(x.v_data);
|
||||||
|
const source = context!.createBufferSource();
|
||||||
|
context!.decodeAudioData(data, function(buffer) {
|
||||||
|
source.buffer = buffer;
|
||||||
|
source.connect(context!.destination);
|
||||||
|
source.start(0);
|
||||||
|
source.onended = () => resolve();
|
||||||
|
|
||||||
|
let sentences = text.match(/[^\.!\?]+[\.!\?]+/g);
|
||||||
|
if (!sentences) sentences = [text];
|
||||||
|
|
||||||
|
const maxTime = buffer.duration * 1000;
|
||||||
|
const totalLength = sentences.join(' ').length;
|
||||||
|
let totalTime = 0;
|
||||||
|
for (let i = 0; i < sentences.length; ++i) {
|
||||||
|
for (const word of sentences[i].split(' ')) {
|
||||||
|
const wordProportion = word.length / totalLength;
|
||||||
|
let wordReadTime = maxTime * wordProportion;
|
||||||
|
if (word.endsWith(',') || word.endsWith('.') || word.endsWith('!')) {
|
||||||
|
wordReadTime += 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => animate(word), totalTime);
|
||||||
|
totalTime += wordReadTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (videoElement) {
|
||||||
|
videoElement.addEventListener('canplaythrough', () => {
|
||||||
|
showVideo = true;
|
||||||
|
const sentences = [...document.querySelectorAll('section')]
|
||||||
|
.map(a => a.textContent!
|
||||||
|
.replace(/ /g, '\n')
|
||||||
|
.split('\n')
|
||||||
|
.filter(a => a)
|
||||||
|
).flat();
|
||||||
|
|
||||||
|
let p = Promise.resolve();
|
||||||
|
for (const sentence of sentences) {
|
||||||
|
p = p.then(() => readOutLoud(sentence));
|
||||||
|
}
|
||||||
|
videoElement?.play();
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="brainrot-container">
|
||||||
|
<button id="brainrot-button" on:click={prepareVideo} class:hidden={showVideo}>
|
||||||
|
<IconSpray />
|
||||||
|
</button>
|
||||||
|
{#if renderVideo}
|
||||||
|
<div id="brainrot-video" class:displayed={showVideo}>
|
||||||
|
<div id="brainrot-text" class:animate={bounce}>
|
||||||
|
{ textDisplayed }
|
||||||
|
</div>
|
||||||
|
<!-- svelte-ignore a11y-media-has-caption -->
|
||||||
|
<video
|
||||||
|
bind:this={videoElement}
|
||||||
|
src="/brainrot.mp4"
|
||||||
|
loop
|
||||||
|
></video>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#brainrot-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 32px;
|
||||||
|
right: 32px;
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#brainrot-button.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#brainrot-button {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#brainrot-video {
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#brainrot-text {
|
||||||
|
font-family: 'Luckiest Guy', cursive;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #fff;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
--stroke-color: red;
|
||||||
|
text-shadow: var(--stroke-color) 3px 0px 0px,
|
||||||
|
var(--stroke-color) 2.83487px 0.981584px 0px,
|
||||||
|
var(--stroke-color) 2.35766px 1.85511px 0px,
|
||||||
|
var(--stroke-color) 1.62091px 2.52441px 0px,
|
||||||
|
var(--stroke-color) 0.705713px 2.91581px 0px,
|
||||||
|
var(--stroke-color) -0.287171px 2.98622px 0px,
|
||||||
|
var(--stroke-color) -1.24844px 2.72789px 0px,
|
||||||
|
var(--stroke-color) -2.07227px 2.16926px 0px,
|
||||||
|
var(--stroke-color) -2.66798px 1.37182px 0px,
|
||||||
|
var(--stroke-color) -2.96998px 0.42336px 0px,
|
||||||
|
var(--stroke-color) -2.94502px -0.571704px 0px,
|
||||||
|
var(--stroke-color) -2.59586px -1.50383px 0px,
|
||||||
|
var(--stroke-color) -1.96093px -2.27041px 0px,
|
||||||
|
var(--stroke-color) -1.11013px -2.78704px 0px,
|
||||||
|
var(--stroke-color) -0.137119px -2.99686px 0px,
|
||||||
|
var(--stroke-color) 0.850987px -2.87677px 0px,
|
||||||
|
var(--stroke-color) 1.74541px -2.43999px 0px,
|
||||||
|
var(--stroke-color) 2.44769px -1.73459px 0px,
|
||||||
|
var(--stroke-color) 2.88051px -0.838247px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate {
|
||||||
|
animation: bounce .1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
from {
|
||||||
|
font-size: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#brainrot-video.displayed {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -6,6 +6,8 @@
|
||||||
import PageNavTab from "$components/subnav/PageNavTab.svelte";
|
import PageNavTab from "$components/subnav/PageNavTab.svelte";
|
||||||
import PageNavSection from "$components/subnav/PageNavSection.svelte";
|
import PageNavSection from "$components/subnav/PageNavSection.svelte";
|
||||||
|
|
||||||
|
import Brainrot from "$components/misc/Brainrot.svelte";
|
||||||
|
|
||||||
import IconLock from "@tabler/icons-svelte/IconLock.svelte";
|
import IconLock from "@tabler/icons-svelte/IconLock.svelte";
|
||||||
import IconComet from "@tabler/icons-svelte/IconComet.svelte";
|
import IconComet from "@tabler/icons-svelte/IconComet.svelte";
|
||||||
import IconLicense from "@tabler/icons-svelte/IconLicense.svelte";
|
import IconLicense from "@tabler/icons-svelte/IconLicense.svelte";
|
||||||
|
@ -65,3 +67,4 @@
|
||||||
|
|
||||||
<slot slot="content"></slot>
|
<slot slot="content"></slot>
|
||||||
</PageNav>
|
</PageNav>
|
||||||
|
<Brainrot />
|
||||||
|
|
BIN
web/static/brainrot.mp4
Normal file
BIN
web/static/brainrot.mp4
Normal file
Binary file not shown.
|
@ -54,6 +54,7 @@ const config = {
|
||||||
"connect-src": ["*"],
|
"connect-src": ["*"],
|
||||||
"default-src": ["none"],
|
"default-src": ["none"],
|
||||||
|
|
||||||
|
"media-src": ["self"],
|
||||||
"font-src": ["self"],
|
"font-src": ["self"],
|
||||||
"style-src": ["self", "unsafe-inline"],
|
"style-src": ["self", "unsafe-inline"],
|
||||||
"img-src": ["*", "data:"],
|
"img-src": ["*", "data:"],
|
||||||
|
|
Loading…
Reference in a new issue