Initial commit.

This commit is contained in:
Amos Blanton 2013-10-28 20:00:20 +00:00
parent dd7f6e2a66
commit 6360de08da
103 changed files with 4819 additions and 339 deletions

136
js/IO.js Normal file
View file

@ -0,0 +1,136 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Scratch HTML5 Player
// IO.js
// Tim Mickel, March 2012
// IO handles JSON communication and processing.
// We make the sprites and threads here.
'use strict';
var IO = function() {
this.data = null;
// In production, simply use the local path (no proxy)
// since we won't be hampered by the same-origin policy.
this.base = 'proxy.php?resource=internalapi/';
this.project_base = this.base + 'project/';
this.project_suffix = '/get/';
this.asset_base = this.base + 'asset/';
this.asset_suffix = '/get/';
this.soundbank_base = 'soundbank/';
this.spriteLayerCount = 0;
}
IO.prototype.loadProject = function(project_id) {
var runningIO = this;
$.getJSON(this.project_base + project_id + this.project_suffix,
function(data) {
runningIO.data = data;
runningIO.makeObjects();
runningIO.loadThreads();
runningIO.loadNotesDrums();
runtime.loadStart(); // Try to run the project.
}
);
}
IO.prototype.soundRequest = function(sound, sprite) {
var request = new XMLHttpRequest();
request.open('GET', this.asset_base + sound['md5'] + this.asset_suffix, true);
request.responseType = 'arraybuffer';
request.onload = function() {
var waveData = request.response;
// Decode the waveData and populate a buffer channel with the samples
var snd = new SoundDecoder(waveData);
var samples = snd.getAllSamples();
sound.buffer = runtime.audioContext.createBuffer(1, samples.length, runtime.audioContext.sampleRate);
var data = sound.buffer.getChannelData(0);
for (var i = 0; i < data.length; i++) {
data[i] = samples[i];
}
sprite.soundsLoaded++;
}
request.send();
}
IO.prototype.loadNotesDrums = function() {
var self = this;
$.each(Instr.wavs, function(name, file) {
var request = new XMLHttpRequest();
request.open('GET', self.soundbank_base + escape(file), true);
request.responseType = 'arraybuffer';
request.onload = function() {
var waveData = new OffsetBuffer(request.response);
// Decode the waveData and populate a buffer channel with the samples
var info = WAVFile.decode(request.response);
waveData.offset = info.sampleDataStart;
var soundBuffer = waveData.readBytes(2 * info.sampleCount);
Instr.samples[name] = soundBuffer;
Instr.wavsLoaded++;
}
request.send();
});
}
IO.prototype.makeObjects = function() {
// Create the stage
runtime.stage = new Stage(this.data);
runtime.stage.attach(runtime.scene);
runtime.stage.attachPenLayer(runtime.scene);
runtime.stage.loadSounds();
// Create the sprites
$.each(this.data.children, function(index, obj) {
var newSprite;
if(!obj.cmd) {
newSprite = new Sprite(obj);
} else {
newSprite = new Reporter(obj);
runtime.reporters.push(newSprite);
}
runtime.sprites.push(newSprite);
newSprite.attach(runtime.scene);
if (!obj.cmd)
newSprite.loadSounds();
});
}
IO.prototype.loadThreads = function() {
var target = runtime.stage;
var scripts = target.data.scripts;
if (scripts) {
for (var s in scripts) {
target.stacks.push(interp.makeBlockList(scripts[s][2]));
}
}
$.each(this.data.children, function(index, obj) {
target = runtime.sprites[index];
if (typeof(target) != 'undefined' && target.data && target.data.scripts) {
$.each(target.data.scripts, function(j, s) {
target.stacks.push(interp.makeBlockList(s[2]));
});
}
});
}
// Returns the number sprite we are rendering
// used for initial layering assignment
IO.prototype.getCount = function() {
var rv = this.spriteLayerCount;
this.spriteLayerCount++;
return rv;
}

335
js/Interpreter.js Normal file
View file

@ -0,0 +1,335 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Scratch HTML5 Player
// Interpreter.js
// Tim Mickel, July 2011
// Based on the original by John Maloney
'use strict';
var Block = function(opAndArgs, optionalSubstack) {
this.op = opAndArgs[0];
this.primFcn = interp.lookupPrim(this.op);
this.args = opAndArgs.slice(1); // arguments can be either or constants (numbers, boolean strings, etc.) or expressions (Blocks)
this.isLoop = false; // set to true for loop blocks the first time they run
this.substack = optionalSubstack;
this.subStack2 = null;
this.nextBlock = null;
this.tmp = -1;
interp.fixArgs(this);
}
var Thread = function(block, target) {
this.nextBlock = block; // next block to run; null when thread is finished
this.firstBlock = block;
this.stack = []; // stack of enclosing control structure blocks
this.target = target; // target object running the thread
this.tmp = null; // used for thread operations like Timer
this.tmpObj = []; // used for Sprite operations like glide
this.firstTime = true;
}
var Interpreter = function() {
// Interpreter state
this.primitiveTable = {}
this.variables = {};
this.threads = [];
this.activeThread = new Thread(null);
this.WorkTime = 30;
this.currentMSecs = null;
this.timer = new Timer();
this.yield = false;
this.doRedraw = false;
this.opCount = 0; // used to benchmark the interpreter
}
// Utilities for building blocks and sequences of blocks
Interpreter.prototype.fixArgs = function(b) {
// Convert the arguments of the given block into blocks or substacks if necessary.
// A block argument can be a constant (numbers, boolean strings, etc.), an expression (Blocks), or a substack (an array of blocks).
var newArgs = [];
for (var i = 0; i < b.args.length; i++) {
var arg = b.args[i];
if (arg && arg.constructor == Array) {
if ((arg.length > 0) && (arg[0].constructor == Array)) {
// if first element arg is itself an array, then arg is a substack
if(!b.substack) {
b.substack = this.makeBlockList(arg);
} else {
b.substack2 = this.makeBlockList(arg);
}
} else {
// arg is a block
newArgs.push(new Block(arg));
}
} else {
newArgs.push(arg); // arg is a constant
}
}
b.args = newArgs;
}
Interpreter.prototype.makeBlockList = function(blockList) {
var firstBlock = null, lastBlock = null;
for (var i = 0; i < blockList.length; i++) {
var b = new Block(blockList[i]);
if (firstBlock == null) firstBlock = b;
if (lastBlock) lastBlock.nextBlock = b;
lastBlock = b;
}
return firstBlock;
}
// The Interpreter proper
Interpreter.prototype.stepThreads = function() {
var startTime;
startTime = this.currentMSecs = this.timer.time();
this.doRedraw = false;
if (this.threads.length == 0) return;
while ((this.currentMSecs - startTime) < this.WorkTime && !this.doRedraw) {
var threadStopped = false;
for (var a = this.threads.length-1; a >= 0; --a) {
this.activeThread = this.threads[a];
this.stepActiveThread();
if (!this.activeThread || this.activeThread.nextBlock == null) {
threadStopped = true;
}
}
if (threadStopped) {
var newThreads = [];
for (var a = this.threads.length-1; a >= 0; --a) {
if (this.threads[a].nextBlock != null) {
newThreads.push(this.threads[a]);
}
}
this.threads = newThreads;
if (this.threads.length == 0) return;
}
this.currentMSecs = this.timer.time();
}
}
Interpreter.prototype.stepActiveThread = function() {
// Run the active thread until it yields.
if(typeof(this.activeThread) == 'undefined') {
return;
}
var b = this.activeThread.nextBlock;
if (b == null) return;
this.yield = false;
while (true) {
this.opCount++;
// Advance the "program counter" to the next block before running the primitive.
// Control flow primitives (e.g. if) may change activeThread.nextBlock.
this.activeThread.nextBlock = b.nextBlock;
b.primFcn(b);
if (this.yield) { this.activeThread.nextBlock = b; return; }
b = this.activeThread.nextBlock; // refresh local variable b in case primitive did some control flow
while (b == null) {
// end of a substack; pop the owning control flow block from stack
// Note: This is a loop to handle nested control flow blocks.
b = this.activeThread.stack.pop();
if ((b == null) || (b.isLoop)) {
this.activeThread.nextBlock = b;
return; // yield at the end of a loop or when stack is empty
}
}
}
}
Interpreter.prototype.toggleThread = function(b, targetObj) {
var newThreads = [], wasRunning = false;
for (var i = 0; i < this.threads.length; i++) {
if (this.threads[i].stack[0] == b) {
wasRunning = true;
} else {
newThreads.push(this.threads[i]);
}
}
this.threads = newThreads;
if(!wasRunning) {
this.startThread(b, targetObj);
}
}
Interpreter.prototype.startThread = function(b, targetObj) {
this.activeThread = new Thread(b, targetObj);
this.threads.push(this.activeThread);
}
Interpreter.prototype.restartThread = function(b, targetObj) {
// used by broadcast; stop any thread running on b, then start a new thread on b
var newThread = new Thread(b, targetObj);
var wasRunning = false;
for (var i = 0; i < this.threads.length; i++) {
if (this.threads[i].stack[0] == b) {
this.threads[i] = newThread;
wasRunning = true;
}
}
if (!wasRunning) {
this.threads.push(newThread);
}
}
Interpreter.prototype.arg = function(block, index) {
var arg = block.args[index];
if ((typeof(arg) == 'object') && (arg.constructor == Block)) {
this.opCount++;
return arg.primFcn(arg); // expression
}
return arg;
}
Interpreter.prototype.targetSprite = function() {
return this.activeThread.target;
}
// Timer
Interpreter.prototype.startTimer = function(secs) {
var waitMSecs = 1000 * secs;
if (waitMSecs < 0) waitMSecs = 0;
this.activeThread.tmp = this.currentMSecs + waitMSecs; // end time in milliseconds
this.activeThread.firstTime = false;
this.yield = true;
}
Interpreter.prototype.checkTimer = function() {
// check for timer expiration and clean up if expired. return true when expired
if (this.currentMSecs >= this.activeThread.tmp) {
// time expired
this.activeThread.tmp = 0;
this.activeThread.firstTime = true;
return true;
} else {
this.yield = true;
return false;
}
}
Interpreter.prototype.redraw = function() {
this.doRedraw = true;
}
// Primitive operations
Interpreter.prototype.initPrims = function() {
this.primitiveTable = {};
this.primitiveTable['whenGreenFlag'] = this.primNoop;
this.primitiveTable['whenKeyPressed'] = this.primNoop;
this.primitiveTable['whenClicked'] = this.primNoop;
this.primitiveTable['if'] = function(b) { if (interp.arg(b, 0)) interp.startSubstack(b) };
this.primitiveTable['doForever'] = function(b) { interp.startSubstack(b, true) };
this.primitiveTable['doForeverIf'] = function(b) { if (interp.arg(b, 0)) interp.startSubstack(b, true); else interp.yield = true; };
this.primitiveTable['doIf'] = function(b) { if (interp.arg(b, 0)) interp.startSubstack(b); };
this.primitiveTable['doRepeat'] = this.primRepeat;
this.primitiveTable['doIfElse'] = function(b) { if (interp.arg(b, 0)) interp.startSubstack(b); else interp.startSubstack(b, false, true); };
this.primitiveTable['doWaitUntil'] = function(b) { if (!interp.arg(b, 0)) interp.yield = true };
this.primitiveTable['doUntil'] = function(b) { if (!interp.arg(b, 0)) interp.startSubstack(b, true) };
this.primitiveTable['doReturn'] = function(b) { interp.activeThread = new Thread(null); };
this.primitiveTable['stopAll'] = function(b) { interp.activeThread = new Thread(null); interp.threads = []; }
this.primitiveTable['whenIReceive'] = this.primNoop;
this.primitiveTable['broadcast:'] = function(b) { interp.broadcast(b, false); };
this.primitiveTable['doBroadcastAndWait'] = function(b) { interp.broadcast(b, true); };
this.primitiveTable['wait:elapsed:from:'] = this.primWait;
// added by John:
this.primitiveTable['showBubble'] = function(b) { console.log(interp.arg(b, 1)) }
this.primitiveTable['timerReset'] = function(b) { interp.timerBase = new Date().getTime() }
this.primitiveTable['timer'] = function(b) { return (new Date().getTime() - interp.timerBase) / 1000 }
new Primitives().addPrimsTo(this.primitiveTable);
}
var timerBase = 0;
Interpreter.prototype.lookupPrim = function(op) {
var fcn = interp.primitiveTable[op];
if (fcn == null) fcn = function(b) { console.log('not implemented: ' + b.op) }
return fcn;
}
Interpreter.prototype.primNoop = function(b) { console.log(b.op); }
Interpreter.prototype.primWait = function(b) {
if (interp.activeThread.firstTime) interp.startTimer(interp.arg(b, 0));
else interp.checkTimer();
}
Interpreter.prototype.primRepeat = function(b) {
if (b.tmp == -1) {
b.tmp = Math.max(interp.arg(b, 0), 0); // Initialize repeat count on this block
}
if (b.tmp > 0) {
b.tmp -= 1; // decrement count
interp.startSubstack(b, true);
} else {
// Done executing this repeat block for this round
b.tmp = -1;
b = null;
}
}
Interpreter.prototype.broadcast = function(b, waitFlag) {
var pair;
if (interp.activeThread.firstTime) {
var receivers = [];
var msg = String(interp.arg(b, 0)).toLowerCase();
var findReceivers = function (stack, target) {
if ((stack.op == "whenIReceive") && (stack.args[0].toLowerCase() == msg)) {
receivers.push([stack, target]);
}
}
runtime.allStacksDo(findReceivers);
for (pair in receivers) interp.restartThread(receivers[pair][0], receivers[pair][1]);
if (!waitFlag) return;
interp.activeThread.tmpObj = receivers;
interp.activeThread.firstTime = false;
}
var done = true;
for (pair in interp.activeThread.tmpObj) {
if (interp.isRunning(interp.activeThread.tmpObj[pair][0])) {
done = false;
}
}
if (done) {
interp.activeThread.tmpObj = null;
interp.activeThread.firstTime = true;
} else {
interp.yield = true;
}
}
Interpreter.prototype.isRunning = function(b) {
for (t in interp.threads) {
if (interp.threads[t].firstBlock == b) {
return true;
}
}
return false;
}
Interpreter.prototype.startSubstack = function(b, isLoop, secondSubstack) {
// Start the substack of a control structure command such as if or forever.
if (isLoop) {
b.isLoop = true;
this.activeThread.stack.push(b); // remember the block that started the substack
}
if(!secondSubstack) {
this.activeThread.nextBlock = b.substack;
} else {
this.activeThread.nextBlock = b.substack2;
}
}

