Add spectrum analyzer theming and visualization styles

- Add 6 color themes: green, blue, purple, red, amber, rainbow
- Add 3 visualization styles: bars, wave, dots
- Add UI controls (dropdowns) to change theme and style
- Persist user preferences in localStorage
- Remove debug logging from parenscript serving and Icecast stats
- Fix dots visualization with proper Math.PI handling
This commit is contained in:
Glenn Thompson 2025-12-06 18:29:53 +03:00 committed by Brian O'Reilly
parent b6be0ebab1
commit 51387bddba
6 changed files with 165 additions and 29 deletions

View File

@ -558,11 +558,9 @@
(cond
;; Serve ParenScript-compiled auth-ui.js
((string= path "js/auth-ui.js")
(format t "~%=== SERVING PARENSCRIPT auth-ui.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-auth-ui-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating auth-ui.js: ~a~%" e)
@ -570,11 +568,9 @@
;; Serve ParenScript-compiled front-page.js
((string= path "js/front-page.js")
(format t "~%=== SERVING PARENSCRIPT front-page.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-front-page-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating front-page.js: ~a~%" e)
@ -582,11 +578,9 @@
;; Serve ParenScript-compiled profile.js
((string= path "js/profile.js")
(format t "~%=== SERVING PARENSCRIPT profile.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-profile-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating profile.js: ~a~%" e)
@ -594,11 +588,9 @@
;; Serve ParenScript-compiled users.js
((string= path "js/users.js")
(format t "~%=== SERVING PARENSCRIPT users.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-users-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating users.js: ~a~%" e)
@ -606,11 +598,9 @@
;; Serve ParenScript-compiled admin.js
((string= path "js/admin.js")
(format t "~%=== SERVING PARENSCRIPT admin.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-admin-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating admin.js: ~a~%" e)
@ -618,11 +608,9 @@
;; Serve ParenScript-compiled player.js
((string= path "js/player.js")
(format t "~%=== SERVING PARENSCRIPT player.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-player-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating player.js: ~a~%" e)
@ -630,11 +618,9 @@
;; Serve ParenScript-compiled recently-played.js
((string= path "js/recently-played.js")
(format t "~%=== SERVING PARENSCRIPT recently-played.js ===~%")
(setf (content-type *response*) "application/javascript")
(handler-case
(let ((js (generate-recently-played-js)))
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
(if js js "// Error: No JavaScript generated"))
(error (e)
(format t "ERROR generating recently-played.js: ~a~%" e)

View File

@ -10,7 +10,6 @@
(response (drakma:http-request icecast-url
:want-stream nil
:basic-authorization '("admin" "asteroid_admin_2024"))))
(format t "DEBUG: Fetching Icecast stats from ~a~%" icecast-url)
(when response
(let ((xml-string (if (stringp response)
response
@ -34,7 +33,6 @@
(aref groups 0)
"Unknown")))
"Unknown")))
(format t "DEBUG: Parsed title=~a, total-listeners=~a~%" title total-listeners)
;; Track recently played if title changed
(when (and title

View File

@ -14,6 +14,18 @@
(defvar *animation-id* nil)
(defvar *media-source* nil)
(defvar *current-audio-element* nil)
(defvar *current-theme* "green")
(defvar *current-style* "bars")
;; Color themes for spectrum analyzer
(defvar *themes*
(ps:create
"green" (ps:create "top" "#00ff00" "mid" "#00aa00" "bottom" "#005500")
"blue" (ps:create "top" "#00ffff" "mid" "#0088ff" "bottom" "#0044aa")
"purple" (ps:create "top" "#ff00ff" "mid" "#aa00aa" "bottom" "#550055")
"red" (ps:create "top" "#ff0000" "mid" "#aa0000" "bottom" "#550000")
"amber" (ps:create "top" "#ffaa00" "mid" "#ff6600" "bottom" "#aa3300")
"rainbow" (ps:create "top" "#ff00ff" "mid" "#00ffff" "bottom" "#00ff00")))
(defun reset-spectrum-analyzer ()
"Reset the spectrum analyzer to allow reconnection after audio element reload"
@ -98,21 +110,60 @@
(setf (ps:@ *canvas-ctx* |fillStyle|) "rgba(0, 0, 0, 0.2)")
(ps:chain *canvas-ctx* (fill-rect 0 0 width height))
;; Draw bars
(dotimes (i buffer-length)
(setf bar-height (/ (* (aref data-array i) height) 256))
;; Create gradient for each bar
(let ((gradient (ps:chain *canvas-ctx*
(create-linear-gradient 0 (- height bar-height) 0 height))))
(ps:chain gradient (add-color-stop 0 "#00ff00"))
(ps:chain gradient (add-color-stop 0.5 "#00aa00"))
(ps:chain gradient (add-color-stop 1 "#005500"))
;; Get current theme colors
(let ((theme (ps:getprop *themes* *current-theme*)))
(cond
;; Bar graph style
((= *current-style* "bars")
(setf x 0)
(dotimes (i buffer-length)
(setf bar-height (/ (* (aref data-array i) height) 256))
;; Create gradient for each bar using theme colors
(let ((gradient (ps:chain *canvas-ctx*
(create-linear-gradient 0 (- height bar-height) 0 height))))
(ps:chain gradient (add-color-stop 0 (ps:@ theme top)))
(ps:chain gradient (add-color-stop 0.5 (ps:@ theme mid)))
(ps:chain gradient (add-color-stop 1 (ps:@ theme bottom)))
(setf (ps:@ *canvas-ctx* |fillStyle|) gradient)
(ps:chain *canvas-ctx* (fill-rect x (- height bar-height) bar-width bar-height))
(incf x bar-width))))
;; Wave/line style
((= *current-style* "wave")
(setf x 0)
(ps:chain *canvas-ctx* (begin-path))
(setf (ps:@ *canvas-ctx* |lineWidth|) 2)
(setf (ps:@ *canvas-ctx* |strokeStyle|) (ps:@ theme top))
(setf (ps:@ *canvas-ctx* |fillStyle|) gradient)
(ps:chain *canvas-ctx* (fill-rect x (- height bar-height) bar-width bar-height))
(dotimes (i buffer-length)
(setf bar-height (/ (* (aref data-array i) height) 256))
(let ((y (- height bar-height)))
(if (= i 0)
(ps:chain *canvas-ctx* (move-to x y))
(ps:chain *canvas-ctx* (line-to x y)))
(incf x bar-width)))
(incf x bar-width)))
(ps:chain *canvas-ctx* (stroke)))
;; Dots/particles style
((= *current-style* "dots")
(setf x 0)
(setf (ps:@ *canvas-ctx* |fillStyle|) (ps:@ theme top))
(dotimes (i buffer-length)
(let* ((value (aref data-array i))
(normalized-height (/ (* value height) 256))
(y (- height normalized-height))
(dot-radius (ps:max 2 (/ normalized-height 20))))
(when (> value 0)
(ps:chain *canvas-ctx* (begin-path))
(ps:chain *canvas-ctx* (arc x y dot-radius 0 6.283185307179586))
(ps:chain *canvas-ctx* (fill)))
(incf x bar-width))))))
;; Draw MUTED indicator if audio is muted
(when is-muted
@ -128,9 +179,47 @@
(cancel-animation-frame *animation-id*)
(setf *animation-id* nil)))
(defun set-spectrum-theme (theme-name)
"Change the spectrum analyzer color theme"
(when (ps:getprop *themes* theme-name)
(setf *current-theme* theme-name)
(ps:chain local-storage (set-item "spectrum-theme" theme-name))
(ps:chain console (log (+ "Spectrum theme changed to: " theme-name)))))
(defun get-available-themes ()
"Return array of available theme names"
(ps:chain |Object| (keys *themes*)))
(defun set-spectrum-style (style-name)
"Change the spectrum analyzer visualization style"
(when (or (= style-name "bars") (= style-name "wave") (= style-name "dots"))
(setf *current-style* style-name)
(ps:chain local-storage (set-item "spectrum-style" style-name))
(ps:chain console (log (+ "Spectrum style changed to: " style-name)))))
(defun get-available-styles ()
"Return array of available visualization styles"
(array "bars" "wave" "dots"))
;; Initialize when audio starts playing
(ps:chain document (add-event-listener "DOMContentLoaded"
(lambda ()
;; Load saved theme and style preferences
(let ((saved-theme (ps:chain local-storage (get-item "spectrum-theme")))
(saved-style (ps:chain local-storage (get-item "spectrum-style"))))
(when (and saved-theme (ps:getprop *themes* saved-theme))
(setf *current-theme* saved-theme))
(when (and saved-style (or (= saved-style "bars") (= saved-style "wave") (= saved-style "dots")))
(setf *current-style* saved-style))
;; Update UI selectors if they exist
(let ((theme-selector (ps:chain document (get-element-by-id "spectrum-theme-selector")))
(style-selector (ps:chain document (get-element-by-id "spectrum-style-selector"))))
(when theme-selector
(setf (ps:@ theme-selector value) *current-theme*))
(when style-selector
(setf (ps:@ style-selector value) *current-style*))))
(let ((audio-element (or (ps:chain document (get-element-by-id "live-audio"))
(ps:chain document (get-element-by-id "persistent-audio")))))

View File

@ -26,6 +26,27 @@
<!-- Spectrum Analyzer Canvas -->
<div style="text-align: center; margin: 15px 0;">
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
<div style="margin-top: 8px; font-size: 0.9em;">
<label style="margin-right: 10px;">
Style:
<select id="spectrum-style-selector" onchange="setSpectrumStyle(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
<option value="bars">Bars</option>
<option value="wave">Wave</option>
<option value="dots">Dots</option>
</select>
</label>
<label>
Theme:
<select id="spectrum-theme-selector" onchange="setSpectrumTheme(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="purple">Purple</option>
<option value="red">Red</option>
<option value="amber">Amber</option>
<option value="rainbow">Rainbow</option>
</select>
</label>
</div>
</div>
<nav class="nav">

View File

@ -26,6 +26,27 @@
<!-- Spectrum Analyzer Canvas -->
<div style="text-align: center; margin: 15px 0;">
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
<div style="margin-top: 8px; font-size: 0.9em;">
<label style="margin-right: 10px;">
Style:
<select id="spectrum-style-selector" onchange="setSpectrumStyle(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
<option value="bars">Bars</option>
<option value="wave">Wave</option>
<option value="dots">Dots</option>
</select>
</label>
<label>
Theme:
<select id="spectrum-theme-selector" onchange="setSpectrumTheme(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="purple">Purple</option>
<option value="red">Red</option>
<option value="amber">Amber</option>
<option value="rainbow">Rainbow</option>
</select>
</label>
</div>
</div>
<nav class="nav">

View File

@ -20,6 +20,27 @@
<!-- Spectrum Analyzer Canvas -->
<div style="text-align: center; margin: 15px 0;">
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
<div style="margin-top: 8px; font-size: 0.9em;">
<label style="margin-right: 10px;">
Style:
<select id="spectrum-style-selector" onchange="setSpectrumStyle(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
<option value="bars">Bars</option>
<option value="wave">Wave</option>
<option value="dots">Dots</option>
</select>
</label>
<label>
Theme:
<select id="spectrum-theme-selector" onchange="setSpectrumTheme(this.value)" style="padding: 3px; background: #000; color: #00ff00; border: 1px solid #00ff00;">
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="purple">Purple</option>
<option value="red">Red</option>
<option value="amber">Amber</option>
<option value="rainbow">Rainbow</option>
</select>
</label>
</div>
</div>
<div class="nav">