feature: add background
This commit is contained in:
parent
75422f7aaf
commit
5afdaec8a0
@ -1,5 +1,6 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"useTabs": false,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
|
@ -2,7 +2,7 @@
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"checkJs": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
|
@ -28,5 +28,8 @@
|
||||
"vite": "^5.0.3",
|
||||
"vitest": "^1.2.0"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"localforage": "^1.10.0"
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,11 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
localforage:
|
||||
specifier: ^1.10.0
|
||||
version: 1.10.0
|
||||
|
||||
devDependencies:
|
||||
'@sveltejs/adapter-auto':
|
||||
specifier: ^3.0.0
|
||||
@ -1271,6 +1276,10 @@ packages:
|
||||
engines: {node: '>= 4'}
|
||||
dev: true
|
||||
|
||||
/immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
dev: false
|
||||
|
||||
/import-fresh@3.3.0:
|
||||
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
||||
engines: {node: '>=6'}
|
||||
@ -1389,6 +1398,12 @@ packages:
|
||||
type-check: 0.4.0
|
||||
dev: true
|
||||
|
||||
/lie@3.1.1:
|
||||
resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
dev: false
|
||||
|
||||
/lilconfig@2.1.0:
|
||||
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -1402,6 +1417,12 @@ packages:
|
||||
pkg-types: 1.1.0
|
||||
dev: true
|
||||
|
||||
/localforage@1.10.0:
|
||||
resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==}
|
||||
dependencies:
|
||||
lie: 3.1.1
|
||||
dev: false
|
||||
|
||||
/locate-character@3.0.0:
|
||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||
dev: true
|
||||
|
44
src/components/background.svelte
Normal file
44
src/components/background.svelte
Normal file
@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { processImage } from '$lib/graphics';
|
||||
import localforage from 'localforage';
|
||||
// Initialize IndexedDB
|
||||
localforage.config({
|
||||
driver: localforage.INDEXEDDB,
|
||||
name: 'audioDB'
|
||||
});
|
||||
|
||||
export let coverId: string;
|
||||
let canvas: HTMLCanvasElement;
|
||||
localforage.getItem(`${coverId}-cover`, function (err, file) {
|
||||
console.log(file);
|
||||
console.log(err);
|
||||
if (file) {
|
||||
const path = URL.createObjectURL(file as File);
|
||||
processImage(16, 4, 96, path, canvas);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="bg">
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
canvas {
|
||||
position: relative;
|
||||
width: 110%;
|
||||
height: 110%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
55
src/lib/graphics/index.ts
Normal file
55
src/lib/graphics/index.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { gaussianBlurHorizontal, gaussianBlurVertical, applyFilter } from "./utils";
|
||||
|
||||
export async function processImage(blockSize: number, resolutionFactor:number, radius:number, path: string, canvas: HTMLCanvasElement) {
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
const image = new Image();
|
||||
image.src = path; // Replace with your image path
|
||||
|
||||
image.onload = function () {
|
||||
// Resize image to 1/9 of the original resolution
|
||||
const resizedWidth = image.width / resolutionFactor;
|
||||
const resizedHeight = image.height / resolutionFactor;
|
||||
|
||||
const num_blocks_x = Math.floor(resizedWidth / blockSize);
|
||||
const num_blocks_y = Math.floor(resizedHeight / blockSize);
|
||||
|
||||
canvas.width = resizedWidth;
|
||||
canvas.height = resizedHeight;
|
||||
|
||||
for (let i = 0; i < num_blocks_y; i++) {
|
||||
for (let j = 0; j < num_blocks_x; j++) {
|
||||
const block = document.createElement("canvas");
|
||||
block.width = blockSize;
|
||||
block.height = blockSize;
|
||||
|
||||
const blockCtx = block.getContext("2d")!;
|
||||
blockCtx.drawImage(
|
||||
image,
|
||||
j * blockSize * resolutionFactor,
|
||||
i * blockSize * resolutionFactor,
|
||||
blockSize * resolutionFactor,
|
||||
blockSize * resolutionFactor,
|
||||
0,
|
||||
0,
|
||||
blockSize,
|
||||
blockSize
|
||||
);
|
||||
|
||||
applyFilter(block);
|
||||
|
||||
ctx.drawImage(block, j * blockSize, i * blockSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal Gaussian blur
|
||||
const imageDataHorizontal = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const blurredImageDataHorizontal = gaussianBlurHorizontal(imageDataHorizontal, radius);
|
||||
|
||||
// Vertical Gaussian blur
|
||||
const imageDataVertical = blurredImageDataHorizontal;
|
||||
const blurredImageData = gaussianBlurVertical(imageDataVertical, radius);
|
||||
|
||||
ctx.putImageData(blurredImageData, 0, 0);
|
||||
};
|
||||
}
|
191
src/lib/graphics/utils.js
Normal file
191
src/lib/graphics/utils.js
Normal file
@ -0,0 +1,191 @@
|
||||
// Function to apply filter to the block
|
||||
export function applyFilter(block) {
|
||||
let imageData = block.getContext("2d").getImageData(0, 0, block.width, block.height);
|
||||
let data = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
let hsv = rgbToHsv(data[i], data[i + 1], data[i + 2]);
|
||||
|
||||
// Mainly adjust saturation and value channels
|
||||
hsv[1] = Math.min(hsv[1] * 1.05 + 30 * Math.log(hsv[1] / 100 + 1), 100);
|
||||
hsv[2] = Math.min(hsv[2] * 1.3 + 40 * Math.log(hsv[2] / 100 + 1), 100);
|
||||
|
||||
let rgb = hsvToRgb(hsv[0], hsv[1], hsv[2]);
|
||||
|
||||
data[i] = rgb[0];
|
||||
data[i + 1] = rgb[1];
|
||||
data[i + 2] = rgb[2];
|
||||
}
|
||||
|
||||
block.getContext("2d").putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
// Function to perform horizontal Gaussian blur
|
||||
export function gaussianBlurHorizontal(imageData, radius) {
|
||||
const width = imageData.width;
|
||||
const height = imageData.height;
|
||||
|
||||
const newData = new Uint8ClampedArray(imageData.data);
|
||||
|
||||
const kernel = generateGaussianKernel(radius);
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0,
|
||||
a = 0;
|
||||
|
||||
for (let kx = -radius; kx <= radius; kx++) {
|
||||
const pixelX = Math.min(width - 1, Math.max(0, x + kx));
|
||||
|
||||
const kernelValue = kernel[kx + radius];
|
||||
|
||||
const index = (y * width + pixelX) * 4;
|
||||
|
||||
r += imageData.data[index] * kernelValue;
|
||||
g += imageData.data[index + 1] * kernelValue;
|
||||
b += imageData.data[index + 2] * kernelValue;
|
||||
a += imageData.data[index + 3] * kernelValue;
|
||||
}
|
||||
|
||||
const dataIndex = (y * width + x) * 4;
|
||||
newData[dataIndex] = r;
|
||||
newData[dataIndex + 1] = g;
|
||||
newData[dataIndex + 2] = b;
|
||||
newData[dataIndex + 3] = a;
|
||||
}
|
||||
}
|
||||
|
||||
return new ImageData(newData, width, height);
|
||||
}
|
||||
|
||||
// Function to perform vertical Gaussian blur
|
||||
export function gaussianBlurVertical(imageData, radius) {
|
||||
const width = imageData.width;
|
||||
const height = imageData.height;
|
||||
|
||||
const newData = new Uint8ClampedArray(imageData.data);
|
||||
|
||||
const kernel = generateGaussianKernel(radius);
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0,
|
||||
a = 0;
|
||||
|
||||
for (let ky = -radius; ky <= radius; ky++) {
|
||||
const pixelY = Math.min(height - 1, Math.max(0, y + ky));
|
||||
|
||||
const kernelValue = kernel[ky + radius];
|
||||
|
||||
const index = (pixelY * width + x) * 4;
|
||||
|
||||
r += imageData.data[index] * kernelValue;
|
||||
g += imageData.data[index + 1] * kernelValue;
|
||||
b += imageData.data[index + 2] * kernelValue;
|
||||
a += imageData.data[index + 3] * kernelValue;
|
||||
}
|
||||
|
||||
const dataIndex = (y * width + x) * 4;
|
||||
newData[dataIndex] = r;
|
||||
newData[dataIndex + 1] = g;
|
||||
newData[dataIndex + 2] = b;
|
||||
newData[dataIndex + 3] = a;
|
||||
}
|
||||
}
|
||||
|
||||
return new ImageData(newData, width, height);
|
||||
}
|
||||
|
||||
// Function to generate Gaussian kernel
|
||||
function generateGaussianKernel(radius) {
|
||||
const size = radius * 2 + 1;
|
||||
const kernel = [];
|
||||
|
||||
const sigma = 0.4 * ((radius - 1) * 0.5 - 1) + 0.8;
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
const x = i - radius;
|
||||
const exponent = Math.exp(-(x * x) / (2 * sigma * sigma));
|
||||
kernel[i] = exponent / (Math.sqrt(2 * Math.PI) * sigma);
|
||||
}
|
||||
|
||||
// Normalize the kernel
|
||||
const sum = kernel.reduce((a, b) => a + b, 0);
|
||||
return kernel.map((value) => value / sum);
|
||||
}
|
||||
|
||||
// Function to convert RGB to HSV
|
||||
function rgbToHsv(r, g, b) {
|
||||
(r /= 255), (g /= 255), (b /= 255);
|
||||
|
||||
let max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b),
|
||||
h,
|
||||
s,
|
||||
v = max;
|
||||
|
||||
let d = max - min;
|
||||
s = max === 0 ? 0 : d / max;
|
||||
|
||||
if (max === min) {
|
||||
h = 0; // achromatic
|
||||
} else {
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return [h * 360, s * 100, v * 100];
|
||||
}
|
||||
|
||||
// Function to convert HSV to RGB
|
||||
function hsvToRgb(h, s, v) {
|
||||
h /= 360;
|
||||
s /= 100;
|
||||
v /= 100;
|
||||
|
||||
let i = Math.floor(h * 6),
|
||||
f = h * 6 - i,
|
||||
p = v * (1 - s),
|
||||
q = v * (1 - f * s),
|
||||
t = v * (1 - (1 - f) * s),
|
||||
r,
|
||||
g,
|
||||
b;
|
||||
|
||||
switch (i % 6) {
|
||||
case 0:
|
||||
(r = v), (g = t), (b = p);
|
||||
break;
|
||||
case 1:
|
||||
(r = q), (g = v), (b = p);
|
||||
break;
|
||||
case 2:
|
||||
(r = p), (g = v), (b = t);
|
||||
break;
|
||||
case 3:
|
||||
(r = p), (g = q), (b = v);
|
||||
break;
|
||||
case 4:
|
||||
(r = t), (g = p), (b = v);
|
||||
break;
|
||||
case 5:
|
||||
(r = v), (g = p), (b = q);
|
||||
break;
|
||||
}
|
||||
|
||||
return [r * 255, g * 255, b * 255];
|
||||
}
|
@ -1 +0,0 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
1
src/routes/+page.server.js
Normal file
1
src/routes/+page.server.js
Normal file
@ -0,0 +1 @@
|
||||
export const ssr = false;
|
@ -1,2 +1,52 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import localforage from 'localforage';
|
||||
import Background from '../components/background.svelte';
|
||||
let audioInput: any;
|
||||
let coverInput: any;
|
||||
const audioId = 'd5a2e306-ddea-4fc3-9927-c79dcb3a4071';
|
||||
|
||||
// Initialize IndexedDB
|
||||
localforage.config({
|
||||
driver: localforage.INDEXEDDB,
|
||||
name: 'audioDB'
|
||||
});
|
||||
onMount(() => {
|
||||
// Handle audio input change
|
||||
audioInput.addEventListener('change', function (e: any) {
|
||||
const file: File = e.target.files[0];
|
||||
console.log(file.size);
|
||||
if (file) {
|
||||
localforage.setItem(audioId + '-file', file);
|
||||
}
|
||||
});
|
||||
|
||||
coverInput.addEventListener('change', function (e: any) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
localforage.setItem(audioId + '-cover', file);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>
|
||||
Select Audio File:
|
||||
<input type="file" bind:this={audioInput} />
|
||||
</p>
|
||||
<p>
|
||||
Select Cover File:
|
||||
<input type="file" bind:this={coverInput} />
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Input song id"
|
||||
id="audioId"
|
||||
value="d5a2e306-ddea-4fc3-9927-c79dcb3a4071"
|
||||
style="width: 36ch"
|
||||
/>
|
||||
|
||||
<Background coverId={audioId} />
|
@ -1,4 +1,5 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
@ -7,7 +8,8 @@ const config = {
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
},
|
||||
preprocess: vitePreprocess()
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
Loading…
Reference in New Issue
Block a user