117
js/Reporter.js Normal file
View file

@ -0,0 +1,117 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
'use strict';
var Reporter = function(data) {
this.cmd = data.cmd;
this.color = data.color;
this.isDiscrete = data.isDiscrete;
this.label = data.label;
this.mode = data.mode;
this.param = data.param;
this.sliderMin = data.sliderMin;
this.sliderMax = data.sliderMax;
this.target = data.target;
this.visible = data.visible;
this.x = data.x;
this.y = data.y;
this.z = io.getCount();
this.el = null; // jQuery Element for the outer box
this.valueEl = null; // jQ element containing the reporter value
this.slider = null; // slider jQ element
}
Reporter.prototype.attach = function(scene) {
switch (this.mode) {
case 1: // Normal
case 3: // Slider
this.el = $('<div class="reporter-normal">' + this.label + '</div>');
this.valueEl = $('<div class="reporter-inset">null</div>');
this.el.append(this.valueEl);
if (this.mode == 3) {
// Slider-specific
// Temporarily, set the value to sliderMin until an update
this.slider = $('<input type="range" min="' + this.sliderMin +
'" max="' + this.sliderMax + '" step="1" value="' +
this.sliderMin + '" data-target="' + this.target +
'" data-var="' + this.param + '">');
this.slider.change(this.changeSlider);
this.el.append('<br>');
this.el.append(this.slider);
}
break;
case 2: // Large
this.el = $('<div class="reporter-large">null</div>');
this.valueEl = this.el;
break;
}
this.el.css('left', this.x);
this.el.css('top', this.y);
this.el.css('z-index', this.z);
var cR = (this.color >> 16);
var cG = (this.color >> 8 & 255);
var cB = (this.color & 255);
this.valueEl.css('background-color', 'rgb(' + cR + ',' + cG + ',' + cB + ')');
this.el.css('display', this.visible ? 'inline-block' : 'none');
scene.append(this.el);
}
Reporter.prototype.update = function() {
this.el.css('display', this.visible ? 'inline-block' : 'none');
if (!this.visible) return;
var newValue ='';
var target = runtime.spriteNamed(this.target);
switch (this.cmd) {
case 'getVar:':
newValue = target.variables[this.param];
if (typeof(newValue) == 'number' && this.mode != 3) newValue = newValue.toFixed(3);
break;
case 'xpos':
newValue = target.scratchX.toFixed(3);
break;
case 'ypos':
newValue = target.scratchY.toFixed(3);
break;
case 'heading':
newValue = target.direction;
break;
case 'scale':
newValue = target.getSize();
break;
case 'sceneName':
newValue = runtime.stage.costumes[runtime.stage.currentCostumeIndex].costumeName;
break;
case 'costumeIndex':
newValue = target.currentCostumeIndex + 1;
break;
}
this.valueEl.html(newValue);
if (this.mode == 3)
this.slider.val(parseInt(newValue));
}
Reporter.prototype.updateLayer = function() {
this.el.css('z-index', this.z);
}
Reporter.prototype.changeSlider = function() {
var newValue = parseInt($(this).val());
var target = runtime.spriteNamed($(this).attr('data-target'));
var variable = $(this).attr('data-var');
target.variables[variable] = newValue;
}

210
js/Runtime.js Normal file
View file

@ -0,0 +1,210 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Scratch HTML5 Player
// Runtime.js
// Tim Mickel, July 2011
// Runtime takes care of the rendering and stepping logic.
'use strict';
var t = new Timer();
var Runtime = function() {
this.scene = null;
this.sprites = [];
this.reporters = [];
this.keysDown = {};
this.mouseDown = false;
this.mousePos = [0, 0];
this.audioContext = null;
this.audioGain = null;
this.audioPlaying = [];
this.notesPlaying = [];
this.projectLoaded = false;
}
// Initializer for the drawing and audio contexts.
Runtime.prototype.init = function() {
this.scene = $('#container');
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.audioContext = new AudioContext();
this.audioGain = this.audioContext.createGainNode();
this.audioGain.connect(runtime.audioContext.destination);
}
// Load start waits for the stage and the sprites to be loaded, without
// hanging the browser. When the loading is finished, we begin the step
// and animate methods.
Runtime.prototype.loadStart = function() {
if (!runtime.stage.isLoaded()) {
setTimeout(function(runtime) { runtime.loadStart(); }, 50, this);
return;
}
for (var obj = 0; obj < runtime.sprites.length; obj++) {
if (typeof(runtime.sprites[obj]) == 'object' && runtime.sprites[obj].constructor == Sprite) {
if (!runtime.sprites[obj].isLoaded()) {
setTimeout(function(runtime) { runtime.loadStart(); }, 50, this);
return;
}
}
}
if (Instr.wavsLoaded != Instr.wavCount) {
setTimeout(function(runtime) { runtime.loadStart(); }, 50, this);
return;
}
$('#info').html("Loaded!");
setInterval(this.step, 33);
this.projectLoaded = true;
}
Runtime.prototype.greenFlag = function() {
if (this.projectLoaded) {
interp.activeThread = new Thread(null);
interp.threads = [];
this.startGreenFlags();
}
}
Runtime.prototype.stopAll = function() {
interp.activeThread = new Thread(null);
interp.threads = [];
stopAllSounds();
// Hide reporters
for (var s = 0; s < runtime.sprites.length; s++) {
if (typeof runtime.sprites[s].hideBubble == 'function')
runtime.sprites[s].hideBubble();
}
}
// Step method for execution - called every 33 milliseconds
Runtime.prototype.step = function() {
interp.stepThreads();
for (var r = 0; r < runtime.reporters.length; r++) {
runtime.reporters[r].update();
}
}
// Stack functions -- push and remove stacks
// to be run by the interpreter as threads.
Runtime.prototype.allStacksDo = function(f) {
var stage = runtime.stage;
var stack;
for (var i = runtime.sprites.length-1; i >= 0; i--) {
var o = runtime.sprites[i];
if(typeof(o) == 'object' && o.constructor == Sprite) {
$.each(o.stacks, function(index, stack) {
f(stack, o);
});
}
}
$.each(stage.stacks, function(index, stack) {
f(stack, stage);
});
}
// Hat triggers
Runtime.prototype.startGreenFlags = function() {
function startIfGreenFlag(stack, target) {
if (stack.op == 'whenGreenFlag') interp.toggleThread(stack, target);
}
this.allStacksDo(startIfGreenFlag);
}
Runtime.prototype.startKeyHats = function(ch) {
var keyName = null;
if (('A'.charCodeAt(0) <= ch) && (ch <= 'Z'.charCodeAt(0)) ||
('a'.charCodeAt(0) <= ch) && (ch <= 'z'.charCodeAt(0)))
keyName = String.fromCharCode(ch).toLowerCase();
if (('0'.charCodeAt(0) <= ch) && (ch <= '9'.charCodeAt(0)))
keyName = String.fromCharCode(ch);
if (ch == 37) keyName = "left arrow";
if (ch == 39) keyName = "right arrow";
if (ch == 38) keyName = "up arrow";
if (ch == 40) keyName = "down arrow";
if (ch == 32) keyName = "space";
if (keyName == null) return;
var startMatchingKeyHats = function (stack, target) {
if ((stack.op == "whenKeyPressed") && (stack.args[0] == keyName)) {
// Only start the stack if it is not already running
if (!interp.isRunning(stack))
interp.toggleThread(stack, target);
}
}
runtime.allStacksDo(startMatchingKeyHats);
}
Runtime.prototype.startClickedHats = function(sprite) {
function startIfClicked(stack, target) {
if(target == sprite && stack.op == "whenClicked") {
if(!interp.isRunning(stack))
interp.toggleThread(stack, target);
}
}
runtime.allStacksDo(startIfClicked);
}
// Returns true if a key is pressed.
Runtime.prototype.keyIsDown = function(ch) {
return this.keysDown[ch] || false;
}
// Sprite named -- returns one of the sprites on the stage.
Runtime.prototype.spriteNamed = function(n) {
if (n == 'Stage') return this.stage;
var selected_sprite = null;
$.each(this.sprites, function(index, s) {
if (s.objName == n) {
selected_sprite = s;
return false;
}
});
return selected_sprite;
}
// Reassigns z-indices for layer functions
Runtime.prototype.reassignZ = function(target, move) {
var sprites = this.sprites;
var oldIndex = -1;
$.each(this.sprites, function(index, sprite) {
if (sprite == target) {
// Splice out the sprite from its old position
oldIndex = index;
sprites.splice(index, 1);
}
});
if (move == null) {
// Move to the front
this.sprites.splice(this.sprites.length, 0, target);
} else if (oldIndex - move >= 0 && oldIndex - move < this.sprites.length+1) {
// Move to the new position
this.sprites.splice(oldIndex - move, 0, target);
} else {
// No change is required
this.sprites.splice(oldIndex, 0, target);
}
// Renumber the z-indices
var newZ = 1;
$.each(this.sprites, function(index, sprite) {
sprite.z = newZ;
sprite.updateLayer();
newZ++;
});
}

122
js/Scratch.js Normal file
View file

@ -0,0 +1,122 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Scratch HTML5 Player
// Scratch.js
// Tim Mickel, July 2011
// Here we define the actions taken on window load.
// The three application-wide global variables are defined here.
'use strict';
var runtime, interp, io, iosAudioActive = false;
$(function () {
runtime = new Runtime();
runtime.init();
$(window).keydown(function(e) {
runtime.keysDown[e.which] = true;
runtime.startKeyHats(e.which);
});
$(window).keyup(function(e) {
delete runtime.keysDown[e.which];
});
// Update the project ID field
$('#project_id').val(project_id);
// Go project button behavior
$('#go_project').click(function() {
window.location = "#" + parseInt($('#project_id').val());
window.location.reload(true);
});
// Green flag behavior
$('#trigger_green_flag, #greenSlide').click(function() {
$('#greenSlide').css('display', 'none');
runtime.greenFlag()
});
// Stop button behavior
$('#trigger_stop').click(function() {
runtime.stopAll();
});
// Canvas container mouse events
$('#container').mousedown(function(e) {
runtime.mouseDown = true;
//e.preventDefault();
});
$('#container').mouseup(function(e) {
runtime.mouseDown = false;
//e.preventDefault();
});
$('#container').mousemove(function(e) {
var absX = e.pageX - this.offsetLeft;
var absY = e.pageY - this.offsetTop;
runtime.mousePos = [absX-240, -absY+180];
});
// Touch events - EXPERIMENTAL
$(window).bind('touchstart', function(e) {
// On iOS, we need to activate the Web Audio API
// with an empty sound play on the first touch event.
if (!iosAudioActive) {
var ibuffer = runtime.audioContext.createBuffer(1, 1, 22050);
var isource = runtime.audioContext.createBufferSource();
isource.buffer = ibuffer;
isource.connect(runtime.audioContext.destination);
isource.noteOn(0);
iosAudioActive = true;
}
});
$('#container').bind('touchstart', function(e) {
runtime.mouseDown = true;
});
$('#container').bind('touchend', function(e) {
runtime.mouseDown = true;
});
$('#container').bind('touchmove', function(e) {
var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
var absX = touch.pageX - this.offsetLeft;
var absY = touch.pageY - this.offsetTop;
runtime.mousePos = [absX-240, -absY+180];
});
// Border touch events - EXPERIMENTAL
$('#left').bind('touchstart touchmove', function(e) { runtime.keysDown[37] = true; runtime.startKeyHats(37); });
$('#left').bind('touchend', function(e) { delete runtime.keysDown[37]; });
$('#up').bind('touchstart touchmove', function(e) { runtime.keysDown[38] = true; runtime.startKeyHats(38); });
$('#up').bind('touchend', function(e) { delete runtime.keysDown[38]; });
$('#right').bind('touchstart touchmove', function(e) { runtime.keysDown[39] = true; runtime.startKeyHats(39); });
$('#right').bind('touchend', function(e) { delete runtime.keysDown[39]; });
$('#down').bind('touchstart touchmove', function(e) { runtime.keysDown[40] = true; runtime.startKeyHats(40); });
$('#down').bind('touchend', function(e) { delete runtime.keysDown[40]; });
// Load the interpreter and primitives
interp = new Interpreter();
interp.initPrims();
// Load the requested project and go!
io = new IO();
io.loadProject(project_id);
});

458
js/Sprite.js Normal file
View file

