blockly/apps/turtle/turtle.js
/**
* Blockly Apps: Turtle Graphics
*
* Copyright 2012 Google Inc.
* https://blockly.googlecode.com/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview JavaScript for Blockly's Turtle application.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
/**
* Create a namespace for the application.
*/
var Turtle = {};
// Supported languages.
BlocklyApps.LANGUAGES =
['ar', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'fa', 'fr', 'hi', 'hrx',
'hu', 'is', 'it', 'ko', 'ms', 'nl', 'pl', 'pms', 'pt-br', 'ro', 'ru',
'sco', 'sv', 'tr', 'uk', 'vi', 'zh-hans', 'zh-hant'];
BlocklyApps.LANG = BlocklyApps.getLang();
document.write('<script type="text/javascript" src="generated/' +
BlocklyApps.LANG + '.js"></script>\n');
Turtle.HEIGHT = 400;
Turtle.WIDTH = 400;
/**
* PID of animation task currently executing.
*/
Turtle.pid = 0;
/**
* Should the turtle be drawn?
*/
Turtle.visible = true;
/**
* Initialize Blockly and the turtle. Called on page load.
*/
Turtle.init = function() {
BlocklyApps.init();
var rtl = BlocklyApps.isRtl();
var blocklyDiv = document.getElementById('blockly');
var visualization = document.getElementById('visualization');
var onresize = function(e) {
var top = visualization.offsetTop;
blocklyDiv.style.top = Math.max(10, top - window.pageYOffset) + 'px';
blocklyDiv.style.left = rtl ? '10px' : '420px';
blocklyDiv.style.width = (window.innerWidth - 440) + 'px';
};
window.addEventListener('scroll', function() {
onresize();
Blockly.fireUiEvent(window, 'resize');
});
window.addEventListener('resize', onresize);
onresize();
var toolbox = document.getElementById('toolbox');
Blockly.inject(document.getElementById('blockly'),
{path: '../../',
rtl: rtl,
toolbox: toolbox,
trashcan: true});
Blockly.JavaScript.INFINITE_LOOP_TRAP = ' BlocklyApps.checkTimeout(%1);\n';
// Add to reserved word list: API, local variables in execution evironment
// (execute) and the infinite loop detection function.
Blockly.JavaScript.addReservedWords('Turtle,code');
window.addEventListener('beforeunload', function(e) {
if (Blockly.mainWorkspace.getAllBlocks().length > 2) {
e.returnValue = BlocklyApps.getMsg('Turtle_unloadWarning'); // Gecko.
return BlocklyApps.getMsg('Turtle_unloadWarning'); // Webkit.
}
return null;
});
// Hide download button if browser lacks support
// (http://caniuse.com/#feat=download).
if (!(goog.userAgent.GECKO ||
(goog.userAgent.WEBKIT && !goog.userAgent.SAFARI))) {
document.getElementById('captureButton').className = 'disabled';
} else {
BlocklyApps.bindClick('captureButton', Turtle.createImageLink);
}
// Initialize the slider.
var sliderSvg = document.getElementById('slider');
Turtle.speedSlider = new Slider(10, 35, 130, sliderSvg);
var defaultXml =
'<xml>' +
' <block type="draw_move" x="70" y="70">' +
' <value name="VALUE">' +
' <block type="math_number">' +
' <field name="NUM">100</field>' +
' </block>' +
' </value>' +
' </block>' +
'</xml>';
BlocklyApps.loadBlocks(defaultXml);
Turtle.ctxDisplay = document.getElementById('display').getContext('2d');
Turtle.ctxScratch = document.getElementById('scratch').getContext('2d');
Turtle.reset();
BlocklyApps.bindClick('runButton', Turtle.runButtonClick);
BlocklyApps.bindClick('resetButton', Turtle.resetButtonClick);
// Lazy-load the syntax-highlighting.
window.setTimeout(BlocklyApps.importPrettify, 1);
};
window.addEventListener('load', Turtle.init);
/**
* Reset the turtle to the start position, clear the display, and kill any
* pending tasks.
*/
Turtle.reset = function() {
// Starting location and heading of the turtle.
Turtle.x = Turtle.HEIGHT / 2;
Turtle.y = Turtle.WIDTH / 2;
Turtle.heading = 0;
Turtle.penDownValue = true;
Turtle.visible = true;
// Clear the display.
Turtle.ctxScratch.canvas.width = Turtle.ctxScratch.canvas.width;
Turtle.ctxScratch.strokeStyle = '#000000';
Turtle.ctxScratch.fillStyle = '#000000';
Turtle.ctxScratch.lineWidth = 1;
Turtle.ctxScratch.lineCap = 'round';
Turtle.ctxScratch.font = 'normal 18pt Arial';
Turtle.display();
// Kill any task.
if (Turtle.pid) {
window.clearTimeout(Turtle.pid);
}
Turtle.pid = 0;
};
/**
* Copy the scratch canvas to the display canvas. Add a turtle marker.
*/
Turtle.display = function() {
Turtle.ctxDisplay.globalCompositeOperation = 'copy';
Turtle.ctxDisplay.drawImage(Turtle.ctxScratch.canvas, 0, 0);
Turtle.ctxDisplay.globalCompositeOperation = 'source-over';
// Draw the turtle.
if (Turtle.visible) {
// Make the turtle the colour of the pen.
Turtle.ctxDisplay.strokeStyle = Turtle.ctxScratch.strokeStyle;
Turtle.ctxDisplay.fillStyle = Turtle.ctxScratch.fillStyle;
// Draw the turtle body.
var radius = Turtle.ctxScratch.lineWidth / 2 + 10;
Turtle.ctxDisplay.beginPath();
Turtle.ctxDisplay.arc(Turtle.x, Turtle.y, radius, 0, 2 * Math.PI, false);
Turtle.ctxDisplay.lineWidth = 3;
Turtle.ctxDisplay.stroke();
// Draw the turtle head.
var WIDTH = 0.3;
var HEAD_TIP = 10;
var ARROW_TIP = 4;
var BEND = 6;
var radians = 2 * Math.PI * Turtle.heading / 360;
var tipX = Turtle.x + (radius + HEAD_TIP) * Math.sin(radians);
var tipY = Turtle.y - (radius + HEAD_TIP) * Math.cos(radians);
radians -= WIDTH;
var leftX = Turtle.x + (radius + ARROW_TIP) * Math.sin(radians);
var leftY = Turtle.y - (radius + ARROW_TIP) * Math.cos(radians);
radians += WIDTH / 2;
var leftControlX = Turtle.x + (radius + BEND) * Math.sin(radians);
var leftControlY = Turtle.y - (radius + BEND) * Math.cos(radians);
radians += WIDTH;
var rightControlX = Turtle.x + (radius + BEND) * Math.sin(radians);
var rightControlY = Turtle.y - (radius + BEND) * Math.cos(radians);
radians += WIDTH / 2;
var rightX = Turtle.x + (radius + ARROW_TIP) * Math.sin(radians);
var rightY = Turtle.y - (radius + ARROW_TIP) * Math.cos(radians);
Turtle.ctxDisplay.beginPath();
Turtle.ctxDisplay.moveTo(tipX, tipY);
Turtle.ctxDisplay.lineTo(leftX, leftY);
Turtle.ctxDisplay.bezierCurveTo(leftControlX, leftControlY,
rightControlX, rightControlY, rightX, rightY);
Turtle.ctxDisplay.closePath();
Turtle.ctxDisplay.fill();
}
};
/**
* Click the run button. Start the program.
*/
Turtle.runButtonClick = function() {
var runButton = document.getElementById('runButton');
var resetButton = document.getElementById('resetButton');
// Ensure that Reset button is at least as wide as Run button.
if (!resetButton.style.minWidth) {
resetButton.style.minWidth = runButton.offsetWidth + 'px';
}
runButton.style.display = 'none';
resetButton.style.display = 'inline';
// Prevent double-clicks or double-taps.
resetButton.disabled = true;
setTimeout(function() {resetButton.disabled = false;},
BlocklyApps.DOUBLE_CLICK_TIME);
document.getElementById('spinner').style.visibility = 'visible';
Blockly.mainWorkspace.traceOn(true);
Turtle.execute();
};
/**
* Click the reset button. Reset the Turtle.
*/
Turtle.resetButtonClick = function() {
var runButton = document.getElementById('runButton');
runButton.style.display = 'inline';
document.getElementById('resetButton').style.display = 'none';
// Prevent double-clicks or double-taps.
runButton.disabled = true;
setTimeout(function() {runButton.disabled = false;},
BlocklyApps.DOUBLE_CLICK_TIME);
document.getElementById('spinner').style.visibility = 'hidden';
Blockly.mainWorkspace.traceOn(false);
Turtle.reset();
};
/**
* Execute the user's code. Heaven help us...
*/
Turtle.execute = function() {
BlocklyApps.log = [];
BlocklyApps.ticks = 1000000;
var code = Blockly.JavaScript.workspaceToCode();
try {
eval(code);
} catch (e) {
// Null is thrown for infinite loop.
// Otherwise, abnormal termination is a user error.
if (e !== Infinity) {
alert(e);
}
}
// BlocklyApps.log now contains a transcript of all the user's actions.
// Reset the graphic and animate the transcript.
Turtle.reset();
Turtle.pid = window.setTimeout(Turtle.animate, 100);
};
/**
* Iterate through the recorded path and animate the turtle's actions.
*/
Turtle.animate = function() {
// All tasks should be complete now. Clean up the PID list.
Turtle.pid = 0;
var tuple = BlocklyApps.log.shift();
if (!tuple) {
document.getElementById('spinner').style.visibility = 'hidden';
BlocklyApps.highlight(null);
return;
}
var command = tuple.shift();
BlocklyApps.highlight(tuple.pop());
Turtle.step(command, tuple);
Turtle.display();
// Scale the speed non-linearly, to give better precision at the fast end.
var stepSpeed = 1000 * Math.pow(1 - Turtle.speedSlider.getValue(), 2);
Turtle.pid = window.setTimeout(Turtle.animate, stepSpeed);
};
/**
* Execute one step.
* @param {string} command Logo-style command (e.g. 'FD' or 'RT').
* @param {!Array} values List of arguments for the command.
*/
Turtle.step = function(command, values) {
switch (command) {
case 'FD': // Forward
if (Turtle.penDownValue) {
Turtle.ctxScratch.beginPath();
Turtle.ctxScratch.moveTo(Turtle.x, Turtle.y);
}
var distance = values[0];
if (distance) {
Turtle.x += distance * Math.sin(2 * Math.PI * Turtle.heading / 360);
Turtle.y -= distance * Math.cos(2 * Math.PI * Turtle.heading / 360);
var bump = 0;
} else {
// WebKit (unlike Gecko) draws nothing for a zero-length line.
var bump = 0.1;
}
if (Turtle.penDownValue) {
Turtle.ctxScratch.lineTo(Turtle.x, Turtle.y + bump);
Turtle.ctxScratch.stroke();
}
break;
case 'RT': // Right Turn
Turtle.heading += values[0];
Turtle.heading %= 360;
if (Turtle.heading < 0) {
Turtle.heading += 360;
}
break;
case 'DP': // Draw Print
Turtle.ctxScratch.save();
Turtle.ctxScratch.translate(Turtle.x, Turtle.y);
Turtle.ctxScratch.rotate(2 * Math.PI * (Turtle.heading - 90) / 360);
Turtle.ctxScratch.fillText(values[0], 0, 0);
Turtle.ctxScratch.restore();
break;
case 'DF': // Draw Font
Turtle.ctxScratch.font = values[2] + ' ' + values[1] + 'pt ' + values[0];
break;
case 'PU': // Pen Up
Turtle.penDownValue = false;
break;
case 'PD': // Pen Down
Turtle.penDownValue = true;
break;
case 'PW': // Pen Width
Turtle.ctxScratch.lineWidth = values[0];
break;
case 'PC': // Pen Colour
Turtle.ctxScratch.strokeStyle = values[0];
Turtle.ctxScratch.fillStyle = values[0];
break;
case 'HT': // Hide Turtle
Turtle.visible = false;
break;
case 'ST': // Show Turtle
Turtle.visible = true;
break;
}
};
/**
* Save an image of the SVG canvas.
*/
Turtle.createImageLink = function() {
var imgLink = document.getElementById('downloadImageLink');
imgLink.setAttribute('href',
document.getElementById('display').toDataURL('image/png'));
var temp = window.onbeforeunload;
window.onbeforeunload = null;
imgLink.click();
window.onbeforeunload = temp;
};
// Turtle API.
Turtle.moveForward = function(distance, id) {
BlocklyApps.log.push(['FD', distance, id]);
};
Turtle.moveBackward = function(distance, id) {
BlocklyApps.log.push(['FD', -distance, id]);
};
Turtle.turnRight = function(angle, id) {
BlocklyApps.log.push(['RT', angle, id]);
};
Turtle.turnLeft = function(angle, id) {
BlocklyApps.log.push(['RT', -angle, id]);
};
Turtle.penUp = function(id) {
BlocklyApps.log.push(['PU', id]);
};
Turtle.penDown = function(id) {
BlocklyApps.log.push(['PD', id]);
};
Turtle.penWidth = function(width, id) {
BlocklyApps.log.push(['PW', Math.max(width, 0), id]);
};
Turtle.penColour = function(colour, id) {
BlocklyApps.log.push(['PC', colour, id]);
};
Turtle.hideTurtle = function(id) {
BlocklyApps.log.push(['HT', id]);
};
Turtle.showTurtle = function(id) {
BlocklyApps.log.push(['ST', id]);
};
Turtle.drawPrint = function(text, id) {
BlocklyApps.log.push(['DP', text, id]);
};
Turtle.drawFont = function(font, size, style, id) {
BlocklyApps.log.push(['DF', font, size, style, id]);
};