diff --git a/core/block_render_svg_horizontal.js b/core/block_render_svg_horizontal.js index c1c4ecce3f..8ccc6119b0 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,14 +312,14 @@ Blockly.BlockSvg.prototype.updateColour = function() { var strokeColour = this.getColourTertiary(); // Render block stroke - this.svgPath_.setAttribute('stroke', strokeColour); + this.blockFrameElement_.setAttribute('stroke', strokeColour); // Render block fill var fillColour = (this.isGlowingBlock_) ? this.getColourSecondary() : this.getColour(); - this.svgPath_.setAttribute('fill', fillColour); + this.blockFrameElement_.setAttribute('fill', fillColour); // Render opacity - 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++) { @@ -337,11 +338,11 @@ Blockly.BlockSvg.prototype.highlightForReplacement = function(add) { if (add) { var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId || 'blocklyReplacementGlowFilter'; - this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); + this.blockFrameElement_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } else { - this.svgPath_.removeAttribute('filter'); + this.blockFrameElement_.removeAttribute('filter'); Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } @@ -545,12 +546,12 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) { this.renderDrawTop_(steps, connectionsXY, metrics); var pathString = steps.join(' '); - this.svgPath_.setAttribute('d', pathString); + this.blockFrameElement_.setAttribute('d', pathString); if (this.RTL) { // Mirror the block's path. // This is awesome. - 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 ca5ea470f3..6a6e9b6541 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. @@ -139,7 +140,17 @@ Blockly.BlockSvg.STATEMENT_INPUT_INNER_SPACE = 2 * Blockly.BlockSvg.GRID_UNIT; * Height of the top hat. * @const */ -Blockly.BlockSvg.START_HAT_HEIGHT = 16; +Object.defineProperty(Blockly.BlockSvg, 'START_HAT_HEIGHT', { + get: function() { + if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) { + return 31; + } + + return 16; + }, + enumerable: true, + configurable: true +}); /** * Height of the vertical separator line for icons that appear at the left edge @@ -152,7 +163,20 @@ 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'; +Object.defineProperty(Blockly.BlockSvg, 'START_HAT_PATH', { + get: function() { + 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'; + } + + return 'c 25,-22 71,-22 96,0'; + }, + enumerable: true, + configurable: true +}); /** * SVG path for drawing next/previous notch from left to right. @@ -476,11 +500,22 @@ 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.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() { + 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'; + } + + 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; + }, + enumerable: true, + configurable: true +}); /** * SVG path for drawing the rounded top-left corner. @@ -518,8 +553,7 @@ Blockly.BlockSvg.prototype.updateColour = function() { } } - // Render block stroke - this.svgPath_.setAttribute('stroke', strokeColour); + this.blockFrameElement_.setAttribute('stroke', strokeColour); // Render block fill if (this.isGlowingBlock_ || renderShadowed) { @@ -532,10 +566,10 @@ Blockly.BlockSvg.prototype.updateColour = function() { } else { var fillColour = this.getColour(); } - this.svgPath_.setAttribute('fill', fillColour); + this.blockFrameElement_.setAttribute('fill', fillColour); // Render opacity - 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++) { @@ -567,11 +601,11 @@ Blockly.BlockSvg.prototype.highlightForReplacement = function(add) { if (add) { var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId || 'blocklyReplacementGlowFilter'; - this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); + this.blockFrameElement_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')'); Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } else { - this.svgPath_.removeAttribute('filter'); + this.blockFrameElement_.removeAttribute('filter'); Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), 'blocklyReplaceable'); } @@ -1119,6 +1153,67 @@ Blockly.BlockSvg.prototype.computeOutputPadding_ = function(inputRows) { row.paddingEnd += Blockly.BlockSvg.SHAPE_IN_SHAPE_PADDING[shape][otherShape]; }; +// 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 (Blockly.theme !== Blockly.Themes.CAT_BLOCKS) { + return; + } + + 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 +1231,9 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) { // No output or previous connection. this.squareTopLeftCorner_ = true; this.startHat_ = true; + if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) { + this.initCatStuff(); + } inputRows.rightEdge = Math.max(inputRows.rightEdge, 100); } @@ -1162,12 +1260,16 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) { this.renderDrawLeft_(steps); var pathString = steps.join(' '); - 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. - 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 6e24dbe86f..259013de93 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,10 +65,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.theme === Blockly.Themes.CAT_BLOCKS) { + 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 +96,7 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { */ this.useDragSurface_ = Blockly.utils.is3dSupported() && !!workspace.blockDragSurface_; - Blockly.Tooltip.bindMouseEvents(this.svgPath_); + Blockly.Tooltip.bindMouseEvents(this.blockFrameElement_); Blockly.BlockSvg.superClass_.constructor.call(this, workspace, prototypeName, opt_id); @@ -160,6 +176,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(); @@ -174,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. */ @@ -218,6 +253,191 @@ Blockly.BlockSvg.prototype.unselect = function() { this.removeSelect(); }; +Blockly.BlockSvg.prototype.initCatStuff = function() { + if (Blockly.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 + 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'); + 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 +452,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.theme === Blockly.Themes.CAT_BLOCKS) { + 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 +1059,18 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { // The block has already been deleted. return; } + 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 +1109,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(); }; diff --git a/core/blockly.js b/core/blockly.js index fa0b484896..0996503923 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.setTheme = 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" +};