@ -0,0 +1,458 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Scratch HTML5 Player
// Sprite.js
// Tim Mickel, July 2011 - March 2012
// The Sprite provides the interface and implementation for Scratch sprite-control
'use strict';
var Sprite = function(data) {
if(!this.data) {
this.data = data;
}
// Public variables used for Scratch-accessible data.
this.visible = (typeof(this.data.visible) == "undefined") ? true : data.visible;
this.scratchX = data.scratchX || 0;
this.scratchY = data.scratchY || 0;
this.scale = data.scale || 1.0;
this.direction = data.direction || 90;
this.rotation = (data.direction - 90) || 0;
this.rotationStyle = data.rotationStyle || 'normal';
this.isFlipped = data.direction < 0 && data.rotationStyle == 'leftRight';
this.costumes = data.costumes || [];
this.currentCostumeIndex = data.currentCostumeIndex || 0;
this.previousCostumeIndex = -1;
this.objName = data.objName || '';
this.variables = {};
if (data.variables) {
for (var i = 0; i < data.variables.length; i++) {
this.variables[data.variables[i]['name']] = data.variables[i]['value'];
}
}
this.lists = {};
if (data.lists) {
for (var i = 0; i < data.lists.length; i++) {
this.lists[data.lists[i]['listName']] = data.lists[i];
}
}
// Used for the pen
this.penIsDown = false;
this.penWidth = 1;
this.penHue = 120; // blue
this.penShade = 50; // full brightness and saturation
this.penColorCache = 0x0000FF;
// Used for layering
if (!this.z) this.z = io.getCount();
// HTML element for the talk bubbles
this.talkBubble = null;
this.talkBubbleBox = null;
this.talkBubbleStyler = null;
this.talkBubbleOn = false;
// Internal variables used for rendering meshes.
this.textures = [];
this.materials = [];
this.geometries = [];
this.mesh = null;
// Sound buffers and data
this.sounds = {};
if (data.sounds) {
for (var i = 0; i < data.sounds.length; i++) {
this.sounds[data.sounds[i]['soundName']] = data.sounds[i];
}
}
this.soundsLoaded = 0;
this.instrument = 1;
// Incremented when images are loaded by the browser.
this.costumesLoaded = 0;
// Stacks to be pushed to the interpreter and run
this.stacks = [];
}
// Attaches a Sprite (<img>) to a Scratch scene
Sprite.prototype.attach = function(scene) {
// Create textures and materials for each of the costumes.
for (var c in this.costumes) {
this.textures[c] = document.createElement('img');
$(this.textures[c])
.load([this, c], function(evo) {
var sprite = evo.handleObj.data[0];
var c = evo.handleObj.data[1];
sprite.costumesLoaded += 1;
sprite.updateCostume();
$(sprite.textures[c]).css('display', (sprite.currentCostumeIndex == c) ? 'inline' : 'none');
$(sprite.textures[c]).css('position', 'absolute').css('left', '0px').css('top', '0px');
$(sprite.textures[c]).bind('dragstart', function(evt) { evt.preventDefault(); })
.bind('selectstart', function(evt) { evt.preventDefault(); })
.bind('touchend', function(evt) { sprite.onClick(evt); $(this).addClass('touched'); })
.click(function(evt) {
if (!$(this).hasClass('touched')) {
sprite.onClick(evt);
} else {
$(this).removeClass('touched');
}
});
scene.append($(sprite.textures[c]));
})
.attr('src', io.asset_base + this.costumes[c].baseLayerMD5 + io.asset_suffix);
}
this.mesh = this.textures[this.currentCostumeIndex];
this.updateLayer();
this.updateVisible();
this.updateTransform();
this.talkBubble = $('<div class="bubble-container"></div>');
this.talkBubble.css('display', 'none');
this.talkBubbleBox = $('<div class="bubble"></div>');
this.talkBubbleStyler = $('<div class="bubble-say"></div>');
this.talkBubble.append(this.talkBubbleBox);
this.talkBubble.append(this.talkBubbleStyler);
runtime.scene.append(this.talkBubble);
}
// Load sounds from the server and buffer them
Sprite.prototype.loadSounds = function() {
var spr = this;
$.each(this.sounds, function (index, sound) {
io.soundRequest(sound, spr);
});
}
// True when all the costumes have been loaded
Sprite.prototype.isLoaded = function() {
return (this.costumesLoaded == this.costumes.length
&& this.soundsLoaded == Object.keys(this.sounds).length);
}
// Step methods
Sprite.prototype.showCostume = function(costume) {
if (costume < 0) {
costume += this.costumes.length;
}
if (!this.textures[costume]) {
this.currentCostumeIndex = 0;
}
else {
this.currentCostumeIndex = costume;
}
this.updateCostume();
}
Sprite.prototype.indexOfCostumeNamed = function(name) {
for(var i in this.costumes) {
var c = this.costumes[i];
if(c['costumeName'] == name) {
return i;
}
}
return null;
}
Sprite.prototype.showCostumeNamed = function(name) {
var index = this.indexOfCostumeNamed(name);
if(!index) return;
this.showCostume(index);
}
Sprite.prototype.updateCostume = function() {
if(!this.textures[this.currentCostumeIndex]) {
this.currentCostumeIndex = 0;
}
$(this.mesh).css('display', 'none');
this.mesh = this.textures[this.currentCostumeIndex];
this.updateVisible();
this.updateTransform();
}
Sprite.prototype.onClick = function(evt) {
// TODO - needs work!!
var boxOffset = $('#container').offset();
var mouseX = runtime.mousePos[0] + 240 + boxOffset.left;
var mouseY = 180 - runtime.mousePos[1] + boxOffset.top;
if (this.mesh.src.indexOf('.svg') == -1) {
// HACK - if the image SRC doesn't indicate it's an SVG,
// then we'll try to detect if the point we clicked is transparent
// by rendering the sprite on a canvas. With an SVG,
// we are forced not to do this for now by Chrome/Webkit SOP:
// http://code.google.com/p/chromium/issues/detail?id=68568
var canv = document.createElement('canvas');
$(canv).css('width', 480).css('height', 360);
var ctx = canv.getContext('2d');
var drawWidth = this.textures[this.currentCostumeIndex].width * this.scale;
var drawHeight = this.textures[this.currentCostumeIndex].height * this.scale;
var rotationCenterX = this.costumes[this.currentCostumeIndex].rotationCenterX;
var rotationCenterY = this.costumes[this.currentCostumeIndex].rotationCenterY;
var drawX = this.scratchX + (480 / 2) - rotationCenterX;
var drawY = -this.scratchY + (360 / 2) - rotationCenterY;
ctx.rotate(this.rotation * Math.PI / 180.0);
ctx.drawImage(this.mesh, 0, 0, drawWidth, drawHeight);
var offsetX = mouseX - drawX - 9; // the 9 is a hack - webkit/moz
var offsetY = mouseY - drawY - 9;
var idata = ctx.getImageData(offsetX, offsetY, 1, 1).data;
var alpha = idata[3];
} else {
var alpha = 1;
}
if (alpha > 0) {
// Start clicked hats if the pixel is non-transparent
runtime.startClickedHats(this);
} else {
// Otherwise, move back a layer and trigger the click event
$(this.mesh).hide();
var underElement = document.elementFromPoint(mouseX, mouseY);
$(underElement).click();
$(this.mesh).show();
}
}
Sprite.prototype.setVisible = function(v) {
this.visible = v;
this.updateVisible();
}
Sprite.prototype.updateLayer = function() {
$(this.mesh).css('z-index', this.z);
if (this.talkBubble) this.talkBubble.css('z-index', this.z);
}
Sprite.prototype.updateVisible = function() {
$(this.mesh).css('display', (this.visible) ? 'inline' : 'none');
if (this.talkBubbleOn)
this.talkBubble.css('display', (this.visible) ? 'inline-block' : 'none');
}
Sprite.prototype.updateTransform = function() {
var texture = this.textures[this.currentCostumeIndex];
var resolution = this.costumes[this.currentCostumeIndex].bitmapResolution || 1;
var drawWidth = texture.width * this.scale / resolution;
var drawHeight = texture.height * this.scale / resolution;
var rotationCenterX = this.costumes[this.currentCostumeIndex].rotationCenterX;
var rotationCenterY = this.costumes[this.currentCostumeIndex].rotationCenterY;
var drawX = this.scratchX + (480 / 2) - rotationCenterX;
var drawY = -this.scratchY + (360 / 2) - rotationCenterY;
var scaleXprepend = '';
if (this.isFlipped) {
scaleXprepend = '-'; // For a leftRight flip, we add a minus
// sign to the X scale.
}
$(this.mesh).css('transform',
'translatex(' + drawX + 'px) \
translatey(' + drawY + 'px) \
rotate(' + this.rotation + 'deg) \
scaleX(' + scaleXprepend + (this.scale / resolution) + ') scaleY(' + (this.scale / resolution) + ')');
$(this.mesh).css('-moz-transform',
'translatex(' + drawX + 'px) \
translatey(' + drawY + 'px) \
rotate(' + this.rotation + 'deg) \
scaleX(' + scaleXprepend + this.scale + ') scaleY(' + this.scale / resolution + ')');
$(this.mesh).css('-webkit-transform',
'translatex(' + drawX + 'px) \
translatey(' + drawY + 'px) \
rotate(' + this.rotation + 'deg) \
scaleX(' + scaleXprepend + (this.scale / resolution) + ') scaleY(' + (this.scale / resolution) + ')');
$(this.mesh).css('-webkit-transform-origin', rotationCenterX + 'px ' + rotationCenterY + 'px');
$(this.mesh).css('-moz-transform-origin', rotationCenterX + 'px ' + rotationCenterY + 'px');
$(this.mesh).css('-ms-transform-origin', rotationCenterX + 'px ' + rotationCenterY + 'px');
$(this.mesh).css('-o-transform-origin', rotationCenterX + 'px ' + rotationCenterY + 'px');
$(this.mesh).css('transform-origin', rotationCenterX + 'px ' + rotationCenterY + 'px');
// Don't forget to update the talk bubble.
if (this.talkBubble) {
var xy = this.getTalkBubbleXY();
this.talkBubble.css('left', xy[0] + 'px');
this.talkBubble.css('top', xy[1] + 'px');
}
this.updateLayer();
}
Sprite.prototype.getTalkBubbleXY = function() {
var texture = this.textures[this.currentCostumeIndex];
var drawWidth = texture.width * this.scale;
var drawHeight = texture.height * this.scale;
var rotationCenterX = this.costumes[this.currentCostumeIndex].rotationCenterX;
var rotationCenterY = this.costumes[this.currentCostumeIndex].rotationCenterY;
var drawX = this.scratchX + (480 / 2) - rotationCenterX;
var drawY = -this.scratchY + (360 / 2) - rotationCenterY;
return [drawX + drawWidth, drawY - drawHeight / 2];
}
Sprite.prototype.showBubble = function(text, type) {
var xy = this.getTalkBubbleXY();
this.talkBubbleOn = true;
this.talkBubble.css('z-index', this.z);
this.talkBubble.css('left', xy[0] + 'px');
this.talkBubble.css('top', xy[1] + 'px');
this.talkBubbleStyler.removeClass('bubble-say');
this.talkBubbleStyler.removeClass('bubble-think');
if (type == 'say') this.talkBubbleStyler.addClass('bubble-say');
else if (type == 'think') this.talkBubbleStyler.addClass('bubble-think');
if (this.visible)
this.talkBubble.css('display', 'inline-block');
this.talkBubbleBox.html(text);
}
Sprite.prototype.hideBubble = function() {
this.talkBubbleOn = false;
this.talkBubble.css('display', 'none');
}
Sprite.prototype.setXY = function(x, y) {
this.scratchX = x;
this.scratchY = y;
this.updateTransform();
}
Sprite.prototype.setDirection = function(d) {
var rotation;
d = d % 360
if (d < 0) d += 360;
this.direction = (d > 180) ? d - 360 : d;
if (this.rotationStyle == 'normal') {
rotation = (this.direction - 90) % 360;
} else if (this.rotationStyle == 'leftRight') {
if (((this.direction - 90) % 360) >= 0) {
this.isFlipped = false;
} else {
this.isFlipped = true;
}
rotation = 0;
} else {
rotation = 0;
}
this.rotation = rotation;
this.updateTransform();
}
Sprite.prototype.setRotationStyle = function(r) {
this.rotationStyle = r;
}
Sprite.prototype.getSize = function() {
return this.scale * 100;
}
Sprite.prototype.setSize = function(percent) {
var newScale = percent / 100.0;
newScale = Math.max(0.05, Math.min(newScale, 100));
this.scale = newScale;
this.updateTransform();
}
// Move functions
Sprite.prototype.keepOnStage = function() {
var x = this.scratchX + 240;
var y = 180 - this.scratchY;
var myBox = this.getRect();
var inset = -Math.min(18, Math.min(myBox.width, myBox.height) / 2);
var edgeBox = new Rectangle(inset, inset, 480 - (2 * inset), 360 - (2 * inset));
if (myBox.intersects(edgeBox)) return; // sprite is sufficiently on stage
if (myBox.right < edgeBox.left) x += edgeBox.left - myBox.right;
if (myBox.left > edgeBox.right) x -= myBox.left - edgeBox.right;
if (myBox.bottom < edgeBox.top) y += edgeBox.top - myBox.bottom;
if (myBox.top > edgeBox.bottom) y -= myBox.top - edgeBox.bottom;
this.scratchX = x - 240;
this.scratchY = 180 - y;
}
Sprite.prototype.getRect = function() {
var cImg = this.textures[this.currentCostumeIndex];
var x = this.scratchX + 240 - (cImg.width/2.0);
var y = 180 - this.scratchY - (cImg.height/2.0);
var myBox = new Rectangle(x, y, cImg.width, cImg.height);
return myBox;
}
// Pen functions
Sprite.prototype.setPenColor = function(c) {
var hsv = Color.rgb2hsv(c);
this.penHue = (200 * hsv[0]) / 360 ;
this.penShade = 50 * hsv[2]; // not quite right; doesn't account for saturation
this.penColorCache = c;
}
Sprite.prototype.setPenHue = function(n) {
this.penHue = n % 200;
if (this.penHue < 0) this.penHue += 200;
this.updateCachedPenColor();
}
Sprite.prototype.setPenShade = function(n) {
this.penShade = n % 200;
if (this.penShade < 0) this.penShade += 200;
this.updateCachedPenColor();
}
Sprite.prototype.updateCachedPenColor = function() {
var c = Color.fromHSV((this.penHue * 180.0) / 100.0, 1, 1);
var shade = (this.penShade > 100) ? 200 - this.penShade : this.penShade; // range 0..100
if (shade < 50) {
this.penColorCache = Color.mixRGB(0, c, (10 + shade) / 60.0);
} else {
this.penColorCache = Color.mixRGB(c, 0xFFFFFF, (shade - 50) / 60);
}
}
Sprite.prototype.stamp = function(canvas, opacity) {
var drawWidth = this.textures[this.currentCostumeIndex].width * this.scale;
var drawHeight = this.textures[this.currentCostumeIndex].height * this.scale;
var drawX = this.scratchX + (480 / 2);
var drawY = -this.scratchY + (360 / 2);
canvas.globalAlpha = opacity / 100.0;
canvas.save();
canvas.translate(drawX, drawY);
canvas.rotate(this.rotation * Math.PI / 180.0);
canvas.drawImage(this.mesh, -drawWidth/2, -drawHeight/2, drawWidth, drawHeight);
canvas.restore();
canvas.globalAlpha = 1;
}
Sprite.prototype.soundNamed = function(name) {
if (name in this.sounds && this.sounds[name].buffer)
return this.sounds[name];
else if (name in runtime.stage.sounds && runtime.stage.sounds[name].buffer)
return runtime.stage.sounds[name];
return null;
}

72
js/Stage.js Normal file
View file

