diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html
index c521641..36321d9 100644
--- a/plugin/contents/ui/dancer.html
+++ b/plugin/contents/ui/dancer.html
@@ -69,41 +69,11 @@ html, body {
/* ============================================================
RETROWAVE GRID FLOOR
============================================================ */
-.floor-wrap {
+#floorCanvas {
position: absolute;
- bottom: 0; left: 0; right: 0;
- height: 42%;
- overflow: hidden;
+ bottom: 0; left: 0;
pointer-events: none;
}
-.floor {
- position: absolute;
- bottom: 0;
- left: -100%; right: -100%;
- height: 400%;
- transform: perspective(700px) rotateX(55deg);
- transform-origin: bottom center;
- background-image:
- repeating-linear-gradient(
- 90deg,
- transparent 0,
- transparent calc(5% - 2px),
- rgba(0, 200, 255, 0.3) calc(5% - 2px),
- rgba(0, 200, 255, 0.3) 5%
- ),
- repeating-linear-gradient(
- 0deg,
- transparent 0,
- transparent 48px,
- rgba(0, 200, 255, 0.2) 48px,
- rgba(0, 200, 255, 0.2) 50px
- );
- animation: gridScroll 0.833s linear infinite;
-}
-@keyframes gridScroll {
- from { background-position: 0 0, 0 0; }
- to { background-position: 0 0, 0 50px; }
-}
/* Horizon glow line */
.horizon {
@@ -716,14 +686,12 @@ html, body {
+
+
+
-
-
-
@@ -878,13 +846,76 @@ window.addEventListener('resize', resizeCanvas);
resizeCanvas();
requestAnimationFrame(drawStars);
-const floorEl = document.querySelector('.floor');
-function updateFloorPerspective() {
- const p = Math.round(window.innerHeight * 1.05);
- floorEl.style.transform = `perspective(${p}px) rotateX(55deg)`;
+/* ============================================================
+ FLOOR CANVAS — perspective grid with true vanishing point
+ ============================================================ */
+const floorCanvas = document.getElementById('floorCanvas');
+const floorCtx = floorCanvas.getContext('2d');
+
+const FLOOR_RATIO = 0.42;
+const V_LINES = 24;
+const H_LINES = 24;
+let floorScroll = 0;
+let lastFloorTs = 0;
+
+function resizeFloor() {
+ floorCanvas.width = window.innerWidth;
+ floorCanvas.height = Math.round(window.innerHeight * FLOOR_RATIO);
}
-window.addEventListener('resize', updateFloorPerspective);
-updateFloorPerspective();
+
+function drawFloor(ts) {
+ const dt = lastFloorTs ? Math.min((ts - lastFloorTs) / 1000, 0.1) : 0;
+ lastFloorTs = ts;
+ floorScroll = (floorScroll + 0.06 * dt) % 1;
+
+ const W = floorCanvas.width;
+ const H = floorCanvas.height;
+ const vx = W / 2;
+
+ floorCtx.clearRect(0, 0, W, H);
+
+ // Vertical lines radiating from vanishing point (center top)
+ // Bottom spread exceeds screen width so laterals are filled
+ const spread = W * 4;
+ const spreadStart = vx - spread / 2;
+ floorCtx.strokeStyle = 'rgba(0, 200, 255, 0.32)';
+ floorCtx.lineWidth = 2;
+ for (let i = 0; i <= V_LINES; i++) {
+ const bx = spreadStart + (i / V_LINES) * spread;
+ floorCtx.beginPath();
+ floorCtx.moveTo(vx, 0);
+ floorCtx.lineTo(bx, H);
+ floorCtx.stroke();
+ }
+
+ // Horizontal lines — perspective-correct spacing, animated
+ for (let i = 0; i < H_LINES; i++) {
+ const t = ((i / H_LINES) + floorScroll) % 1;
+ const depth = Math.pow(t, 2.2); // power curve: compressed near horizon
+ const y = depth * H;
+ const x1 = vx - (spread / 2) * depth;
+ const x2 = vx + (spread / 2) * depth;
+ floorCtx.strokeStyle = `rgba(0, 200, 255, ${(0.12 + depth * 0.25).toFixed(2)})`;
+ floorCtx.lineWidth = Math.max(1, depth * 11);
+ floorCtx.beginPath();
+ floorCtx.moveTo(x1, y);
+ floorCtx.lineTo(x2, y);
+ floorCtx.stroke();
+ }
+
+ // Fade near horizon to hide sub-pixel lines
+ const fade = floorCtx.createLinearGradient(0, 0, 0, H * 0.3);
+ fade.addColorStop(0, '#000814');
+ fade.addColorStop(1, 'rgba(0,8,20,0)');
+ floorCtx.fillStyle = fade;
+ floorCtx.fillRect(0, 0, W, H * 0.3);
+
+ requestAnimationFrame(drawFloor);
+}
+
+window.addEventListener('resize', resizeFloor);
+resizeFloor();
+requestAnimationFrame(drawFloor);
/* ============================================================