frontend for xcvr appview

maybe this should work?

+211 -13
+8 -1
package-lock.json
··· 12 12 "@protobuf-ts/runtime": "^2.11.0", 13 13 "@rachel-mp4/lrcproto": "^1.0.4", 14 14 "fast-diff": "^1.3.0", 15 - "linkifyjs": "^4.3.2" 15 + "linkifyjs": "^4.3.2", 16 + "ogl": "^1.0.11" 16 17 }, 17 18 "devDependencies": { 18 19 "@sveltejs/adapter-auto": "^6.0.0", ··· 1291 1292 "engines": { 1292 1293 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1293 1294 } 1295 + }, 1296 + "node_modules/ogl": { 1297 + "version": "1.0.11", 1298 + "resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz", 1299 + "integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==", 1300 + "license": "Unlicense" 1294 1301 }, 1295 1302 "node_modules/picocolors": { 1296 1303 "version": "1.1.1",
+2 -1
package.json
··· 26 26 "@protobuf-ts/runtime": "^2.11.0", 27 27 "@rachel-mp4/lrcproto": "^1.0.4", 28 28 "fast-diff": "^1.3.0", 29 - "linkifyjs": "^4.3.2" 29 + "linkifyjs": "^4.3.2", 30 + "ogl": "^1.0.11" 30 31 } 31 32 }
+2 -1
src/app.html
··· 74 74 </filter> 75 75 </defs> 76 76 </svg> 77 - <div style="display: flex; flex-direction:column; justify-content: space-between; height:100vh; height: 100dvh;">%sveltekit.body%</div><script type="module"> 77 + <div style="display: flex; flex-direction:column; justify-content: space-between; height:100vh; height: 100dvh;">%sveltekit.body%</div> 78 + <script type="module"> 78 79 import { 79 80 Polyline, 80 81 Renderer,
+197
src/lib/components/CursorEffect.svelte
··· 1 + <script lang="ts"> 2 + import { onMount, onDestroy } from "svelte"; 3 + import { browser } from "$app/environment"; 4 + import { Polyline, Renderer, Transform, Vec3, Color } from "ogl"; 5 + interface Props { 6 + enabled: boolean; 7 + colors?: Array<string>; 8 + } 9 + 10 + let { enabled, colors = ["#e18f39", "#c5c042", "#387f4d", "#1d4633"] } = 11 + $props(); 12 + 13 + let canvas: undefined | HTMLCanvasElement = $state(); 14 + const thickness: [number, number] = [14, 35]; 15 + const spring: [number, number] = [0.02, 0.1]; 16 + const friction: [number, number] = [0.7, 0.95]; 17 + let animationId: null | number = null; 18 + let renderer: null | Renderer = null; 19 + let scene: Transform; 20 + let lines = [] as { 21 + points: Vec3[]; 22 + polyline: Polyline; 23 + spring: number; 24 + friction: number; 25 + mouseVelocity: Vec3; 26 + mouseOffset: Vec3; 27 + }[]; 28 + let mouse: Vec3; 29 + let tmp: Vec3; 30 + const vertex = ` 31 + attribute vec3 position; 32 + attribute vec3 next; 33 + attribute vec3 prev; 34 + attribute vec2 uv; 35 + attribute float side; 36 + 37 + uniform vec2 uResolution; 38 + uniform float uDPR; 39 + uniform float uThickness; 40 + 41 + vec4 getPosition() { 42 + vec2 aspect = vec2(uResolution.x / uResolution.y, 1); 43 + vec2 nextScreen = next.xy * aspect; 44 + vec2 prevScreen = prev.xy * aspect; 45 + 46 + vec2 tangent = normalize(nextScreen - prevScreen); 47 + vec2 normal = vec2(-tangent.y, tangent.x); 48 + normal /= aspect; 49 + normal *= 1.0 - pow(abs(uv.y - 0.5) * 1.9, 2.0); 50 + 51 + float pixelWidth = 1.0 / (uResolution.y / uDPR); 52 + normal *= pixelWidth * uThickness; 53 + 54 + float dist = length(nextScreen - prevScreen); 55 + normal *= smoothstep(0.0, 0.02, dist); 56 + 57 + vec4 current = vec4(position, 1); 58 + current.xy -= normal * side; 59 + return current; 60 + } 61 + 62 + void main() { 63 + gl_Position = getPosition(); 64 + } 65 + `; 66 + const random = (array: [number, number]): number => { 67 + const alpha = Math.random(); 68 + return array[0] * (1.0 - alpha) + array[1] * alpha; 69 + }; 70 + function initializeCursorEffect() { 71 + renderer = new Renderer({ 72 + dpr: 2, 73 + alpha: true, 74 + premultipliedAlpha: true, 75 + canvas, 76 + }); 77 + if (!browser || !enabled) return; 78 + const gl = renderer.gl; 79 + gl.clearColor(0, 0, 0, 0); 80 + gl.enable(gl.BLEND); 81 + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 82 + scene = new Transform(); 83 + mouse = new Vec3(); 84 + tmp = new Vec3(); 85 + colors.forEach((color) => { 86 + const count = 20; 87 + const points = [] as Vec3[]; 88 + for (let i = 0; i < count; i++) { 89 + points.push(new Vec3()); 90 + } 91 + const line = { 92 + points: points, 93 + polyline: new Polyline(gl, { 94 + points, 95 + vertex, 96 + uniforms: { 97 + uColor: { value: new Color(color) }, 98 + uThickness: { value: random(thickness) }, 99 + }, 100 + }), 101 + spring: random(spring), 102 + friction: random(friction), 103 + mouseVelocity: new Vec3(), 104 + mouseOffset: new Vec3(random([-1, 1]) * 0.02), 105 + }; 106 + line.polyline.mesh.setParent(scene); 107 + lines.push(line); 108 + }); 109 + resize(); 110 + startAnimation(); 111 + } 112 + function resize() { 113 + renderer?.setSize(window.innerWidth, window.innerHeight); 114 + lines.forEach((line) => line.polyline.resize()); 115 + } 116 + function updateMouse(e: MouseEvent) { 117 + if (!mouse) return; 118 + const x = e.pageX; 119 + const y = e.pageY; 120 + mouse.set( 121 + (x / (renderer?.gl.renderer.width ?? 1)) * 2 - 1, 122 + (y / (renderer?.gl.renderer.height ?? 1)) * -2 + 1, 123 + 0, 124 + ); 125 + } 126 + function update() { 127 + if (!enabled) { 128 + animationId = null; 129 + return; 130 + } 131 + animationId = requestAnimationFrame(update); 132 + lines.forEach((line) => { 133 + for (let i = line.points.length - 1; i >= 0; i--) { 134 + if (!i) { 135 + tmp 136 + .copy(mouse) 137 + .add(line.mouseOffset) 138 + .sub(line.points[i]) 139 + .multiply(line.spring); 140 + line.mouseVelocity.add(tmp).multiply(line.friction); 141 + line.points[i].add(line.mouseVelocity); 142 + } else { 143 + line.points[i].lerp(line.points[i - 1], 0.9); 144 + } 145 + } 146 + line.polyline.updateGeometry(); 147 + }); 148 + renderer?.render({ scene }); 149 + } 150 + function startAnimation() { 151 + if (!animationId) { 152 + requestAnimationFrame(update); 153 + } 154 + } 155 + function stopAnimation() { 156 + if (animationId) { 157 + cancelAnimationFrame(animationId); 158 + animationId = null; 159 + } 160 + } 161 + $effect(() => { 162 + if (enabled) { 163 + startAnimation(); 164 + } else { 165 + stopAnimation(); 166 + } 167 + }); 168 + onMount(() => { 169 + if (!browser) return; 170 + initializeCursorEffect(); 171 + window.addEventListener("resize", resize); 172 + window.addEventListener("mousemove", updateMouse); 173 + }); 174 + onDestroy(() => { 175 + if (!browser) return; 176 + stopAnimation(); 177 + window.removeEventListener("resize", resize); 178 + window.removeEventListener("mousemove", updateMouse); 179 + }); 180 + </script> 181 + 182 + {#if enabled} 183 + <canvas bind:this={canvas} id="cursor-canvas"></canvas> 184 + {/if} 185 + 186 + <style> 187 + #cursor-canvas { 188 + position: fixed; 189 + top: 0; 190 + left: 0; 191 + width: 100vw; 192 + height: 100vw; 193 + pointer-events: none; 194 + z-index: -1; 195 + filter: blur(1rem); 196 + } 197 + </style>
+2
src/routes/+layout.svelte
··· 4 4 import type { LayoutProps } from "./$types"; 5 5 import Spectrum from "$lib/components/Spectrum.svelte"; 6 6 import { browser } from "$app/environment"; 7 + import CursorEffect from "$lib/components/CursorEffect.svelte"; 7 8 let { data, children }: LayoutProps = $props(); 8 9 let innerWidth = $state(0); 9 10 let isDesktop = $derived(innerWidth > 1000); ··· 167 168 <label for="ccccc">ccccc</label> 168 169 </div> 169 170 </div> 171 + <CursorEffect enabled={true} /> 170 172 {/if} 171 173 172 174 <style>
-10
static/main.css
··· 100 100 #content:not(.desktop) .desktop { 101 101 display: none; 102 102 } 103 - #cursor-canvas { 104 - position: fixed; 105 - top: 0; 106 - left: 0; 107 - width: 100vw; 108 - height: 100vw; 109 - pointer-events: none; 110 - z-index: -1; 111 - filter: blur(1rem); 112 - }