scratch-blocks 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,7 @@ goog.provide('Blockly.BlockSvg.render');
29
29
  goog.require('Blockly.BlockSvg');
30
30
  goog.require('Blockly.scratchBlocksUtils');
31
31
  goog.require('Blockly.utils');
32
+ goog.require('Blockly.constants');
32
33
 
33
34
 
34
35
  // UI constants for rendering blocks.
@@ -139,7 +140,17 @@ Blockly.BlockSvg.STATEMENT_INPUT_INNER_SPACE = 2 * Blockly.BlockSvg.GRID_UNIT;
139
140
  * Height of the top hat.
140
141
  * @const
141
142
  */
142
- Blockly.BlockSvg.START_HAT_HEIGHT = 16;
143
+ Object.defineProperty(Blockly.BlockSvg, 'START_HAT_HEIGHT', {
144
+ get: function() {
145
+ if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) {
146
+ return 31;
147
+ }
148
+
149
+ return 16;
150
+ },
151
+ enumerable: true,
152
+ configurable: true
153
+ });
143
154
 
144
155
  /**
145
156
  * 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;
152
163
  * Path of the top hat's curve.
153
164
  * @const
154
165
  */
155
- Blockly.BlockSvg.START_HAT_PATH = 'c 25,-22 71,-22 96,0';
166
+ Object.defineProperty(Blockly.BlockSvg, 'START_HAT_PATH', {
167
+ get: function() {
168
+ if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) {
169
+ return 'c2.6,-2.3 5.5,-4.3 8.5,-6.2' +
170
+ 'c-1,-12.5 5.3,-23.3 8.4,-24.8c3.7,-1.8 16.5,13.1 18.4,15.4' +
171
+ 'c8.4,-1.3 17,-1.3 25.4,0c1.9,-2.3 14.7,-17.2 18.4,-15.4' +
172
+ 'c3.1,1.5 9.4,12.3 8.4,24.8c3,1.8 5.9,3.9 8.5,6.1';
173
+ }
174
+
175
+ return 'c 25,-22 71,-22 96,0';
176
+ },
177
+ enumerable: true,
178
+ configurable: true
179
+ });
156
180
 
157
181
  /**
158
182
  * 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;
476
500
  * SVG path for drawing the rounded top-left corner.
477
501
  * @const
478
502
  */
479
- Blockly.BlockSvg.TOP_LEFT_CORNER_DEFINE_HAT =
480
- 'a ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' +
481
- Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ' 0 0,1 ' +
482
- Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',-' +
483
- Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS;
503
+ Object.defineProperty(Blockly.BlockSvg, 'TOP_LEFT_CORNER_DEFINE_HAT', {
504
+ get: function() {
505
+ if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) {
506
+ return 'c0,-7.1 3.7,-13.3 9.3,-16.9c1.7,-7.5 5.4,-13.2 7.6,-14.2' +
507
+ 'c2.6,-1.3 10,6 14.6,11.1h33c4.6,-5.1 11.9,-12.4 14.6,-11.1' +
508
+ 'c1.9,0.9 4.9,5.2 6.8,11.1c2.6,0,5.2,0,7.8,0';
509
+ }
510
+
511
+ return 'a ' + Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',' +
512
+ Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ' 0 0,1 ' +
513
+ Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS + ',-' +
514
+ Blockly.BlockSvg.DEFINE_HAT_CORNER_RADIUS;
515
+ },
516
+ enumerable: true,
517
+ configurable: true
518
+ });
484
519
 
