Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: stage mode gets per-token dynamic contrast shadows (disk.mjs algorithm)

Replace cycling CSS animation with the luminance-based shadow algorithm
from disk.mjs getShadowColorForText(). Each syntax token gets a
contrasting shadow computed from its color:
- Dark colors get lightened shadows (85% mix with white)
- Bright colors get dark purple-blue shadows
- Channel-specific tinting for pure R/G/B colors
- Near-black gets bright white shadow

Shadows are baked into per-token CSS classes via _getStageShadowForColor()
and scoped to body.stage-mode so normal editing is unaffected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+100 -15
+1 -14
system/public/kidlisp.com/index.html
··· 321 321 background: transparent !important; 322 322 } 323 323 324 - /* Cycling color drop shadow on editor text for readability over animation */ 325 - body.stage-mode:not(.device-mode) .monaco-editor .view-lines span { 326 - text-shadow: 1px 1px 2px var(--stage-shadow-color, rgba(0,0,0,0.8)); 327 - animation: stage-shadow-cycle 6s linear infinite; 328 - } 329 - @keyframes stage-shadow-cycle { 330 - 0% { --stage-shadow-color: rgba(0, 0, 0, 0.8); } 331 - 16% { --stage-shadow-color: rgba(255, 255, 255, 0.9); } 332 - 33% { --stage-shadow-color: rgba(0, 200, 255, 0.8); } 333 - 50% { --stage-shadow-color: rgba(255, 0, 180, 0.8); } 334 - 66% { --stage-shadow-color: rgba(255, 220, 0, 0.8); } 335 - 83% { --stage-shadow-color: rgba(0, 255, 120, 0.8); } 336 - 100% { --stage-shadow-color: rgba(0, 0, 0, 0.8); } 337 - } 324 + /* Stage mode text shadows are applied per-token via JS (dynamic contrast shadows) */ 338 325 339 326 /* Hide line numbers in stage mode */ 340 327 body.stage-mode:not(.device-mode) .monaco-editor .margin {
+99 -1
system/public/kidlisp.com/js/monaco-kidlisp-highlighting.mjs
··· 313 313 const shadow = this._getShadowForColor(cssColor); 314 314 const shadowStyle = shadow ? ` text-shadow: ${shadow};` : ''; 315 315 316 - style.textContent = `.monaco-editor .view-line > span > span.${cssClass} { color: ${cssColor}; font-weight: bold;${shadowStyle} }`; 316 + // Stage mode always gets a dynamic contrast shadow for readability over animation 317 + const stageShadow = this._getStageShadowForColor(cssColor); 318 + const stageRule = stageShadow 319 + ? ` body.stage-mode:not(.device-mode) .monaco-editor .view-line > span > span.${cssClass} { text-shadow: ${stageShadow}; }` 320 + : ''; 321 + 322 + style.textContent = `.monaco-editor .view-line > span > span.${cssClass} { color: ${cssColor}; font-weight: bold;${shadowStyle} }${stageRule}`; 317 323 document.head.appendChild(style); 318 324 } 319 325 ··· 361 367 return '1px 1px 0px rgba(220, 220, 220, 0.8)'; 362 368 } else if (luminance < 100) { 363 369 return '1px 1px 0px rgba(200, 200, 200, 0.6)'; 370 + } 371 + 372 + return null; 373 + } 374 + 375 + /** 376 + * Get dynamic contrast shadow for stage mode (algorithm from disk.mjs) 377 + * Always returns a shadow — ensures readability over any background animation. 378 + * @private 379 + */ 380 + _getStageShadowForColor(colorStr) { 381 + let rgb = this._parseColorToRGB(colorStr); 382 + if (!rgb) return '1px 1px 2px rgba(0,0,0,0.8)'; // fallback dark shadow 383 + 384 + const [r, g, b] = rgb; 385 + 386 + // Very dark colors (near black) get bright shadow 387 + if (r <= 24 && g <= 24 && b <= 24) { 388 + return '1px 1px 2px rgba(220,220,220,0.9)'; 389 + } 390 + 391 + const luminance = (0.299 * r + 0.587 * g + 0.114 * b); 392 + 393 + // Bright colors get dark shadow 394 + if (luminance > 140) { 395 + return '1px 1px 2px rgba(30,20,50,0.9)'; 396 + } 397 + 398 + // Red channel special case 399 + if (r >= 0 && g === 0 && b === 0) { 400 + const sr = Math.max(32, Math.round(r * 0.4)); 401 + return `1px 1px 2px rgb(${sr},0,0)`; 402 + } 403 + 404 + // Green channel special case 405 + if (r === 0 && g >= 0 && b === 0) { 406 + const sg = Math.max(32, Math.round(g * 0.4)); 407 + return `1px 1px 2px rgb(0,${sg},0)`; 408 + } 409 + 410 + // Blue/cyan channel special case 411 + if (r === 0 && b >= 0 && g >= 0) { 412 + const expectedG = Math.round(b * 0.75); 413 + if (Math.abs(g - expectedG) <= 1) { 414 + const sg = Math.max(24, Math.round(g * 0.4)); 415 + const sb = Math.max(32, Math.round(b * 0.4)); 416 + return `1px 1px 2px rgb(0,${sg},${sb})`; 417 + } 418 + } 419 + 420 + // Dark colors — lighten shadow (mix 85% with white) 421 + if (luminance < 128) { 422 + const factor = 0.85; 423 + const sr = Math.round(r + (255 - r) * factor); 424 + const sg = Math.round(g + (255 - g) * factor); 425 + const sb = Math.round(b + (255 - b) * factor); 426 + return `1px 1px 2px rgb(${sr},${sg},${sb})`; 427 + } 428 + 429 + // Light colors — darken shadow (60% darker) 430 + const factor = 0.6; 431 + const sr = Math.round(r * (1 - factor)); 432 + const sg = Math.round(g * (1 - factor)); 433 + const sb = Math.round(b * (1 - factor)); 434 + return `1px 1px 2px rgb(${sr},${sg},${sb})`; 435 + } 436 + 437 + /** 438 + * Parse a CSS color string to [r, g, b] 439 + * @private 440 + */ 441 + _parseColorToRGB(colorStr) { 442 + if (!colorStr) return null; 443 + 444 + // rgb(...) format 445 + if (colorStr.startsWith('rgb')) { 446 + const parts = colorStr.replace(/rgb\(|\)/g, '').split(',').map(v => parseInt(v.trim())); 447 + return parts.length >= 3 ? parts : null; 448 + } 449 + 450 + // Named color — look up in cssColorMap 451 + const lower = colorStr.toLowerCase(); 452 + if (this.cssColorMap[lower]) return this.cssColorMap[lower]; 453 + 454 + // Hex format 455 + if (colorStr.startsWith('#')) { 456 + const hex = colorStr.slice(1); 457 + if (hex.length === 3) { 458 + return [parseInt(hex[0]+hex[0],16), parseInt(hex[1]+hex[1],16), parseInt(hex[2]+hex[2],16)]; 459 + } else if (hex.length >= 6) { 460 + return [parseInt(hex.slice(0,2),16), parseInt(hex.slice(2,4),16), parseInt(hex.slice(4,6),16)]; 461 + } 364 462 } 365 463 366 464 return null;