From 45a93bb780fb55885027e552c6746f1e8049511a Mon Sep 17 00:00:00 2001 From: Kaloyan Manolov Date: Tue, 4 Nov 2025 17:07:37 +0200 Subject: [PATCH 1/4] feat: make cat-blocks code configurable behind a flag --- core/block_render_svg_horizontal.js | 42 ++++- core/block_render_svg_vertical.js | 118 +++++++++++-- core/block_svg.js | 246 +++++++++++++++++++++++++++- 3 files changed, 383 insertions(+), 23 deletions(-) diff --git a/core/block_render_svg_horizontal.js b/core/block_render_svg_horizontal.js index c1c4ecce3f..e2844a6279 100644 --- a/core/block_render_svg_horizontal.js +++ b/core/block_render_svg_horizontal.js @@ -311,14 +311,26 @@ Blockly.BlockSvg.prototype.updateColour = function() { var strokeColour = this.getColourTertiary(); // Render block stroke - this.svgPath_.setAttribute('stroke', strokeColour); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('stroke', strokeColour); + } else { + this.svgPath_.setAttribute('stroke', strokeColour); + } // Render block fill var fillColour = (this.isGlowingBlock_) ? this.getColourSecondary() : this.getColour(); - this.svgPath_.setAttribute('fill', fillColour); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('fill', fillColour); + } else { + this.svgPath_.setAttribute('fill', fillColour); + } // Render opacity - this.svgPath_.setAttribute('fill-opacity', this.getOpacity()); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('fill-opacity', this.getOpacity()); + } else { + this.svgPath_.setAttribute('fill-opacity', this.getOpacity()); + } // Bump every dropdown to change its colour. for (var x = 0, input; input = this.inputList[x]; x++) { @@ -337,11 +349,19 @@ Blockly.BlockSvg.prototype.highlightForReplacement = function(add) { if (add) { var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId || 'blocklyReplacementGlowFilter'; - this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); + } else { + this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); + } Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } else { - this.svgPath_.removeAttribute('filter'); + if (Blockly.useCatBlocks) { + this.svgPathBody_.removeAttribute('filter'); + } else { + this.svgPath_.removeAttribute('filter'); + } Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } @@ -545,12 +565,20 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) { this.renderDrawTop_(steps, connectionsXY, metrics); var pathString = steps.join(' '); - this.svgPath_.setAttribute('d', pathString); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('d', pathString); + } else { + this.svgPath_.setAttribute('d', pathString); + } if (this.RTL) { // Mirror the block's path. // This is awesome. - this.svgPath_.setAttribute('transform', 'scale(-1 1)'); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('transform', 'scale(-1 1)'); + } else { + this.svgPath_.setAttribute('transform', 'scale(-1 1)'); + } } // Horizontal blocks have a single Image Field that is specially positioned diff --git a/core/block_render_svg_vertical.js b/core/block_render_svg_vertical.js index ca5ea470f3..d4dc534ddb 100644 --- a/core/block_render_svg_vertical.js +++ b/core/block_render_svg_vertical.js @@ -139,7 +139,7 @@ Blockly.BlockSvg.STATEMENT_INPUT_INNER_SPACE = 2 * Blockly.BlockSvg.GRID_UNIT; * Height of the top hat. * @const */ -Blockly.BlockSvg.START_HAT_HEIGHT = 16; +Blockly.BlockSvg.START_HAT_HEIGHT = Blockly.useCatBlocks ? 31 : 16; /** * Height of the vertical separator line for icons that appear at the left edge @@ -152,7 +152,12 @@ Blockly.BlockSvg.ICON_SEPARATOR_HEIGHT = 10 * Blockly.BlockSvg.GRID_UNIT; * Path of the top hat's curve. * @const */ -Blockly.BlockSvg.START_HAT_PATH = 'c 25,-22 71,-22 96,0'; +Blockly.BlockSvg.START_HAT_PATH = Blockly.useCatBlocks + ? 'c2.6,-2.3 5.5,-4.3 8.5,-6.2' + + 'c-1,-12.5 5.3,-23.3 8.4,-24.8c3.7,-1.8 16.5,13.1 18.4,15.4' + + 'c8.4,-1.3 17,-1.3 25.4,0c1.9,-2.3 14.7,-17.2 18.4,-15.4' + + 'c3.1,1.5 9.4,12.3 8.4,24.8c3,1.8 5.9,3.9 8.5,6.1' + : 'c 25,-22 71,-22 96,0'; /** * SVG path for drawing next/previous notch from left to right. @@ -476,8 +481,11 @@ Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS = 5 * Blockly.BlockSvg.GRID_UNIT; * SVG path for drawing the rounded top-left corner. * @const */ -Blockly.BlockSvg.TOP_LEFT_CORNER_DEFINE_HAT = - 'a ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' + +Blockly.BlockSvg.TOP_LEFT_CORNER_DEFINE_HAT = Blockly.useCatBlocks + ? 'c0,-7.1 3.7,-13.3 9.3,-16.9c1.7,-7.5 5.4,-13.2 7.6,-14.2' + + 'c2.6,-1.3 10,6 14.6,11.1h33c4.6,-5.1 11.9,-12.4 14.6,-11.1' + + 'c1.9,0.9 4.9,5.2 6.8,11.1c2.6,0,5.2,0,7.8,0' + : 'a ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ' 0 0,1 ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',-' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS; @@ -519,7 +527,11 @@ Blockly.BlockSvg.prototype.updateColour = function() { } // Render block stroke - this.svgPath_.setAttribute('stroke', strokeColour); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('stroke', strokeColour); + } else { + this.svgPath_.setAttribute('stroke', strokeColour); + } // Render block fill if (this.isGlowingBlock_ || renderShadowed) { @@ -532,10 +544,18 @@ Blockly.BlockSvg.prototype.updateColour = function() { } else { var fillColour = this.getColour(); } - this.svgPath_.setAttribute('fill', fillColour); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('fill', fillColour); + } else { + this.svgPath_.setAttribute('fill', fillColour); + } // Render opacity - this.svgPath_.setAttribute('fill-opacity', this.getOpacity()); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('fill-opacity', this.getOpacity()); + } else { + this.svgPath_.setAttribute('fill-opacity', this.getOpacity()); + } // Update colours of input shapes. for (var i = 0, input; input = this.inputList[i]; i++) { @@ -567,11 +587,19 @@ Blockly.BlockSvg.prototype.highlightForReplacement = function(add) { if (add) { var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId || 'blocklyReplacementGlowFilter'; - this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); + } else { + this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); + } Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } else { - this.svgPath_.removeAttribute('filter'); + if (Blockly.useCatBlocks) { + this.svgPathBody_.removeAttribute('filter'); + } else { + this.svgPath_.removeAttribute('filter'); + } Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } @@ -1119,6 +1147,60 @@ Blockly.BlockSvg.prototype.computeOutputPadding_ = function(inputRows) { row.paddingEnd += Blockly.BlockSvg.SHAPE_IN_SHAPE_PADDING[shape][otherShape]; }; +// Cat face and ear animation for CatBlocks +Blockly.BlockSvg.prototype.renderCatFace_ = function() { + this.svgPath_.svgFace.setAttribute('fill','#000000'); + + var closedEye = Blockly.utils.createSvgElement('path', {}, this.svgFace_); + closedEye.setAttribute('d','M25.2-1.1c0.1,0,0.2,0,0.2,0l8.3-2.1l-7-4.8' + + 'c-0.5-0.3-1.1-0.2-1.4,0.3s-0.2,1.1,0.3,1.4L29-4.1l-4,1' + + 'c-0.5,0.1-0.9,0.7-0.7,1.2C24.3-1.4,24.7-1.1,25.2-1.1z'); + closedEye.setAttribute('fill-opacity','0'); + this.svgPath_.svgFace.closedEye = closedEye; + + var closedEye2 = Blockly.utils.createSvgElement('path', {}, this.svgFace_); + closedEye2.setAttribute('d','M62.4-1.1c-0.1,0-0.2,0-0.2,0l-8.3-2.1l7-4.8' + + 'c0.5-0.3,1.1-0.2,1.4,0.3s0.2,1.1-0.3,1.4l-3.4,2.3l4,1' + + 'c0.5,0.1,0.9,0.7,0.7,1.2C63.2-1.4,62.8-1.1,62.4-1.1z'); + closedEye2.setAttribute('fill-opacity','0'); + this.svgPath_.svgFace.closedEye2 = closedEye2; + + var eye = Blockly.utils.createSvgElement('circle', {}, this.svgFace_); + eye.setAttribute('cx','59.2'); + eye.setAttribute('cy','-3.3'); + eye.setAttribute('r','3.4'); + eye.setAttribute('fill-opacity','0.6'); + this.svgPath_.svgFace.eye = eye; + + var eye2 = Blockly.utils.createSvgElement('circle', {}, this.svgFace_); + eye2.setAttribute('cx','29.1'); + eye2.setAttribute('cy','-3.3'); + eye2.setAttribute('r','3.4'); + eye2.setAttribute('fill-opacity','0.6'); + this.svgPath_.svgFace.eye2 = eye2; + + var mouth = Blockly.utils.createSvgElement('path', {}, this.svgFace_); + mouth.setAttribute('d','M45.6,0.1c-0.9,0-1.7-0.3-2.3-0.9' + + 'c-0.6,0.6-1.3,0.9-2.2,0.9c-0.9,0-1.8-0.3-2.3-0.9c-1-1.1-1.1-2.6-1.1-2.8' + + 'c0-0.5,0.5-1,1-1l0,0c0.6,0,1,0.5,1,1c0,0.4,0.1,1.7,1.4,1.7' + + 'c0.5,0,0.7-0.2,0.8-0.3c0.3-0.3,0.4-1,0.4-1.3c0-0.1,0-0.1,0-0.2' + + 'c0-0.5,0.5-1,1-1l0,0c0.5,0,1,0.4,1,1c0,0,0,0.1,0,0.2' + + 'c0,0.3,0.1,0.9,0.4,1.2C44.8-2.2,45-2,45.5-2s0.7-0.2,0.8-0.3' + + 'c0.3-0.4,0.4-1.1,0.3-1.3c0-0.5,0.4-1,0.9-1.1c0.5,0,1,0.4,1.1,0.9' + + 'c0,0.2,0.1,1.8-0.8,2.8C47.5-0.4,46.8,0.1,45.6,0.1z'); + mouth.setAttribute('fill-opacity','0.6'); + + this.svgPath_.ear.setAttribute('d','M73.1-15.6c1.7-4.2,4.5-9.1,5.8-8.5' + + 'c1.6,0.8,5.4,7.9,5,15.4c0,0.6-0.7,0.7-1.1,0.5c-3-1.6-6.4-2.8-8.6-3.6' + + 'C72.8-12.3,72.4-13.7,73.1-15.6z'); + this.svgPath_.ear.setAttribute('fill','#FFD5E6'); + + this.svgPath_.ear2.setAttribute('d','M22.4-15.6c-1.7-4.2-4.5-9.1-5.8-8.5' + + 'c-1.6,0.8-5.4,7.9-5,15.4c0,0.6,0.7,0.7,1.1,0.5c3-1.6,6.4-2.8,8.6-3.6' + + 'C22.8-12.3,23.2-13.7,22.4-15.6z'); + this.svgPath_.ear2.setAttribute('fill','#FFD5E6'); +}; + /** * Draw the path of the block. * Move the fields to the correct locations. @@ -1136,6 +1218,9 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) { // No output or previous connection. this.squareTopLeftCorner_ = true; this.startHat_ = true; + if (Blockly.useCatBlocks) { + this.initCatStuff(); + } inputRows.rightEdge = Math.max(inputRows.rightEdge, 100); } @@ -1162,12 +1247,23 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) { this.renderDrawLeft_(steps); var pathString = steps.join(' '); - this.svgPath_.setAttribute('d', pathString); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('d', pathString); + if (this.startHat_ && !this.svgFace_.firstChild) { + this.renderCatFace_(); + } + } else { + this.svgPath_.setAttribute('d', pathString); + } if (this.RTL) { // Mirror the block's path. // This is awesome. - this.svgPath_.setAttribute('transform', 'scale(-1 1)'); + if (Blockly.useCatBlocks) { + this.svgPathBody_.setAttribute('transform', 'scale(-1 1)'); + } else { + this.svgPath_.setAttribute('transform', 'scale(-1 1)'); + } } }; diff --git a/core/block_svg.js b/core/block_svg.js index 6e24dbe86f..4cccf31583 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -64,10 +64,25 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { */ this.svgGroup_ = Blockly.utils.createSvgElement('g', {}, null); /** @type {SVGElement} */ - this.svgPath_ = Blockly.utils.createSvgElement('path', - {'class': 'blocklyPath blocklyBlockBackground'}, - this.svgGroup_); - this.svgPath_.tooltip = this; + if (Blockly.useCatBlocks) { + this.svgPath_ = Blockly.utils.createSvgElement('g', {}, this.svgGroup_); + this.svgPathBody_ = Blockly.utils.createSvgElement('path', + {'class': 'blocklyPath blocklyBlockBackground'}, this.svgPath_); + + this.svgFace_ = Blockly.utils.createSvgElement('g', {}, + this.svgPath_); + this.svgGroup_.svgPath = this.svgPath_; + this.svgPath_.svgFace = this.svgFace_; + this.svgPath_.svgBody = this.svgPathBody_; + this.lastCallTime = 0; + this.CALL_FREQUENCY_MS = 60; + + this.svgPathBody_.tooltip = this; + } else { + this.svgPath_ = Blockly.utils.createSvgElement('path', {'class': 'blocklyPath blocklyBlockBackground'}, + this.svgGroup_); + this.svgPath_.tooltip = this; + } /** @type {boolean} */ this.rendered = false; @@ -80,7 +95,11 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { */ this.useDragSurface_ = Blockly.utils.is3dSupported() && !!workspace.blockDragSurface_; - Blockly.Tooltip.bindMouseEvents(this.svgPath_); + if (Blockly.useCatBlocks) { + Blockly.Tooltip.bindMouseEvents(this.svgPathBody_); + } else { + Blockly.Tooltip.bindMouseEvents(this.svgPath_); + } Blockly.BlockSvg.superClass_.constructor.call(this, workspace, prototypeName, opt_id); @@ -160,6 +179,8 @@ Blockly.BlockSvg.prototype.initSvg = function() { for (i = 0; i < icons.length; i++) { icons[i].createIcon(); } + } else if (this.svgPathBody_) { + this.svgPathBody_.setAttribute('stroke-opacity', '0'); } this.updateColour(); this.updateMovable(); @@ -218,6 +239,189 @@ Blockly.BlockSvg.prototype.unselect = function() { this.removeSelect(); }; +Blockly.BlockSvg.prototype.initCatStuff = function() { + if (this.hasInitCatStuff) return; + this.hasInitCatStuff = true; + + // Ear part of the SVG path for hat blocks + var LEFT_EAR_UP = 'c-1,-12.5 5.3,-23.3 8.4,-24.8c3.7,-1.8 16.5,13.1 18.4,15.4'; + var LEFT_EAR_DOWN = 'c-5.8,-4.8 -8,-18 -4.9,-19.5c3.7,-1.8 24.5,11.1 31.7,10.1'; + var RIGHT_EAR_UP = 'c1.9,-2.3 14.7,-17.2 18.4,-15.4c3.1,1.5 9.4,12.3 8.4,24.8'; + var RIGHT_EAR_DOWN = 'c7.2,1 28,-11.9 31.7,-10.1c3.1,1.5 0.9,14.7 -4.9,19.5'; + // Ears look slightly different for define hat blocks + var DEFINE_HAT_LEFT_EAR_UP = 'c0,-7.1 3.7,-13.3 9.3,-16.9c1.7,-7.5 5.4,-13.2 7.6,-14.2c2.6,-1.3 10,6 14.6,11.1'; + var DEFINE_HAT_RIGHT_EAR_UP = 'h33c4.6,-5.1 11.9,-12.4 14.6,-11.1c1.9,0.9 4.9,5.2 6.8,11.1c2.6,0,5.2,0,7.8,0'; + var DEFINE_HAT_LEFT_EAR_DOWN = 'c0,-4.6 1.6,-8.9 4.3,-12.3c-2.4,-5.6 -2.9,-12.4 -0.7,-13.4c2.1,-1 9.6,2.6 17,5.8' + + 'c2.6,0 6.2,0 10.9,0'; + var DEFINE_HAT_RIGHT_EAR_DOWN = 'c0,0 25.6,0 44,0c7.4,-3.2 14.8,-6.8 16.9,-5.8c1.2,0.6 1.6,2.9 1.3,5.8'; + + var that = this; + this.svgPath_.ear = Blockly.utils.createSvgElement('path', {}, this.svgPath_); + this.svgPath_.ear2 = Blockly.utils.createSvgElement('path', {}, this.svgPath_); + if (this.RTL) { + // Mirror the ears. + this.svgPath_.ear.setAttribute('transform', 'scale(-1 1)'); + this.svgPath_.ear2.setAttribute('transform', 'scale(-1 1)'); + } + this.svgPath_.addEventListener("mouseenter", function(event) { + clearTimeout(that.blinkFn); + // blink + if (event.target.svgFace.eye) { + event.target.svgFace.eye.setAttribute('fill-opacity','0'); + event.target.svgFace.eye2.setAttribute('fill-opacity','0'); + event.target.svgFace.closedEye.setAttribute('fill-opacity','0.6'); + event.target.svgFace.closedEye2.setAttribute('fill-opacity','0.6'); + } + + // reset after a short delay + that.blinkFn = setTimeout(function() { + if (event.target.svgFace.eye) { + event.target.svgFace.eye.setAttribute('fill-opacity','0.6'); + event.target.svgFace.eye2.setAttribute('fill-opacity','0.6'); + event.target.svgFace.closedEye.setAttribute('fill-opacity','0'); + event.target.svgFace.closedEye2.setAttribute('fill-opacity','0'); + } + }, 100); + }); + + this.svgPath_.ear.addEventListener("mouseenter", function() { + clearTimeout(that.earFn); + clearTimeout(that.ear2Fn); + // ear flick + that.svgPath_.ear.setAttribute('fill-opacity','0'); + that.svgPath_.ear2.setAttribute('fill-opacity',''); + var bodyPath = that.svgPath_.svgBody.getAttribute('d'); + bodyPath = bodyPath.replace(RIGHT_EAR_UP, RIGHT_EAR_DOWN); + bodyPath = bodyPath.replace(DEFINE_HAT_RIGHT_EAR_UP, DEFINE_HAT_RIGHT_EAR_DOWN); + bodyPath = bodyPath.replace(LEFT_EAR_DOWN, LEFT_EAR_UP); + bodyPath = bodyPath.replace(DEFINE_HAT_LEFT_EAR_DOWN, DEFINE_HAT_LEFT_EAR_UP); + that.svgPath_.svgBody.setAttribute('d', bodyPath); + + // reset after a short delay + that.earFn = setTimeout(function() { + that.svgPath_.ear.setAttribute('fill-opacity',''); + var bodyPath = that.svgPath_.svgBody.getAttribute('d'); + bodyPath = bodyPath.replace(RIGHT_EAR_DOWN, RIGHT_EAR_UP); + bodyPath = bodyPath.replace(DEFINE_HAT_RIGHT_EAR_DOWN, DEFINE_HAT_RIGHT_EAR_UP); + that.svgPath_.svgBody.setAttribute('d', bodyPath); + }, 50); + }); + this.svgPath_.ear2.addEventListener("mouseenter", function() { + clearTimeout(that.earFn); + clearTimeout(that.ear2Fn); + // ear flick + that.svgPath_.ear2.setAttribute('fill-opacity','0'); + that.svgPath_.ear.setAttribute('fill-opacity',''); + var bodyPath = that.svgPath_.svgBody.getAttribute('d'); + bodyPath = bodyPath.replace(LEFT_EAR_UP, LEFT_EAR_DOWN); + bodyPath = bodyPath.replace(DEFINE_HAT_LEFT_EAR_UP, DEFINE_HAT_LEFT_EAR_DOWN); + bodyPath = bodyPath.replace(RIGHT_EAR_DOWN, RIGHT_EAR_UP); + bodyPath = bodyPath.replace(DEFINE_HAT_RIGHT_EAR_DOWN, DEFINE_HAT_RIGHT_EAR_UP); + that.svgPath_.svgBody.setAttribute('d', bodyPath); + + // reset after a short delay + that.ear2Fn = setTimeout(function() { + that.svgPath_.ear2.setAttribute('fill-opacity',''); + var bodyPath = that.svgPath_.svgBody.getAttribute('d'); + var bodyPath = that.svgPath_.svgBody.getAttribute('d'); + bodyPath = bodyPath.replace(LEFT_EAR_DOWN, LEFT_EAR_UP); + bodyPath = bodyPath.replace(DEFINE_HAT_LEFT_EAR_DOWN, DEFINE_HAT_LEFT_EAR_UP); + that.svgPath_.svgBody.setAttribute('d', bodyPath); + }, 50); + }); + this.windowListener = function(event) { + var time = Date.now(); + if (time < that.lastCallTime + that.CALL_FREQUENCY_MS) return; + that.lastCallTime = time; + if (!that.shouldWatchMouse()) return; + + // mouse watching + if (that.workspace) { // not disposed + var xy = that.getCatFacePosition(); + var mouseLocation = { + x: event.x / that.workspace.scale, + y: event.y / that.workspace.scale + }; + + var dx = mouseLocation.x - xy.x; + var dy = mouseLocation.y - xy.y; + var theta = Math.atan2(dx, dy); + + // Map the vector from the cat face to the mouse location to a much shorter + // vector in the same direction, which will be the translation vector for + // the cat face + var delta = Math.sqrt(dx * dx + dy * dy); + var scaleFactor = delta / (delta + 1); + + // Equation for radius of ellipse at theta for axes with length a and b + var a = 2; + var b = 5; + var r = a * b / Math.sqrt(Math.pow(b * Math.cos(theta), 2) + Math.pow(a * Math.sin(theta), 2)); + + // Convert polar coordinate back to x, y coordinate + dx = (r * scaleFactor) * Math.sin(theta); + dy = (r * scaleFactor) * Math.cos(theta); + + if (that.RTL) dx -= 87; // Translate face over + that.svgFace_.style.transform = 'translate(' + dx + 'px, ' + dy + 'px)'; + } + }; + if (this.RTL) { + // Set to the correct initial position + this.svgFace_.style.transform = 'translate(-87px, 0px)'; + } + if (this.shouldWatchMouse()) { + document.addEventListener('mousemove', this.windowListener); + } +}; + +/** + * Get cat face position + * @return {Object} coordinates of center of cat face + */ +Blockly.BlockSvg.prototype.getCatFacePosition = function() { + // getBoundingClientRect is not performant + //var offset = that.workspace.getParentSvg().getBoundingClientRect(); + var offset = {x:0, y:92}; + + offset.x += 120; // scratchCategoryMenu width + + if (!this.isInFlyout && this.workspace.getFlyout()) { + offset.x += this.workspace.getFlyout().getWidth(); + } + + offset.x += this.workspace.scrollX; + offset.y += this.workspace.scrollY; + + var xy = this.getRelativeToSurfaceXY(this.svgGroup_); + if (this.RTL) { + xy.x = this.workspace.getWidth() - xy.x - this.width; + } + // convert to workspace units + xy.x += offset.x / this.workspace.scale; + xy.y += offset.y / this.workspace.scale; + // distance to center of face + xy.x -= 43.5; + xy.y -= 4; + if (this.RTL) { + // We've been calculating from the right edge. Convert x to from left edge. + xy.x = screen.width - xy.x; + } + return xy; +}; + +/** + * True if cat should watch mouse + * @return {boolean} true if the block should be watching the mouse + */ +Blockly.BlockSvg.prototype.shouldWatchMouse = function() { + if (window.vmLoadHigh || !window.CAT_CHASE_MOUSE) return false; + var xy = this.getCatFacePosition(); + var blockXOnScreen = xy.x > 0 && xy.x < screen.width / this.workspace.scale; + var blockYOnScreen = xy.y > 0 && xy.y < screen.height / this.workspace.scale; + return this.startHat_ && !this.isGlowingStack_ && blockXOnScreen && blockYOnScreen; +}; + /** * Glow only this particular block, to highlight it visually as if it's running. * @param {boolean} isGlowingBlock Whether the block should glow. @@ -232,6 +436,23 @@ Blockly.BlockSvg.prototype.setGlowBlock = function(isGlowingBlock) { * @param {boolean} isGlowingStack Whether the stack starting with this block should glow. */ Blockly.BlockSvg.prototype.setGlowStack = function(isGlowingStack) { + if (Blockly.useCatBlocks) { + if (isGlowingStack) { + // For performance, don't follow the mouse when the stack is glowing + document.removeEventListener('mousemove', this.windowListener); + if (this.workspace && this.svgFace_.style) { + // reset face direction + if (this.RTL) { + this.svgFace_.style.transform = 'translate(-87px, 0px)'; + } else { + this.svgFace_.style.transform = ''; + } + } + } else { + document.addEventListener('mousemove', this.windowListener); + } + } + this.isGlowingStack_ = isGlowingStack; // Update the applied SVG filter if the property has changed var svg = this.getSvgRoot(); @@ -822,6 +1043,19 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { // The block has already been deleted. return; } + // TODO: Can we skip the checks and always clear the timeouts? + if (this.blinkFn) { + clearTimeout(this.blinkFn); + } + if (this.earFn) { + clearTimeout(this.earFn); + } + if (this.ear2Fn) { + clearTimeout(this.ear2Fn); + } + if (this.windowListener) { + document.removeEventListener('mousemove', this.windowListener); + } Blockly.Tooltip.hide(); Blockly.Field.startCache(); // Save the block's workspace temporarily so we can resize the @@ -860,6 +1094,8 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { // Sever JavaScript to DOM connections. this.svgGroup_ = null; this.svgPath_ = null; + this.svgPathBody_ = null; + this.svgFace_ = null; Blockly.Field.stopCache(); }; From 4bd96183c840bbbb1b2e7b60e13ca0f0968ab617 Mon Sep 17 00:00:00 2001 From: Kaloyan Manolov Date: Thu, 6 Nov 2025 15:54:53 +0200 Subject: [PATCH 2/4] fix: use getters to dynamically update svg block constants --- core/block_render_svg_vertical.js | 48 +++++++++++++++++++++---------- core/block_svg.js | 1 - 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/core/block_render_svg_vertical.js b/core/block_render_svg_vertical.js index d4dc534ddb..c498ac708b 100644 --- a/core/block_render_svg_vertical.js +++ b/core/block_render_svg_vertical.js @@ -139,7 +139,13 @@ Blockly.BlockSvg.STATEMENT_INPUT_INNER_SPACE = 2 * Blockly.BlockSvg.GRID_UNIT; * Height of the top hat. * @const */ -Blockly.BlockSvg.START_HAT_HEIGHT = Blockly.useCatBlocks ? 31 : 16; +Object.defineProperty(Blockly.BlockSvg, 'START_HAT_HEIGHT', { + get: function() { + return Blockly.useCatBlocks ? 31 : 16; + }, + enumerable: true, + configurable: true +}); /** * Height of the vertical separator line for icons that appear at the left edge @@ -152,12 +158,18 @@ Blockly.BlockSvg.ICON_SEPARATOR_HEIGHT = 10 * Blockly.BlockSvg.GRID_UNIT; * Path of the top hat's curve. * @const */ -Blockly.BlockSvg.START_HAT_PATH = Blockly.useCatBlocks - ? 'c2.6,-2.3 5.5,-4.3 8.5,-6.2' + - 'c-1,-12.5 5.3,-23.3 8.4,-24.8c3.7,-1.8 16.5,13.1 18.4,15.4' + - 'c8.4,-1.3 17,-1.3 25.4,0c1.9,-2.3 14.7,-17.2 18.4,-15.4' + - 'c3.1,1.5 9.4,12.3 8.4,24.8c3,1.8 5.9,3.9 8.5,6.1' - : 'c 25,-22 71,-22 96,0'; +Object.defineProperty(Blockly.BlockSvg, 'START_HAT_PATH', { + get: function() { + return Blockly.useCatBlocks + ? 'c2.6,-2.3 5.5,-4.3 8.5,-6.2' + + 'c-1,-12.5 5.3,-23.3 8.4,-24.8c3.7,-1.8 16.5,13.1 18.4,15.4' + + 'c8.4,-1.3 17,-1.3 25.4,0c1.9,-2.3 14.7,-17.2 18.4,-15.4' + + 'c3.1,1.5 9.4,12.3 8.4,24.8c3,1.8 5.9,3.9 8.5,6.1' + : 'c 25,-22 71,-22 96,0'; + }, + enumerable: true, + configurable: true +}); /** * SVG path for drawing next/previous notch from left to right. @@ -481,14 +493,20 @@ Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS = 5 * Blockly.BlockSvg.GRID_UNIT; * SVG path for drawing the rounded top-left corner. * @const */ -Blockly.BlockSvg.TOP_LEFT_CORNER_DEFINE_HAT = Blockly.useCatBlocks - ? 'c0,-7.1 3.7,-13.3 9.3,-16.9c1.7,-7.5 5.4,-13.2 7.6,-14.2' + - 'c2.6,-1.3 10,6 14.6,11.1h33c4.6,-5.1 11.9,-12.4 14.6,-11.1' + - 'c1.9,0.9 4.9,5.2 6.8,11.1c2.6,0,5.2,0,7.8,0' - : 'a ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' + - Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ' 0 0,1 ' + - Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',-' + - Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS; +Object.defineProperty(Blockly.BlockSvg, 'TOP_LEFT_CORNER_DEFINE_HAT', { + get: function() { + return Blockly.useCatBlocks + ? 'c0,-7.1 3.7,-13.3 9.3,-16.9c1.7,-7.5 5.4,-13.2 7.6,-14.2' + + 'c2.6,-1.3 10,6 14.6,11.1h33c4.6,-5.1 11.9,-12.4 14.6,-11.1' + + 'c1.9,0.9 4.9,5.2 6.8,11.1c2.6,0,5.2,0,7.8,0' + : 'a ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' + + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ' 0 0,1 ' + + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',-' + + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS; + }, + enumerable: true, + configurable: true +}); /** * SVG path for drawing the rounded top-left corner. diff --git a/core/block_svg.js b/core/block_svg.js index 4cccf31583..a3fd52c0a0 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -323,7 +323,6 @@ Blockly.BlockSvg.prototype.initCatStuff = function() { that.ear2Fn = setTimeout(function() { that.svgPath_.ear2.setAttribute('fill-opacity',''); var bodyPath = that.svgPath_.svgBody.getAttribute('d'); - var bodyPath = that.svgPath_.svgBody.getAttribute('d'); bodyPath = bodyPath.replace(LEFT_EAR_DOWN, LEFT_EAR_UP); bodyPath = bodyPath.replace(DEFINE_HAT_LEFT_EAR_DOWN, DEFINE_HAT_LEFT_EAR_UP); that.svgPath_.svgBody.setAttribute('d', bodyPath); From c44cb57f2e7fe8d8a144ec5201ed836d1b8d64f0 Mon Sep 17 00:00:00 2001 From: Kaloyan Manolov Date: Wed, 12 Nov 2025 17:24:06 +0200 Subject: [PATCH 3/4] feat: abstract away some of the cat-blocks implementation logic --- core/block_render_svg_horizontal.js | 37 +++---------- core/block_render_svg_vertical.js | 84 +++++++++++++---------------- core/block_svg.js | 34 ++++++++---- core/blockly.js | 29 ++++++++++ core/constants.js | 9 ++++ 5 files changed, 106 insertions(+), 87 deletions(-) diff --git a/core/block_render_svg_horizontal.js b/core/block_render_svg_horizontal.js index e2844a6279..48f2d24521 100644 --- a/core/block_render_svg_horizontal.js +++ b/core/block_render_svg_horizontal.js @@ -27,6 +27,7 @@ goog.provide('Blockly.BlockSvg.render'); goog.require('Blockly.BlockSvg'); +goog.require('Blockly.constants'); // UI constants for rendering blocks. @@ -311,26 +312,14 @@ Blockly.BlockSvg.prototype.updateColour = function() { var strokeColour = this.getColourTertiary(); // Render block stroke - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('stroke', strokeColour); - } else { - this.svgPath_.setAttribute('stroke', strokeColour); - } + this.blockFrameElement_.setAttribute('stroke', strokeColour); // Render block fill var fillColour = (this.isGlowingBlock_) ? this.getColourSecondary() : this.getColour(); - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('fill', fillColour); - } else { - this.svgPath_.setAttribute('fill', fillColour); - } + this.blockFrameElement_.setAttribute('fill', fillColour); // Render opacity - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('fill-opacity', this.getOpacity()); - } else { - this.svgPath_.setAttribute('fill-opacity', this.getOpacity()); - } + this.blockFrameElement_.setAttribute('fill-opacity', this.getOpacity()); // Bump every dropdown to change its colour. for (var x = 0, input; input = this.inputList[x]; x++) { @@ -349,19 +338,11 @@ Blockly.BlockSvg.prototype.highlightForReplacement = function(add) { if (add) { var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId || 'blocklyReplacementGlowFilter'; - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); - } else { - this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); - } + this.blockFrameElement_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } else { - if (Blockly.useCatBlocks) { - this.svgPathBody_.removeAttribute('filter'); - } else { - this.svgPath_.removeAttribute('filter'); - } + this.blockFrameElement_.removeAttribute('filter'); Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } @@ -565,11 +546,7 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) { this.renderDrawTop_(steps, connectionsXY, metrics); var pathString = steps.join(' '); - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('d', pathString); - } else { - this.svgPath_.setAttribute('d', pathString); - } + this.blockFrameElement_.setAttribute('d', pathString); if (this.RTL) { // Mirror the block's path. diff --git a/core/block_render_svg_vertical.js b/core/block_render_svg_vertical.js index c498ac708b..a0d3e9295e 100644 --- a/core/block_render_svg_vertical.js +++ b/core/block_render_svg_vertical.js @@ -29,6 +29,7 @@ goog.provide('Blockly.BlockSvg.render'); goog.require('Blockly.BlockSvg'); goog.require('Blockly.scratchBlocksUtils'); goog.require('Blockly.utils'); +goog.require('Blockly.constants'); // UI constants for rendering blocks. @@ -141,7 +142,11 @@ Blockly.BlockSvg.STATEMENT_INPUT_INNER_SPACE = 2 * Blockly.BlockSvg.GRID_UNIT; */ Object.defineProperty(Blockly.BlockSvg, 'START_HAT_HEIGHT', { get: function() { - return Blockly.useCatBlocks ? 31 : 16; + if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) { + return 31; + } + + return 16; }, enumerable: true, configurable: true @@ -160,12 +165,14 @@ Blockly.BlockSvg.ICON_SEPARATOR_HEIGHT = 10 * Blockly.BlockSvg.GRID_UNIT; */ Object.defineProperty(Blockly.BlockSvg, 'START_HAT_PATH', { get: function() { - return Blockly.useCatBlocks - ? 'c2.6,-2.3 5.5,-4.3 8.5,-6.2' + + if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) { + return 'c2.6,-2.3 5.5,-4.3 8.5,-6.2' + 'c-1,-12.5 5.3,-23.3 8.4,-24.8c3.7,-1.8 16.5,13.1 18.4,15.4' + 'c8.4,-1.3 17,-1.3 25.4,0c1.9,-2.3 14.7,-17.2 18.4,-15.4' + - 'c3.1,1.5 9.4,12.3 8.4,24.8c3,1.8 5.9,3.9 8.5,6.1' - : 'c 25,-22 71,-22 96,0'; + 'c3.1,1.5 9.4,12.3 8.4,24.8c3,1.8 5.9,3.9 8.5,6.1'; + } + + return 'c 25,-22 71,-22 96,0'; }, enumerable: true, configurable: true @@ -495,11 +502,13 @@ Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS = 5 * Blockly.BlockSvg.GRID_UNIT; */ Object.defineProperty(Blockly.BlockSvg, 'TOP_LEFT_CORNER_DEFINE_HAT', { get: function() { - return Blockly.useCatBlocks - ? 'c0,-7.1 3.7,-13.3 9.3,-16.9c1.7,-7.5 5.4,-13.2 7.6,-14.2' + + if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) { + return 'c0,-7.1 3.7,-13.3 9.3,-16.9c1.7,-7.5 5.4,-13.2 7.6,-14.2' + 'c2.6,-1.3 10,6 14.6,11.1h33c4.6,-5.1 11.9,-12.4 14.6,-11.1' + - 'c1.9,0.9 4.9,5.2 6.8,11.1c2.6,0,5.2,0,7.8,0' - : 'a ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' + + 'c1.9,0.9 4.9,5.2 6.8,11.1c2.6,0,5.2,0,7.8,0'; + } + + return 'a ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ' 0 0,1 ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',-' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS; @@ -544,12 +553,7 @@ Blockly.BlockSvg.prototype.updateColour = function() { } } - // Render block stroke - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('stroke', strokeColour); - } else { - this.svgPath_.setAttribute('stroke', strokeColour); - } + this.blockFrameElement_.setAttribute('stroke', strokeColour); // Render block fill if (this.isGlowingBlock_ || renderShadowed) { @@ -562,18 +566,10 @@ Blockly.BlockSvg.prototype.updateColour = function() { } else { var fillColour = this.getColour(); } - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('fill', fillColour); - } else { - this.svgPath_.setAttribute('fill', fillColour); - } + this.blockFrameElement_.setAttribute('fill', fillColour); // Render opacity - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('fill-opacity', this.getOpacity()); - } else { - this.svgPath_.setAttribute('fill-opacity', this.getOpacity()); - } + this.blockFrameElement_.setAttribute('fill-opacity', this.getOpacity()); // Update colours of input shapes. for (var i = 0, input; input = this.inputList[i]; i++) { @@ -605,19 +601,11 @@ Blockly.BlockSvg.prototype.highlightForReplacement = function(add) { if (add) { var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId || 'blocklyReplacementGlowFilter'; - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); - } else { - this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); - } + this.blockFrameElement_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } else { - if (Blockly.useCatBlocks) { - this.svgPathBody_.removeAttribute('filter'); - } else { - this.svgPath_.removeAttribute('filter'); - } + this.blockFrameElement_.removeAttribute('filter'); Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } @@ -1166,7 +1154,14 @@ Blockly.BlockSvg.prototype.computeOutputPadding_ = function(inputRows) { }; // Cat face and ear animation for CatBlocks +// TODO: Do we want to move this and `initCatFace_` to a separate file? +// Or would just that complicate unforking? Blockly.BlockSvg.prototype.renderCatFace_ = function() { + // This only makes sense in the context of the Cat Blocks theme. + if (this.theme !== Blockly.Themes.CAT_BLOCKS) { + return; + } + this.svgPath_.svgFace.setAttribute('fill','#000000'); var closedEye = Blockly.utils.createSvgElement('path', {}, this.svgFace_); @@ -1236,7 +1231,7 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) { // No output or previous connection. this.squareTopLeftCorner_ = true; this.startHat_ = true; - if (Blockly.useCatBlocks) { + if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) { this.initCatStuff(); } inputRows.rightEdge = Math.max(inputRows.rightEdge, 100); @@ -1265,23 +1260,16 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) { this.renderDrawLeft_(steps); var pathString = steps.join(' '); - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('d', pathString); - if (this.startHat_ && !this.svgFace_.firstChild) { - this.renderCatFace_(); - } - } else { - this.svgPath_.setAttribute('d', pathString); + this.blockFrameElement_.setAttribute('d', pathString); + + if (Blockly.theme === Blockly.Themes.CAT_BLOCKS && this.startHat_ && !this.svgFace_.firstChild) { + this.renderCatFace_(); } if (this.RTL) { // Mirror the block's path. // This is awesome. - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('transform', 'scale(-1 1)'); - } else { - this.svgPath_.setAttribute('transform', 'scale(-1 1)'); - } + this.blockFrameElement_.setAttribute('transform', 'scale(-1 1)'); } }; diff --git a/core/block_svg.js b/core/block_svg.js index a3fd52c0a0..447baee52d 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -37,6 +37,7 @@ goog.require('Blockly.scratchBlocksUtils'); goog.require('Blockly.Tooltip'); goog.require('Blockly.Touch'); goog.require('Blockly.utils'); +goog.require('Blockly.constants'); goog.require('goog.Timer'); goog.require('goog.asserts'); @@ -64,7 +65,7 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { */ this.svgGroup_ = Blockly.utils.createSvgElement('g', {}, null); /** @type {SVGElement} */ - if (Blockly.useCatBlocks) { + if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) { this.svgPath_ = Blockly.utils.createSvgElement('g', {}, this.svgGroup_); this.svgPathBody_ = Blockly.utils.createSvgElement('path', {'class': 'blocklyPath blocklyBlockBackground'}, this.svgPath_); @@ -95,11 +96,7 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { */ this.useDragSurface_ = Blockly.utils.is3dSupported() && !!workspace.blockDragSurface_; - if (Blockly.useCatBlocks) { - Blockly.Tooltip.bindMouseEvents(this.svgPathBody_); - } else { - Blockly.Tooltip.bindMouseEvents(this.svgPath_); - } + Blockly.Tooltip.bindMouseEvents(this.blockFrameElement_); Blockly.BlockSvg.superClass_.constructor.call(this, workspace, prototypeName, opt_id); @@ -195,6 +192,23 @@ Blockly.BlockSvg.prototype.initSvg = function() { } }; +Object.defineProperty(Blockly.BlockSvg.prototype, 'blockFrameElement_', { + /** + * The svg element (e.g. svgPath_ or svgPathBody_) that is + * responsible for the outline of the block, based on the current theme. + * @return {!SVGElement} The SVG element forming the outline. + * @this {Blockly.BlockSvg} + */ + get: function() { + if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) { + return this.svgPathBody_; + } + return this.svgPath_; + }, + enumerable: true, + configurable: true +}); + /** * Select this block. Highlight it visually. */ @@ -240,7 +254,10 @@ Blockly.BlockSvg.prototype.unselect = function() { }; Blockly.BlockSvg.prototype.initCatStuff = function() { - if (this.hasInitCatStuff) return; + if (this.theme !== Blockly.Themes.CAT_BLOCKS || this.hasInitCatStuff) { + return; + } + // TODO: Test what happens if we turn on and off Cat Blocks several times this.hasInitCatStuff = true; // Ear part of the SVG path for hat blocks @@ -435,7 +452,7 @@ Blockly.BlockSvg.prototype.setGlowBlock = function(isGlowingBlock) { * @param {boolean} isGlowingStack Whether the stack starting with this block should glow. */ Blockly.BlockSvg.prototype.setGlowStack = function(isGlowingStack) { - if (Blockly.useCatBlocks) { + if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) { if (isGlowingStack) { // For performance, don't follow the mouse when the stack is glowing document.removeEventListener('mousemove', this.windowListener); @@ -1042,7 +1059,6 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { // The block has already been deleted. return; } - // TODO: Can we skip the checks and always clear the timeouts? if (this.blinkFn) { clearTimeout(this.blinkFn); } diff --git a/core/blockly.js b/core/blockly.js index fa0b484896..c766302069 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -112,6 +112,13 @@ Blockly.clipboardSource_ = null; */ Blockly.cache3dSupported_ = null; +/** + * Active theme. + * @type {!Blockly.Themes} + * @private + */ +Blockly.theme_ = Blockly.Themes.CLASSIC; + /** * Convert a hue (HSV model) into an RGB hex triplet. * @param {number} hue Hue on a colour wheel (0-360). @@ -175,6 +182,28 @@ Blockly.svgResize = function(workspace) { mainWorkspace.resize(); }; +/** + * Apply a global theme to Blockly. This will then be used in all workspaces - + * both newly created and already existing. + */ +Object.defineProperty(Blockly, 'theme', { + /** + * Get the current theme. + * @return {!Blockly.Themes} The current global theme. + */ + get: function() { + return Blockly.theme_; + } +}); + +Blockly.switchTheme = function(theme) { + if (theme === Blockly.Themes.CAT_BLOCKS) { + Blockly.theme_ = theme; + } else { + Blockly.theme_ = Blockly.Themes.CLASSIC; + } +}; + /** * Handle a key-down on SVG drawing surface. Does nothing if the main workspace is not visible. * @param {!Event} e Key down event. diff --git a/core/constants.js b/core/constants.js index 2b6d653586..539879c164 100644 --- a/core/constants.js +++ b/core/constants.js @@ -385,3 +385,12 @@ Blockly.StatusButtonState = { "READY": "ready", "NOT_READY": "not ready", }; + +/** + * ENUM defining supported themes. + * @enum {string} + */ +Blockly.Themes = { + CLASSIC: "classic", + CAT_BLOCKS: "catblocks" +}; From acfb3e6434e6bb101ac45fbb2e730daa472b715f Mon Sep 17 00:00:00 2001 From: Kaloyan Manolov Date: Thu, 13 Nov 2025 17:31:53 +0200 Subject: [PATCH 4/4] fix: some silly issues and a rename --- core/block_render_svg_horizontal.js | 6 +----- core/block_render_svg_vertical.js | 2 +- core/block_svg.js | 2 +- core/blockly.js | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/core/block_render_svg_horizontal.js b/core/block_render_svg_horizontal.js index 48f2d24521..8ccc6119b0 100644 --- a/core/block_render_svg_horizontal.js +++ b/core/block_render_svg_horizontal.js @@ -551,11 +551,7 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) { if (this.RTL) { // Mirror the block's path. // This is awesome. - if (Blockly.useCatBlocks) { - this.svgPathBody_.setAttribute('transform', 'scale(-1 1)'); - } else { - this.svgPath_.setAttribute('transform', 'scale(-1 1)'); - } + this.blockFrameElement_.setAttribute('transform', 'scale(-1 1)'); } // Horizontal blocks have a single Image Field that is specially positioned diff --git a/core/block_render_svg_vertical.js b/core/block_render_svg_vertical.js index a0d3e9295e..6a6e9b6541 100644 --- a/core/block_render_svg_vertical.js +++ b/core/block_render_svg_vertical.js @@ -1158,7 +1158,7 @@ Blockly.BlockSvg.prototype.computeOutputPadding_ = function(inputRows) { // Or would just that complicate unforking? Blockly.BlockSvg.prototype.renderCatFace_ = function() { // This only makes sense in the context of the Cat Blocks theme. - if (this.theme !== Blockly.Themes.CAT_BLOCKS) { + if (Blockly.theme !== Blockly.Themes.CAT_BLOCKS) { return; } diff --git a/core/block_svg.js b/core/block_svg.js index 447baee52d..259013de93 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -254,7 +254,7 @@ Blockly.BlockSvg.prototype.unselect = function() { }; Blockly.BlockSvg.prototype.initCatStuff = function() { - if (this.theme !== Blockly.Themes.CAT_BLOCKS || this.hasInitCatStuff) { + if (Blockly.theme !== Blockly.Themes.CAT_BLOCKS || this.hasInitCatStuff) { return; } // TODO: Test what happens if we turn on and off Cat Blocks several times diff --git a/core/blockly.js b/core/blockly.js index c766302069..0996503923 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -196,7 +196,7 @@ Object.defineProperty(Blockly, 'theme', { } }); -Blockly.switchTheme = function(theme) { +Blockly.setTheme = function(theme) { if (theme === Blockly.Themes.CAT_BLOCKS) { Blockly.theme_ = theme; } else {