485
520
  /**
486
521
  * SVG path for drawing the rounded top-left corner.
@@ -518,8 +553,7 @@ Blockly.BlockSvg.prototype.updateColour = function() {
518
553
  }
519
554
  }
520
555
 
521
- // Render block stroke
522
- this.svgPath_.setAttribute('stroke', strokeColour);
556
+ this.blockFrameElement_.setAttribute('stroke', strokeColour);
523
557
 
524
558
  // Render block fill
525
559
  if (this.isGlowingBlock_ || renderShadowed) {
@@ -532,10 +566,10 @@ Blockly.BlockSvg.prototype.updateColour = function() {
532
566
  } else {
533
567
  var fillColour = this.getColour();
534
568
  }
535
- this.svgPath_.setAttribute('fill', fillColour);
569
+ this.blockFrameElement_.setAttribute('fill', fillColour);
536
570
 
537
571
  // Render opacity
538
- this.svgPath_.setAttribute('fill-opacity', this.getOpacity());
572
+ this.blockFrameElement_.setAttribute('fill-opacity', this.getOpacity());
539
573
 
540
574
  // Update colours of input shapes.
541
575
  for (var i = 0, input; input = this.inputList[i]; i++) {
@@ -567,11 +601,11 @@ Blockly.BlockSvg.prototype.highlightForReplacement = function(add) {
567
601
  if (add) {
568
602
  var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId
569
603
  || 'blocklyReplacementGlowFilter';
570
- this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')');
604
+ this.blockFrameElement_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')');
571
605
  Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
572
606
  'blocklyReplaceable');
573
607
  } else {
574
- this.svgPath_.removeAttribute('filter');
608
+ this.blockFrameElement_.removeAttribute('filter');
575
609
  Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
576
610
  'blocklyReplaceable');
577
611
  }
@@ -1119,6 +1153,67 @@ Blockly.BlockSvg.prototype.computeOutputPadding_ = function(inputRows) {
1119
1153
  row.paddingEnd += Blockly.BlockSvg.SHAPE_IN_SHAPE_PADDING[shape][otherShape];
1120
1154
  };
1121
1155
 
1156
+ // Cat face and ear animation for CatBlocks
1157
+ // TODO: Do we want to move this and `initCatFace_` to a separate file?
1158
+ // Or would just that complicate unforking?
1159
+ Blockly.BlockSvg.prototype.renderCatFace_ = function() {
1160
+ // This only makes sense in the context of the Cat Blocks theme.
1161
+ if (Blockly.theme !== Blockly.Themes.CAT_BLOCKS) {
1162
+ return;
1163
+ }
1164
+
1165
+ this.svgPath_.svgFace.setAttribute('fill','#000000');
1166
+
1167
+ var closedEye = Blockly.utils.createSvgElement('path', {}, this.svgFace_);
1168
+ closedEye.setAttribute('d','M25.2-1.1c0.1,0,0.2,0,0.2,0l8.3-2.1l-7-4.8' +
1169
+ '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' +
1170
+ 'c-0.5,0.1-0.9,0.7-0.7,1.2C24.3-1.4,24.7-1.1,25.2-1.1z');
1171
+ closedEye.setAttribute('fill-opacity','0');
1172
+ this.svgPath_.svgFace.closedEye = closedEye;
1173
+
1174
+ var closedEye2 = Blockly.utils.createSvgElement('path', {}, this.svgFace_);
1175
+ closedEye2.setAttribute('d','M62.4-1.1c-0.1,0-0.2,0-0.2,0l-8.3-2.1l7-4.8' +
1176
+ 'c0.5-0.3,1.1-0.2,1.4,0.3s0.2,1.1-0.3,1.4l-3.4,2.3l4,1' +
1177
+ 'c0.5,0.1,0.9,0.7,0.7,1.2C63.2-1.4,62.8-1.1,62.4-1.1z');
1178
+ closedEye2.setAttribute('fill-opacity','0');
1179
+ this.svgPath_.svgFace.closedEye2 = closedEye2;
1180
+
1181
+ var eye = Blockly.utils.createSvgElement('circle', {}, this.svgFace_);
1182
+ eye.setAttribute('cx','59.2');
1183
+ eye.setAttribute('cy','-3.3');
1184
+ eye.setAttribute('r','3.4');
1185
+ eye.setAttribute('fill-opacity','0.6');
1186
+ this.svgPath_.svgFace.eye = eye;
1187
+
1188
+ var eye2 = Blockly.utils.createSvgElement('circle', {}, this.svgFace_);
1189
+ eye2.setAttribute('cx','29.1');
1190
+ eye2.setAttribute('cy','-3.3');
1191
+ eye2.setAttribute('r','3.4');
1192
+ eye2.setAttribute('fill-opacity','0.6');
1193
+ this.svgPath_.svgFace.eye2 = eye2;
1194
+
1195
+ var mouth = Blockly.utils.createSvgElement('path', {}, this.svgFace_);
1196
+ mouth.setAttribute('d','M45.6,0.1c-0.9,0-1.7-0.3-2.3-0.9' +
1197
+ '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' +
1198
+ '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' +
1199
+ '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' +
1200
+ 'c0-0.5,0.5-1,1-1l0,0c0.5,0,1,0.4,1,1c0,0,0,0.1,0,0.2' +
1201
+ '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' +
1202
+ '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' +
1203
+ 'c0,0.2,0.1,1.8-0.8,2.8C47.5-0.4,46.8,0.1,45.6,0.1z');
1204
+ mouth.setAttribute('fill-opacity','0.6');
1205
+
1206
+ this.svgPath_.ear.setAttribute('d','M73.1-15.6c1.7-4.2,4.5-9.1,5.8-8.5' +
1207
+ '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' +
1208
+ 'C72.8-12.3,72.4-13.7,73.1-15.6z');
1209
+ this.svgPath_.ear.setAttribute('fill','#FFD5E6');
1210
+
1211
+ this.svgPath_.ear2.setAttribute('d','M22.4-15.6c-1.7-4.2-4.5-9.1-5.8-8.5' +
1212
+ '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' +
1213
+ 'C22.8-12.3,23.2-13.7,22.4-15.6z');
1214
+ this.svgPath_.ear2.setAttribute('fill','#FFD5E6');
1215
+ };
1216
+
1122
1217
  /**
1123
1218
  * Draw the path of the block.
1124
1219
  * Move the fields to the correct locations.
@@ -1136,6 +1231,9 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) {
1136
1231
  // No output or previous connection.
1137
1232
  this.squareTopLeftCorner_ = true;
1138
1233
  this.startHat_ = true;
1234
+ if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) {
1235
+ this.initCatStuff();
1236
+ }
1139
1237
  inputRows.rightEdge = Math.max(inputRows.rightEdge, 100);
1140
1238
  }
1141
1239
 
@@ -1162,12 +1260,16 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) {
1162
1260
  this.renderDrawLeft_(steps);
1163
1261
 
1164
1262
  var pathString = steps.join(' ');
1165
- this.svgPath_.setAttribute('d', pathString);
1263
+ this.blockFrameElement_.setAttribute('d', pathString);
1264
+
1265
+ if (Blockly.theme === Blockly.Themes.CAT_BLOCKS && this.startHat_ && !this.svgFace_.firstChild) {
1266
+ this.renderCatFace_();
1267
+ }
1166
1268
 
1167
1269
  if (this.RTL) {
1168
1270
  // Mirror the block's path.
1169
1271
  // This is awesome.
1170
- this.svgPath_.setAttribute('transform', 'scale(-1 1)');
1272
+ this.blockFrameElement_.setAttribute('transform', 'scale(-1 1)');
1171
1273
  }
1172
1274
  };
1173
1275
 
package/core/block_svg.js CHANGED
@@ -37,6 +37,7 @@ goog.require('Blockly.scratchBlocksUtils');
37
37
  goog.require('Blockly.Tooltip');
38
38
  goog.require('Blockly.Touch');
39
39
  goog.require('Blockly.utils');
40
+ goog.require('Blockly.constants');
40
41
 
41
42
  goog.require('goog.Timer');
42
43
  goog.require('goog.asserts');
@@ -64,10 +65,25 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) {
64
65
  */