@ -0,0 +1,72 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Scratch HTML5 Player
// Stage.js
// Tim Mickel, July 2011 - March 2012
// Provides the basic logic for the Stage, a special kind of Sprite.
'use strict';
var Stage = function(data) {
// Place the background layer in the very back.
// The pen layer is right above the stage background,
// and all sprites are above that.
this.z = -2;
// Pen layer and canvas cache.
this.penLayerLoaded = false;
this.lineCanvas = document.createElement('canvas');
this.lineCanvas.width = 480;
this.lineCanvas.height = 360;
this.lineCache = this.lineCanvas.getContext('2d');
Sprite.call(this, data);
}
Stage.prototype = Object.create(Sprite.prototype);
Stage.prototype.constructor = Stage;
Stage.prototype.attachPenLayer = function(scene) {
if (this.penLayerLoaded)
return;
this.penLayerLoaded = true;
$(this.lineCanvas).css('position', 'absolute');
$(this.lineCanvas).css('z-index', '-1');
scene.append(this.lineCanvas);
}
Stage.prototype.isLoaded = function() {
return (this.penLayerLoaded
&& this.costumesLoaded == this.costumes.length
&& this.soundsLoaded == Object.keys(this.sounds).length);
}
// Pen functions
Stage.prototype.clearPenStrokes = function() {
this.lineCache.clearRect(0, 0, 480, 360);
}
Stage.prototype.stroke = function(from, to, width, color) {
this.lineCache.lineWidth = width;
this.lineCache.lineCap = 'round';
this.lineCache.beginPath();
// Use .5 offsets for canvas rigid pixel drawing
this.lineCache.moveTo(from[0] + 240.5, 180.5 - from[1]);
this.lineCache.lineTo(to[0] + 240.5, 180.5 - to[1]);
this.lineCache.strokeStyle = 'rgb(' + (color >> 16) + ',' + (color >> 8 & 255) + ',' + (color & 255) + ')';
this.lineCache.stroke();
}

178
js/primitives/LooksPrims.js Normal file
View file

@ -0,0 +1,178 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
'use strict';
var LooksPrims = function() {}
LooksPrims.prototype.addPrimsTo = function(primTable) {
primTable["show"] = this.primShow;
primTable["hide"] = this.primHide;
primTable["nextCostume"] = this.primNextCostume;
primTable["lookLike:"] = this.primShowCostume;
primTable["costumeIndex"] = this.primCostumeNum;
primTable["nextScene"] = this.primNextCostume;
primTable["showBackground:"] = this.primShowCostume;
primTable["backgroundIndex"] = this.primCostumeNum;
primTable["startScene"] = this.primStartScene;
primTable["backgroundIndex"] = this.primCostumeNum;
primTable["changeSizeBy:"] = this.primChangeSize;
primTable["setSizeTo:"] = this.primSetSize;
primTable["scale"] = this.primSize;
primTable["comeToFront"] = this.primGoFront;
primTable["goBackByLayers:"] = this.primGoBack;
primTable["changeGraphicEffect:by:"] = this.primChangeEffect;
primTable["setGraphicEffect:to:"] = this.primSetEffect;
primTable["filterReset"] = this.primClearEffects;
primTable["say:"] = function(b) { showBubble(b, 'say'); };
primTable["say:duration:elapsed:from:"] = function(b) { showBubbleAndWait(b, 'say'); };
primTable["think:"] = function(b) { showBubble(b, 'think'); };
primTable["think:duration:elapsed:from:"] = function(b) { showBubbleAndWait(b, 'think'); };
}
LooksPrims.prototype.primShow = function(b) {
interp.targetSprite().setVisible(true);
interp.redraw();
}
LooksPrims.prototype.primHide = function(b) {
interp.targetSprite().setVisible(false);
interp.redraw();
}
LooksPrims.prototype.primNextCostume = function(b) {
interp.targetSprite().showCostume(interp.targetSprite().currentCostumeIndex + 1);
interp.redraw();
}
LooksPrims.prototype.primShowCostume = function(b) {
var s = interp.targetSprite();
if (s == null) return;
var arg = interp.arg(b, 0);
if (typeof(arg) == 'number') {
s.showCostume(arg - 1);
} else {
if ((arg == 'CAMERA') || (arg == "CAMERA - MIRROR")) {
s.showCostumeNamed(arg);
return;
}
var i = s.indexOfCostumeNamed(arg);
if (i >= 0) {
s.showCostume(i);
} else {
var n = Number(arg);
if (!isNaN(n)) {
s.showCostume(n - 1);
} else {
return; // arg did not match a costume name nor is a valid number
}
}
}
if (s.visible) interp.redraw();
}
LooksPrims.prototype.primStartScene = function(b) {
var s = runtime.stage;
var arg = interp.arg(b, 0);
if (typeof(arg) == 'number') {
s.showCostume(arg - 1);
} else {
if ((arg == 'CAMERA') || (arg == "CAMERA - MIRROR")) {
s.showCostumeNamed(arg);
return;
}
var i = s.indexOfCostumeNamed(arg);
if (i >= 0) {
s.showCostume(i);
} else {
var n = Number(arg);
if (!isNaN(n)) {
s.showCostume(n - 1);
} else {
return; // arg did not match a costume name nor is a valid number
}
}
}
if (s.visible) interp.redraw();
}
LooksPrims.prototype.primCostumeNum = function(b) {
var s = interp.targetSprite();
return (s == null) ? 1 : s.currentCostumeIndex + 1;
}
LooksPrims.prototype.primChangeSize = function(b) {
var s = interp.targetSprite();
if (s == null) return;
s.setSize(s.getSize() + interp.arg(b, 0));
if (s.visible) interp.redraw();
}
LooksPrims.prototype.primSetSize = function(b) {
var s = interp.targetSprite();
if (s == null) return;
s.setSize(interp.arg(b, 0));
if (s.visible) interp.redraw();
}
LooksPrims.prototype.primSize = function(b) {
var s = interp.targetSprite();
if (s == null) return 100;
return s.getSize();
}
LooksPrims.prototype.primGoFront = function(b) {
var s = interp.targetSprite();
runtime.reassignZ(s, null);
if(s.visible) interp.redraw();
}
LooksPrims.prototype.primGoBack = function(b) {
var s = interp.targetSprite();
runtime.reassignZ(s, interp.arg(b, 0));
if(s.visible) interp.redraw();
}
LooksPrims.prototype.primChangeEffect = function(b) {}
LooksPrims.prototype.primSetEffect = function(b) {}
LooksPrims.prototype.primClearEffects = function(b) {}
var showBubble = function(b, type) {
var s = interp.targetSprite();
if (s != null) s.showBubble(interp.arg(b, 0), type);
}
var showBubbleAndWait = function(b, type) {
var s = interp.targetSprite();
if (s == null) return;
if (interp.activeThread.firstTime) {
var text = interp.arg(b, 0);
var secs = interp.arg(b, 1);
s.showBubble(text, type);
if (s.visible) interp.redraw();
interp.startTimer(secs);
} else {
if (interp.checkTimer()) s.hideBubble();
}
}

View file

@ -0,0 +1,329 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
'use strict';
var MotionAndPenPrims = function() {}
MotionAndPenPrims.prototype.addPrimsTo = function(primTable) {
primTable['forward:'] = this.primMove;
primTable['turnLeft:'] = this.primTurnLeft;
primTable['turnRight:'] = this.primTurnRight;
primTable['heading:'] = this.primSetDirection;
primTable["pointTowards:"] = this.primPointTowards;
primTable["gotoX:y:"] = this.primGoTo;
primTable["gotoSpriteOrMouse:"] = this.primGoToSpriteOrMouse;
primTable["glideSecs:toX:y:elapsed:from:"] = this.primGlide;
primTable["changeXposBy:"] = this.primChangeX;
primTable["xpos:"] = this.primSetX;
primTable["changeYposBy:"] = this.primChangeY;
primTable["ypos:"] = this.primSetY;
primTable["bounceOffEdge"] = this.primBounceOffEdge;
primTable["setRotationStyle"] = this.primSetRotationStyle;
primTable["xpos"] = this.primXPosition;
primTable["ypos"] = this.primYPosition;
primTable["heading"] = this.primDirection;
primTable["clearPenTrails"] = this.primClear;
primTable["putPenDown"] = this.primPenDown;
primTable["putPenUp"] = this.primPenUp;
primTable["penColor:"] = this.primSetPenColor;
primTable["setPenHueTo:"] = this.primSetPenHue;
primTable["changePenHueBy:"] = this.primChangePenHue;
primTable["setPenShadeTo:"] = this.primSetPenShade;
primTable["changePenShadeBy:"] = this.primChangePenShade;
primTable["penSize:"] = this.primSetPenSize;
primTable["changePenSizeBy:"] = this.primChangePenSize;
primTable["stampCostume"] = this.primStamp;
primTable["stampTransparent"] = this.primStampTransparent;
}
MotionAndPenPrims.prototype.primMove = function(b) {
var s = interp.targetSprite();
var radians = ((Math.PI * (90 - s.direction)) / 180);
var d = interp.arg(b, 0);
moveSpriteTo(s, s.scratchX + (d * Math.cos(radians)),
s.scratchY + (d * Math.sin(radians)));
if(s.visible) interp.redraw();
}
MotionAndPenPrims.prototype.primTurnLeft = function(b) {
var s = interp.targetSprite();
var d = s.direction - interp.arg(b, 0);
s.setDirection(d);
if(s.visible) interp.redraw();
}
MotionAndPenPrims.prototype.primTurnRight = function(b) {
var s = interp.targetSprite();
var d = s.direction + interp.arg(b, 0);
s.setDirection(d);
if(s.visible) interp.redraw();
}
MotionAndPenPrims.prototype.primSetDirection = function(b) {
var s = interp.targetSprite();
s.setDirection(interp.arg(b, 0));
if(s.visible) interp.redraw();
}
MotionAndPenPrims.prototype.primPointTowards = function(b) {
var s = interp.targetSprite();
var p = mouseOrSpritePosition(interp.arg(b, 0));
if ((s == null) || (p == null)) return;
var dx = p.x - s.scratchX;
var dy = p.y - s.scratchY;
var angle = 90 - ((Math.atan2(dy, dx) * 180) / Math.PI);
s.setDirection(angle);
if (s.visible) interp.redraw();
}
MotionAndPenPrims.prototype.primGoTo = function(b) {
var s = interp.targetSprite();
if (s != null) moveSpriteTo(s, interp.arg(b, 0), interp.arg(b, 1));
}
MotionAndPenPrims.prototype.primGoToSpriteOrMouse = function(b) {
var s = interp.targetSprite();
var p = mouseOrSpritePosition(interp.arg(b, 0));
if ((s == null) || (p == null)) return;
moveSpriteTo(s, p.x, p.y);
}
MotionAndPenPrims.prototype.primGlide = function(b) {
var s = interp.targetSprite();
if (s == null) return;
if (interp.activeThread.firstTime) {
var secs = interp.arg(b, 0);
var destX = interp.arg(b, 1);
var destY = interp.arg(b, 2);
if (secs <= 0) {
moveSpriteTo(s, destX, destY);
return;
}
// record state: [0]start msecs, [1]duration, [2]startX, [3]startY, [4]endX, [5]endY
interp.activeThread.tmpObj =
[interp.currentMSecs, 1000 * secs, s.scratchX, s.scratchY, destX, destY];
interp.startTimer(secs);
} else {
var state = interp.activeThread.tmpObj;
if (!interp.checkTimer()) {
// in progress: move to intermediate position along path
var frac = (interp.currentMSecs - state[0]) / state[1];
var newX = state[2] + (frac * (state[4] - state[2]));
var newY = state[3] + (frac * (state[5] - state[3]));
moveSpriteTo(s, newX, newY);
} else {
// finished: move to final position and clear state
moveSpriteTo(s, state[4], state[5]);
interp.activeThread.tmpObj = null;
}
}
}
MotionAndPenPrims.prototype.primChangeX = function(b) {
var s = interp.targetSprite();
if (s != null) moveSpriteTo(s, s.scratchX + interp.arg(b, 0), s.scratchY);
}
MotionAndPenPrims.prototype.primSetX = function(b) {
var s = interp.targetSprite();
if (s != null) moveSpriteTo(s, interp.arg(b, 0), s.scratchY);
}
MotionAndPenPrims.prototype.primChangeY = function(b) {
var s = interp.targetSprite();
if (s != null) moveSpriteTo(s, s.scratchX, s.scratchY + interp.arg(b, 0));
}
MotionAndPenPrims.prototype.primSetY = function(b) {
var s = interp.targetSprite();
if (s != null) moveSpriteTo(s, s.scratchX, interp.arg(b, 0));
}
MotionAndPenPrims.prototype.primBounceOffEdge = function(b) {
var s = interp.targetSprite();
if (s == null) return;
if (!turnAwayFromEdge(s)) return;
ensureOnStageOnBounce(s);
if (s.visible) interp.redraw();
}
MotionAndPenPrims.prototype.primSetRotationStyle = function(b) {
var s = interp.targetSprite();
if (s == null) return;
var request = interp.arg(b, 0);
var rotationStyle = 'normal';
if (request == 'all around') rotationStyle = 'normal';
else if (request == 'left-right') rotationStyle = 'leftRight';
else if (request == 'none') rotationStyle = 'none';
s.setRotationStyle(rotationStyle);
}
MotionAndPenPrims.prototype.primXPosition = function(b) {
var s = interp.targetSprite();
return (s != null) ? s.scratchX : 0;
}
MotionAndPenPrims.prototype.primYPosition = function(b) {
var s = interp.targetSprite();
return (s != null) ? s.scratchY : 0;
}
MotionAndPenPrims.prototype.primDirection = function(b) {
var s = interp.targetSprite();
return (s != null) ? s.direction : 0;
}
MotionAndPenPrims.prototype.primClear = function(b) {
runtime.stage.clearPenStrokes();
interp.redraw();
}
MotionAndPenPrims.prototype.primPenDown = function(b) {
var s = interp.targetSprite();
if (s != null) s.penIsDown = true;
stroke(s, s.scratchX, s.scratchY, s.scratchX + 0.2, s.scratchY + 0.2);
interp.redraw();
}
MotionAndPenPrims.prototype.primPenUp = function(b) {
var s = interp.targetSprite();
if (s != null) s.penIsDown = false;
}
MotionAndPenPrims.prototype.primSetPenColor = function(b) {
var s = interp.targetSprite();
if (s != null) s.setPenColor(interp.arg(b, 0));
}
MotionAndPenPrims.prototype.primSetPenHue = function(b) {
var s = interp.targetSprite();
if (s != null) s.setPenHue(interp.arg(b, 0));
}
MotionAndPenPrims.prototype.primChangePenHue = function(b) {
var s = interp.targetSprite();
if (s != null) s.setPenHue(s.penHue + interp.arg(b, 0));
}
MotionAndPenPrims.prototype.primSetPenShade = function(b) {
var s = interp.targetSprite();
if (s != null) s.setPenShade(interp.arg(b, 0));
}
MotionAndPenPrims.prototype.primChangePenShade = function(b) {
var s = interp.targetSprite();
if (s != null) s.setPenShade(s.penShade + interp.arg(b, 0));
}
MotionAndPenPrims.prototype.primSetPenSize = function(b) {
var s = interp.targetSprite();
var w = Math.max(0, Math.min(interp.arg(b, 0), 100));
if (s != null) s.penWidth = w;
}
MotionAndPenPrims.prototype.primChangePenSize = function(b) {
var s = interp.targetSprite();
var w = Math.max(0, Math.min(s.penWidth + interp.arg(b, 0), 100));
if (s != null) s.penWidth = w;
}
MotionAndPenPrims.prototype.primStamp = function(b) {
var s = interp.targetSprite();
s.stamp(runtime.stage.lineCache, 100);
}
MotionAndPenPrims.prototype.primStampTransparent = function(b) {
var s = interp.targetSprite();
var transparency = Math.max(0, Math.min(interp.arg(b, 0), 100));
var alpha = 100 - transparency;
s.stamp(runtime.stage.lineCache, alpha);
}
// Helpers
var stroke = function(s, oldX, oldY, newX, newY) {
runtime.stage.stroke([oldX, oldY],
[newX, newY], s.penWidth, s.penColorCache);
interp.redraw();
}
var mouseOrSpritePosition = function(arg) {
if (arg == "_mouse_") {
var w = runtime.stage;
return new Point(runtime.mousePos[0], runtime.mousePos[1]);
} else {
var s = runtime.spriteNamed(arg);
if (s == null) return null;
return new Point(s.scratchX, s.scratchY);
}
return null;
}
var moveSpriteTo = function(s, newX, newY) {
var oldX = s.scratchX;
var oldY = s.scratchY;
s.setXY(newX, newY);
s.keepOnStage();
if (s.penIsDown) stroke(s, oldX, oldY, s.scratchX, s.scratchY);
if ((s.penIsDown) || (s.visible)) interp.redraw();
}
var turnAwayFromEdge = function(s) {
// turn away from the nearest edge if it's close enough; otherwise do nothing
// Note: comparisions are in the stage coordinates, with origin (0, 0)
// use bounding rect of the sprite to account for costume rotation and scale
var r = s.getRect();
// measure distance to edges
var d1 = Math.max(0, r.left);
var d2 = Math.max(0, r.top);
var d3 = Math.max(0, 480 - r.right);
var d4 = Math.max(0, 360 - r.bottom);
// find the nearest edge
var e = 0, minDist = 100000;
if (d1 < minDist) { minDist = d1; e = 1 }
if (d2 < minDist) { minDist = d2; e = 2 }
if (d3 < minDist) { minDist = d3; e = 3 }
if (d4 < minDist) { minDist = d4; e = 4 }
if (minDist > 0) return false; // not touching to any edge
// point away from nearest edge
var radians = ((90 - s.direction) * Math.PI) / 180;
var dx = Math.cos(radians);
var dy = -Math.sin(radians);
if (e == 1) { dx = Math.max(0.2, Math.abs(dx)) }
if (e == 2) { dy = Math.max(0.2, Math.abs(dy)) }
if (e == 3) { dx = 0 - Math.max(0.2, Math.abs(dx)) }
if (e == 4) { dy = 0 - Math.max(0.2, Math.abs(dy)) }
var newDir = ((180 * Math.atan2(dy, dx)) / Math.PI) + 90;
s.direction = newDir;
return true;
}
var ensureOnStageOnBounce = function(s) {
var r = s.getRect();
if (r.left < 0) moveSpriteTo(s, s.scratchX - r.left, s.scratchY);
if (r.top < 0) moveSpriteTo(s, s.scratchX, s.scratchY + r.top);
if (r.right > 480) {
moveSpriteTo(s, s.scratchX - (r.right - 480), s.scratchY);
}
if (r.bottom > 360) {
moveSpriteTo(s, s.scratchX, s.scratchY + (r.bottom - 360));
}
}

