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':
|
||||
specifier: ^5.0.13
|
||||
version: 5.0.13
|
||||
'@fontsource/luckiest-guy':
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
'@fontsource/redaction-10':
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
|
@ -506,6 +509,9 @@ packages:
|
|||
'@fontsource/ibm-plex-mono@5.0.13':
|
||||
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':
|
||||
resolution: {integrity: sha512-PODxYvb06YrNxdUBGcygiMibpgcZihzmvkmlX/TQAA2F7BUU/anfSKQi/VnLdJ/8LIK81/bUY+i7L/GP27FkVw==}
|
||||
|
||||
|
@ -2470,6 +2476,8 @@ snapshots:
|
|||
|
||||
'@fontsource/ibm-plex-mono@5.0.13': {}
|
||||
|
||||
'@fontsource/luckiest-guy@5.1.0': {}
|
||||
|
||||
'@fontsource/redaction-10@5.0.2': {}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"@eslint/js": "^9.5.0",
|
||||
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
||||
"@fontsource/ibm-plex-mono": "^5.0.13",
|
||||
"@fontsource/luckiest-guy": "^5.1.0",
|
||||
"@fontsource/redaction-10": "^5.0.2",
|
||||
"@imput/libav.js-remux-cli": "^5.5.6",
|
||||
"@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 PageNavSection from "$components/subnav/PageNavSection.svelte";
|
||||
|
||||
import Brainrot from "$components/misc/Brainrot.svelte";
|
||||
|
||||
import IconLock from "@tabler/icons-svelte/IconLock.svelte";
|
||||
import IconComet from "@tabler/icons-svelte/IconComet.svelte";
|
||||
import IconLicense from "@tabler/icons-svelte/IconLicense.svelte";
|
||||
|
@ -65,3 +67,4 @@
|
|||
|
||||
<slot slot="content"></slot>
|
||||
</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": ["*"],
|
||||
"default-src": ["none"],
|
||||
|
||||
"media-src": ["self"],
|
||||
"font-src": ["self"],
|
||||
"style-src": ["self", "unsafe-inline"],
|
||||
"img-src": ["*", "data:"],
|
||||
|
|
Loading…
Reference in a new issue