65
66
  this.svgGroup_ = Blockly.utils.createSvgElement('g', {}, null);
66
67
  /** @type {SVGElement} */
67
- this.svgPath_ = Blockly.utils.createSvgElement('path',
68
- {'class': 'blocklyPath blocklyBlockBackground'},
69
- this.svgGroup_);
70
- this.svgPath_.tooltip = this;
68
+ if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) {
69
+ this.svgPath_ = Blockly.utils.createSvgElement('g', {}, this.svgGroup_);
70
+ this.svgPathBody_ = Blockly.utils.createSvgElement('path',
71
+ {'class': 'blocklyPath blocklyBlockBackground'}, this.svgPath_);
72
+
73
+ this.svgFace_ = Blockly.utils.createSvgElement('g', {},
74
+ this.svgPath_);
75
+ this.svgGroup_.svgPath = this.svgPath_;
76
+ this.svgPath_.svgFace = this.svgFace_;
77
+ this.svgPath_.svgBody = this.svgPathBody_;
78
+ this.lastCallTime = 0;
79
+ this.CALL_FREQUENCY_MS = 60;
80
+
81
+ this.svgPathBody_.tooltip = this;
82
+ } else {
83
+ this.svgPath_ = Blockly.utils.createSvgElement('path', {'class': 'blocklyPath blocklyBlockBackground'},
84
+ this.svgGroup_);
85
+ this.svgPath_.tooltip = this;
86
+ }
71
87
 
