diff --git a/src/glows.js b/src/glows.js new file mode 100644 index 0000000000..7172e6c9dc --- /dev/null +++ b/src/glows.js @@ -0,0 +1,78 @@ +import * as Blockly from 'blockly/core'; +import {Colours} from '../core/colours.js'; + +/** + * Glow/unglow a stack in the workspace. + * @param {?string} id ID of block which starts the stack. + * @param {boolean} isGlowingStack Whether to glow the stack. + */ +export function glowStack(id, isGlowingStack) { + if (id) { + const block = Blockly.getMainWorkspace().getBlockById(id) || + Blockly.getMainWorkspace().getFlyout().getWorkspace().getBlockById(id); + if (!block) { + throw 'Tried to glow block that does not exist.'; + } + + const svg = block.getSvgRoot(); + if (isGlowingStack && !svg.hasAttribute('filter')) { + svg.setAttribute('filter', 'url(#blocklyStackGlowFilter)'); + } else if (!isGlowingStack && svg.hasAttribute('filter')) { + svg.removeAttribute('filter'); + } + } +} + +export function buildGlowFilter(workspace) { + const svg = workspace.getParentSvg(); + const defs = Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.DEFS, {}, svg); + // Using a dilate distorts the block shape. + // Instead use a gaussian blur, and then set all alpha to 1 with a transfer. + const stackGlowFilter = Blockly.utils.dom.createSvgElement('filter', + { + 'id': 'blocklyStackGlowFilter', + 'height': '160%', + 'width': '180%', + y: '-30%', + x: '-40%' + }, + defs); + Blockly.utils.dom.createSvgElement('feGaussianBlur', + { + 'in': 'SourceGraphic', + 'stdDeviation': Colours.stackGlowSize + }, + stackGlowFilter); + // Set all gaussian blur pixels to 1 opacity before applying flood + const componentTransfer = Blockly.utils.dom.createSvgElement( + 'feComponentTransfer', {'result': 'outBlur'}, stackGlowFilter); + Blockly.utils.dom.createSvgElement('feFuncA', + { + 'type': 'table', + 'tableValues': '0' + ' 1'.repeat(16), + }, + componentTransfer); + // Color the highlight + Blockly.utils.dom.createSvgElement('feFlood', + { + 'flood-color': Colours.stackGlow, + 'flood-opacity': Colours.stackGlowOpacity, + 'result': 'outColor' + }, + stackGlowFilter); + Blockly.utils.dom.createSvgElement('feComposite', + { + 'in': 'outColor', + 'in2': 'outBlur', + 'operator': 'in', + 'result': 'outGlow' + }, + stackGlowFilter); + Blockly.utils.dom.createSvgElement('feComposite', + { + 'in': 'SourceGraphic', + 'in2': 'outGlow', + 'operator': 'over' + }, + stackGlowFilter); +} diff --git a/src/index.js b/src/index.js index 2c58a3aed1..aab92e6a22 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,7 @@ import { ContinuousMetrics, } from '@blockly/continuous-toolbox'; import {CheckableContinuousFlyout} from './checkable_continuous_flyout.js'; +import {buildGlowFilter, glowStack} from './glows.js'; import './scratch_continuous_category.js'; export * from 'blockly'; @@ -41,6 +42,7 @@ export * from '../core/field_matrix.js'; export * from '../core/field_note.js'; export * from '../core/field_number.js'; export * from '../msg/scratch_msgs.js'; +export {glowStack}; export {scratchBlocksUtils}; export {CheckableContinuousFlyout}; @@ -53,6 +55,15 @@ export function inject(container, options) { }, }); const workspace = Blockly.inject(container, options); + workspace.getRenderer().getConstants().selectedGlowFilterId = ''; + + const flyout = workspace.getFlyout(); + if (flyout) { + flyout.getWorkspace().getRenderer().getConstants().selectedGlowFilterId = ''; + } + + buildGlowFilter(workspace); + return workspace; }