106
js/primitives/Primitives.js Normal file
View file

@ -0,0 +1,106 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Scratch HTML5 Player
// Primitives.js
// Tim Mickel, July 2011
// Provides the basic primitives for the interpreter and loads in the more
// complicated primitives, e.g. MotionAndPenPrims.
'use strict';
var Primitives = function() {}
Primitives.prototype.addPrimsTo = function(primTable) {
// Math primitives
primTable["+"] = function(b) { return interp.arg(b, 0) + interp.arg(b, 1) };
primTable["-"] = function(b) { return interp.arg(b, 0) - interp.arg(b, 1) };
primTable["*"] = function(b) { return interp.arg(b, 0) * interp.arg(b, 1) };
primTable["/"] = function(b) { return interp.arg(b, 0) / interp.arg(b, 1) };
primTable["%"] = function(b) { return interp.arg(b, 0) % interp.arg(b, 1) };
primTable["randomFrom:to:"] = this.primRandom;
primTable["<"] = function(b) { return (interp.arg(b, 0) < interp.arg(b, 1)) };
primTable["="] = function(b) { return (interp.arg(b, 0) == interp.arg(b, 1)) };
primTable[">"] = function(b) { return (interp.arg(b, 0) > interp.arg(b, 1)) };
primTable["&"] = function(b) { return interp.arg(b, 0) && interp.arg(b, 1) };
primTable["|"] = function(b) { return interp.arg(b, 0) || interp.arg(b, 1) };
primTable["not"] = function(b) { return !interp.arg(b, 0) };
primTable["abs"] = function(b) { return Math.abs(interp.arg(b, 0)) };
primTable["sqrt"] = function(b) { return Math.sqrt(interp.arg(b, 0)) };
primTable["\\\\"] = this.primModulo;
primTable["rounded"] = function(b) { return Math.round(interp.arg(b, 0)) };
primTable["computeFunction:of:"] = this.primMathFunction;
// String primitives
primTable["concatenate:with:"] = function(b) { return "" + interp.arg(b, 0) + interp.arg(b, 1) };
primTable["letter:of:"] = this.primLetterOf;
primTable["stringLength:"] = function(b) { return interp.arg(b, 0).length };
new VarListPrims().addPrimsTo(primTable);
new MotionAndPenPrims().addPrimsTo(primTable);
new LooksPrims().addPrimsTo(primTable);
new SensingPrims().addPrimsTo(primTable);
new SoundPrims().addPrimsTo(primTable);
}
Primitives.prototype.primRandom = function(b) {
var n1 = interp.arg(b, 0);
var n2 = interp.arg(b, 1);
var low = (n1 <= n2) ? n1 : n2;
var hi = (n1 <= n2) ? n2 : n1;
if(low == hi) return low;
// if both low and hi are ints, truncate the result to an int
if ((Math.floor(low) == low) && (Math.floor(hi) == hi)) {
return low + Math.floor(Math.random() * ((hi + 1) - low));
}
return (Math.random() * (hi - low)) + low;
}
Primitives.prototype.primLetterOf = function(b) {
var s = interp.arg(b, 1);
var i = interp.arg(b, 0) - 1;
if ((i < 0) || (i >= s.length)) return "";
return s.charAt(i);
}
Primitives.prototype.primModulo = function(b) {
var modulus = interp.arg(b, 1);
var n = interp.arg(b, 0) % modulus;
if (n < 0) n += modulus;
return n;
}
Primitives.prototype.primMathFunction = function(b) {
var op = interp.arg(b, 0);
var n = interp.arg(b, 1);
switch(op) {
case "abs": return Math.abs(n);
case "sqrt": return Math.sqrt(n);
case "sin": return Math.sin((Math.PI * n) / 180);
case "cos": return Math.cos((Math.PI * n) / 180);
case "tan": return Math.tan((Math.PI * n) / 180);
case "asin": return (Math.asin(n) * 180) / Math.PI;
case "acos": return (Math.acos(n) * 180) / Math.PI;
case "atan": return (Math.atan(n) * 180) / Math.PI;
case "ln": return Math.log(n);
case "log": return Math.log(n) / Math.LN10;
case "e ^": return Math.exp(n);
case "10 ^": return Math.exp(n * Math.LN10);
}
return 0;
}

View file

@ -0,0 +1,220 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
'use strict';
var SensingPrims = function() {}
SensingPrims.prototype.addPrimsTo = function(primTable) {
primTable['touching:'] = this.primTouching;
primTable['touchingColor:'] = this.primTouchingColor;
primTable['color:sees:'] = this.primColorTouchingColor;
primTable['keyPressed:'] = this.primKeyPressed;
primTable['mousePressed'] = function(b) { return runtime.mouseDown; };
primTable['mouseX'] = function(b) { return runtime.mousePos[0]; };
primTable['mouseY'] = function(b) { return runtime.mousePos[1]; };
primTable['distanceTo:'] = this.primDistanceTo;
primTable['getAttribute:of:'] = this.primGetAttribute;
}
SensingPrims.prototype.primTouching = function(b) {
var s = interp.targetSprite();
if (s == null || !s.visible) return false;
var arg = interp.arg(b, 0);
if (arg == '_edge_') {
return false; // TODO
}
if (arg == '_mouse_') {
return false; // TODO
}
var s2 = runtime.spriteNamed(arg);
if (s2 == null || !s2.visible) return false;
return spriteHitTest(s, s2);
}
SensingPrims.prototype.primTouchingColor = function(b) {
var s = interp.targetSprite();
if (s == null || !s.visible) return false;
var color = interp.arg(b, 0);
return stageColorHitTest(s, color);
}
SensingPrims.prototype.primColorTouchingColor = function(b) {
var s = interp.targetSprite();
if (s == null || !s.visible) return false;
var myColor = interp.arg(b, 0);
var stageColor = interp.arg(b, 1);
return stageColorByColorHitTest(s, myColor, stageColor);
}
var spriteHitTest = function(a, b) {
var hitCanvas = document.createElement('canvas');
hitCanvas.width = 480;
hitCanvas.height = 360;
var hitTester = hitCanvas.getContext('2d');
hitTester.globalCompositeOperation = 'source-over';
a.stamp(hitTester, 100);
hitTester.globalCompositeOperation = 'source-in';
b.stamp(hitTester, 100);
var aData = hitTester.getImageData(0, 0, 480, 360).data;
var pxCount = aData.length;
for (var i = 0; i < pxCount; i += 4) {
if (aData[i+3] > 0) {
return true;
}
}
return false;
}
var stageColorHitTest = function(target, color) {
var r, g, b;
r = (color >> 16);
g = (color >> 8 & 255);
b = (color & 255);
var targetCanvas = document.createElement('canvas');
targetCanvas.width = 480;
targetCanvas.height = 360;
var targetTester = targetCanvas.getContext('2d');
target.stamp(targetTester, 100);
var stageCanvas = document.createElement('canvas');
stageCanvas.width = 480;
stageCanvas.height = 360;
var stageContext = stageCanvas.getContext('2d');
$.each(runtime.sprites, function(i, sprite) {
if (sprite != target)
sprite.stamp(stageContext, 100);
});
var hitData = stageContext.getImageData(0, 0, stageCanvas.width, stageCanvas.height).data;
var meshData = targetTester.getImageData(0, 0, targetCanvas.width, targetCanvas.height).data;
var pxCount = meshData.length;
for (var i = 0; i < pxCount; i += 4) {
if (meshData[i+3] > 0 && hitData[i] == r && hitData[i+1] == g && hitData[i+2] == b)
return true;
}
return false;
}
var stageColorByColorHitTest = function(target, myColor, otherColor) {
var threshold_acceptable = function(a, b, c, x, y, z) {
diff_a = Math.abs(a-x);
diff_b = Math.abs(b-y);
diff_c = Math.abs(c-z);
if (diff_a + diff_b + diff_c < 100) {
return true;
}
return false;
}
var targetCanvas = document.createElement('canvas');
targetCanvas.width = 480;
targetCanvas.height = 360;
var targetTester = targetCanvas.getContext('2d');
target.stamp(targetTester, 100);
var targetData = targetTester.getImageData(0, 0, targetCanvas.width, targetCanvas.height).data;
// Calculate RGB values of the colors - TODO thresholding
//myColor = Math.abs(myColor);
//otherColor = Math.abs(otherColor);
var mr, mg, mb, or, og, ob;
mr = (myColor >> 16);
mg = (myColor >> 8 & 255);
mb = (myColor & 255);
or = (otherColor >> 16);
og = (otherColor >> 8 & 255);
ob = (otherColor & 255);
// Create the hit canvas for comparison
var hitCanvas = document.createElement('canvas');
hitCanvas.width = 480;
hitCanvas.height = 360;
hitCtx = hitCanvas.getContext('2d');
$.each(runtime.sprites, function(i, sprite) {
if (sprite != target)
sprite.stamp(hitCtx, 100);
});
var hitData = hitCtx.getImageData(0, 0, hitCanvas.width, hitCanvas.height).data;
var pxCount = targetData.length;
for (var i = 0; i < pxCount; i += 4) {
if (threshold_acceptable(targetData[i], targetData[i+1], targetData[i+2], mr, mg, mb)
&& threshold_acceptable(hitData[i], hitData[i+1], hitData[i+2], or, og, ob)) {
return true;
}
}
return false;
}
SensingPrims.prototype.primKeyPressed = function(b) {
var key = interp.arg(b, 0);
var ch = key.charCodeAt(0);
if (ch > 127) return false;
if (key == "left arrow") ch = 37;
if (key == "right arrow") ch = 39;
if (key == "up arrow") ch = 38;
if (key == "down arrow") ch = 40;
if (key == "space") ch = 32;
return (typeof(runtime.keysDown[ch]) != 'undefined');
}
SensingPrims.prototype.primDistanceTo = function(b) {
var s = interp.targetSprite();
var p = mouseOrSpritePosition(interp.arg(b, 0));
if (s == null || p == null) return 0;
var dx = p.x - s.scratchX;
var dy = p.y - s.scratchY;
return Math.sqrt((dx * dx) + (dy * dy));
}
SensingPrims.prototype.primGetAttribute = function(b) {
var attr = interp.arg(b, 0);
var targetSprite = runtime.spriteNamed(interp.arg(b, 1));
if (targetSprite == null) return 0;
if (attr == 'x position') return targetSprite.scratchX;
if (attr == 'y position') return targetSprite.scratchY;
if (attr == 'direction') return targetSprite.direction;
if (attr == 'costume #') return targetSprite.currentCostumeIndex + 1;
if (attr == 'costume name') return targetSprite.costumes[targetSprite.currentCostumeIndex]['costumeName'];
if (attr == 'size') return targetSprite.getSize();
if (attr == 'volume') return targetSprite.volume;
return 0;
}
// Helpers
SensingPrims.prototype.mouseOrSpritePosition = function(arg) {
if (arg == "_mouse_") {
var w = runtime.stage;
return new Point(runtime.mousePos[0], runtime.mousePos[1]);
} else {
var s = runtime.spriteNamed(arg);
if (s == null) return null;
return new Point(s.scratchX, s.scratchY);
}
return null;
}