72
88
  /** @type {boolean} */
73
89
  this.rendered = false;
@@ -80,7 +96,7 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) {
80
96
  */
81
97
  this.useDragSurface_ = Blockly.utils.is3dSupported() && !!workspace.blockDragSurface_;
82
98
 
83
- Blockly.Tooltip.bindMouseEvents(this.svgPath_);
99
+ Blockly.Tooltip.bindMouseEvents(this.blockFrameElement_);
84
100
  Blockly.BlockSvg.superClass_.constructor.call(this,
85
101
  workspace, prototypeName, opt_id);
86
102
 
@@ -160,6 +176,8 @@ Blockly.BlockSvg.prototype.initSvg = function() {
160
176
  for (i = 0; i < icons.length; i++) {
161
177
  icons[i].createIcon();
162
178
  }
179
+ } else if (this.svgPathBody_) {
180
+ this.svgPathBody_.setAttribute('stroke-opacity', '0');
163
181
  }
164
182
  this.updateColour();
165
183
  this.updateMovable();
@@ -174,6 +192,23 @@ Blockly.BlockSvg.prototype.initSvg = function() {
174
192
  }
175
193
  };
176
194
 
195
+ Object.defineProperty(Blockly.BlockSvg.prototype, 'blockFrameElement_', {
196
+ /**
197
+ * The svg element (e.g. svgPath_ or svgPathBody_) that is
198
+ * responsible for the outline of the block, based on the current theme.
199
+ * @return {!SVGElement} The SVG element forming the outline.
200
+ * @this {Blockly.BlockSvg}
201
+ */
202
+ get: function() {
203
+ if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) {
204
+ return this.svgPathBody_;
205
+ }
206
+ return this.svgPath_;
207
+ },
208
+ enumerable: true,
209
+ configurable: true
210
+ });
211
+
177
212
  /**
178
213
  * Select this block. Highlight it visually.
179
214
  */
@@ -218,6 +253,191 @@ Blockly.BlockSvg.prototype.unselect = function() {
218
253
  this.removeSelect();
219
254
  };
220
255
 
