feat: background changer and such

This commit is contained in:
2025-04-18 23:07:32 +02:00
parent 32bc8f4494
commit 9ffe38fc8e
8 changed files with 126 additions and 39 deletions

View File

@@ -1,17 +1,26 @@
<script lang="ts">
import { onMount } from 'svelte';
import { setMode } from 'mode-watcher';
import { state as appState } from '@/state.svelte';
export let videoSelector: string = "#bg-video";
export let updateInterval: number = 2000;
console.log('update')
let { videoSelector }: { videoSelector: string } = $props();
function analyzeVideoBrightness(video: HTMLVideoElement, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
if (video.paused || video.ended) return;
const perf = window.performance.now();
if (video.paused || video.ended || video.readyState < 2) return;
canvas.width = 32;
canvas.height = 32;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const sourceY = Math.floor(video.videoHeight * 0.67); // take the top two-thirds
const sourceHeight = Math.floor(video.videoHeight * 0.33); // take the bottom third
// draw the bottom third of the video to the canvas
// prettier-ignore
ctx.drawImage(
video,
0, sourceY, video.videoWidth, sourceHeight,
0, 0, canvas.width, canvas.height
);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
@@ -30,30 +39,40 @@
}
const avgBrightness = totalBrightness / pixelCount;
const isDark = avgBrightness < 0.5;
const isDark = avgBrightness < 0.45;
setMode(isDark ? 'dark' : 'light');
const now = window.performance.now();
console.log(`brightness: ${avgBrightness}, time taken: ${now - perf}ms`);
}
onMount(() => {
$effect(() => {
const videoId = appState.currentBackgroundId;
if (!videoId) return;
const video = document.querySelector(videoSelector) as HTMLVideoElement;
if (!video) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return;
const analyzeWhenReady = () => {
if (video.readyState >= 2) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return;
analyzeVideoBrightness(video, canvas, ctx);
video.removeEventListener('loadeddata', analyzeWhenReady);
}
};
function updateGlobalTextColor() {
analyzeVideoBrightness(video, canvas, ctx!);
if (video.readyState >= 2) {
analyzeWhenReady();
}
video.addEventListener('loadeddata', updateGlobalTextColor);
const interval = setInterval(updateGlobalTextColor, updateInterval);
video.addEventListener('loadeddata', analyzeWhenReady);
return () => {
clearInterval(interval);
video.removeEventListener('loadeddata', updateGlobalTextColor);
video.removeEventListener('loadeddata', analyzeWhenReady);
};
});
</script>

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { state as appState } from '@/state.svelte';
import Image from '@lucide/svelte/icons/image';
let selectedBackgroundId = $state(appState.currentBackgroundId!.toString());
$effect(() => {
appState.currentBackgroundId = selectedBackgroundId;
})
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger class={buttonVariants({ variant: 'default', size: 'icon' })}>
<Image />
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="max-h-[50vh] overflow-y-auto"
>
<DropdownMenu.Group>
<DropdownMenu.GroupHeading>Select background</DropdownMenu.GroupHeading>
<DropdownMenu.Separator />
<DropdownMenu.RadioGroup bind:value={selectedBackgroundId} class="grid grid-cols-2 md:grid-cols-4">
{#each appState.backgrounds as background}
<DropdownMenu.RadioItem value={background.id.toString()} hideRadio>
<div class="relative flex items-center flex-col">
<div class="absolute bottom-0 left-0 w-full bg-black/50 text-white text-sm text-center p-1">
{background.name}
</div>
<img
src={background.thumbnailUrl}
alt={background.name}
class="size-32 object-cover rounded-sm"
/>
</div>
</DropdownMenu.RadioItem>
{/each}
</DropdownMenu.RadioGroup>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -1,5 +1,20 @@
<script lang="ts">
import { state as appState } from '@/state.svelte';
let video: HTMLVideoElement | null = null;
$effect(() => {
const backgroundId = appState.currentBackgroundId;
if (backgroundId && video) {
const resolvedBg = appState.backgrounds.find((bg) => bg.id === backgroundId);
// i may or may not have yoinked this cors proxy for other purposes
video.src = `https://cors.notesnook.com/${resolvedBg?.landscapeUrl}` || '';
}
})
</script>
<video
src={true ? "https://ch-cdn.srizan.dev/discord-rain.mp4" : "https://ch-cdn.srizan.dev/flower-shop-beachside-moewalls-com.mp4"}
bind:this={video}
src={"https://example.com"}
autoplay
loop
muted
@@ -7,4 +22,4 @@
class="absolute top-0 left-0 w-full h-full object-cover -z-10"
id="bg-video"
crossorigin="anonymous"
></video>
></video>

View File

@@ -1,4 +1,5 @@
<script>
import BgDropdown from '@/components/app/bg-dropdown.svelte';
import Disclaimer from '@/components/app/disclaimer.svelte';
import MusicPlayer from '@/components/app/now-playing.svelte';
import StationDropdown from '@/components/app/station-dropdown.svelte';
@@ -12,6 +13,7 @@
<div class="hidden sm:block flex-1"></div>
<div class="flex gap-4 mt-3 sm:mt-0">
<StationDropdown />
<BgDropdown />
<Disclaimer />
</div>
</div>

View File

@@ -5,7 +5,6 @@
// svelte-ignore non_reactive_update
let audioElement: HTMLAudioElement;
let isInteracting = false;
function togglePlayback(play: boolean) {
if (!audioElement) return;
@@ -31,17 +30,16 @@
const data = await getGeneralData();
state.presets = data.presets;
state.stations = data.stations;
state.backgrounds = data.backgrounds;
// TODO: support parent backgrounds
state.backgrounds = data.backgrounds.filter(bg => bg.isActive === 1 && !bg.parentId);
state.atmospheres = data.atmospheres;
if (data.stations.length > 0 && state.currentStation === null) {
state.currentStation = data.stations[0].id;
}
if (data.backgrounds.length > 0 && state.currentBackgroundId === null) {
const firstActiveBg = data.backgrounds.find((bg) => bg.isActive && !bg.parentId);
state.currentBackgroundId = firstActiveBg
? firstActiveBg.id
: data.backgrounds[0]?.id || null;
if (state.backgrounds.length > 0 && state.currentBackgroundId === null) {
state.currentBackgroundId = state.backgrounds[0].id;
console.log('asdf', state.currentBackgroundId);
} else {
state.error = 'Failed to load initial data (empty response).';
}

View File

@@ -7,24 +7,36 @@
ref = $bindable(null),
class: className,
children: childrenProp,
hideRadio = $bindable(false),
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
}: FixedProps = $props();
interface FixedProps extends WithoutChild<DropdownMenuPrimitive.RadioItemProps> {
/**
* Wether if the circle should be shown or not
* @default false
*/
hideRadio?: boolean;
}
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
class={cn(
'data-[highlighted]:bg-white/20 data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'data-[highlighted]:bg-white/20 data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
hideRadio ? 'pl-2' : 'pl-8',
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<Circle class="size-2 fill-current" />
{/if}
</span>
{#if !hideRadio}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<Circle class="size-2 fill-current" />
{/if}
</span>
{/if}
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>
</DropdownMenuPrimitive.RadioItem>

View File

@@ -11,6 +11,6 @@
<DropdownMenuPrimitive.Separator
bind:ref
class={cn('bg-muted -mx-1 my-1 h-px', className)}
class={cn('bg-white/20 -mx-1 my-1 h-px', className)}
{...restProps}
/>

View File

@@ -9,7 +9,7 @@
<BgImage />
<Daemon />
<BackgroundAnalyzer videoSelector="#bg-video" updateInterval={2000} />
<BackgroundAnalyzer videoSelector="#bg-video" />
{#if state.isLoading && !state.hasInteracted}
<div class="flex flex-col h-screen w-full items-center justify-center space-y-2">