213
js/primitives/SoundPrims.js Normal file
View file

@ -0,0 +1,213 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
'use strict';
var SoundPrims = function() {}
SoundPrims.prototype.addPrimsTo = function(primTable) {
primTable['playSound:'] = this.primPlaySound;
primTable['doPlaySoundAndWait'] = this.primPlaySoundUntilDone;
primTable['stopAllSounds'] = this.primStopAllSounds;
primTable['playDrum'] = this.primPlayDrum;
primTable['rest:elapsed:from:'] = this.primPlayRest;
primTable['noteOn:duration:elapsed:from:'] = this.primPlayNote;
primTable['instrument:'] = this.primSetInstrument;
/*primTable['changeVolumeBy:'] = this.primChangeVolume;
primTable['setVolumeTo:'] = this.primSetVolume;
primTable['volume'] = this.primVolume;*/
primTable['changeTempoBy:'] = function(b) { runtime.stage.data.tempoBPM = runtime.stage.data.tempoBPM + interp.arg(b, 0); };
primTable['setTempoTo:'] = function(b) { runtime.stage.data.tempoBPM = interp.arg(b, 0); };
primTable['tempo'] = function(b) { return runtime.stage.data.tempoBPM; };
}
var playSound = function(snd) {
if (snd.source) {
// If this particular sound is already playing, stop it.
snd.source.noteOff(0);
snd.source = null;
}
snd.source = runtime.audioContext.createBufferSource();
snd.source.buffer = snd.buffer;
snd.source.connect(runtime.audioGain);
// Track the sound's completion state
snd.source.done = false;
snd.source.finished = function() {
// Remove from the active audio list and disconnect the source from
// the sound dictionary.
var i = runtime.audioPlaying.indexOf(snd);
if (i > -1 && runtime.audioPlaying[i].source != null) {
runtime.audioPlaying[i].source.done = true;
runtime.audioPlaying[i].source = null;
runtime.audioPlaying.splice(i, 1);
}
}
window.setTimeout(snd.source.finished, snd.buffer.duration * 1000);
// Add the global list of playing sounds and start playing.
runtime.audioPlaying.push(snd);
snd.source.noteOn(0);
return snd.source;
}
var playDrum = function(drum, secs, client) {
var player = SoundBank.getDrumPlayer(drum, secs);
player.client = client;
player.setDuration(secs);
var source = runtime.audioContext.createScriptProcessor(4096, 1, 1);
source.onaudioprocess = function(e) { player.writeSampleData(e); };
source.soundPlayer = player;
source.connect(runtime.audioGain);
runtime.notesPlaying.push(source);
source.finished = function() {
var i = runtime.notesPlaying.indexOf(source);
if (i > -1 && runtime.notesPlaying[i] != null) {
runtime.notesPlaying.splice(i, 1);
}
}
window.setTimeout(source.finished, secs * 1000);
return player;
}
var playNote = function(instrument, midiKey, secs, client) {
var player = SoundBank.getNotePlayer(instrument, midiKey);
player.client = client;
player.setNoteAndDuration(midiKey, secs);
var source = runtime.audioContext.createScriptProcessor(4096, 1, 1);
source.onaudioprocess = function(e) { player.writeSampleData(e); };
source.connect(runtime.audioGain);
runtime.notesPlaying.push(source);
source.finished = function() {
var i = runtime.notesPlaying.indexOf(source);
if (i > -1 && runtime.notesPlaying[i] != null) {
runtime.notesPlaying.splice(i, 1);
}
}
window.setTimeout(source.finished, secs * 1000);
return player;
}
var stopAllSounds = function() {
var oldPlaying = runtime.audioPlaying;
runtime.audioPlaying = [];
for (var s = 0; s < oldPlaying.length; s++) {
if (oldPlaying[s].source) {
oldPlaying[s].source.noteOff(0);
oldPlaying[s].source.finished();
}
}
var oldPlaying = runtime.notesPlaying;
runtime.notesPlaying = [];
for (var s = 0; s < oldPlaying.length; s++) {
if (oldPlaying[s]) {
oldPlaying[s].disconnect();
oldPlaying[s].finished();
}
}
}
SoundPrims.prototype.primPlaySound = function(b) {
var s = interp.targetSprite();
if (s == null) return;
var snd = s.soundNamed(interp.arg(b, 0));
if (snd != null) playSound(snd);
}
SoundPrims.prototype.primPlaySoundUntilDone = function(b) {
var activeThread = interp.activeThread;
if (activeThread.firstTime) {
var snd = interp.targetSprite().soundNamed(interp.arg(b, 0));
if (snd == null) return;
activeThread.tmpObj = playSound(snd);
activeThread.firstTime = false;
}
var player = activeThread.tmpObj;
if (player == null || player.done || player.playbackState == 3) {
activeThread.tmpObj = null;
activeThread.firstTime = true;
} else {
interp.yield = true;
}
}
var beatsToSeconds = function(beats) {
return (beats * 60) / runtime.stage.data.tempoBPM;
}
SoundPrims.prototype.primPlayNote = function(b) {
var s = interp.targetSprite();
if (s == null) return;
if (interp.activeThread.firstTime) {
var key = interp.arg(b, 0);
var secs = beatsToSeconds(interp.arg(b, 1));
playNote(s.instrument, key, secs, s);
interp.startTimer(secs);
} else {
interp.checkTimer();
}
}
SoundPrims.prototype.primPlayDrum = function(b) {
var s = interp.targetSprite();
if (s == null) return;
if (interp.activeThread.firstTime) {
var drum = Math.round(interp.arg(b, 0));
var secs = beatsToSeconds(interp.arg(b, 1));
playDrum(drum, secs, s);
interp.startTimer(secs);
} else {
interp.checkTimer();
}
}
SoundPrims.prototype.primPlayRest = function(b) {
var s = interp.targetSprite();
if (s == null) return;
if (interp.activeThread.firstTime) {
var secs = beatsToSeconds(interp.arg(b, 0));
interp.startTimer(secs);
} else {
interp.checkTimer();
}
}
SoundPrims.prototype.primSetInstrument = function(b) {
var s = interp.targetSprite();
if (s != null) s.instrument = interp.arg(b, 0);
}
SoundPrims.prototype.primStopAllSounds = function(b) {
stopAllSounds();
}
SoundPrims.prototype.primChangeVolume = function(b) {
var s = interp.targetSprite();
if (s != null) s.volume += interp.arg(b, 0);
}
SoundPrims.prototype.primSetVolume = function(b) {
var s = interp.targetSprite();
if (s != null) s.volume = interp.arg(b, 0);
}
SoundPrims.prototype.primVolume = function(b) {
var s = interp.targetSprite();
return (s != null) ? s.volume : 0;
}

View file

@ -0,0 +1,171 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
'use strict';
var VarListPrims = function() {}
VarListPrims.prototype.addPrimsTo = function(primTable) {
// Variable primitives
primTable['readVariable'] = this.primReadVar;
primTable['setVar:to:'] = this.primSetVar;
primTable['changeVar:by:'] = this.primChangeVar;
primTable['hideVariable:'] = this.primHideVar;
primTable['showVariable:'] = this.primShowVar;
// List primitives
primTable['append:toList:'] = this.primListAppend;
primTable['deleteLine:ofList:'] = this.primListDeleteLine;
primTable['insert:at:ofList:'] = this.primListInsertAt;
primTable['setLine:ofList:to:'] = this.primListSetLine;
primTable['lineCountOfList:'] = this.primListLength;
primTable['getLine:ofList:'] = this.primListGetLine;
primTable['list:contains:'] = this.primListContains;
}
// Variable primitive implementations
VarListPrims.prototype.primReadVar = function(b) {
var s = interp.targetSprite();
if (s == null) return;
var targetVar = interp.arg(b, 0);
if (targetVar in s.variables)
return s.variables[targetVar];
else if (targetVar in runtime.stage.variables)
return runtime.stage.variables[targetVar];
}
VarListPrims.prototype.primSetVar = function(b) {
var s = interp.targetSprite();
if (s == null) return;
var targetVar = interp.arg(b, 0);
if (targetVar in s.variables)
s.variables[targetVar] = interp.arg(b, 1);
else if (targetVar in runtime.stage.variables)
runtime.stage.variables[targetVar] = interp.arg(b, 1);
}
VarListPrims.prototype.primChangeVar = function(b) {
var s = interp.targetSprite();
if (s == null) return;
var targetVar = interp.arg(b, 0);
if (targetVar in s.variables) {
s.variables[targetVar] = parseFloat(s.variables[targetVar]);
s.variables[targetVar] += interp.arg(b, 1);
} else if (targetVar in runtime.stage.variables) {
runtime.stage.variables[targetVar] = parseFloat(runtime.stage.variables[targetVar]);
runtime.stage.variables[targetVar] += interp.arg(b, 1);
}
}
VarListPrims.prototype.primHideVar = function(b) {
var targetVar = interp.arg(b, 0);
for (var r = 0; r < runtime.reporters.length; r++) {
if (runtime.reporters[r].cmd == 'getVar:' && runtime.reporters[r].param == targetVar) {
runtime.reporters[r].visible = false;
return;
}
}
}
VarListPrims.prototype.primShowVar = function(b) {
var targetVar = interp.arg(b, 0);
for (var r = 0; r < runtime.reporters.length; r++) {
if (runtime.reporters[r].cmd == 'getVar:' && runtime.reporters[r].param == targetVar) {
runtime.reporters[r].visible = true;
return;
}
}
}
// List primitive implementations
// Take a list name and target sprite and return the JS list itself
var findList = function(targetSprite, listName) {
if (targetSprite == null) targetSprite = runtime.stage;
if (listName in targetSprite.lists) {
return targetSprite.lists[listName].contents;
} else if (listName in runtime.stage.lists) {
return runtime.stage.lists[listName].contents;
}
return null;
}
VarListPrims.prototype.primListAppend = function(b) {
var list = findList(interp.targetSprite(), interp.arg(b, 1));
if (list) list.push(interp.arg(b, 0));
}
VarListPrims.prototype.primListDeleteLine = function(b) {
var list = findList(interp.targetSprite(), interp.arg(b, 1));
if (!list) return;
var line = interp.arg(b, 0);
if (line == 'all' || list.length == 0) list.length = 0;
else if (line == 'last') list.splice(list.length - 1, 1);
else if (parseInt(line) - 1 in list) list.splice(parseInt(line) - 1, 1);
}
VarListPrims.prototype.primListInsertAt = function(b) {
var list = findList(interp.targetSprite(), interp.arg(b, 2));
if (!list) return;
var newItem = interp.arg(b, 0);
var position = interp.arg(b, 1);
if (position == 'last') position = list.length;
else if (position == 'random') position = Math.round(Math.random() * list.length);
else position = parseInt(position) - 1;
if (position > list.length) return;
list.splice(position, 0, newItem);
}
VarListPrims.prototype.primListSetLine = function(b) {
var list = findList(interp.targetSprite(), interp.arg(b, 1));
if (!list) return;
var newItem = interp.arg(b, 2);
var position = interp.arg(b, 0);
if (position == 'last') position = list.length - 1;
else if (position == 'random') position = Math.floor(Math.random() * list.length);
else position = parseInt(position) - 1;
if (position > list.length - 1) return;
list[position] = newItem;
}
VarListPrims.prototype.primListLength = function(b) {
var list = findList(interp.targetSprite(), interp.arg(b, 0));
if (!list) return 0;
return list.length;
}
VarListPrims.prototype.primListGetLine = function(b) {
var list = findList(interp.targetSprite(), interp.arg(b, 1));
if (!list) return 0;
var line = interp.arg(b, 0);
if (list.length == 0) return 0;
if (line == 'random') line = Math.round(Math.random() * list.length);
else if (line == 'last') line = list.length;
else if (list.length < line) return 0;
return list[line - 1];
}
VarListPrims.prototype.primListContains = function(b) {
var list = findList(interp.targetSprite(), interp.arg(b, 0));
if (!list) return 0;
var searchItem = interp.arg(b, 1);
if (parseFloat(searchItem) == searchItem) searchItem = parseFloat(searchItem);
return $.inArray(searchItem, list) > -1;
}

128
js/sound/NotePlayer.js Normal file
View file

@ -0,0 +1,128 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// NotePlayer.js
// Tim Mickel, 2013
// Based entirely on the AS version by John Maloney
//
// Subclass of SoundDecoder to play notes on a sampled instrument or drum.
//
// A sampled instrument outputs interpolated sound samples from an array of signed,
// 16-bit integers with an original sampling rate of 22050 samples/sec. The pitch is
// shifted by change the step size while iterating through this array. An instrument
// may also be looped so that it can be sustained and it may have a volume envelope
// to control the attack and decay of the note.
var NotePlayer = function(wavFileData, originalPitch, loopStart, loopEnd, env) {
this.originalPitch = originalPitch || null;
this.index = 0;
this.samplesRemaining = 0; // determines note duration
// Looping
this.isLooped = false;
this.loopPoint = 0; // final sample in loop
this.loopLength = 0;
// Volume Envelope
this.envelopeValue = 1;
this.samplesSinceStart = 0;
this.attackEnd = 0;
this.attackRate = 0;
this.holdEnd = 0;
this.decayRate = 1;
if (wavFileData == null) wavFileData = new ArrayBuffer();
var stepSize = 0.5; // default - no pitch shift
var startOffset = 0;
this.endOffset = wavFileData.byteLength / 2; // end of sample data
var getSample = function() { return 0; } // called once at startup time
this.soundData = new Uint8Array(wavFileData);
if ((loopStart >= 0) && (loopStart < this.endOffset)) {
this.isLooped = true;
this.loopPoint = loopStart;
if ((loopEnd > 0) && (loopEnd <= this.endOffset)) this.endOffset = loopEnd;
this.loopLength = this.endOffset - this.loopPoint;
// Compute the original pitch more exactly from the loop length:
var oneCycle = 22050 / this.originalPitch;
var cycles = Math.round(this.loopLength / oneCycle);
this.originalPitch = 22050 / (this.loopLength / cycles);
}
if (env) {
this.attackEnd = env[0] * 44.100;
if (this.attackEnd > 0) this.attackRate = Math.pow(33000, 1 / this.attackEnd);
this.holdEnd = this.attackEnd + (env[1] * 44.100);
var decayCount = env[2] * 44100;
this.decayRate = (decayCount == 0) ? 1 : Math.pow(33000, -1 / decayCount);
}
}
NotePlayer.prototype = Object.create(SoundDecoder.prototype);
NotePlayer.prototype.constructor = NotePlayer;
NotePlayer.prototype.setNoteAndDuration = function(midiKey, secs) {
midiKey = Math.max(0, Math.min(midiKey, 127));
var pitch = 440 * Math.pow(2, (midiKey - 69) / 12); // midi key 69 is A (440 Hz)
this.stepSize = pitch / (2 * this.originalPitch); // adjust for original sampling rate of 22050
this.setDuration(secs);
}
NotePlayer.prototype.setDuration = function(secs) {
this.samplesSinceStart = 0;
this.samplesRemaining = 44100 * secs;
if (!this.isLooped) this.samplesRemaining = Math.min(this.samplesRemaining, this.endOffset / this.stepSize);
this.envelopeValue = (this.attackEnd > 0) ? 1 / 33000 : 1;
}
NotePlayer.prototype.interpolatedSample = function() {
if (this.samplesRemaining-- <= 0) { this.noteFinished(); return 0; }
this.index += this.stepSize;
while (this.index >= this.endOffset) {
if (!this.isLooped) return 0;
this.index -= this.loopLength;
}
var i = Math.floor(this.index);
var frac = this.index - i;
var curr = this.rawSample(i);
var next = this.rawSample(i + 1);
var sample = (curr + (frac * (next - curr))) / 100000; // xxx 32000; attenuate...
if (this.samplesRemaining < 1000) sample *= (this.samplesRemaining / 1000.0); // relaase phease
this.updateEnvelope();
return this.envelopeValue * sample;
}
NotePlayer.prototype.rawSample = function(sampleIndex) {
if (sampleIndex >= this.endOffset) {
if (this.isLooped) sampleIndex = this.loopPoint;
else return 0;
}
var byteIndex = 2 * sampleIndex;
var result = (this.soundData[byteIndex + 1] << 8) + this.soundData[byteIndex];
return (result <= 32767) ? result : result - 65536;
}
NotePlayer.prototype.updateEnvelope = function() {
// Compute envelopeValue for the current sample.
this.samplesSinceStart++;
if (this.samplesSinceStart < this.attackEnd) {
this.envelopeValue *= this.attackRate;
} else if (this.samplesSinceStart == this.attackEnd) {
this.envelopeValue = 1;
} else if (this.samplesSinceStart > this.holdEnd) {
if (this.decayRate < 1) this.envelopeValue *= this.decayRate;
}
}

