mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 17:57:43 +02:00
[eric] canvas: grain rides the wash layer with baked alpha, an evicted tile drops both to tint instead of the grain cutoff seam
This commit is contained in:
@@ -14,7 +14,7 @@ import MinimizedStack from '../desktop/MinimizedStack';
|
||||
import ApplicationsWindow from '../desktop/ApplicationsWindow';
|
||||
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
import { useThemeAccent, useThemeWash } from '@/shared/styles/ThemeContext';
|
||||
import { GRAIN_URL } from '@/shared/styles/grainTexture';
|
||||
import { useGrainTileUrl } from '@/shared/styles/useGrainTileUrl';
|
||||
import { washOpaqueBackgroundUrl, washUnderlayColor, effectiveWashStops } from '@/shared/styles/washBackground';
|
||||
|
||||
// How far the dot grid bleeds past the viewport; must exceed one tile period (24px * max zoom) so the compositor phase translate can never expose an edge.
|
||||
@@ -172,6 +172,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
// Memoized: this component re-renders every card-drag frame, and rebuilding these strings (SVG encode + hex blends) per frame is pure waste.
|
||||
const washUnderlay = React.useMemo(() => washUnderlayColor(washStops, washOpacity, c.bg.page), [washStops, washOpacity, c.bg.page]);
|
||||
const washUrl = React.useMemo(() => washOpaqueBackgroundUrl(washStops, washOpacity, c.bg.page), [washStops, washOpacity, c.bg.page]);
|
||||
const grainTileUrl = useGrainTileUrl(grain);
|
||||
const gridTileUrl = React.useMemo(() => `url("data:image/svg+xml,${encodeURIComponent(
|
||||
`<svg xmlns='http://www.w3.org/2000/svg' width='${dotSpacing}' height='${dotSpacing}'><circle cx='${dotSpacing / 2}' cy='${dotSpacing / 2}' r='${dotSize}' fill='${c.border.medium}'/></svg>`,
|
||||
)}")`, [dotSpacing, dotSize, c.border.medium]);
|
||||
@@ -414,19 +415,10 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: washUnderlay,
|
||||
backgroundImage: washUrl,
|
||||
backgroundSize: '100% 100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{grain > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
opacity: grain,
|
||||
backgroundImage: GRAIN_URL,
|
||||
// Grain rides the SAME element (alpha pre-baked): one raster, so an evicted tile drops both and paints the tint, never a grain-only seam.
|
||||
backgroundImage: grainTileUrl ? `${grainTileUrl}, ${washUrl}` : washUrl,
|
||||
backgroundSize: grainTileUrl ? 'auto, 100% 100%' : '100% 100%',
|
||||
backgroundRepeat: grainTileUrl ? 'repeat, no-repeat' : 'no-repeat',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// The grain PNG with the slider's opacity BAKED INTO its alpha, so grain can ride the wash element
|
||||
// as a second background layer: one raster means a GPU-evicted tile drops wash AND grain together
|
||||
// and falls back to the same flat tint, instead of the grain-only cutoff seam (the ENG-151 family).
|
||||
const cache = new Map<number, string>();
|
||||
let sourceImage: HTMLImageElement | null = null;
|
||||
|
||||
export function useGrainTileUrl(opacity: number): string | null {
|
||||
const key = Math.round(Math.max(0, Math.min(1, opacity)) * 100) / 100;
|
||||
const [url, setUrl] = useState<string | null>(cache.get(key) ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
if (key <= 0) { setUrl(null); return; }
|
||||
const hit = cache.get(key);
|
||||
if (hit) { setUrl(hit); return; }
|
||||
let alive = true;
|
||||
const bake = (img: HTMLImageElement): void => {
|
||||
const cv = document.createElement('canvas');
|
||||
cv.width = img.naturalWidth;
|
||||
cv.height = img.naturalHeight;
|
||||
const ctx = cv.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.globalAlpha = key;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const baked = `url("${cv.toDataURL('image/png')}")`;
|
||||
cache.set(key, baked);
|
||||
if (alive) setUrl(baked);
|
||||
};
|
||||
if (sourceImage && sourceImage.complete) {
|
||||
bake(sourceImage);
|
||||
} else {
|
||||
const img = sourceImage ?? new Image();
|
||||
sourceImage = img;
|
||||
img.onload = () => bake(img);
|
||||
if (!img.src) img.src = './grain-texture.png';
|
||||
if (img.complete && img.naturalWidth > 0) bake(img);
|
||||
}
|
||||
return () => { alive = false; };
|
||||
}, [key]);
|
||||
|
||||
return key > 0 ? url : null;
|
||||
}
|
||||
Reference in New Issue
Block a user