scratch-gui / src /lib /themes /guiHelpers.js
soiz1's picture
Upload folder using huggingface_hub
8fd7a1d verified
import {Theme} from '.';
import AddonHooks from '../../addons/hooks';
import {applyThemeFonts} from '../theme-fonts';
import './global-styles.css';
const BLOCK_COLOR_NAMES = [
// Corresponds to the name of the object in blockColors
'motion',
'looks',
'sounds',
'control',
'event',
'sensing',
'pen',
'operators',
'data',
'data_lists',
'more',
'addons'
];
/**
* @param {string} css CSS color or var(--...)
* @returns {string} evaluated CSS
*/
const evaluateCSS = css => {
const variableMatch = css.match(/^var\(([\w-]+)\)$/);
if (variableMatch) {
return document.documentElement.style.getPropertyValue(variableMatch[1]);
}
return css;
};
/**
* Convert hex color to rgba with given opacity
* @param {string} hex hex color string
* @param {number} opacity opacity value (0-1)
* @returns {string} rgba color string
*/
const hexToRgba = (hex, opacity) => {
const cleanHex = hex.replace('#', '');
const r = parseInt(cleanHex.substr(0, 2), 16);
const g = parseInt(cleanHex.substr(2, 2), 16);
const b = parseInt(cleanHex.substr(4, 2), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
};
/**
* @param {Theme} theme the theme
*/
const applyGuiColors = theme => {
const doc = document.documentElement;
const defaultGuiColors = Theme.light.getGuiColors();
for (const [name, value] of Object.entries(defaultGuiColors)) {
doc.style.setProperty(`--${name}-default`, value);
}
const guiColors = theme.getGuiColors();
for (const [name, value] of Object.entries(guiColors)) {
doc.style.setProperty(`--${name}`, value);
// Convert hex colors to RGB values for overlay purposes
if (name === 'ui-primary' && value.startsWith('#')) {
const hex = value.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
doc.style.setProperty('--ui-primary-rgb', `${r}, ${g}, ${b}`);
} else if (name === 'ui-primary' && value.startsWith('hsla')) {
// For HSLA values, set a default RGB fallback
doc.style.setProperty('--ui-primary-rgb', '229, 240, 255');
}
}
const blockColors = theme.getBlockColors();
doc.style.setProperty('--editorTheme3-blockText', blockColors.text);
doc.style.setProperty('--editorTheme3-inputColor', blockColors.textField);
doc.style.setProperty('--editorTheme3-inputColor-text', blockColors.textFieldText);
for (const color of BLOCK_COLOR_NAMES) {
doc.style.setProperty(`--editorTheme3-${color}-primary`, blockColors[color].primary);
doc.style.setProperty(`--editorTheme3-${color}-secondary`, blockColors[color].secondary);
doc.style.setProperty(`--editorTheme3-${color}-tertiary`, blockColors[color].tertiary);
doc.style.setProperty(`--editorTheme3-${color}-field-background`, blockColors[color].quaternary);
}
// Some browsers will color their interfaces to match theme-color, so if we make it the same color as our
// menu bar, it'll look pretty cool.
let metaThemeColor = document.head.querySelector('meta[name=theme-color]');
if (!metaThemeColor) {
metaThemeColor = document.createElement('meta');
metaThemeColor.setAttribute('name', 'theme-color');
document.head.appendChild(metaThemeColor);
}
metaThemeColor.setAttribute('content', evaluateCSS(guiColors['menu-bar-background']));
// a horrible hack for icons...
window.Recolor = {
primary: guiColors['looks-secondary']
};
AddonHooks.recolorCallbacks.forEach(i => i());
// Apply wallpaper
applyWallpaper(theme.wallpaper);
// Apply fonts (async but don't block UI)
applyThemeFonts(theme.fonts).catch(console.error);
};
/**
* Apply wallpaper background to the GUI
* @param {Object} wallpaper wallpaper configuration
*/
const applyWallpaper = wallpaper => {
const target = document.querySelector("[class*='gui_body-wrapper_']");
let checkCountTarget = 0;
if (!target) {
const maxChecks = 50;
const checkInterval = setInterval(() => {
checkCountTarget++;
const newTarget = document.querySelector("[class*='gui_body-wrapper_']");
if (newTarget) {
applyWallpaper(wallpaper);
clearInterval(checkInterval);
} else if (checkCountTarget >= maxChecks) {
clearInterval(checkInterval);
}
}, 500);
return;
}
if (wallpaper.url) {
// Apply opacity by creating a semi-transparent overlay
const opacity = Math.max(0.1, Math.min(1, wallpaper.opacity || 0.3));
const overlayOpacity = 1 - opacity;
// Apply darkness tinting with black overlay
const darkness = Math.max(0, Math.min(0.8, wallpaper.darkness || 0));
// Create a composite background with the image and darkness overlay
// The darkness overlay is applied as a black semi-transparent layer over the image
if (darkness > 0) {
target.style.backgroundImage = `
linear-gradient(rgba(0, 0, 0, ${darkness}), rgba(0, 0, 0, ${darkness})),
url("${wallpaper.url}")
`;
} else {
target.style.backgroundImage = `url("${wallpaper.url}")`;
}
target.style.backgroundSize = 'cover';
target.style.backgroundPosition = 'center';
target.style.backgroundRepeat = 'no-repeat';
target.style.backgroundAttachment = 'fixed';
// Use CSS custom properties for overlay and darkness
document.documentElement.style.setProperty('--wallpaper-overlay-opacity', overlayOpacity.toString());
document.documentElement.style.setProperty('--wallpaper-darkness', darkness.toString());
// Apply JavaScript-based transparency to blocks workspace
applyBlocksWorkspaceTransparency(true, opacity);
// Update observer state for future blocks workspace changes
updateWallpaperObserverState(true, opacity);
// Also set up a periodic check for blocks workspace in case it loads later
let checkCount = 0;
const maxChecks = 50;
const checkInterval = setInterval(() => {
checkCount++;
const blocksSvg = document.querySelector('svg.blocklySvg');
if (blocksSvg) {
// Found the blocks workspace, apply transparency and stop checking
applyTransparencyToElement(blocksSvg, true, opacity);
clearInterval(checkInterval);
} else if (checkCount >= maxChecks) {
// Stop checking after max attempts
clearInterval(checkInterval);
}
}, 500);
} else {
// Remove wallpaper
target.style.backgroundImage = '';
target.style.backgroundSize = '';
target.style.backgroundPosition = '';
target.style.backgroundRepeat = '';
target.style.backgroundAttachment = '';
document.documentElement.style.removeProperty('--wallpaper-overlay-opacity');
document.documentElement.style.removeProperty('--wallpaper-darkness');
// Remove transparency from blocks workspace
applyBlocksWorkspaceTransparency(false);
// Update observer state
updateWallpaperObserverState(false);
}
};
/**
* Apply or remove transparency from the blocks workspace using JavaScript
* @param {boolean} hasWallpaper whether wallpaper is active
* @param {number} wallpaperOpacity opacity of the wallpaper (0.1 to 1.0)
* @param {number} retryCount current retry attempt (for internal use)
*/
const applyBlocksWorkspaceTransparency = (hasWallpaper, wallpaperOpacity = 0.3, retryCount = 0) => {
// Find the blocks workspace SVG element using the specific selector
const blocksSvg = document.querySelector("svg.blocklySvg");
if (!blocksSvg) {
// Fallback to a more general selector if the specific one doesn't work
const fallbackSvg = document.querySelector('svg.blocklySvg');
if (fallbackSvg) {
applyTransparencyToElement(fallbackSvg, hasWallpaper, wallpaperOpacity);
return;
}
// If no blocks workspace is found and we haven't retried too many times, try again
// This handles cases where wallpaper is applied before blocks are loaded
const maxRetries = 20; // Try for up to 10 seconds (50ms * 20 = 1000ms, then exponential backoff)
if (retryCount < maxRetries) {
// Use exponential backoff: start with 50ms, then increase
const delay = retryCount < 10 ? 50 : Math.min(500, 50 * Math.pow(2, retryCount - 10));
setTimeout(() => {
applyBlocksWorkspaceTransparency(hasWallpaper, wallpaperOpacity, retryCount + 1);
}, delay);
}
return;
}
applyTransparencyToElement(blocksSvg, hasWallpaper, wallpaperOpacity);
};
/**
* Apply transparency styling to a specific element
* @param {Element} element the element to apply transparency to
* @param {boolean} hasWallpaper whether wallpaper is active
* @param {number} wallpaperOpacity opacity of the wallpaper (0.1 to 1.0)
*/
const applyTransparencyToElement = (element, hasWallpaper, wallpaperOpacity = 0.3) => {
if (hasWallpaper) {
// Calculate appropriate background transparency based on wallpaper opacity
// Higher wallpaper opacity means we need more workspace transparency to see through
const backgroundOpacity = Math.max(0.2, Math.min(0.8, 1 - wallpaperOpacity + 0.1));
const guiColors = document.documentElement.style.getPropertyValue('--ui-primary') || '#e5f0ff';
const backgroundColor = guiColors.startsWith('#') ?
hexToRgba(guiColors, backgroundOpacity) :
`rgba(229, 240, 255, ${backgroundOpacity})`;
element.style.backgroundColor = backgroundColor;
} else {
// Remove transparency styling
element.style.backgroundColor = '';
}
};
// Keep track of the current wallpaper state for observer
let currentWallpaperState = {hasWallpaper: false, opacity: 0.3};
/**
* Observer to watch for blocks workspace changes and apply transparency when needed
*/
const createBlocksWorkspaceObserver = () => {
// Only create observer if we don't already have one
if (window.blocksWorkspaceObserver) {
return;
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
// Check for both specific and general blocks workspace selectors
const blocksSvg = document.querySelector('svg.blocklySvg');
if (blocksSvg && currentWallpaperState.hasWallpaper) {
// Apply transparency to newly created blocks workspace
applyTransparencyToElement(blocksSvg, true, currentWallpaperState.opacity);
// Also check for any nested SVG elements that might need transparency
const nestedSvgs = blocksSvg.querySelectorAll('svg');
nestedSvgs.forEach(svg => {
applyTransparencyToElement(svg, true, currentWallpaperState.opacity);
});
break;
}
}
}
});
// Observe the entire document for changes
observer.observe(document.body, {
childList: true,
subtree: true
});
window.blocksWorkspaceObserver = observer;
};
/**
* Update the observer's knowledge of current wallpaper state
* @param {boolean} hasWallpaper whether wallpaper is active
* @param {number} opacity wallpaper opacity
*/
const updateWallpaperObserverState = (hasWallpaper, opacity = 0.3) => {
currentWallpaperState = {hasWallpaper, opacity};
if (hasWallpaper) {
createBlocksWorkspaceObserver();
// Also add a listener for workspace creation events if available
if (window.AddonHooks && window.AddonHooks.workspaceCreated) {
window.AddonHooks.workspaceCreated(() => {
if (currentWallpaperState.hasWallpaper) {
// Small delay to ensure workspace is fully initialized
setTimeout(() => {
applyBlocksWorkspaceTransparency(true, currentWallpaperState.opacity);
}, 50);
}
});
}
} else if (window.blocksWorkspaceObserver) {
// Clean up observer when no wallpaper is active
window.blocksWorkspaceObserver.disconnect();
window.blocksWorkspaceObserver = null;
}
};
export {
applyGuiColors,
applyWallpaper,
applyBlocksWorkspaceTransparency
};