215
js/sound/SoundBank.js Normal file
View file

@ -0,0 +1,215 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// SoundBank.js
// Tim Mickel, 2013
// Based on the original AS by John Maloney - Scratch 1.4 compatibility removed
//
// A collection of instrument and drum resources to support the note and drum commands.
var SoundBank = function() {};
// -----------------------------
// Scratch 2.0 Instrument Definitions
//------------------------------
// Each instrument is an array of one or more key-span entries of the following form:
//
// top key of key span, sampleName, midiKey, loopStart, loopEnd, [attack, hold, decay]
//
// The loop points are -1 if the sound is unlooped (e.g. Marimba).
// The three-element envelop array may be omitted if the instrument has no envelope.
SoundBank.instruments = [
[[38, 'AcousticPiano_As3', 58, 10266, 17053, [0, 100, 22]],
[44, 'AcousticPiano_C4', 60, 13968, 18975, [0, 100, 20]],
[51, 'AcousticPiano_G4', 67, 12200, 12370, [0, 80, 18]],
[62, 'AcousticPiano_C6', 84, 13042, 13276, [0, 80, 16]],
[70, 'AcousticPiano_F5', 77, 12425, 12965, [0, 40, 14]],
[77, 'AcousticPiano_Ds6', 87, 12368, 12869, [0, 20, 10]],
[85, 'AcousticPiano_Ds6', 87, 12368, 12869, [0, 0, 8]],
[90, 'AcousticPiano_Ds6', 87, 12368, 12869, [0, 0, 6]],
[96, 'AcousticPiano_D7', 98, 7454, 7606, [0, 0, 3]],
[128, 'AcousticPiano_D7', 98, 7454, 7606, [0, 0, 2]]],
[[48, 'ElectricPiano_C2', 36, 15338, 17360, [0, 80, 10]],
[74, 'ElectricPiano_C4', 60, 11426, 12016, [0, 40, 8]],
[128, 'ElectricPiano_C4', 60, 11426, 12016, [0, 0, 6]]],
[[128, 'Organ_G2', 43, 1306, 3330]],
[[40, 'AcousticGuitar_F3', 53, 36665, 36791, [0, 0, 15]],
[56, 'AcousticGuitar_F3', 53, 36665, 36791, [0, 0, 13.5]],
[60, 'AcousticGuitar_F3', 53, 36665, 36791, [0, 0, 12]],
[67, 'AcousticGuitar_F3', 53, 36665, 36791, [0, 0, 8.5]],
[72, 'AcousticGuitar_F3', 53, 36665, 36791, [0, 0, 7]],
[83, 'AcousticGuitar_F3', 53, 36665, 36791, [0, 0, 5.5]],
[128, 'AcousticGuitar_F3', 53, 36665, 36791, [0, 0, 4.5]]],
[[40, 'ElectricGuitar_F3', 53, 34692, 34945, [0, 0, 15]],
[56, 'ElectricGuitar_F3', 53, 34692, 34945, [0, 0, 13.5]],
[60, 'ElectricGuitar_F3', 53, 34692, 34945, [0, 0, 12]],
[67, 'ElectricGuitar_F3', 53, 34692, 34945, [0, 0, 8.5]],
[72, 'ElectricGuitar_F3', 53, 34692, 34945, [0, 0, 7]],
[83, 'ElectricGuitar_F3', 53, 34692, 34945, [0, 0, 5.5]],
[128, 'ElectricGuitar_F3', 53, 34692, 34945, [0, 0, 4.5]]],
[[34, 'ElectricBass_G1', 31, 41912, 42363, [0, 0, 17]],
[48, 'ElectricBass_G1', 31, 41912, 42363, [0, 0, 14]],
[64, 'ElectricBass_G1', 31, 41912, 42363, [0, 0, 12]],
[128, 'ElectricBass_G1', 31, 41912, 42363, [0, 0, 10]]],
[[38, 'Pizz_G2', 43, 8554, 8782, [0, 0, 5]],
[45, 'Pizz_G2', 43, 8554, 8782, [0, 12, 4]],
[56, 'Pizz_A3', 57, 11460, 11659, [0, 0, 4]],
[64, 'Pizz_A3', 57, 11460, 11659, [0, 0, 3.2]],
[72, 'Pizz_E4', 64, 17525, 17592, [0, 0, 2.8]],
[80, 'Pizz_E4', 64, 17525, 17592, [0, 0, 2.2]],
[128, 'Pizz_E4', 64, 17525, 17592, [0, 0, 1.5]]],
[[41, 'Cello_C2', 36, 8548, 8885],
[52, 'Cello_As2', 46, 7465, 7845],
[62, 'Violin_D4', 62, 10608, 11360],
[75, 'Violin_A4', 69, 3111, 3314, [70, 0, 0]],
[128, 'Violin_E5', 76, 2383, 2484]],
[[30, 'BassTrombone_A2_3', 45, 1357, 2360],
[40, 'BassTrombone_A2_2', 45, 1893, 2896],
[55, 'Trombone_B3', 59, 2646, 3897],
[88, 'Trombone_B3', 59, 2646, 3897, [50, 0, 0]],
[128, 'Trumpet_E5', 76, 2884, 3152]],
[[128, 'Clarinet_C4', 60, 14540, 15468]],
[[40, 'TenorSax_C3', 48, 8939, 10794],
[50, 'TenorSax_C3', 48, 8939, 10794, [20, 0, 0]],
[59, 'TenorSax_C3', 48, 8939, 10794, [40, 0, 0]],
[67, 'AltoSax_A3', 57, 8546, 9049],
[75, 'AltoSax_A3', 57, 8546, 9049, [20, 0, 0]],
[80, 'AltoSax_A3', 57, 8546, 9049, [20, 0, 0]],
[128, 'AltoSax_C6', 84, 1258, 1848]],
[[61, 'Flute_B5_2', 83, 1859, 2259],
[128, 'Flute_B5_1', 83, 2418, 2818]],
[[128, 'WoodenFlute_C5', 72, 11426, 15724]],
[[57, 'Bassoon_C3', 48, 2428, 4284],
[67, 'Bassoon_C3', 48, 2428, 4284, [40, 0, 0]],
[76, 'Bassoon_C3', 48, 2428, 4284, [80, 0, 0]],
[84, 'EnglishHorn_F3', 53, 7538, 8930, [40, 0, 0]],
[128, 'EnglishHorn_D4', 62, 4857, 5231]],
[[39, 'Choir_F3', 53, 14007, 41281],
[50, 'Choir_F3', 53, 14007, 41281, [40, 0, 0]],
[61, 'Choir_F3', 53, 14007, 41281, [60, 0, 0]],
[72, 'Choir_F4', 65, 16351, 46436],
[128, 'Choir_F5', 77, 18440, 45391]],
[[38, 'Vibraphone_C3', 48, 6202, 6370, [0, 100, 8]],
[48, 'Vibraphone_C3', 48, 6202, 6370, [0, 100, 7.5]],
[59, 'Vibraphone_C3', 48, 6202, 6370, [0, 60, 7]],
[70, 'Vibraphone_C3', 48, 6202, 6370, [0, 40, 6]],
[78, 'Vibraphone_C3', 48, 6202, 6370, [0, 20, 5]],
[86, 'Vibraphone_C3', 48, 6202, 6370, [0, 0, 4]],
[128, 'Vibraphone_C3', 48, 6202, 6370, [0, 0, 3]]],
[[128, 'MusicBox_C4', 60, 14278, 14700, [0, 0, 2]]],
[[128, 'SteelDrum_D5', 74.4, -1, -1, [0, 0, 2]]],
[[128, 'Marimba_C4', 60, -1, -1]],
[[80, 'SynthLead_C4', 60, 135, 1400],
[128, 'SynthLead_C6', 84, 124, 356]],
[[38, 'SynthPad_A3', 57, 4212, 88017, [50, 0, 0]],
[50, 'SynthPad_A3', 57, 4212, 88017, [80, 0, 0]],
[62, 'SynthPad_A3', 57, 4212, 88017, [110, 0, 0]],
[74, 'SynthPad_A3', 57, 4212, 88017, [150, 0, 0]],
[86, 'SynthPad_A3', 57, 4212, 88017, [200, 0, 0]],
[128, 'SynthPad_C6', 84, 2575, 9202]],
];
// -----------------------------
// Scratch 2.0 Drum Definitions
//------------------------------
// Each drum entry is an array of of the form:
//
// sampleName, pitchAdjust, [loopStart, loopEnd, decay]
//
// pitchAdjust (pitch shift in semitones) adjusts the original pitch.
// The loop points and decay parameter may be omitted if the drum is unlooped.
// (A few drums are looped to create several different pitched drums from one sample.)
SoundBank.drums = [
['SnareDrum', 0],
['Tom', 0],
['SideStick', 0],
['Crash', -7],
['HiHatOpen', -8],
['HiHatClosed', 0],
['Tambourine', 0],
['Clap', 0],
['Claves', 0],
['WoodBlock', -4],
['Cowbell', 0],
['Triangle', -6, 16843, 17255, 2],
['Bongo', 2],
['Conga', -7, 4247, 4499, 2], // jhm decay
['Cabasa', 0],
['GuiroLong', 0],
['Vibraslap', -6],
['Cuica', -5],
];
SoundBank.getNotePlayer = function(instNum, midiKey) {
// Return a NotePlayer for the given Scratch 2.0 instrument number (1..21)
// and MIDI key (0..127). If the instrument is out of range, use 1.
var r = SoundBank.getNoteRecord(instNum - 1, midiKey);
var env = (r.length > 5) ? r[5] : null;
return new NotePlayer(Instr.samples[r[1]], SoundBank.pitchForKey(r[2]), r[3], r[4], env);
}
SoundBank.getNoteRecord = function(instNum, midiKey) {
// Get a note record for the given instrument number.
if ((instNum < 0) || (instNum >= SoundBank.instruments.length)) instNum = 0;
var keyRanges = SoundBank.instruments[instNum];
for (var r = 0; r < keyRanges.length; r++) {
var topOfKeyRange = keyRanges[r][0];
if (midiKey <= topOfKeyRange) return keyRanges[r];
}
return keyRanges[keyRanges.length - 1]; // return the note record for the top key range.
}
SoundBank.pitchForKey = function(midiKey) {
return 440 * Math.pow(2, (midiKey - 69) / 12); // midi key 69 is A=440 Hz
}
SoundBank.getDrumPlayer = function(drumNum, secs) {
// Return a NotePlayer for the given drum number.
var entry = SoundBank.drums[drumNum - 1];
if (entry == null) entry = SoundBank.drums[2];
var loopStart = -1, loopEnd = -1, env = null;
if (entry.length >= 4) {
loopStart = entry[2];
loopEnd = entry[3];
}
if (entry.length >= 5) env = [0, 0, entry[4]];
var player = new NotePlayer(Instr.samples[entry[0]], SoundBank.pitchForKey(60), loopStart, loopEnd, env);
player.setNoteAndDuration(60 + entry[1], 0);
return player;
}

193
js/sound/SoundDecoder.js Normal file
View file

