From 694a7047a440856a9e40758f01004accd6236f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sat, 29 Nov 2025 17:51:00 +0100 Subject: [PATCH] Initial commit --- .claude/settings.local.json | 21 + .dockerignore | 28 + .gitignore | 32 + .nvmrc | 1 + Dockerfile | 33 + README.md | 108 ++ archetypes/default.md | 5 + archetypes/tracks.md | 17 + assets/css/main.css | 299 ++++ assets/js/logo/reactive-logo.js | 181 ++ assets/js/main.js | 257 +++ assets/js/visualizer/particles.js | 145 ++ assets/js/visualizer/scene.js | 110 ++ assets/jsconfig.json | 10 + config/_default/hugo.toml | 29 + config/_default/menus.toml | 14 + config/_default/params.toml | 16 + config/development/hugo.toml | 4 + config/production/hugo.toml | 7 + content/_index.md | 4 + content/about.md | 21 + content/imprint.md | 20 + content/tracks/_index.md | 4 + .../tracks/changed-her-mind-again/cover.png | Bin 0 -> 651039 bytes .../tracks/changed-her-mind-again/index.md | 17 + .../tracks/changed-her-mind-again/preview.mp4 | Bin 0 -> 801228 bytes content/tracks/shadow/cover.png | Bin 0 -> 1621975 bytes content/tracks/shadow/index.md | 17 + content/tracks/shadow/preview.mp4 | Bin 0 -> 3870139 bytes content/tracks/the-end-of-all/cover.png | Bin 0 -> 1808813 bytes content/tracks/the-end-of-all/index.md | 17 + content/tracks/the-end-of-all/preview.mp4 | Bin 0 -> 2026533 bytes content/tracks/the-moon/cover.png | Bin 0 -> 3666371 bytes content/tracks/the-moon/index.md | 17 + content/tracks/the-moon/preview.mp4 | Bin 0 -> 4905733 bytes layouts/_default/baseof.html | 138 ++ layouts/_default/home.html | 113 ++ layouts/_default/list.html | 26 + layouts/_default/single.html | 18 + layouts/partials/analytics.html | 8 + layouts/partials/footer.html | 34 + layouts/partials/head/favicon.html | 15 + layouts/partials/head/json-ld.html | 47 + layouts/partials/head/meta.html | 29 + layouts/partials/head/opengraph.html | 30 + layouts/partials/head/preload.html | 16 + layouts/partials/head/twitter.html | 21 + layouts/partials/header.html | 44 + layouts/partials/player.html | 122 ++ layouts/partials/track-card.html | 70 + layouts/tracks/list.html | 43 + layouts/tracks/single.html | 138 ++ nginx.conf | 48 + package.json | 23 + pnpm-lock.yaml | 1011 ++++++++++++ postcss.config.js | 5 + static/css/main.css | 1466 +++++++++++++++++ static/favicon/apple-touch-icon.png | Bin 0 -> 11068 bytes static/favicon/favicon-96x96.png | Bin 0 -> 5799 bytes static/favicon/favicon.ico | Bin 0 -> 15086 bytes static/favicon/favicon.svg | 63 + static/favicon/site.webmanifest | 21 + static/favicon/web-app-manifest-192x192.png | Bin 0 -> 12593 bytes static/favicon/web-app-manifest-512x512.png | Bin 0 -> 39026 bytes static/icon-large.svg | 61 + static/icon.svg | 61 + 66 files changed, 5105 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 archetypes/default.md create mode 100644 archetypes/tracks.md create mode 100644 assets/css/main.css create mode 100644 assets/js/logo/reactive-logo.js create mode 100644 assets/js/main.js create mode 100644 assets/js/visualizer/particles.js create mode 100644 assets/js/visualizer/scene.js create mode 100644 assets/jsconfig.json create mode 100644 config/_default/hugo.toml create mode 100644 config/_default/menus.toml create mode 100644 config/_default/params.toml create mode 100644 config/development/hugo.toml create mode 100644 config/production/hugo.toml create mode 100644 content/_index.md create mode 100644 content/about.md create mode 100644 content/imprint.md create mode 100644 content/tracks/_index.md create mode 100644 content/tracks/changed-her-mind-again/cover.png create mode 100644 content/tracks/changed-her-mind-again/index.md create mode 100644 content/tracks/changed-her-mind-again/preview.mp4 create mode 100644 content/tracks/shadow/cover.png create mode 100644 content/tracks/shadow/index.md create mode 100644 content/tracks/shadow/preview.mp4 create mode 100644 content/tracks/the-end-of-all/cover.png create mode 100644 content/tracks/the-end-of-all/index.md create mode 100644 content/tracks/the-end-of-all/preview.mp4 create mode 100644 content/tracks/the-moon/cover.png create mode 100644 content/tracks/the-moon/index.md create mode 100644 content/tracks/the-moon/preview.mp4 create mode 100755 layouts/_default/baseof.html create mode 100755 layouts/_default/home.html create mode 100755 layouts/_default/list.html create mode 100755 layouts/_default/single.html create mode 100755 layouts/partials/analytics.html create mode 100755 layouts/partials/footer.html create mode 100644 layouts/partials/head/favicon.html create mode 100755 layouts/partials/head/json-ld.html create mode 100755 layouts/partials/head/meta.html create mode 100755 layouts/partials/head/opengraph.html create mode 100755 layouts/partials/head/preload.html create mode 100755 layouts/partials/head/twitter.html create mode 100755 layouts/partials/header.html create mode 100755 layouts/partials/player.html create mode 100755 layouts/partials/track-card.html create mode 100755 layouts/tracks/list.html create mode 100755 layouts/tracks/single.html create mode 100644 nginx.conf create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.js create mode 100644 static/css/main.css create mode 100644 static/favicon/apple-touch-icon.png create mode 100644 static/favicon/favicon-96x96.png create mode 100644 static/favicon/favicon.ico create mode 100644 static/favicon/favicon.svg create mode 100644 static/favicon/site.webmanifest create mode 100644 static/favicon/web-app-manifest-192x192.png create mode 100644 static/favicon/web-app-manifest-512x512.png create mode 100644 static/icon-large.svg create mode 100644 static/icon.svg diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..3f8642b --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,21 @@ +{ + "permissions": { + "allow": [ + "Bash(chmod:*)", + "Bash(npx tailwindcss:*)", + "Bash(pnpm add:*)", + "Bash(pnpm exec tailwindcss:*)", + "Bash(pnpm css:*)", + "Bash(hugo:*)", + "WebFetch(domain:pivoine.art)", + "Bash(curl:*)", + "WebFetch(domain:github.com)", + "WebSearch", + "WebFetch(domain:htmx.org)", + "Bash(cat:*)", + "Bash(git remote:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4d53e16 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules/ + +# Build output +public/ +resources/ + +# Git +.git/ +.gitignore + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Hugo +hugo_stats.json + +# Development +*.log +*.md +!README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37282ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Dependencies +node_modules/ + +# Build output +public/ +resources/_gen/ + +# Hugo +hugo_stats.json +.hugo_build.lock + +# Environment +.env +.env.* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Cache +.cache/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..da8f41f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# Stage 1: Build +FROM node:22-alpine AS builder + +# Install Hugo +RUN apk add --no-cache hugo + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +# Copy package files first (for layer caching) +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +# Copy source files +COPY . . + +# Build CSS and Hugo site +RUN pnpm build + +# Stage 2: Production +FROM nginx:alpine + +# Copy built site +COPY --from=builder /app/public /usr/share/nginx/html + +# Copy nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c36fb2e --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +``` + · · · + · · + · · + · · + · · + · · + · · + · · + · · + · · + · · + · · + · · · +``` + +# PIVOINE.ART + +> Technology and sound, creating massive beats to push the boundaries of audio perception. + +--- + +## About + +An immersive audio experience. Electronic music meets WebGL visualization. +5000 particles react to every frequency. The logo breathes with the beat. + +Dark. Minimal. Precise. + +--- + +## Features + +``` +▸ WebGL Visualizer 5000 particles · 512 FFT · bass/mid/high reactivity +▸ Reactive Logo Canvas-based · audio-driven · mouse-aware +▸ Persistent Player Play · seek · volume · progress +▸ Web Audio API Real-time frequency analysis +▸ SPA Navigation htmx · smooth transitions · Alpine.js state +``` + +--- + +## Stack + +``` +Hugo ─────────────── Static site generation +Tailwind CSS 4 ───── Utility-first styling +Alpine.js ────────── Lightweight reactivity +Three.js ─────────── WebGL rendering +Web Audio API ────── Frequency analysis +pnpm ─────────────── Package management +``` + +--- + +## Development + +```bash +# Install dependencies +pnpm install + +# Development server +pnpm dev + +# Build for production +pnpm build +``` + +--- + +## Deploy + +```bash +# Build image +docker build -t pivoine-art . + +# Run container +docker run -p 8080:80 pivoine-art +``` + +--- + +## Structure + +``` +├── assets/ +│ ├── css/ # Tailwind source +│ └── js/ # Visualizer · Logo · Main +├── config/ # Hugo configuration +├── content/ # Markdown content +├── layouts/ # Hugo templates +└── static/ # Compiled assets +``` + +--- + +## Author + +**Valknar** +valknar@pivoine.art + +--- + +## License + +All audio content © Valknar. All rights reserved. +Code: MIT diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..25b6752 --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,5 @@ ++++ +date = '{{ .Date }}' +draft = true +title = '{{ replace .File.ContentBaseName "-" " " | title }}' ++++ diff --git a/archetypes/tracks.md b/archetypes/tracks.md new file mode 100644 index 0000000..4cda9d0 --- /dev/null +++ b/archetypes/tracks.md @@ -0,0 +1,17 @@ +--- +title: "{{ replace .File.ContentBaseName "-" " " | title }}" +date: {{ .Date }} +draft: true +description: "" + +# Audio +audio: "" +duration: "" + +# Metadata +artist: "Valknar" +genre: "" + +# Taxonomies +tags: [] +--- diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..ec3dfe6 --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,299 @@ +@import "tailwindcss"; + +@source "../../layouts/_default/*.html"; +@source "../../layouts/partials/*.html"; +@source "../../layouts/partials/head/*.html"; +@source "../../layouts/tracks/*.html"; + +html { + scroll-padding-top: 4rem; /* Account for fixed header height */ +} + +:root { + /* === SURFACE COLORS (Dark Theme) === */ + --color-surface-0: #0a0a0a; + --color-surface-1: #121212; + --color-surface-2: #1a1a1a; + --color-surface-3: #232323; + --color-surface-4: #2c2c2c; + + /* === TEXT COLORS === */ + --color-text-primary: #e8e8e8; + --color-text-secondary: #888888; + --color-text-muted: #555555; + + /* === ACCENT === */ + --color-accent: #ffffff; + --color-accent-dim: #666666; + --color-accent-glow: rgba(255, 255, 255, 0.1); + + /* === BORDERS === */ + --color-border: #2a2a2a; + + /* === TYPOGRAPHY === */ + --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace; + + /* === ANIMATION === */ + --duration-fast: 150ms; + --duration-normal: 300ms; + --duration-slow: 500ms; + --duration-page: 400ms; + --ease-out: cubic-bezier(0, 0, 0.2, 1); + + /* === SPACING === */ + --radius-sm: 0.125rem; + --radius-md: 0.25rem; + --radius-lg: 0.5rem; + --radius-full: 9999px; +} + +@theme inline { + /* Map CSS vars to Tailwind colors */ + --color-surface-0: var(--color-surface-0); + --color-surface-1: var(--color-surface-1); + --color-surface-2: var(--color-surface-2); + --color-surface-3: var(--color-surface-3); + --color-surface-4: var(--color-surface-4); + --color-text-primary: var(--color-text-primary); + --color-text-secondary: var(--color-text-secondary); + --color-text-muted: var(--color-text-muted); + --color-accent: var(--color-accent); + --color-border: var(--color-border); + + /* Font family */ + --font-family-mono: var(--font-mono); + + /* Z-index */ + --z-index-sticky: 200; + --z-index-visualizer: 5; + --z-index-player: 400; +} + +@layer base { + html { + font-family: var(--font-mono); + background: var(--color-surface-0); + color: var(--color-text-primary); + letter-spacing: 0.02em; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + } + + body { + min-height: 100vh; + } + + ::selection { + background: var(--color-accent); + color: var(--color-surface-0); + } + + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + ::-webkit-scrollbar-track { + background: var(--color-surface-1); + } + ::-webkit-scrollbar-thumb { + background: var(--color-surface-3); + border-radius: var(--radius-full); + } + ::-webkit-scrollbar-thumb:hover { + background: var(--color-surface-4); + } + + :focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; + } + + a { + color: var(--color-text-primary); + text-decoration: none; + transition: color var(--duration-fast) var(--ease-out); + } + a:hover { + color: var(--color-accent); + } +} + +@layer components { + /* htmx transitions */ + .htmx-request #main-content { + opacity: 0; + transform: translateY(8px); + transition: all var(--duration-page) var(--ease-out); + } + .htmx-settling #main-content { + opacity: 1; + transform: translateY(0); + } + + /* Animations */ + .fade-in-up { + opacity: 0; + transform: translateY(24px); + transition: all var(--duration-slow) var(--ease-out); + } + .fade-in-up.is-visible { + opacity: 1; + transform: translateY(0); + } + + .link-hover { + position: relative; + } + .link-hover::after { + content: ""; + position: absolute; + bottom: -2px; + left: 0; + width: 100%; + height: 1px; + background: var(--color-accent); + transform: scaleX(0); + transform-origin: right; + transition: transform var(--duration-normal) var(--ease-out); + } + .link-hover:hover::after { + transform: scaleX(1); + transform-origin: left; + } + + /* Audio Player */ + .audio-player { + background: var(--color-surface-1); + border-top: 1px solid var(--color-border); + } + .audio-player__progress { + appearance: none; + width: 100%; + height: 4px; + background: var(--color-surface-3); + cursor: pointer; + } + .audio-player__progress::-webkit-slider-thumb { + appearance: none; + width: 12px; + height: 12px; + background: var(--color-accent); + border-radius: var(--radius-full); + } + + /* Track Card */ + .track-card { + background: rgba(18, 18, 18, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--color-border); + transition: all var(--duration-normal) var(--ease-out); + } + .track-card:hover { + background: rgba(18, 18, 18, 0.8); + border-color: var(--color-surface-4); + transform: translateY(-2px); + } + .track-card__cover { + aspect-ratio: 1; + object-fit: cover; + filter: grayscale(20%); + transition: filter var(--duration-normal); + } + .track-card:hover .track-card__cover { + filter: grayscale(0%); + } + + /* Prose */ + .prose { + color: var(--color-text-secondary); + line-height: 1.75; + } + .prose h1, + .prose h2, + .prose h3, + .prose h4 { + color: var(--color-text-primary); + font-weight: 500; + margin-top: 2em; + margin-bottom: 0.5em; + } + .prose h1 { + font-size: 2rem; + } + .prose h2 { + font-size: 1.5rem; + } + .prose h3 { + font-size: 1.25rem; + } + .prose p { + margin-bottom: 1.25em; + } + .prose a { + text-decoration: underline; + text-underline-offset: 2px; + } + .prose strong { + color: var(--color-text-primary); + font-weight: 600; + } + .prose ul, + .prose ol { + margin-bottom: 1.25em; + padding-left: 1.5em; + } + .prose li { + margin-bottom: 0.5em; + } + .prose code { + background: var(--color-surface-2); + padding: 0.2em 0.4em; + border-radius: var(--radius-sm); + font-size: 0.9em; + } + .prose pre { + background: var(--color-surface-1); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 1em; + overflow-x: auto; + margin: 1.5em 0; + } + .prose pre code { + background: none; + padding: 0; + } +} + +@layer utilities { + .container-narrow { + max-width: 48rem; + margin-inline: auto; + padding-inline: 1.5rem; + } + .container-wide { + max-width: 80rem; + margin-inline: auto; + padding-inline: 1.5rem; + } + .z-sticky { + z-index: 200; + } + .z-visualizer { + z-index: -1; + opacity: 0.25; + } + .z-player { + z-index: 400; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/assets/js/logo/reactive-logo.js b/assets/js/logo/reactive-logo.js new file mode 100644 index 0000000..19a0236 --- /dev/null +++ b/assets/js/logo/reactive-logo.js @@ -0,0 +1,181 @@ +/** + * Reactive Logo + * Small WebGL canvas logo that reacts to audio and mouse + */ + +import * as THREE from 'three'; + +export class ReactiveLogo { + constructor(canvas, audioManager) { + this.canvas = canvas; + this.audioManager = audioManager; + this.mouse = { x: 0, y: 0 }; + this.running = false; + this.time = 0; + + if (!canvas) { + console.warn('ReactiveLogo: No canvas provided'); + return; + } + + this.init(); + } + + init() { + const size = 32; + + // Scene + this.scene = new THREE.Scene(); + + // Camera + this.camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100); + this.camera.position.z = 3; + + // Renderer + this.renderer = new THREE.WebGLRenderer({ + canvas: this.canvas, + alpha: true, + antialias: true + }); + this.renderer.setSize(size, size); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + + // Create logo geometry (simple points in a circular pattern) + this.createLogoParticles(); + + // Mouse tracking + this.canvas.addEventListener('mouseenter', () => this.onMouseEnter()); + this.canvas.addEventListener('mouseleave', () => this.onMouseLeave()); + window.addEventListener('mousemove', (e) => this.onMouseMove(e)); + + this.start(); + } + + createLogoParticles() { + const count = 50; + const geometry = new THREE.BufferGeometry(); + const positions = new Float32Array(count * 3); + const randoms = new Float32Array(count); + + // Create points in a circular pattern + for (let i = 0; i < count; i++) { + const angle = (i / count) * Math.PI * 2; + const radius = 0.8 + Math.random() * 0.2; + + positions[i * 3] = Math.cos(angle) * radius; + positions[i * 3 + 1] = Math.sin(angle) * radius; + positions[i * 3 + 2] = (Math.random() - 0.5) * 0.2; + + randoms[i] = Math.random(); + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1)); + + const material = new THREE.ShaderMaterial({ + vertexShader: ` + uniform float uTime; + uniform float uAudio; + attribute float aRandom; + + void main() { + vec3 pos = position; + + // Bouncy pulsing + float pulse = 1.0 + sin(uTime * 5.0 + aRandom * 6.28) * 0.15; + pulse += sin(uTime * 8.0 + aRandom * 3.14) * 0.1; + pos *= pulse; + + // Audio reactivity - bouncy + float audioBounce = uAudio * (1.0 + sin(uTime * 10.0) * 0.3); + pos *= 1.0 + audioBounce * 0.6; + + vec4 mvPos = modelViewMatrix * vec4(pos, 1.0); + gl_PointSize = 3.0 * (1.0 + uAudio * 0.8); + gl_Position = projectionMatrix * mvPos; + } + `, + fragmentShader: ` + void main() { + float dist = length(gl_PointCoord - 0.5); + if (dist > 0.5) discard; + float alpha = 1.0 - smoothstep(0.2, 0.5, dist); + gl_FragColor = vec4(1.0, 1.0, 1.0, alpha); + } + `, + uniforms: { + uTime: { value: 0 }, + uAudio: { value: 0 } + }, + transparent: true, + blending: THREE.AdditiveBlending, + depthWrite: false + }); + + this.logoMesh = new THREE.Points(geometry, material); + this.scene.add(this.logoMesh); + } + + onMouseEnter() { + this.isHovered = true; + } + + onMouseLeave() { + this.isHovered = false; + } + + onMouseMove(e) { + if (!this.isHovered) return; + const rect = this.canvas.getBoundingClientRect(); + this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + } + + start() { + if (this.running) return; + this.running = true; + this.animate(); + } + + pause() { + this.running = false; + } + + animate() { + if (!this.running) return; + requestAnimationFrame(() => this.animate()); + + this.time += 0.016; + + // Get audio level + let audioLevel = 0; + if (this.audioManager?.isInitialized) { + const bands = this.audioManager.getFrequencyBands(); + audioLevel = (bands.low + bands.mid + bands.high) / 3; + } + + // Update uniforms + if (this.logoMesh?.material?.uniforms) { + this.logoMesh.material.uniforms.uTime.value = this.time; + this.logoMesh.material.uniforms.uAudio.value = audioLevel; + } + + // Rotate based on mouse + if (this.logoMesh) { + this.logoMesh.rotation.x += (this.mouse.y * 0.5 - this.logoMesh.rotation.x) * 0.1; + this.logoMesh.rotation.y += (this.mouse.x * 0.5 - this.logoMesh.rotation.y) * 0.1; + this.logoMesh.rotation.z = this.time * 0.2; + } + + this.renderer.render(this.scene, this.camera); + } + + destroy() { + this.running = false; + this.logoMesh?.geometry?.dispose(); + this.logoMesh?.material?.dispose(); + this.renderer?.dispose(); + } +} + +export default ReactiveLogo; diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..a57061e --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,257 @@ +/** + * Pivoine.art - Main JavaScript + * Audio blog with WebGL visualizers + */ + +import { Visualizer } from './visualizer/scene.js'; +import { ReactiveLogo } from './logo/reactive-logo.js'; + +// Audio Manager - Web Audio API wrapper +class AudioManager { + constructor() { + this.audio = document.createElement('audio'); + this.audio.crossOrigin = 'anonymous'; + this.audioContext = null; + this.analyser = null; + this.source = null; + this.frequencyData = null; + this.isInitialized = false; + } + + async init() { + if (this.isInitialized) return; + + try { + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + this.analyser = this.audioContext.createAnalyser(); + this.analyser.fftSize = 512; + this.analyser.smoothingTimeConstant = 0.8; + + this.source = this.audioContext.createMediaElementSource(this.audio); + this.source.connect(this.analyser); + this.analyser.connect(this.audioContext.destination); + + this.frequencyData = new Uint8Array(this.analyser.frequencyBinCount); + this.isInitialized = true; + + // Update Alpine store on audio events + this.audio.addEventListener('timeupdate', () => { + if (window.Alpine) { + Alpine.store('audio').progress = this.audio.currentTime; + Alpine.store('audio').duration = this.audio.duration || 0; + } + }); + + this.audio.addEventListener('ended', () => { + if (window.Alpine) { + Alpine.store('audio').isPlaying = false; + } + }); + + this.audio.addEventListener('play', () => { + if (window.Alpine) { + Alpine.store('audio').isPlaying = true; + } + }); + + this.audio.addEventListener('pause', () => { + if (window.Alpine) { + Alpine.store('audio').isPlaying = false; + } + }); + } catch (e) { + console.error('Failed to initialize audio context:', e); + } + } + + async play(url) { + await this.init(); + + if (this.audioContext?.state === 'suspended') { + await this.audioContext.resume(); + } + + if (url && url !== this.audio.src) { + this.audio.src = url; + } + + await this.audio.play(); + } + + pause() { + this.audio.pause(); + } + + toggle() { + if (this.audio.paused) { + this.audio.play(); + } else { + this.audio.pause(); + } + } + + seek(time) { + this.audio.currentTime = time; + } + + setVolume(v) { + this.audio.volume = Math.max(0, Math.min(1, v)); + } + + getFrequencyData() { + if (this.analyser) { + this.analyser.getByteFrequencyData(this.frequencyData); + } + return this.frequencyData; + } + + getFrequencyBands() { + const data = this.getFrequencyData(); + if (!data) return { low: 0, mid: 0, high: 0 }; + + const len = data.length; + const low = this._avg(data, 0, len * 0.1) / 255; + const mid = this._avg(data, len * 0.1, len * 0.5) / 255; + const high = this._avg(data, len * 0.5, len) / 255; + return { low, mid, high }; + } + + _avg(data, start, end) { + let sum = 0; + for (let i = Math.floor(start); i < Math.floor(end); i++) { + sum += data[i]; + } + return sum / (end - start); + } +} + +// Initialize global instances +if (!window.__pivoine) { + const audioManager = new AudioManager(); + + window.__pivoine = { + audioManager, + visualizer: null, + logo: null + }; + + // Initialize WebGL components after DOM is ready + const initWebGL = () => { + // Main visualizer (fullscreen background) + const visualizerCanvas = document.getElementById('visualizer'); + if (visualizerCanvas && !window.__pivoine.visualizer) { + window.__pivoine.visualizer = new Visualizer(visualizerCanvas, audioManager); + } + + // Logo in header + const logoCanvas = document.getElementById('logo-canvas'); + if (logoCanvas && !window.__pivoine.logo) { + window.__pivoine.logo = new ReactiveLogo(logoCanvas, audioManager); + } + }; + + // Initialize on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initWebGL); + } else { + initWebGL(); + } +} + +// Alpine.js initialization +document.addEventListener('alpine:init', () => { + // Global audio store + Alpine.store('audio', { + currentTrack: null, + isPlaying: false, + progress: 0, + duration: 0, + volume: 0.8 + }); + + // Main app component + Alpine.data('app', () => ({ + init() { + // Restore volume from localStorage + const savedVolume = localStorage.getItem('pivoine-volume'); + if (savedVolume) { + Alpine.store('audio').volume = parseFloat(savedVolume); + window.__pivoine.audioManager.setVolume(parseFloat(savedVolume)); + } + } + })); + + // Player UI component + Alpine.data('playerUI', () => ({ + togglePlay() { + window.__pivoine.audioManager.toggle(); + }, + + seek(time) { + window.__pivoine.audioManager.seek(parseFloat(time)); + }, + + setVolume(v) { + const volume = parseFloat(v); + Alpine.store('audio').volume = volume; + window.__pivoine.audioManager.setVolume(volume); + localStorage.setItem('pivoine-volume', volume); + }, + + toggleMute() { + const store = Alpine.store('audio'); + if (store.volume > 0) { + this._previousVolume = store.volume; + this.setVolume(0); + } else { + this.setVolume(this._previousVolume || 0.8); + } + }, + + formatTime(seconds) { + if (!seconds || isNaN(seconds)) return '0:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; + }, + + _previousVolume: 0.8 + })); +}); + +// htmx lifecycle hooks +document.body.addEventListener('htmx:beforeSwap', () => { + // Pause visualizer during page transition for performance + window.__pivoine?.visualizer?.pause(); +}); + +document.body.addEventListener('htmx:afterSwap', () => { + // Resume visualizer after swap + window.__pivoine?.visualizer?.resume(); + // Re-initialize scroll animations + window.dispatchEvent(new CustomEvent('page:loaded')); +}); + +// Scroll animations with Intersection Observer +function initScrollAnimations() { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('is-visible'); + } + }); + }, + { threshold: 0.1, rootMargin: '0px 0px -50px 0px' } + ); + + document.querySelectorAll('.fade-in-up').forEach((el) => { + observer.observe(el); + }); +} + +// Initialize on page load +document.addEventListener('DOMContentLoaded', initScrollAnimations); +window.addEventListener('page:loaded', initScrollAnimations); + +export { AudioManager }; diff --git a/assets/js/visualizer/particles.js b/assets/js/visualizer/particles.js new file mode 100644 index 0000000..dfb6adb --- /dev/null +++ b/assets/js/visualizer/particles.js @@ -0,0 +1,145 @@ +/** + * Particle System + * Audio-reactive 3D particle cloud + */ + +import * as THREE from 'three'; + +// Vertex Shader +const vertexShader = ` +uniform float uTime; +uniform float uLow; +uniform float uMid; +uniform float uHigh; +uniform float uSize; + +attribute float aRandom; +attribute float aScale; + +varying float vAlpha; + +void main() { + vec3 pos = position; + + // Base rotation + float angle = uTime * 0.2; + mat3 rotation = mat3( + cos(angle), 0.0, sin(angle), + 0.0, 1.0, 0.0, + -sin(angle), 0.0, cos(angle) + ); + pos = rotation * pos; + + // Bass affects scale/expansion + float bassScale = 1.0 + uLow * 0.4; + pos *= bassScale; + + // Mid frequencies create wave motion + float wave = sin(pos.x * 0.1 + uTime * 2.0) * uMid * 8.0; + pos.y += wave * aRandom; + + // High frequencies add jitter/sparkle + vec3 jitter = normalize(pos) * uHigh * aRandom * 5.0; + pos += jitter; + + // Calculate view position + vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); + + // Point size with perspective + float size = uSize * aScale; + size *= (1.0 + uLow * 0.5); // Pulse with bass + gl_PointSize = size * (300.0 / -mvPosition.z); + + // Alpha based on distance and audio + vAlpha = 0.6 + uLow * 0.3; + + gl_Position = projectionMatrix * mvPosition; +} +`; + +// Fragment Shader +const fragmentShader = ` +varying float vAlpha; + +void main() { + // Circular point with soft edge + float dist = length(gl_PointCoord - vec2(0.5)); + if (dist > 0.5) discard; + + float alpha = 1.0 - smoothstep(0.2, 0.5, dist); + alpha *= vAlpha; + + // Pure white color for minimal aesthetic + gl_FragColor = vec4(1.0, 1.0, 1.0, alpha); +} +`; + +export class ParticleSystem { + constructor(count = 5000) { + this.count = count; + this.createGeometry(); + this.createMaterial(); + this.mesh = new THREE.Points(this.geometry, this.material); + } + + createGeometry() { + this.geometry = new THREE.BufferGeometry(); + + const positions = new Float32Array(this.count * 3); + const randoms = new Float32Array(this.count); + const scales = new Float32Array(this.count); + + for (let i = 0; i < this.count; i++) { + const i3 = i * 3; + + // Spherical distribution + const radius = 20 + Math.random() * 30; + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2 * Math.random() - 1); + + positions[i3] = radius * Math.sin(phi) * Math.cos(theta); + positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta); + positions[i3 + 2] = radius * Math.cos(phi); + + randoms[i] = Math.random(); + scales[i] = 0.5 + Math.random() * 1.5; + } + + this.geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + this.geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1)); + this.geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1)); + } + + createMaterial() { + this.material = new THREE.ShaderMaterial({ + vertexShader, + fragmentShader, + uniforms: { + uTime: { value: 0 }, + uLow: { value: 0 }, + uMid: { value: 0 }, + uHigh: { value: 0 }, + uSize: { value: 2.0 } + }, + transparent: true, + blending: THREE.AdditiveBlending, + depthWrite: false + }); + } + + update(bands, time) { + if (!this.material) return; + + this.material.uniforms.uTime.value = time; + this.material.uniforms.uLow.value = bands.low || 0; + this.material.uniforms.uMid.value = bands.mid || 0; + this.material.uniforms.uHigh.value = bands.high || 0; + } + + dispose() { + this.geometry?.dispose(); + this.material?.dispose(); + } +} + +export default ParticleSystem; diff --git a/assets/js/visualizer/scene.js b/assets/js/visualizer/scene.js new file mode 100644 index 0000000..deaf287 --- /dev/null +++ b/assets/js/visualizer/scene.js @@ -0,0 +1,110 @@ +/** + * WebGL Visualizer Scene + * Three.js particle system that reacts to audio frequency data + */ + +import * as THREE from 'three'; +import { ParticleSystem } from './particles.js'; + +export class Visualizer { + constructor(canvas, audioManager) { + this.canvas = canvas; + this.audioManager = audioManager; + this.running = false; + this.time = 0; + + if (!canvas) { + console.warn('Visualizer: No canvas provided'); + return; + } + + this.init(); + } + + init() { + // Scene setup + this.scene = new THREE.Scene(); + + // Camera + this.camera = new THREE.PerspectiveCamera( + 75, + window.innerWidth / window.innerHeight, + 0.1, + 1000 + ); + this.camera.position.z = 50; + + // Renderer + this.renderer = new THREE.WebGLRenderer({ + canvas: this.canvas, + alpha: true, + antialias: true, + powerPreference: 'high-performance' + }); + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + + // Particles + this.particles = new ParticleSystem(5000); + this.scene.add(this.particles.mesh); + + // Event listeners + window.addEventListener('resize', () => this.resize()); + + // Start animation + this.start(); + } + + resize() { + if (!this.camera || !this.renderer) return; + + this.camera.aspect = window.innerWidth / window.innerHeight; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(window.innerWidth, window.innerHeight); + } + + start() { + if (this.running) return; + this.running = true; + this.animate(); + } + + pause() { + this.running = false; + } + + resume() { + this.start(); + } + + animate() { + if (!this.running) return; + requestAnimationFrame(() => this.animate()); + + this.time += 0.01; + + // Get audio frequency bands + let bands = { low: 0, mid: 0, high: 0 }; + if (this.audioManager?.isInitialized) { + bands = this.audioManager.getFrequencyBands(); + } + + // Update particles with audio data + this.particles.update(bands, this.time); + + // Subtle camera movement + this.camera.position.x = Math.sin(this.time * 0.1) * 3; + this.camera.position.y = Math.cos(this.time * 0.15) * 2; + this.camera.lookAt(0, 0, 0); + + this.renderer.render(this.scene, this.camera); + } + + destroy() { + this.running = false; + this.particles?.dispose(); + this.renderer?.dispose(); + } +} + +export default Visualizer; diff --git a/assets/jsconfig.json b/assets/jsconfig.json new file mode 100644 index 0000000..377218c --- /dev/null +++ b/assets/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": [ + "*" + ] + } + } +} \ No newline at end of file diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml new file mode 100644 index 0000000..20809c9 --- /dev/null +++ b/config/_default/hugo.toml @@ -0,0 +1,29 @@ +baseURL = "https://pivoine.art/" +languageCode = "en-us" +title = "Valknar's" + +[permalinks] + tracks = "/tracks/:slug/" + +[taxonomies] + tag = "tags" + genre = "genres" + +[outputs] + home = ["HTML", "RSS"] + section = ["HTML", "RSS"] + +[sitemap] + changefreq = "weekly" + priority = 0.5 + +[markup.goldmark.renderer] + unsafe = true + +[build] + writeStats = true + +[module] + [module.hugoVersion] + extended = false + min = "0.128.0" diff --git a/config/_default/menus.toml b/config/_default/menus.toml new file mode 100644 index 0000000..107e918 --- /dev/null +++ b/config/_default/menus.toml @@ -0,0 +1,14 @@ +[[main]] + name = "Tracks" + url = "/tracks/" + weight = 10 + +[[main]] + name = "About" + url = "/about/" + weight = 20 + +[[main]] + name = "Imprint" + url = "/imprint/" + weight = 30 diff --git a/config/_default/params.toml b/config/_default/params.toml new file mode 100644 index 0000000..7b281b2 --- /dev/null +++ b/config/_default/params.toml @@ -0,0 +1,16 @@ +description = "Valknar's audio" +author = "Valknar" +email = "valknar@pivoine.art" + +[jellyfin] + baseURL = "https://jellyfin.media.pivoine.art" + # API key via environment variable: HUGO_PARAMS_JELLYFIN_APIKEY + +[umami] + enabled = true + websiteID = "" # Set your Umami website ID + src = "" # Your Umami instance URL + +[visualizer] + particleCount = 5000 + fftSize = 512 diff --git a/config/development/hugo.toml b/config/development/hugo.toml new file mode 100644 index 0000000..a283b79 --- /dev/null +++ b/config/development/hugo.toml @@ -0,0 +1,4 @@ +baseURL = "http://localhost:1313/" + +[params.umami] + enabled = false diff --git a/config/production/hugo.toml b/config/production/hugo.toml new file mode 100644 index 0000000..c61b0b4 --- /dev/null +++ b/config/production/hugo.toml @@ -0,0 +1,7 @@ +baseURL = "https://pivoine.art/" + +[minify] + minifyOutput = true + +[params.umami] + enabled = true diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..fe01af1 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,4 @@ +--- +title: "Valknar's" +description: "Valknar's audio" +--- diff --git a/content/about.md b/content/about.md new file mode 100644 index 0000000..a2663ac --- /dev/null +++ b/content/about.md @@ -0,0 +1,21 @@ +--- +title: "About" +description: "About Valknar and his music" +--- + +## About Valknar + +Technology and sound, creating massive beats to push the boundaries of audio perception. + +### Equipment + +Debian GNU/Linux 13, KDE-Plasma 6 on Wayland, 16 × AMD Ryzen 7 8745HS, AI + +### Dedication + +The love of my life, Palina. + +### Contact + +For collaborations, licensing, or just to say hello: +**valknar@pivoine.art** diff --git a/content/imprint.md b/content/imprint.md new file mode 100644 index 0000000..90b2fc6 --- /dev/null +++ b/content/imprint.md @@ -0,0 +1,20 @@ +--- +title: "Imprint" +description: "Legal information and imprint" +--- + +## Imprint + +**Responsible for content:** + +[Valknar](mailto:valknar@pivoine.art) + +## Privacy + +This website uses [Umami](https://umami.is) for privacy-respecting analytics. No personal data is collected or shared with third parties. + +## Copyright + +All audio content on this site is created by Valknar unless otherwise noted. All rights reserved. + +For licensing inquiries, please contact valknar@pivoine.art. diff --git a/content/tracks/_index.md b/content/tracks/_index.md new file mode 100644 index 0000000..a60a53e --- /dev/null +++ b/content/tracks/_index.md @@ -0,0 +1,4 @@ +--- +title: "Tracks" +description: "All audio" +--- diff --git a/content/tracks/changed-her-mind-again/cover.png b/content/tracks/changed-her-mind-again/cover.png new file mode 100644 index 0000000000000000000000000000000000000000..da494842deae0890c62b6c662117870653118571 GIT binary patch literal 651039 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