256
+ Blockly.BlockSvg.prototype.initCatStuff = function() {
257
+ if (Blockly.theme !== Blockly.Themes.CAT_BLOCKS || this.hasInitCatStuff) {
258
+ return;
259
+ }
260
+ // TODO: Test what happens if we turn on and off Cat Blocks several times
261
+ this.hasInitCatStuff = true;
262
+
263
+ // Ear part of the SVG path for hat blocks
264
+ 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';
265
+ 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';
266
+ 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';
267
+ 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';
268
+ // Ears look slightly different for define hat blocks
269
+ 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';
270
+ 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';
271
+ 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' +
272
+ 'c2.6,0 6.2,0 10.9,0';
273
+ 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';
274
+
275
+ var that = this;
276
+ this.svgPath_.ear = Blockly.utils.createSvgElement('path', {}, this.svgPath_);
277
+ this.svgPath_.ear2 = Blockly.utils.createSvgElement('path', {}, this.svgPath_);
278
+ if (this.RTL) {
279
+ // Mirror the ears.
280
+ this.svgPath_.ear.setAttribute('transform', 'scale(-1 1)');
281
+ this.svgPath_.ear2.setAttribute('transform', 'scale(-1 1)');
282
+ }
283
+ this.svgPath_.addEventListener("mouseenter", function(event) {
284
+ clearTimeout(that.blinkFn);
285
+ // blink
286
+ if (event.target.svgFace.eye) {
287
+ event.target.svgFace.eye.setAttribute('fill-opacity','0');
288
+ event.target.svgFace.eye2.setAttribute('fill-opacity','0');
289
+ event.target.svgFace.closedEye.setAttribute('fill-opacity','0.6');
290
+ event.target.svgFace.closedEye2.setAttribute('fill-opacity','0.6');
291
+ }
292
+
293
+ // reset after a short delay
294
+ that.blinkFn = setTimeout(function() {
295
+ if (event.target.svgFace.eye) {
296
+ event.target.svgFace.eye.setAttribute('fill-opacity','0.6');
297
+ event.target.svgFace.eye2.setAttribute('fill-opacity','0.6');
298
+ event.target.svgFace.closedEye.setAttribute('fill-opacity','0');
299
+ event.target.svgFace.closedEye2.setAttribute('fill-opacity','0');
300
+ }
301
+ }, 100);
302
+ });
303
+
304
+ this.svgPath_.ear.addEventListener("mouseenter", function() {
305
+ clearTimeout(that.earFn);
306
+ clearTimeout(that.ear2Fn);
307
+ // ear flick
308
+ that.svgPath_.ear.setAttribute('fill-opacity','0');
309
+ that.svgPath_.ear2.setAttribute('fill-opacity','');
310
+ var bodyPath = that.svgPath_.svgBody.getAttribute('d');
311
+ bodyPath = bodyPath.replace(RIGHT_EAR_UP, RIGHT_EAR_DOWN);
312
+ bodyPath = bodyPath.replace(DEFINE_HAT_RIGHT_EAR_UP, DEFINE_HAT_RIGHT_EAR_DOWN);
313
+ bodyPath = bodyPath.replace(LEFT_EAR_DOWN, LEFT_EAR_UP);
314
+ bodyPath = bodyPath.replace(DEFINE_HAT_LEFT_EAR_DOWN, DEFINE_HAT_LEFT_EAR_UP);
315
+ that.svgPath_.svgBody.setAttribute('d', bodyPath);
316
+
317
+ // reset after a short delay
318
+ that.earFn = setTimeout(function() {
319
+ that.svgPath_.ear.setAttribute('fill-opacity','');
320
+ var bodyPath = that.svgPath_.svgBody.getAttribute('d');
321
+ bodyPath = bodyPath.replace(RIGHT_EAR_DOWN, RIGHT_EAR_UP);
322
+ bodyPath = bodyPath.replace(DEFINE_HAT_RIGHT_EAR_DOWN, DEFINE_HAT_RIGHT_EAR_UP);
323
+ that.svgPath_.svgBody.setAttribute('d', bodyPath);
324
+ }, 50);
325
+ });
326
+ this.svgPath_.ear2.addEventListener("mouseenter", function() {
327
+ clearTimeout(that.earFn);
328
+ clearTimeout(that.ear2Fn);
329
+ // ear flick
330
+ that.svgPath_.ear2.setAttribute('fill-opacity','0');
331
+ that.svgPath_.ear.setAttribute('fill-opacity','');
332
+ var bodyPath = that.svgPath_.svgBody.getAttribute('d');
333
+ bodyPath = bodyPath.replace(LEFT_EAR_UP, LEFT_EAR_DOWN);
334
+ bodyPath = bodyPath.replace(DEFINE_HAT_LEFT_EAR_UP, DEFINE_HAT_LEFT_EAR_DOWN);
335
+ bodyPath = bodyPath.replace(RIGHT_EAR_DOWN, RIGHT_EAR_UP);
336
+ bodyPath = bodyPath.replace(DEFINE_HAT_RIGHT_EAR_DOWN, DEFINE_HAT_RIGHT_EAR_UP);
337
+ that.svgPath_.svgBody.setAttribute('d', bodyPath);
338
+
339
+ // reset after a short delay
340
+ that.ear2Fn = setTimeout(function() {
341
+ that.svgPath_.ear2.setAttribute('fill-opacity','');
342
+ var bodyPath = that.svgPath_.svgBody.getAttribute('d');
343
+ bodyPath = bodyPath.replace(LEFT_EAR_DOWN, LEFT_EAR_UP);
344
+ bodyPath = bodyPath.replace(DEFINE_HAT_LEFT_EAR_DOWN, DEFINE_HAT_LEFT_EAR_UP);
345
+ that.svgPath_.svgBody.setAttribute('d', bodyPath);
346
+ }, 50);
347
+ });
348
+ this.windowListener = function(event) {
349
+ var time = Date.now();
350
+ if (time < that.lastCallTime + that.CALL_FREQUENCY_MS) return;
351
+ that.lastCallTime = time;
352
+ if (!that.shouldWatchMouse()) return;
353
+
354
+ // mouse watching
355
+ if (that.workspace) { // not disposed
356
+ var xy = that.getCatFacePosition();
357
+ var mouseLocation = {
358
+ x: event.x / that.workspace.scale,
359
+ y: event.y / that.workspace.scale
360
+ };
361
+
362
+ var dx = mouseLocation.x - xy.x;
363
+ var dy = mouseLocation.y - xy.y;
364
+ var theta = Math.atan2(dx, dy);
365
+
366
+ // Map the vector from the cat face to the mouse location to a much shorter
367
+ // vector in the same direction, which will be the translation vector for
368
+ // the cat face
369
+ var delta = Math.sqrt(dx * dx + dy * dy);
370
+ var scaleFactor = delta / (delta + 1);
371
+
372
+ // Equation for radius of ellipse at theta for axes with length a and b
373
+ var a = 2;
374
+ var b = 5;
375
+ var r = a * b / Math.sqrt(Math.pow(b * Math.cos(theta), 2) + Math.pow(a * Math.sin(theta), 2));
376
+
377
+ // Convert polar coordinate back to x, y coordinate
378
+ dx = (r * scaleFactor) * Math.sin(theta);
379
+ dy = (r * scaleFactor) * Math.cos(theta);
380
+
381
+ if (that.RTL) dx -= 87; // Translate face over
382
+ that.svgFace_.style.transform = 'translate(' + dx + 'px, ' + dy + 'px)';
383
+ }
384
+ };
385
+ if (this.RTL) {
386
+ // Set to the correct initial position
387
+ this.svgFace_.style.transform = 'translate(-87px, 0px)';
388
+ }
389
+ if (this.shouldWatchMouse()) {
390
+ document.addEventListener('mousemove', this.windowListener);
391
+ }
392
+ };
393
+
394
+ /**
395
+ * Get cat face position
396
+ * @return {Object} coordinates of center of cat face
397
+ */
398
+ Blockly.BlockSvg.prototype.getCatFacePosition = function() {
399
+ // getBoundingClientRect is not performant
400
+ //var offset = that.workspace.getParentSvg().getBoundingClientRect();
401
+ var offset = {x:0, y:92};
402
+
403
+ offset.x += 120; // scratchCategoryMenu width
404
+
405
+ if (!this.isInFlyout && this.workspace.getFlyout()) {
406
+ offset.x += this.workspace.getFlyout().getWidth();
407
+ }
408
+
409
+ offset.x += this.workspace.scrollX;
410
+ offset.y += this.workspace.scrollY;
411
+
412
+ var xy = this.getRelativeToSurfaceXY(this.svgGroup_);
413
+ if (this.RTL) {
414
+ xy.x = this.workspace.getWidth() - xy.x - this.width;
415
+ }
416
+ // convert to workspace units
417
+ xy.x += offset.x / this.workspace.scale;
418
+ xy.y += offset.y / this.workspace.scale;
419
+ // distance to center of face
420
+ xy.x -= 43.5;
421
+ xy.y -= 4;
422
+ if (this.RTL) {
423
+ // We've been calculating from the right edge. Convert x to from left edge.
424
+ xy.x = screen.width - xy.x;
425
+ }
426
+ return xy;
427
+ };
428
+
429
+ /**
430
+ * True if cat should watch mouse
431
+ * @return {boolean} true if the block should be watching the mouse
432
+ */
433
+ Blockly.BlockSvg.prototype.shouldWatchMouse = function() {
434
+ if (window.vmLoadHigh || !window.CAT_CHASE_MOUSE) return false;
435
+ var xy = this.getCatFacePosition();
436
+ var blockXOnScreen = xy.x > 0 && xy.x < screen.width / this.workspace.scale;
437
+ var blockYOnScreen = xy.y > 0 && xy.y < screen.height / this.workspace.scale;
438
+ return this.startHat_ && !this.isGlowingStack_ && blockXOnScreen && blockYOnScreen;
439
+ };
440
+
221
441
  /**
222
442
  * Glow only this particular block, to highlight it visually as if it's running.
223
443
  * @param {boolean} isGlowingBlock Whether the block should glow.
@@ -232,6 +452,23 @@ Blockly.BlockSvg.prototype.setGlowBlock = function(isGlowingBlock) {
232
452
  * @param {boolean} isGlowingStack Whether the stack starting with this block should glow.
233
453
  */
