// Copyright (c) 2014,2015 Walter Bender
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the The GNU Affero General Public
// License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
//
// You should have received a copy of the GNU Affero General Public
// License along with this library; if not, write to the Free Software
// Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA
function format(str, data) {
str = str.replace(/{([a-zA-Z0-9.]*)}/g,
function (match, name) {
x = data;
name.split('.').forEach(function (v) {
if (x === undefined) {
console.log('Undefined value in template string', str, name, x, v);
}
x = x[v];
});
return x;
});
return str.replace(/{_([a-zA-Z0-9]+)}/g,
function (match, item) {
return _(item);
});
};
function canvasPixelRatio() {
var devicePixelRatio = window.devicePixelRatio || 1;
var context = document.querySelector('#myCanvas').getContext('2d');
var backingStoreRatio = context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return devicePixelRatio / backingStoreRatio;
};
function windowHeight() {
var onAndroid = /Android/i.test(navigator.userAgent);
if (onAndroid) {
return window.outerHeight;
} else {
return window.innerHeight;
}
};
function windowWidth() {
var onAndroid = /Android/i.test(navigator.userAgent);
if (onAndroid) {
return window.outerWidth;
} else {
return window.innerWidth;
}
};
function httpGet(projectName) {
var xmlHttp = null;
xmlHttp = new XMLHttpRequest();
if (projectName === null) {
xmlHttp.open("GET", window.server, false);
xmlHttp.setRequestHeader('x-api-key', '3tgTzMXbbw6xEKX7');
} else {
xmlHttp.open("GET", window.server + projectName, false);
xmlHttp.setRequestHeader('x-api-key', '3tgTzMXbbw6xEKX7');
}
xmlHttp.send();
if (xmlHttp.status > 299) {
throw 'Error from server';
}
return xmlHttp.responseText;
};
function httpPost(projectName, data) {
var xmlHttp = null;
xmlHttp = new XMLHttpRequest();
xmlHttp.open("POST", window.server + projectName, false);
xmlHttp.setRequestHeader('x-api-key', '3tgTzMXbbw6xEKX7');
xmlHttp.send(data);
// return xmlHttp.responseText;
return 'https://apps.facebook.com/turtleblocks/?file=' + projectName;
};
function HttpRequest(url, loadCallback, userCallback) {
// userCallback is an optional callback-handler.
var req = this.request = new XMLHttpRequest();
this.handler = loadCallback;
this.url = url;
this.localmode = Boolean(self.location.href.search(/^file:/i) === 0);
this.userCallback = userCallback;
var objref = this;
try {
req.open('GET', url);
req.onreadystatechange = function() { objref.handler(); };
req.send('');
}
catch(e) {
if (self.console) console.log('Failed to load resource from ' + url + ': Network error.');
if (typeof userCallback === 'function') userCallback(false, 'network error');
this.request = this.handler = this.userCallback = null;
}
};
function docByTagName(tag) {
document.getElementsByTagName(tag);
};
function docById(id) {
return document.getElementById(id);
};
function last(myList) {
var i = myList.length;
if (i === 0) {
return null;
} else {
return myList[i - 1];
}
};
function doSVG(canvas, logo, turtles, width, height, scale) {
// Aggregate SVG output from each turtle. If there is none, use
// the MUSICICON.
var MUSICICON = '';
var turtleSVG = '';
for (var turtle in turtles.turtleList) {
turtles.turtleList[turtle].closeSVG();
turtleSVG += turtles.turtleList[turtle].svgOutput;
}
var svg = '\n';
return svg;
};
function isSVGEmpty(turtles) {
for (var turtle in turtles.turtleList) {
turtles.turtleList[turtle].closeSVG();
if (turtles.turtleList[turtle].svgOutput !== '') {
return false;
}
}
return true;
};
function fileExt(file) {
var parts = file.split('.');
if (parts.length === 1 || (parts[0] === '' && parts.length === 2)) {
return '';
}
return parts.pop();
};
function fileBasename(file) {
var parts = file.split('.');
if (parts.length === 1) {
return parts[0];
} else if (parts[0] === '' && parts.length === 2) {
return file;
} else {
parts.pop(); // throw away suffix
return parts.join('.');
}
};
// Needed to generate new data for localization.ini
// var translated = "";
function _(text) {
replaced = text;
replace = [",", "(", ")", "?", "¿", "<", ">", ".", '"\n', '"', ":", "%s", "%d", "/", "'", ";", "×", "!", "¡"];
for (var p = 0; p < replace.length; p++) {
replaced = replaced.replace(replace[p], "");
}
replaced = replaced.replace(/ /g, '-');
// Needed to generate new data for localization.ini
// txt = "\n" + replaced + " = " + text;
// if (translated.lastIndexOf(txt) === -1) {
// translated = translated + txt;
// }
// You can log translated in console.log(translated)
try {
translation = document.webL10n.get(replaced);
if (translation === '') {
translation = text;
};
return translation;
} catch (e) {
console.log('i18n error: ' + text);
return text;
}
};
function toTitleCase(str) {
if (typeof str !== 'string')
return;
var tempStr = '';
if (str.length > 1)
tempStr = str.substring(1);
return str.toUpperCase()[0] + tempStr;
}
function processRawPluginData(rawData, palettes, blocks, errorMsg, evalFlowDict, evalArgDict, evalParameterDict, evalSetterDict, evalOnStartList, evalOnStopList) {
// console.log(rawData);
var lineData = rawData.split('\n');
var cleanData = '';
// We need to remove blank lines and comments and then
// join the data back together for processing as JSON.
for (var i = 0; i < lineData.length; i++) {
if (lineData[i].length === 0) {
continue;
}
if (lineData[i][0] === '/') {
continue;
}
cleanData += lineData[i];
}
// Note to plugin developers: You may want to comment out this
// try/catch while debugging your plugin.
try {
var obj = processPluginData(cleanData.replace(/\n/g,''), palettes, blocks, evalFlowDict, evalArgDict, evalParameterDict, evalSetterDict, evalOnStartList, evalOnStopList);
} catch (e) {
var obj = null;
errorMsg('Error loading plugin: ' + e);
}
return obj;
};
function processPluginData(pluginData, palettes, blocks, evalFlowDict, evalArgDict, evalParameterDict, evalSetterDict, evalOnStartList, evalOnStopList) {
// Plugins are JSON-encoded dictionaries.
// console.log(pluginData);
var obj = JSON.parse(pluginData);
// Create a palette entry.
var newPalette = false;
if ('PALETTEPLUGINS' in obj) {
for (var name in obj['PALETTEPLUGINS']) {
PALETTEICONS[name] = obj['PALETTEPLUGINS'][name];
var fillColor = '#ff0066';
if ('PALETTEFILLCOLORS' in obj) {
if (name in obj['PALETTEFILLCOLORS']) {
var fillColor = obj['PALETTEFILLCOLORS'][name];
// console.log(fillColor);
}
}
PALETTEFILLCOLORS[name] = fillColor;
var strokeColor = '#ef003e';
if ('PALETTESTROKECOLORS' in obj) {
if (name in obj['PALETTESTROKECOLORS']) {
var strokeColor = obj['PALETTESTROKECOLORS'][name];
// console.log(strokeColor);
}
}
PALETTESTROKECOLORS[name] = strokeColor;
var highlightColor = '#ffb1b3';
if ('PALETTEHIGHLIGHTCOLORS' in obj) {
if (name in obj['PALETTEHIGHLIGHTCOLORS']) {
var highlightColor = obj['PALETTEHIGHLIGHTCOLORS'][name];
// console.log(highlightColor);
}
}
PALETTEHIGHLIGHTCOLORS[name] = highlightColor;
var strokeHighlightColor = '#404040';
if ('HIGHLIGHTSTROKECOLORS' in obj) {
if (name in obj['HIGHLIGHTSTROKECOLORS']) {
var strokeHighlightColor = obj['HIGHLIGHTSTROKECOLORS'][name];
// console.log(highlightColor);
}
}
HIGHLIGHTSTROKECOLORS[name] = strokeHighlightColor;
if (name in palettes.buttons) {
console.log('palette ' + name + ' already exists');
} else {
console.log('adding palette ' + name);
palettes.add(name);
newPalette = true;
}
}
}
if (newPalette) {
try {
palettes.makePalettes();
} catch (e) {
console.log('makePalettes: ' + e);
}
}
// Define the image blocks
if ('IMAGES' in obj) {
for (var blkName in obj['IMAGES']) {
pluginsImages[blkName] = obj['IMAGES'][blkName];
}
}
// Populate the flow-block dictionary, i.e., the code that is
// eval'd by this block.
if ('FLOWPLUGINS' in obj) {
for (var flow in obj['FLOWPLUGINS']) {
evalFlowDict[flow] = obj['FLOWPLUGINS'][flow];
}
}
// Populate the arg-block dictionary, i.e., the code that is
// eval'd by this block.
if ('ARGPLUGINS' in obj) {
for (var arg in obj['ARGPLUGINS']) {
evalArgDict[arg] = obj['ARGPLUGINS'][arg];
}
}
// Populate the setter dictionary, i.e., the code that is
// used to set a value block.
if ('SETTERPLUGINS' in obj) {
for (var setter in obj['SETTERPLUGINS']) {
evalSetterDict[setter] = obj['SETTERPLUGINS'][setter];
}
}
// Create the plugin protoblocks.
if ('BLOCKPLUGINS' in obj) {
for (var block in obj['BLOCKPLUGINS']) {
console.log('adding plugin block ' + block);
try {
eval(obj['BLOCKPLUGINS'][block]);
} catch (e) {
console.log('Failed to load plugin for ' + block + ': ' + e);
}
}
}
// Create the globals.
if ('GLOBALS' in obj) {
eval(obj['GLOBALS']);
}
if ('PARAMETERPLUGINS' in obj) {
for (var parameter in obj['PARAMETERPLUGINS']) {
evalParameterDict[parameter] = obj['PARAMETERPLUGINS'][parameter];
}
}
// Code to execute when plugin is loaded
if ('ONLOAD' in obj) {
for (var arg in obj['ONLOAD']) {
eval(obj['ONLOAD'][arg]);
}
}
// Code to execute when turtle code is started
if ('ONSTART' in obj) {
for (var arg in obj['ONSTART']) {
evalOnStartList[arg] = obj['ONSTART'][arg];
}
}
// Code to execute when turtle code is stopped
if ('ONSTOP' in obj) {
for (var arg in obj['ONSTOP']) {
evalOnStopList[arg] = obj['ONSTOP'][arg];
}
}
// Push the protoblocks onto their palettes.
for (var protoblock in blocks.protoBlockDict) {
if (blocks.protoBlockDict[protoblock].palette === undefined) {
console.log('Cannot find palette for protoblock ' + protoblock);
} else {
blocks.protoBlockDict[protoblock].palette.add(blocks.protoBlockDict[protoblock]);
}
}
palettes.updatePalettes();
// Populate the lists of block types.
blocks.findBlockTypes();
// Return the object in case we need to save it to local storage.
return obj;
};
function updatePluginObj(obj) {
for (var name in obj['PALETTEPLUGINS']) {
pluginObjs['PALETTEPLUGINS'][name] = obj['PALETTEPLUGINS'][name];
}
for (var name in obj['PALETTEFILLCOLORS']) {
pluginObjs['PALETTEFILLCOLORS'][name] = obj['PALETTEFILLCOLORS'][name];
}
for (var name in obj['PALETTESTROKECOLORS']) {
pluginObjs['PALETTESTROKECOLORS'][name] = obj['PALETTESTROKECOLORS'][name];
}
for (var name in obj['PALETTEHIGHLIGHTCOLORS']) {
pluginObjs['PALETTEHIGHLIGHTCOLORS'][name] = obj['PALETTEHIGHLIGHTCOLORS'][name];
}
for (var flow in obj['FLOWPLUGINS']) {
pluginObjs['FLOWPLUGINS'][flow] = obj['FLOWPLUGINS'][flow];
}
for (var arg in obj['ARGPLUGINS']) {
pluginObjs['ARGPLUGINS'][arg] = obj['ARGPLUGINS'][arg];
}
for (var block in obj['BLOCKPLUGINS']) {
pluginObjs['BLOCKPLUGINS'][block] = obj['BLOCKPLUGINS'][block];
}
if ('GLOBALS' in obj) {
if (!('GLOBALS' in pluginObjs)) {
pluginObjs['GLOBALS'] = '';
}
pluginObjs['GLOBALS'] += obj['GLOBALS'];
}
if ('IMAGES' in obj) {
pluginObjs['IMAGES'] = obj['IMAGES'];
}
for (var name in obj['ONLOAD']) {
pluginObjs['ONLOAD'][name] = obj['ONLOAD'][name];
}
for (var name in obj['ONSTART']) {
pluginObjs['ONSTART'][name] = obj['ONSTART'][name];
}
for (var name in obj['ONSTOP']) {
pluginObjs['ONSTOP'][name] = obj['ONSTOP'][name];
}
};
function preparePluginExports(obj) {
// add obj to plugin dictionary and return as JSON encoded text
updatePluginObj(obj);
return JSON.stringify(pluginObjs);
};
function processMacroData(macroData, palettes, blocks, macroDict) {
// Macros are stored in a JSON-encoded dictionary.
if (macroData !== '{}') {
var obj = JSON.parse(macroData);
palettes.add('myblocks', 'black', '#a0a0a0');
for (var name in obj) {
console.log('adding ' + name + ' to macroDict');
macroDict[name] = obj[name];
blocks.addToMyPalette(name, macroDict[name]);
}
palettes.makePalettes();
}
};
function prepareMacroExports(name, stack, macroDict) {
if (name !== null) {
macroDict[name] = stack;
}
return JSON.stringify(macroDict);
};
function doSaveSVG(logo, desc) {
var svg = doSVG(logo.canvas, logo, logo.turtles, logo.canvas.width, logo.canvas.height, 1.0);
download(desc, 'data:image/svg+xml;utf8,' + svg, desc, '"width=' + logo.canvas.width + ', height=' + logo.canvas.height + '"');
};
function doSaveLilypond(logo, desc) {
download(desc, 'data:text;utf8,' + encodeURIComponent(logo.lilypondOutput), desc, '"width=' + logo.canvas.width + ', height=' + logo.canvas.height + '"');
};
function download(filename, data) {
var a = document.createElement('a');
a.setAttribute('href', data);
a.setAttribute('download', filename);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
// Some block-specific code
// Publish to FB
function doPublish(desc) {
var url = doSave();
console.log('push ' + url + ' to FB');
var descElem = docById("description");
var msg = desc + ' ' + descElem.value + ' ' + url;
console.log('comment: ' + msg);
var post_cb = function() {
FB.api('/me/feed', 'post', {
message: msg
});
};
FB.login(post_cb, {
scope: 'publish_actions'
});
};
// TODO: Move to camera plugin
var hasSetupCamera = false;
function doUseCamera(args, turtles, turtle, isVideo, cameraID, setCameraID, errorMsg) {
var w = 320;
var h = 240;
var streaming = false;
var video = document.querySelector('#camVideo');
var canvas = document.querySelector('#camCanvas');
navigator.getMedia = (navigator.getUserMedia ||
navigator.mozGetUserMedia ||
navigator.webkitGetUserMedia ||
navigator.msGetUserMedia);
if (navigator.getMedia === undefined) {
errorMsg('Your browser does not support the webcam');
}
if (!hasSetupCamera) {
navigator.getMedia(
{video: true, audio: false},
function (stream) {
if (navigator.mozGetUserMedia) {
video.mozSrcObject = stream;
} else {
var vendorURL = window.URL || window.webkitURL;
video.src = vendorURL.createObjectURL(stream);
}
video.play();
hasSetupCamera = true;
}, function (error) {
errorMsg('Could not connect to camera');
console.log('Could not connect to camera', error);
});
} else {
streaming = true;
video.play();
if (isVideo) {
cameraID = window.setInterval(draw, 100);
setCameraID(cameraID);
} else {
draw();
}
}
video.addEventListener('canplay', function (event) {
console.log('canplay', streaming, hasSetupCamera);
if (!streaming) {
video.setAttribute('width', w);
video.setAttribute('height', h);
canvas.setAttribute('width', w);
canvas.setAttribute('height', h);
streaming = true;
if (isVideo) {
cameraID = window.setInterval(draw, 100);
setCameraID(cameraID);
} else {
draw();
}
}
}, false);
function draw() {
canvas.width = w;
canvas.height = h;
canvas.getContext('2d').drawImage(video, 0, 0, w, h);
var data = canvas.toDataURL('image/png');
turtles.turtleList[turtle].doShowImage(args[0], data);
};
};
function doStopVideoCam(cameraID, setCameraID) {
if (cameraID !== null) {
window.clearInterval(cameraID);
}
setCameraID(null);
document.querySelector('#camVideo').pause();
};
function hideDOMLabel() {
var textLabel = docById('textLabel');
if (textLabel !== null) {
textLabel.style.display = 'none';
}
var numberLabel = docById('numberLabel');
if (numberLabel !== null) {
numberLabel.style.display = 'none';
}
var solfegeLabel = docById('solfegeLabel');
if (solfegeLabel !== null) {
solfegeLabel.style.display = 'none';
}
var notenameLabel = docById('notenameLabel');
if (notenameLabel !== null) {
notenameLabel.style.display = 'none';
}
var noteattrLabel = docById('noteattrLabel');
if (noteattrLabel !== null) {
noteattrLabel.style.display = 'none';
}
var drumnameLabel = docById('drumnameLabel');
if (drumnameLabel !== null) {
drumnameLabel.style.display = 'none';
}
var voicenameLabel = docById('voicenameLabel');
if (voicenameLabel !== null) {
voicenameLabel.style.display = 'none';
}
var modenameLabel = docById('modenameLabel');
if (modenameLabel !== null) {
modenameLabel.style.display = 'none';
}
};
function displayMsg(blocks, text) {
/*
var msgContainer = blocks.msgText.parent;
msgContainer.visible = true;
blocks.msgText.text = text;
msgContainer.updateCache();
blocks.stage.setChildIndex(msgContainer, blocks.stage.getNumChildren() - 1);
*/
return;
};
function toFixed2 (d) {
// Return number as fixed 2 precision
var floor = Math.floor(d);
if (d !== floor) {
return d.toFixed(2).toString();
} else {
return d.toString();
}
};
function mixedNumber (d) {
// Return number as a mixed fraction string, e.g., "2 1/4"
var floor = Math.floor(d);
if (d > floor) {
var obj = rationalToFraction(d - floor);
if (floor === 0) {
return obj[0] + '/' + obj[1];
} else {
if (obj[0] === 1 && obj[1] === 1) {
return floor + 1;
} else {
if (obj[1] > 99) {
return d.toFixed(2);
} else {
return floor + ' ' + obj[0] + '/' + obj[1];
}
}
}
} else {
return d.toString();
}
};
function LCD(a, b) {
return Math.abs((a * b) / GCD(a, b));
};
function GCD(a, b) {
a = Math.abs(a);
b = Math.abs(b);
while(b) {
var n = b;
b = a % b;
a = n;
}
return a;
};
function rationalToFraction (d) {
/*
Convert float to its approximate fractional representation. '''
This code was translated to JavaScript from the answers at
http://stackoverflow.com/questions/95727/how-to-convert-floats-to-human-\
readable-fractions/681534#681534
For example:
>>> 3./5
0.59999999999999998
>>> rationalToFraction(3./5)
"3/5"
*/
if (d > 1) {
var invert = true;
d = 1 / d;
} else {
var invert = false;
}
var df = 1.0;
var top = 1;
var bot = 1;
while (Math.abs(df - d) > 0.00000001) {
if (df < d) {
top += 1;
} else {
bot += 1;
top = Math.floor(d * bot);
}
df = top / bot;
}
if (bot === 0 || top === 0) {
return [0, 1];
}
if (invert) {
return [bot, top];
} else {
return [top, bot];
}
};