@ -0,0 +1,193 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// SoundDecoder.js
// Decode WAV Files (8-bit, 16-bit, and ADPCM) for playing by Sprites.
// For best performance, this should be run only once per WAV and
// the decoded buffer should be cached.
// Based almost entirely on John Maloney's AS implementation.
var SoundDecoder = function(wavFileData) {
this.scratchSound = null;
this.soundData = null;
this.startOffset = 0;
this.endOffset = 0;
this.stepSize = 0;
this.adpcmBlockSize = 0;
this.bytePosition = 0;
this.soundChannel = null;
this.lastBufferTime = 0;
this.getSample = null;
this.fraction = 0.0;
this.thisSample = 0;
// decoder state
this.sample = 0;
this.index = 0;
this.lastByte = -1; // -1 indicates that there is no saved lastByte
this.nextSample = 0;
this.info = null;
getSample = this.getSample16Uncompressed;
if (wavFileData != null) {
var info = WAVFile.decode(wavFileData);
this.info = info;
this.startOffset = info.sampleDataStart;
this.endOffset = this.startOffset + info.sampleDataSize;
this.soundData = new Uint8Array(wavFileData.slice(this.startOffset, this.endOffset));
this.stepSize = info.samplesPerSecond / 44100.0;
if (info.encoding == 17) {
this.adpcmBlockSize = info.adpcmBlockSize;
this.getSample = this.getSampleADPCM;
} else {
if (info.bitsPerSample == 8) this.getSample = this.getSample8Uncompressed;
if (info.bitsPerSample == 16) this.getSample = this.getSample16Uncompressed;
}
}
}
SoundDecoder.prototype.noteFinished = function() {
// Called by subclasses to force ending condition to be true in writeSampleData()
this.bytePosition = this.endOffset;
}
// Used for Notes and Drums - Web Audio API ScriptProcessorNodes use this
// as a callback function to fill the buffers with sample data.
SoundDecoder.prototype.writeSampleData = function(evt) {
var i = 0;
var output = evt.outputBuffer.getChannelData(0);
//this.updateVolume();
for (i = 0; i < output.length; i++) {
var n = this.interpolatedSample();
output[i] = n;
}
}
// For pre-caching the samples of WAV sounds
// Return a full list of samples generated by the decoder.
SoundDecoder.prototype.getAllSamples = function() {
var samples = [], smp = 0;
smp = this.interpolatedSample();
while (smp != null) {
samples.push(smp);
smp = this.interpolatedSample();
}
return samples;
}
// Provide the next sample for the buffer
SoundDecoder.prototype.interpolatedSample = function() {
this.fraction += this.stepSize;
while (this.fraction >= 1.0) {
this.thisSample = this.nextSample;
this.nextSample = this.getSample();
this.fraction -= 1.0;
}
if (this.nextSample == null) { return null; }
var out = (this.fraction == 0) ?
this.thisSample :
this.thisSample + (this.fraction * (this.nextSample - this.thisSample));
return (out) / 32768.0;
}
// 16-bit samples, big-endian
SoundDecoder.prototype.getSample16Uncompressed = function() {
var result = 0;
if (this.bytePosition <= (this.info.sampleDataSize - 2)) {
result = (this.soundData[this.bytePosition + 1] << 8) + this.soundData[this.bytePosition];
if (result > 32767) result -= 65536;
this.bytePosition += 2;
} else {
this.bytePosition = this.endOffset;
result = null;
}
return result;
}
// 8-bit samples, uncompressed
SoundDecoder.prototype.getSample8Uncompressed = function() {
if (this.bytePosition >= this.info.sampleDataSize) return null;
return (this.soundData[this.bytePosition++] - 128) << 8;
}
/*SoundDecoder.prototype.updateVolume = function() {
if (this.client == null) {
this.volume = 1.0;
return;
}
if (this.client.volume == this.lastClientVolume) return; // optimization
this.volume = Math.max(0.0, Math.min(this.client.volume / 100.0, 1.0));
this.lastClientVolume = this.client.volume;
}*/
// Decoder for IMA ADPCM compressed sounds
SoundDecoder.indexTable = [-1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8];
SoundDecoder.stepTable = [
7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45,
50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230,
253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963,
1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327,
3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487,
12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767
];
SoundDecoder.prototype.getSampleADPCM = function() {
// Decompress sample data using the IMA ADPCM algorithm.
// Note: Handles only one channel, 4-bits/sample.
var step = 0, code = 0, delta = 0;
if (((this.bytePosition % this.adpcmBlockSize) == 0) && (this.lastByte < 0)) { // read block header
if (this.bytePosition > (this.info.sampleDataSize - 4)) return null;
this.sample = (this.soundData[this.bytePosition + 1] << 8) + this.soundData[this.bytePosition];
if (this.sample > 32767) this.sample -= 65536;
this.index = this.soundData[this.bytePosition + 2];
this.bytePosition += 4;
if (this.index > 88) this.index = 88;
this.lastByte = -1;
return this.sample;
} else {
// read 4-bit code and compute delta
if (this.lastByte < 0) {
if (this.bytePosition >= this.info.sampleDataSize) return null;
this.lastByte = this.soundData[this.bytePosition++];
code = this.lastByte & 0xF;
} else {
code = (this.lastByte >> 4) & 0xF;
this.lastByte = -1;
}
step = SoundDecoder.stepTable[this.index];
delta = 0;
if (code & 4) delta += step;
if (code & 2) delta += step >> 1;
if (code & 1) delta += step >> 2;
delta += step >> 3;
// compute next index
this.index += SoundDecoder.indexTable[code];
if (this.index > 88) this.index = 88;
if (this.index < 0) this.index = 0;
// compute and output sample
this.sample += ((code & 8) ? -delta : delta);
if (this.sample > 32767) this.sample = 32767;
if (this.sample < -32768) this.sample = -32768;
return this.sample;
}
}

113
js/sound/WAVFile.js Normal file
View file

@ -0,0 +1,113 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// WAVFile.js
// Utility class for reading and decoding WAV file metadata
// Based directly on John Maloney's AS version for the Scratch Flash Player
var WAVFile = function() {};
WAVFile.decode = function(waveData) {
// Decode the given WAV file data and return an Object with the format and sample data.
var result = {};
var data = new OffsetBuffer(waveData);
// read WAVE File Header
if (data.readString(4) != 'RIFF') { console.log("WAVFile: bad file header"); return; }
var totalSize = data.readInt();
if (data.getLength() != (totalSize + 8)) console.log("WAVFile: bad RIFF size; ignoring");
if (data.readString(4) != 'WAVE') { console.log("WAVFile: not a WAVE file"); return; }
// read format chunk
var formatChunk = WAVFile.extractChunk('fmt ', data);
if (formatChunk.getLength() < 16) { console.log("WAVFile: format chunk is too small"); return; }
var encoding = formatChunk.readShort();
result.encoding = encoding;
result.channels = formatChunk.readShort();
result.samplesPerSecond = formatChunk.readInt();
result.bytesPerSecond = formatChunk.readInt();
result.blockAlignment = formatChunk.readShort();
result.bitsPerSample = formatChunk.readShort();
// get size of data chunk
var sampleDataStartAndSize = WAVFile.dataChunkStartAndSize(data);
result.sampleDataStart = sampleDataStartAndSize[0];
result.sampleDataSize = sampleDataStartAndSize[1];
// handle various encodings
if (encoding == 1) {
if (!((result.bitsPerSample == 8) || (result.bitsPerSample == 16))) {
console.log("WAVFile: can only handle 8-bit or 16-bit uncompressed PCM data");
return;
}
result.sampleCount = result.sampleDataSize / 2;
} else if (encoding == 17) {
if (formatChunk.length < 20) { console.log("WAVFile: adpcm format chunk is too small"); return; }
if (result.channels != 1) { console.log("WAVFile: adpcm supports only one channel (monophonic)"); return; }
formatChunk.offset += 2; // skip extra header byte count
var samplesPerBlock = formatChunk.readShort();
result.adpcmBlockSize = ((samplesPerBlock - 1) / 2) + 4; // block size in bytes
var factChunk = WAVFile.extractChunk('fact', data);
if ((factChunk != null) && (factChunk.getLength() == 4)) {
result.sampleCount = factChunk.readInt();
} else {
// this should never happen, since there should always be a 'fact' chunk
// slight over-estimate (doesn't take ADPCM headers into account)
result.sampleCount = 2 * result.sampleDataSize;
}
} else {
console.log("WAVFile: unknown encoding " + encoding);
return;
}
return result;
}
WAVFile.extractChunk = function(desiredType, data) {
// Return the contents of the first chunk of the given type or an empty OffsetBuffer if it is not found.
data.offset = 12;
while (data.bytesAvailable() > 8) {
var chunkType = data.readString(4);
var chunkSize = data.readUint();
if (chunkType == desiredType) {
if (chunkSize > data.bytesAvailable()) return null;
var result = new OffsetBuffer(data.readBytes(chunkSize));
return result;
} else {
data.offset += chunkSize;
}
}
return new OffsetBuffer(new ArrayBuffer());
}
WAVFile.dataChunkStartAndSize = function(data) {
// Return an array with the starting offset and size of the first chunk of the given type.
data.offset = 12;
while (data.bytesAvailable() >= 8) {
var chunkType = data.readString(4);
var chunkSize = data.readUint();
if (chunkType == 'data') {
if (chunkSize > data.bytesAvailable()) return [0, 0]; // bad wave file
return [data.offset, chunkSize];
} else {
data.offset += chunkSize;
}
}
return [0, 0]; // chunk not found; bad wave file
}

89
js/util/Color.js Normal file
View file

@ -0,0 +1,89 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Scratch HTML5 Player
// Color.js
// Based on the original by John Maloney
Color = function() {};
Color.fromHSV = function(h, s, v) {
var r, g, b;
h = h % 360;
if (h < 0) h += 360;
s = Math.max(0, Math.min(s, 1));
v = Math.max(0, Math.min(v, 1));
var i = Math.floor(h / 60);
var f = (h / 60) - i;
var p = v * (1 - s);
var q = v * (1 - (s * f));
var t = v * (1 - (s * (1 - f)));
if (i == 0) { r = v; g = t; b = p; }
else if (i == 1) { r = q; g = v; b = p; }
else if (i == 2) { r = p; g = v; b = t; }
else if (i == 3) { r = p; g = q; b = v; }
else if (i == 4) { r = t; g = p; b = v; }
else if (i == 5) { r = v; g = p; b = q; }
r = Math.floor(r * 255);
g = Math.floor(g * 255);
b = Math.floor(b * 255);
return (r << 16) | (g << 8) | b;
}
Color.rgb2hsv = function(rgb) {
var h, s, v, x, f, i;
var r = ((rgb >> 16) & 255) / 255;
var g = ((rgb >> 8) & 255) / 255;
var b = (rgb & 255) / 255;
x = Math.min(Math.min(r, g), b);
v = Math.max(Math.max(r, g), b);
if (x == v) return [0, 0, v]; // gray; hue arbitrarily reported as zero
f = (r == x) ? g - b : ((g == x) ? b - r : r - g);
i = (r == x) ? 3 : ((g == x) ? 5 : 1);
h = ((i - (f / (v - x))) * 60) % 360;
s = (v - x) / v;
return [h, s, v];
}
Color.scaleBrightness = function(rgb, scale) {
var hsv = Color.rgb2hsv(rgb);
scale = Math.max(0, Math.min(scale, 1));
return Color.fromHSV(hsv[0], hsv[1], scale * hsv[2]);
}
Color.mixRGB = function(rgb1, rgb2, fraction) {
// Mix rgb1 with rgb2. 0 gives all rgb1, 1 gives rbg2, .5 mixes them 50/50.
if (fraction <= 0) return rgb1;
if (fraction >= 1) return rgb2;
var r1 = (rgb1 >> 16) & 255;
var g1 = (rgb1 >> 8) & 255;
var b1 = rgb1 & 255
var r2 = (rgb2 >> 16) & 255;
var g2 = (rgb2 >> 8) & 255;
var b2 = rgb2 & 255
var r = ((fraction * r2) + ((1.0 - fraction) * r1)) & 255;
var g = ((fraction * g2) + ((1.0 - fraction) * g1)) & 255;
var b = ((fraction * b2) + ((1.0 - fraction) * b1)) & 255;
return (r << 16) | (g << 8) | b;
}
Color.random = function() {
// return a random color
var h = 360 * Math.random();
var s = 0.7 + (0.3 * Math.random());
var v = 0.6 + (0.4 * Math.random());
return Color.fromHSV(h, s, v);
}

81
js/util/OffsetBuffer.js Normal file
View file

@ -0,0 +1,81 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Provides the equivalent functionality of an AS ByteArray
// using JavaScript ArrayBuffers and viewers
var OffsetBuffer = function(data) {
this.offset = 0;
this.ab = data;
}
// Read various datatypes from the ArrayBuffer, seeking the offset.
OffsetBuffer.prototype.readString = function(length) {
var str = this.ab2str(this.ab.slice(this.offset, this.offset + length));
this.offset += length;
return str;
}
OffsetBuffer.prototype.readInt = function() {
var num = this.ab2int(this.ab.slice(this.offset, this.offset + 4));
this.offset += 4;
return num;
}
OffsetBuffer.prototype.readUint = function() {
var num = this.ab2uint(this.ab.slice(this.offset, this.offset + 4));
this.offset += 4;
return num;
}
OffsetBuffer.prototype.readShort = function() {
var num = this.ab2short(this.ab.slice(this.offset, this.offset + 2));
this.offset += 2;
return num;
}
OffsetBuffer.prototype.readBytes = function(length) {
var bytes = this.ab.slice(this.offset, this.offset + length);
this.offset += length;
return bytes;
}
// Length of the internal buffer
OffsetBuffer.prototype.getLength = function() {
return this.ab.byteLength;
}
// Number of bytes remaining from the current offset
OffsetBuffer.prototype.bytesAvailable = function() {
return (this.getLength() - this.offset);
}
// ArrayBuffer -> JS type conversion methods
OffsetBuffer.prototype.ab2str = function(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
// These create Javascript Numbers
OffsetBuffer.prototype.ab2int = function(buf) {
return new Int32Array(buf)[0];
}
OffsetBuffer.prototype.ab2uint = function(buf) {
return new Uint32Array(buf)[0];
}
OffsetBuffer.prototype.ab2short = function(buf) {
return new Int16Array(buf)[0];
}

36
js/util/Rectangle.js Normal file
View file

@ -0,0 +1,36 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
var Point = function(x, y) {
this.x = x;
this.y = y;
}
var Rectangle = function(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.left = x;
this.right = x + width;
this.top = y;
this.bottom = y + height;
}
Rectangle.prototype.intersects = function(other) {
return !(this.left > other.right ||
this.right < other.left ||
this.top > other.bottom ||
this.bottom < other.top);
}

60
js/util/Timer.js Normal file
View file

@ -0,0 +1,60 @@
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
/*
* Timer for the interpeter and performance testing
* Tim Mickel, July 2011
*/
var Timer = function() {
var trials = [];
var last_trial = 0;
var start_time = 0;
}
Timer.prototype.time = function() {
return (new Date()).getTime();
}
Timer.prototype.start = function() {
start_time = this.time();
}
Timer.prototype.stop = function() {
end = this.time();
last_trial = end - start_time;
trials.push(last_trial);
}
Timer.prototype.count = function() {
return trials.length;
}
Timer.prototype.average = function() {
sum = 0;
for(i = 0; i < this.count(); i++) {
sum += trials[i];
}
return sum / this.count();
}
Timer.prototype.print = function(element) {
text = "Trial: " + last_trial + "ms" +
"<br />\nTrials: " + this.count() + ", Avg: " + this.average() + "ms";
if(element) {
$(element).html(text);
} else {
console.log(text);
}
}