234
454
  Blockly.BlockSvg.prototype.setGlowStack = function(isGlowingStack) {
455
+ if (Blockly.theme === Blockly.Themes.CAT_BLOCKS) {
456
+ if (isGlowingStack) {
457
+ // For performance, don't follow the mouse when the stack is glowing
458
+ document.removeEventListener('mousemove', this.windowListener);
459
+ if (this.workspace && this.svgFace_.style) {
460
+ // reset face direction
461
+ if (this.RTL) {
462
+ this.svgFace_.style.transform = 'translate(-87px, 0px)';
463
+ } else {
464
+ this.svgFace_.style.transform = '';
465
+ }
466
+ }
467
+ } else {
468
+ document.addEventListener('mousemove', this.windowListener);
469
+ }
470
+ }
471
+
235
472
  this.isGlowingStack_ = isGlowingStack;
236
473
  // Update the applied SVG filter if the property has changed
237
474
  var svg = this.getSvgRoot();
@@ -822,6 +1059,18 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) {
822
1059
  // The block has already been deleted.
823
1060
  return;
824
1061
  }
1062
+ if (this.blinkFn) {
1063
+ clearTimeout(this.blinkFn);
1064
+ }
1065
+ if (this.earFn) {
1066
+ clearTimeout(this.earFn);
1067
+ }
1068
+ if (this.ear2Fn) {
1069
+ clearTimeout(this.ear2Fn);
1070
+ }
1071
+ if (this.windowListener) {
1072
+ document.removeEventListener('mousemove', this.windowListener);
1073
+ }
825
1074
  Blockly.Tooltip.hide();
826
1075
  Blockly.Field.startCache();
827
1076
  // Save the block's workspace temporarily so we can resize the
@@ -860,6 +1109,8 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) {
860
1109
  // Sever JavaScript to DOM connections.
861
1110
  this.svgGroup_ = null;
862
1111
  this.svgPath_ = null;
1112
+ this.svgPathBody_ = null;
1113
+ this.svgFace_ = null;
863
1114
  Blockly.Field.stopCache();
864
1115
  };
865
1116
 
package/core/blockly.js CHANGED
@@ -112,6 +112,13 @@ Blockly.clipboardSource_ = null;
112
112
  */
113
113
  Blockly.cache3dSupported_ = null;
114
114
 
115
+ /**
116
+ * Active theme.
117
+ * @type {!Blockly.Themes}
118
+ * @private
119
+ */
120
+ Blockly.theme_ = Blockly.Themes.CLASSIC;
121
+
115
122
  /**
116
123
  * Convert a hue (HSV model) into an RGB hex triplet.
117
124
  * @param {number} hue Hue on a colour wheel (0-360).
@@ -175,6 +182,33 @@ Blockly.svgResize = function(workspace) {
175
182
  mainWorkspace.resize();
176
183
  };
177
184
 
185
+ /**
186
+ * Apply a global theme to Blockly. This will then be used in all workspaces -
187
+ * both newly created and already existing.
188
+ */
189
+ Object.defineProperty(Blockly, 'theme', {
190
+ /**
191
+ * Get the current theme.
192
+ * @return {!Blockly.Themes} The current global theme.
193
+ */
194
+ get: function() {
195
+ return Blockly.theme_;
196
+ }
197
+ });
198
+
199
+ /**
200
+ * Sets the global theme, which is applied to all workspaces.
201
+ * If the passed theme depends on initialization logic, that will only be applied to newly created workspaces.
202
+ * @param {!Blockly.Themes} theme the theme to set as the global theme.
203
+ */
204
+ Blockly.setTheme = function(theme) {
205
+ if (theme === Blockly.Themes.CAT_BLOCKS) {
206
+ Blockly.theme_ = theme;
207
+ } else {
208
+ Blockly.theme_ = Blockly.Themes.CLASSIC;
209
+ }
210
+ };
211
+
178
212
  /**
179
213
  * Handle a key-down on SVG drawing surface. Does nothing if the main workspace is not visible.
180
214
  * @param {!Event} e Key down event.
package/core/constants.js CHANGED
@@ -385,3 +385,12 @@ Blockly.StatusButtonState = {
385
385
  "READY": "ready",
386
386
  "NOT_READY": "not ready",
387
387
  };
388
+
389
+ /**
390
+ * ENUM defining supported themes.
391
+ * @enum {string}
392
+ */
393
+ Blockly.Themes = {
394
+ CLASSIC: "classic",
395
+ CAT_BLOCKS: "catblocks"
396
+ };