/* Copyright 2012 Nathan Dinsmore Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. last change: 2013-March-20 by Jens Moenig */ var sb = (function (sb) { 'use strict'; function extend(o, p) { var key; for (key in p) if (p.hasOwnProperty(key)) { o[key] = p[key]; } } sb.$extend = extend; extend((sb.Ref = function (id) { this.id = id; }).prototype, { isRef: true }); extend((sb.Dictionary = function (keys, values) { this.keys = keys; this.values = values; }).prototype, { get: function (key) { return this.values[this.keys.indexOf(key)]; }, set: function (key, value) { var i = this.keys.indexOf(key); if (i === -1) { this.keys.push(key); this.values.push(value); } else { this.values[i] = value; } } }); sb.Color = function (rgb, a) { this.r = (rgb / 0x100000 | 0) % 0x400 / (0x400 - 1); this.g = (rgb / 0x400 | 0) % 0x400 / (0x400 - 1); this.b = (rgb % 0x400) / (0x400 - 1); this.a = a / (0x100 - 1); }; sb.Point = function (x, y) { this.x = x; this.y = y; }; sb.Rectangle = function (x, y, x2, y2) { this.origin = new sb.Point(x, y); this.corner = new sb.Point(x2, y2); }; sb.indexedColors = [ [1, 1, 1, 1], [0, 0, 0, 1], [1, 1, 1, 1], [.5, .5, .5, 1], [1, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 1], [0, 1, 1, 1], [1, 1, 0, 1], [1, 0, 1, 1], [.125, .125, .125, 1], [.25, .25, .25, 1], [.375, .375, .375, 1], [.625, .625, .625, 1], [.75, .75, .75, 1], [.875, .875, .875, 1] ]; (function () { var i, r, g, b, grayVal; for (i = 1; i <= 31; ++i) { if (i % 4 != 0) { grayVal = i / 32; sb.indexedColors[i + 15] = [grayVal, grayVal, grayVal, 1]; } } for (r = 0; r < 6; ++r) { for (g = 0; g < 6; ++g) { for (b = 0; b < 6; ++b) { i = 40 + 36 * r + 6 * b + g; sb.indexedColors[i] = [r / 5, g / 5, b / 5, 1]; } } } })(); sb.colorDepthLengths = { 1: 1, 2: 2, 4: 4, 8: 8, 15: 5, 16: 5, 12: 4, 9: 3 }; extend((sb.ColorStream = function (bitmap, depth) { var i = 0, l = bitmap.length, b = this.bits = [], n; this.length = sb.colorDepthLengths[this.depth = depth]; this.position = 0; while (i < l) { n = bitmap[i++]; b.push(n / 0x80 & 1); b.push(n / 0x40 & 1); b.push(n / 0x20 & 1); b.push(n / 0x10 & 1); b.push(n / 0x8 & 1); b.push(n / 0x4 & 1); b.push(n / 0x2 & 1); b.push(n & 1); } }).prototype, { read: function () { var i = this.length, b = this.bits, n = 0; while (i--) { n = n * 2 + b[this.position++]; } return n; } }); extend((sb.ColorStreamIndexed = function (bitmap, depth) { sb.ColorStream.call(this, bitmap, depth); }).prototype = Object.create(sb.ColorStream.prototype), { next: function (b) { var c = sb.indexedColors[this.read()]; if (!c) return c; b[0] = c[0]; b[1] = c[1]; b[2] = c[2]; b[3] = c[3]; } }); extend((sb.ColorStreamRGB = function (bitmap, depth) { sb.ColorStream.call(this, bitmap, depth); this.max = Math.pow(2, this.length) - 1; }).prototype = Object.create(sb.ColorStream.prototype), { next: function (b) { var c; switch (this.depth) { case 16: ++this.position; c = [this.read() / this.max, this.read() / this.max, this.read() / this.max, 1]; if (c[0] + c[1] + c[2] === 0) { return b[0] = b[1] = b[2] = b[3] = 0; } b[0] = c[0]; b[1] = c[1]; b[2] = c[2]; b[3] = c[3]; return; case 12: this.position += 3; break; } b[0] = this.read() / this.max; b[1] = this.read() / this.max; b[2] = this.read() / this.max; b[3] = 1; } }); extend((sb.ColorStream32 = function (bitmap, depth) { this.bytes = bitmap; if (depth === 32) { this.next = this.nextAlpha; } this.position = 0; }).prototype, { next: function (b) { b[0] = this.read() / 255; b[1] = this.read() / 255; b[2] = this.read() / 255; b[3] = 1; }, nextAlpha: function (b) { var a = this.read() / 255; b[0] = this.read() / 255; b[1] = this.read() / 255; b[2] = this.read() / 255 b[3] = a; }, read: function () { return this.bytes[this.position++]; } }); sb.colorStreams = { 1: sb.ColorStreamIndexed, 2: sb.ColorStreamIndexed, 4: sb.ColorStreamIndexed, 8: sb.ColorStreamIndexed, 15: sb.ColorStreamRGB, 16: sb.ColorStreamRGB, 32: sb.ColorStream32, 24: sb.ColorStream32, 12: sb.ColorStreamRGB, 9: sb.ColorStreamRGB }; sb.getColorStream = function (bitmap, depth) { return new sb.colorStreams[depth](bitmap, depth); }; extend((sb.Form = function (w, h, d, o, b) { this.width = w; this.height = h; this.depth = d; this.offset = o; this.bits = b; }).prototype, { init: function (bm) { if (this.bits.isBitmap) { this.bitmap = this.bits; } else { this.decompress(bm); } this.image = document.createElement('canvas'); this.image.width = this.width; this.image.height = this.height; }, decompress: function (bm) { var b = this.bits, p = 0, q = 0, r = !!bm, length = (i = b[p++], i <= 223 ? i : i < 255 ? (i - 224) * 256 + b[p++] : (b[p++] << 24) + (b[p++] << 16) + (b[p++] << 8) + b[p++]), bm, i, n, d, e, f, g; // stream = new sb.Reader().on(this.bits), this.bitmap = bm || (bm = []); while (p < b.length) { i = b[p++]; if (i > 223) { i = i < 255 ? (i - 224) * 256 + b[p++] : (b[p++] << 24) + (b[p++] << 16) + (b[p++] << 8) + b[p++]; } n = i >> 2; switch (i & 3) { case 1: d = b[p++]; n *= 4; while (n--) { bm[q++] = d; } break; case 2: // d = stream.readBytes(4); d = b[p++]; e = b[p++]; f = b[p++]; g = b[p++]; while (n--) { if (r) { bm[q++] = e; bm[q++] = f; bm[q++] = g; bm[q++] = d; } else { bm[q++] = d; bm[q++] = e; bm[q++] = f; bm[q++] = g; } } break; case 3: while (n--) { if (r) { d = b[p++]; bm[q++] = b[p++]; bm[q++] = b[p++]; bm[q++] = b[p++]; bm[q++] = d; } else { bm[q++] = b[p++]; bm[q++] = b[p++]; bm[q++] = b[p++]; bm[q++] = b[p++]; } } // bm.push.apply(bm, stream.readBytes(n * 4)); break; } } }, load: function () { var w = this.width, h = this.height, x, y, i, imageData, data, context, colors, color, b; if (this.depth === 32) { this.image = document.createElement('canvas'); this.image.width = this.width; this.image.height = this.height; data = (imageData = (context = this.image.getContext('2d')).createImageData(this.width, this.height)).data; this.decompress(data); context.putImageData(imageData, 0, 0); return; } color = [0, 0, 0, 0]; if (this.depth === 16) { w = this.width += 1; } this.init(); colors = sb.getColorStream(this.bitmap, this.depth); data = (imageData = (context = this.image.getContext('2d')).createImageData(this.width, this.height)).data; for (x = 0; x < w; ++x) { for (y = 0; y < h; ++y) { colors.next(color); if (!color) continue; i = (x * h + y) * 4; data[i] = color[0] * 255; data[i + 1] = color[1] * 255; data[i + 2] = color[2] * 255; data[i + 3] = color[3] * 255; } } context.putImageData(imageData, 0, 0); } }); extend((sb.ColorForm = function (w, h, d, o, b, c) { this.width = w; this.height = h; this.depth = d; this.offset = o; this.bits = b; this.colors = c; }).prototype = Object.create(sb.Form.prototype), { load: function () { var w = this.width, h = this.height, colors = this.colors, bits, x, y, i, imageData, data, context, color; this.init(); data = (imageData = (context = this.image.getContext('2d')).createImageData(this.width, this.height)).data; bits = this.bitmap; for (x = 0; x < w; ++x) { for (y = 0; y < h; ++y) { color = colors[bits[x * h + y]]; if (!color) continue; i = (x * h + y) * 4; data[i] = color.r * 255; data[i + 1] = color.g * 255; data[i + 2] = color.b * 255; data[i + 3] = color.a * 255; } } context.putImageData(imageData, 0, 0); } }); sb.fields = {}; sb.classIDs = {}; sb.classNames = {}; sb.addFields = function (id, name, base, fields) { sb.classIDs[name] = id; sb.classNames[id] = name; fields = fields.length ? fields.split(',') : []; if (base) { base = sb.fields[sb.classIDs[base]]; if (!base) throw new Error('Initialization error'); fields = base.concat(fields); } sb.fields[id] = fields; }; sb.addFields(100, 'Morph', '', 'bounds,owner,submorphs,color,flags,properties'); sb.addFields(101, 'BorderedMorph', 'Morph', 'borderWidth,borderColor'); sb.addFields(102, 'RectangleMorph', 'BorderedMorph', ''); sb.addFields(103, 'EllipseMorph', 'BorderedMorph', ''); sb.addFields(104, 'AlignmentMorph', 'RectangleMorph', 'orientation,centering,hResizing,vResizing,inset'); sb.addFields(105, 'StringMorph', 'Morph', 'fontSpec,emphasis,contents'); sb.addFields(-1, 'Slider', 'BorderedMorph', 'slider,value,setValueSelector,sliderShadow,sliderColor,descending,model'); sb.addFields(-2, 'AbstractSound', '', ''); sb.addFields(-3, 'ScriptableScratchMorph', 'Morph', 'objName,vars,blocksBin,customBlocks,isClone,media,costume'); sb.addFields(-4, 'ArgMorph', 'BorderedMorph', 'labelMorph'); sb.addFields(-5, 'PasteUpMorph', 'BorderedMorph', ''); sb.addFields(-6, 'ScratchMedia', '', 'mediaName'); sb.addFields(-7, 'ScrollFrameMorph', 'BorderedMorph', ''); sb.addFields(106, 'UpdatingStringMorph', 'StringMorph', 'format,target,getSelector,putSelector,parameter,floatPrecision,growable,stepTime'); sb.addFields(107, 'SimpleSliderMorph', 'Slider', 'target,arguments,minVal,maxVal,truncate,sliderThickness'); sb.addFields(108, 'SimpleButtonMorph', 'RectangleMorph', 'target,actionSelector,arguments,actWhen'); sb.addFields(109, 'SampledSound', 'AbstractSound', 'envelopes,scaledVol,initialCount,samples,originalSamplingRate,samplesSize,scaledIncrement,scaledInitialIndex'); sb.addFields(110, 'ImageMorph', 'Morph', 'form,transparency'); sb.addFields(111, 'SketchMorph', 'Morph', 'originalForm,rotationCenter,rotationDegrees,rotationStyle,scalePoint,offsetWhenRotated'); sb.addFields(123, 'SensorBoardMorph', 'Morph', 'portNum'); sb.addFields(124, 'ScratchSpriteMorph', 'ScriptableScratchMorph', 'visibility,scalePoint,rotationDegrees,rotationStyle,volume,tempoBPM,draggable,sceneStates,lists,virtualScale,ownerSprite,subsprites,rotateWithOwner,refPos,prototype,deletedAttributes'); sb.addFields(125, 'ScratchStageMorph', 'ScriptableScratchMorph', 'zoom,hPan,vPan,obsoleteSavedState,sprites,volume,tempoBPM,sceneStates,lists'); sb.addFields(140, 'ChoiceArgMorph', 'ArgMorph', 'isBoolean,options,choice,getOptionsSelector'); sb.addFields(141, 'ColorArgMorph', 'ArgMorph', ''); sb.addFields(142, 'ExpressionArgMorph', 'ArgMorph', 'isNumber'); sb.addFields(145, 'SpriteArgMorph', 'ArgMorph', 'morph'); sb.addFields(147, 'BlockMorph', 'Morph', 'isSpecialForm,oldColor'); sb.addFields(148, 'CommandBlockMorph', 'BlockMorph', 'commandSpec,argMorphs,titleMorph,receiver,selector,isReporter,isTimed,wantsName,wantsPossession'); sb.addFields(149, 'CBlockMorph', 'CommandBlockMorph', 'nestedBlock,nextBlock'); sb.addFields(151, 'HatBlockMorph', 'CommandBlockMorph', 'scriptNameMorph,indicatorMorph,scriptOwner,parameters,isClickable'); sb.addFields(153, 'ScratchScriptsMorph', 'PasteUpMorph', ''); sb.addFields(154, 'ScratchSliderMorph', 'AlignmentMorph', 'slider,sliderMin,sliderMax,variable'); sb.addFields(155, 'WatcherMorph', 'AlignmentMorph', 'titleMorph,readout,readoutFrame,scratchSlider,watcher,isSpriteSpecific,unused,sliderMin,sliderMax,isLarge'); sb.addFields(157, 'SetterBlockMorph', 'CommandBlockMorph', 'variable'); sb.addFields(158, 'EventHatMorph', 'HatBlockMorph', ''); sb.addFields(170, 'ReporterBlockMorph', 'CommandBlockMorph', 'isBoolean'); sb.addFields(160, 'VariableBlockMorph', 'ReporterBlockMorph', ''); sb.addFields(162, 'ImageMedia', 'ScratchMedia', 'form,rotationCenter,textBox,jpegBytes,compositeForm'); sb.addFields(163, 'MovieMedia', 'ScratchMedia', 'fileName,fade,fadeColor,zoom,hPan,vPan,msecsPerFrame,currentFrame,moviePlaying'); sb.addFields(164, 'SoundMedia', 'ScratchMedia', 'originalSound,volume,balance,compressedSampleRate,compressedBitsPerSample,compressedData'); sb.addFields(165, 'KeyEventHatMorph', 'HatBlockMorph', ''); sb.addFields(166, 'BooleanArgMorph', 'ArgMorph', ''); sb.addFields(167, 'EventTitleMorph', 'ArgMorph', ''); sb.addFields(168, 'MouseClickEventHatMorph', 'HatBlockMorph', ''); sb.addFields(169, 'ExpressionArgMorphWithMenu', 'ExpressionArgMorph', 'menuMorph,getMenuSelector,specialValue'); sb.addFields(171, 'MultilineStringMorph', 'BorderedMorph', 'fontSpec,textColor,selectionColor,lines'); sb.addFields(172, 'ToggleButton', 'SimpleButtonMorph', 'onForm,offForm,overForm,disabledForm,isMomentary,toggleMode,isOn,isDisabled'); sb.addFields(173, 'WatcherReadoutFrameMorph', 'BorderedMorph', ''); sb.addFields(174, 'WatcherSliderMorph', 'SimpleSliderMorph', ''); sb.addFields(175, 'ScratchListMorph', 'BorderedMorph', 'listName,strings,target,complex'); sb.addFields(176, 'ScrollingStringMorph', 'BorderedMorph', 'fontSpec,showScrollbar,firstVisibleLine,textColor,selectionColor,lines'); sb.addFields(180, 'ScrollFrameMorph2', 'ScrollFrameMorph', ''); sb.addFields(181, 'ListMultilineStringMorph', 'MultilineStringMorph', ''); sb.addFields(182, 'ScratchScrollBar', 'Morph', ''); sb.addFields(200, 'CustomCommandBlockMorph', 'CommandBlockMorph', 'userSpec'); sb.addFields(201, 'CustomBlockDefinition', '', 'userSpec,blockVars,isAtomic,isReporter,isBoolean,body,answer,type,category,declarations,defaults,isGlobal'); sb.addFields(203, 'ReporterScriptBlockMorph', 'ReporterBlockMorph', ''); sb.addFields(202, 'CommandScriptBlockMorph', 'ReporterScriptBlockMorph', ''); sb.addFields(205, 'VariableFrame', '', 'vars'); sb.addFields(206, 'CustomReporterBlockMorph', 'ReporterBlockMorph', 'userSpec'); sb.addFields(207, 'CReporterSlotMorph', 'ReporterScriptBlockMorph', ''); sb.addFields(300, 'StringFieldMorph', 'BorderedMorph', ''); sb.addFields(301, 'MultiArgReporterBlockMorph', 'ReporterBlockMorph', ''); (function (C, p) { p.on = function (bytes) { this.bytes = bytes; if (!bytes.subarray) bytes.subarray = bytes.slice; this.position = 0; return this; }; p.readYPR = function (bytes) { var version, infoSize, info, stage; console.time('readSB'); this.on(bytes); // skip header this.matchBytes([66, 108, 111, 120, 69, 120, 112, 86]); version = +(String.fromCharCode(this.next()) + String.fromCharCode(this.next())); if (version < 1) { throw new Error('Invalid version'); } // read info infoSize = this.uint32(); info = this.read(); this.position = infoSize + 14; // header + uint32 stage = this.read(); this.onload({ reader: this, info: info, stage: stage }); console.timeEnd('readSB'); }; p.readInfo = function (bytes) { var version, infoSize, info; this.on(bytes); this.matchBytes([66, 108, 111, 120, 69, 120, 112, 86]); version = +(String.fromCharCode(this.next()) + String.fromCharCode(this.next())); if (version < 1) { throw new Error('Invalid version'); } // read info infoSize = this.uint32(); info = this.read(); this.onload({ reader: this, info: info }); }; p.read = function () { var i, objectCount; this.objects = []; this.readHeader(); objectCount = this.uint32(); i = objectCount; while (i--) { this.objects.push(this.readObject()); } i = objectCount; while (i--) { this.fixReferences(this.objects[i]); } return this.objects[0][1]; }; p.fixReferences = function (object) { var classID = object[0], value = object[1], i = 0, fields, source; if (classID < 99) { this.fixFixedFormat(classID, value); } else { this.fixArray(object[3]); fields = sb.fields[classID]; if (!fields) throw new Error('Invalid class ID ' + classID); source = value.fields; delete value.fields; value.className = sb.classNames[classID]; i = fields.length; while (i--) { value[fields[i]] = source[i]; } } }; p.fixArray = function (a) { var i = a.length, o; while (i--) { if ((o = a[i]) && o.isRef) { if (o.id > this.objects.length) { throw new Error('Invalid object reference'); } a[i] = this.objects[o.id - 1][1]; } } }; p.targetObjectFor = function (o) { if (o && o.isRef) { if (o.id > this.objects.length) throw new Error('Invalid object reference'); return this.objects[o.id - 1][1]; } return o; }; p.fixFixedFormat = function (classID, object) { switch (classID) { case 20: // Array case 21: // OrderedCollection case 22: // Set case 23: // IdentitySet this.fixArray(object); return object; case 24: // Dictionary case 25: // IdentityDictionary this.fixArray(object.keys); this.fixArray(object.values); break; case 32: // Point object.x = this.targetObjectFor(object.x); object.y = this.targetObjectFor(object.y); break; case 33: // Rectangle object.origin.x = this.targetObjectFor(object.origin.x); object.origin.y = this.targetObjectFor(object.origin.y); object.corner.x = this.targetObjectFor(object.corner.x); object.corner.y = this.targetObjectFor(object.corner.y); break; case 34: // Form object.offset = this.targetObjectFor(object.offset); object.bits = this.targetObjectFor(object.bits); object.load(); break; case 35: // ColorForm object.offset = this.targetObjectFor(object.offset); object.bits = this.targetObjectFor(object.bits); object.colors = this.targetObjectFor(object.colors); object.load(); break; } return object; }; p.readObject = function () { var classID = this.next(), version, fieldCount, fields; if (classID > 99) { version = this.next(); fieldCount = this.next(); fields = []; while (fieldCount--) { fields.push(this.readField()); } return [classID, { fields: fields }, version, fields]; } return [classID, this.readFixedFormat(classID)]; }; p.readField = function () { var classID = this.next(); if (classID === 99) { return new sb.Ref(this.uint24()); } return this.readFixedFormat(classID); }; p.readFixedFormat = function (classID) { var a, n; switch (classID) { case 1: // UndefinedObject return null; case 2: // True return true; case 3: // False return false; case 4: // SmallInteger return this.int32(); case 5: // SmallInteger16 return this.int16(); case 6: // LargePositiveInteger case 7: // LargeNegativeInteger n = this.uint16(); a = 0; while (n--) { a *= 0x100; a += this.next(); } return a; case 8: // Float return this.float64(); case 9: // String case 10: // Symbol case 14: // UTF8 a = [].slice.call(this.readBytes(n = this.uint32())); while (n--) { a[n] = String.fromCharCode(a[n]); } return a.join(''); case 11: // ByteArray return this.readBytes(this.uint32()); case 12: // SoundBuffer a = []; n = this.uint32(); while (n--) { a.push(this.int16()); } return a; case 13: // Bitmap a = []; a.isBitmap = true; n = this.uint32(); while (n--) { a.push(this.uint32()); } return a; case 20: // Array case 21: // OrderedCollection case 22: // Set case 23: // IdentitySet a = []; n = this.uint32(); while (n--) { a.push(this.readField()); } return a; case 24: // Dictionary case 25: // IdentityDictionary a = new sb.Dictionary([], []); n = this.uint32(); while (n--) { a.keys.push(this.readField()); a.values.push(this.readField()); } return a; case 30: // Color return new sb.Color(this.uint32(), 255); case 31: // TranslucentColor return new sb.Color(this.uint32(), this.uint8()); case 32: // Point return new sb.Point(this.readField(), this.readField()); case 33: // Rectangle return new sb.Rectangle(this.readField(), this.readField(), this.readField(), this.readField()); case 34: // Form return new sb.Form(this.readField(), this.readField(), this.readField(), this.readField(), this.readField()); case 35: // ColorForm return new sb.ColorForm(this.readField(), this.readField(), this.readField(), this.readField(), this.readField(), this.readField()); } throw new Error('Invalid fixed-format class ID'); }; p.readHeader = function () { this.matchBytes([79, 98, 106, 83, 1, 83, 116, 99, 104, 1]); }; p.readBytes = function (length) { return this.bytes.subarray(this.position, this.position += length); }; p.matchBytes = function (bytes) { var i = bytes.length, r = this.readBytes(i); while (i--) { if (r[i] !== bytes[i]) { throw new Error('Invalid format'); } } }; p.skip = function (length) { this.position += length; }; p.next = p.uint8 = function () { return this.bytes[this.position++]; }; p.hasNext = function () { return this.position < this.bytes.length; }; p.uint16 = function () { return this.next() * 0x100 + this.next(); }; p.uint24 = function () { return this.next() * 0x10000 + this.next() * 0x100 + this.next(); }; p.uint32 = function () { return this.next() * 0x1000000 + this.next() * 0x10000 + this.next() * 0x100 + this.next(); }; p.int8 = function () { var v = this.bytes[++this.position]; return v >= 0x80 ? v - 0x100 : v; }; p.int16 = function () { var d = this.next(), v = d * 0x100 + this.next(); return d >= 0x80 ? v - 0x10000 : v; }; p.int24 = function () { var d = this.next(), v = d * 0x10000 + this.next() * 0x100 + this.next(); return d >= 0x80 ? v - 0x1000000 : v; }; p.int32 = function () { var d = this.next(), v = d * 0x1000000 + this.next() * 0x10000 + this.next() * 0x100 + this.next(); return d >= 0x80 ? v - 0x100000000 : v; }; p.string = function () { var length = this.uint16(), bytes = this.readBytes(length), i = length; while (i--) { bytes[i] = String.fromCharCode(bytes[i]); } return bytes.join(''); }; p.float64 = function () { return this.ieee(8, 11, 52, 1023); }; p.ieee = function (n, ebits, mbits) { var bias = (1 << (ebits - 1)) - 1, string = '', i = n, b, sign, exponent, mantissa, result; while (i--) { b = this.next().toString(2); string = string + Array(9 - b.length).join('0') + b; } sign = string.charAt(0) === '0' ? 1 : -1; exponent = parseInt(string.substr(1, ebits), 2); mantissa = parseInt(string.substr(ebits + 1), 2); if (exponent === 0) { return mantissa === 0 ? sign * 0 : sign * Math.pow(2, 1 - bias) * mantissa / Math.pow(2, mbits); } if (exponent === (1 << ebits) - 1) { return mantissa === 0 ? sign / 0 : NaN; } return sign * Math.pow(2, exponent - bias) * (1 + mantissa / Math.pow(2, mbits)); }; })(sb.Reader = function () {}, sb.Reader.prototype); (function (C, p) { var rotationStyles = { normal: 1, leftRight: 2, none: 0 }, customBlockInputs = { object: '%obj', objectList: '%mult%obj', number: '%n', numberList: '%mult%n', text: '%txt', textList: '%mult%txt', list: '%l', listList: '%mult%l', any: '%s', anyList: '%mult%s', 'boolean': '%b', booleanList: '%mult%b', command: '%cmdRing', commandList: '%mult%cmdRing', reporter: '%repRing', reporterList: '%mult%repRing', predicate: '%predRing', predicateList: '%mult%predRing', loop: '%cs', loopList: '%mult%cs', unevaluated: '%anyUE', unevaluatedList: '%mult%anyUE', unevaluatedBoolean: '%boolUE', unevaluatedBooleanList: '%mult%boolUE', template: '%upvar' }, blockSelectors = { // Motion': '', 'forward:': 'forward', 'turnLeft:': 'turnLeft', 'turnRight:': 'turn', 'heading:': 'setHeading', 'pointTowards:': 'doFaceTowards', 'gotoX:y:': 'gotoXY', 'gotoSpriteOrMouse:': 'doGotoObject', 'glideSecs:toX:y:elapsed:from:': 'doGlide', 'changeXposBy:': 'changeXPosition', 'xpos:': 'setXPosition', 'changeYposBy:': 'changeYPosition', 'ypos:': 'setYPosition', 'bounceOffEdge': 'bounceOffEdge', 'xpos': 'xPosition', 'ypos': 'yPosition', 'heading': 'direction', // Looks 'lookLike:': 'doSwitchToCostume', 'showBackground:': 'doSwitchToCostume', 'nextCostume': 'doWearNextCostume', 'nextBackground': 'doWearNextCostume', 'costumeIndex': 'getCostumeIdx', 'say:duration:elapsed:from:': 'doSayFor', 'say:': 'bubble', 'think:duration:elapsed:from:': 'doThinkFor', 'think:': 'doThink', 'changeGraphicEffect:by:': 'changeEffect', 'setGraphicEffect:to:': 'setEffect', 'filterReset': 'clearEffects', 'setSizeTo:': 'setScale', 'changeSizeBy:': 'changeScale', 'scale': 'getScale', 'show': 'show', 'hide': 'hide', 'comeToFront': 'comeToFront', 'goBackByLayers:': 'goBack', // Sound 'playSound:': 'playSound', 'doPlaySoundAndWait': 'doPlaySoundUntilDone', 'stopAllSounds': 'doStopAllSounds', // 'drum:duration:elapsed:from:': '', 'rest:elapsed:from:': 'doRest', 'noteOn:duration:elapsed:from:': 'doPlayNote', // 'midiInstrument:': '', // 'changeVolumeBy:': '', // 'setVolumeTo:': '', // 'volume': '', 'changeTempoBy:': 'doChangeTempo', 'setTempoTo:': 'doSetTempo', 'tempo': 'getTempo', // Pen 'clearPenTrails': 'clear', 'putPenDown': 'down', 'putPenUp': 'up', 'penColor:': 'setColor', 'changePenHueBy:': 'changeHue', 'setPenHueTo:': 'setHue', '_changePenHueBy:': 'changeHue', '_setPenHueTo:': 'setHue', 'changePenShadeBy:': 'changeBrightness', 'setPenShadeTo:': 'setBrightness', 'changePenSizeBy:': 'changeSize', 'penSize:': 'setSize', 'stampCostume': 'doStamp', // Control // 'whenStartClicked': '', // 'whenKeyPressed:': '', // 'whenSpriteClicked': '', 'wait:elapsed:from:': 'doWait', 'doForever': 'doForever', 'doRepeat': 'doRepeat', 'broadcast:': 'doBroadcast', 'doBroadcastAndWait': 'doBroadcastAndWait', // 'whenMessageReceived:': '', // TODO 'doForeverIf': '', 'doIf': 'doIf', 'doIfElse': 'doIfElse', 'doWaitUntil': 'doWaitUntil', 'doUntil': 'doUntil', 'doReturn': 'doStop', 'stopAll': 'doStopAll', 'doRun': 'doRun', 'doRunBlockWithArgs': 'doRun', 'doRunBlockWithArgList': 'doRun', 'doFork': 'fork', 'doForkBlockWithArgs': 'fork', 'doForkBlockWithArgList': 'fork', 'doReport': 'evaluate', 'doCallBlockWithArgs': 'evaluate', 'doCallBlockWithArgList': 'evaluate', 'doAnswer': 'doReport', 'doStopBlock': 'doStopBlock', // 'doPauseThread': '', // 'doPauseThreadReporter': '', // Sensing 'touching:': 'reportTouchingObject', 'touchingColor:': 'reportTouchingColor', 'color:sees:': 'reportColorIsTouchingColor', 'doAsk': 'doAsk', 'answer': 'reportLastAnswer', 'mouseX': 'reportMouseX', 'mouseY': 'reportMouseY', 'mousePressed': 'reportMouseDown', 'keyPressed:': 'reportKeyPressed', 'distanceTo:': 'reportDistanceTo', 'timerReset': 'doResetTimer', 'timer': 'reportTimer', // 'getAttribute:of:': '', // 'attribute:of:': '', // 'soundLevel': '', // 'isLoud': '', // 'sensor:': '', // 'sensorPressed:': '', // 'getObject:': '', // 'get:': '', // Operators '+': 'reportSum', '-': 'reportDifference', '*': 'reportProduct', '/': 'reportQuotient', 'randomFrom:to:': 'reportRandom', '<': 'reportLessThan', '=': 'reportEquals', '>': 'reportGreaterThan', '&': 'reportAnd', '|': 'reportOr', 'not': 'reportNot', 'getTrue': 'reportTrue', 'getFalse': 'reportFalse', 'concatenate:with:': 'reportJoinWords', 'letter:of:': 'reportLetter', 'stringLength:': 'reportStringSize', 'asciiCodeOf:': 'reportUnicode', 'asciiLetter:': 'reportUnicodeAsLetter', '\\\\': 'reportModulus', 'rounded': 'reportRound', 'computeFunction:of:': 'reportMonadic', 'isObject:type:': 'reportIsA', // 'procedure': 'reifyScript', // 'procedureWithArgs': 'reifyScript', // 'function': 'reifyReporter', // 'functionWithArgs': 'reifyReporter', // 'spawn': '', // Variables 'setVar:to:': 'doSetVar', 'changeVar:by:': 'doChangeVar', // 'deleteObject:': '', 'showVariable:': 'doShowVar', 'hideVariable:': 'doHideVar', 'doDeclareVariables': 'doDeclareVariables', 'newList:': 'reportNewList', 'append:toList:': 'doAddToList', 'deleteLine:ofList:': 'doDeleteFromList', 'insert:at:ofList:': 'doInsertInList', 'setLine:ofList:to:': 'doReplaceInList', 'getLine:ofList:': 'reportListItem', 'lineCountOfList:': 'reportListLength', 'list:contains:': 'reportListContainsItem', // 'contentsOfList:': '', // 'copyOfList:': '' // Kludge _warp: 'doWarp' }; function escapeXML(string) { // over-cautious due to some morphic bugs return string.replace(/&/g, '&')/*.replace(/[\n\r]|\r\n/g, ' ')*/.replace(/'); this.children.forEach(function (child) { if (!child.toXML) { console.error('Invalid', child); } child.toXML(string); }); string.push(''); } else { string.push('/>'); } }; function n(tagName, attributes, children) { var el = new Element(tagName), key; el.attributes = attributes || {}; el.children = children || []; return el; } function t(value) { return new Text(value); } p.write = function (projectName, project) { var a, xml, out, objectID = 0, customBlocks = {}; function id(n) { return n.attributes.id ? n('ref', { id: n.attributes.id }) : n.attributes.id = ++objectID, n; } function preCustomBlocks(scope, blocks) { var sc = customBlocks[scope] = {}; if (!blocks) return; blocks.forEach(function (ypr) { sc[ypr.userSpec] = ypr; ypr.userSpec = ypr.userSpec.replace(/^\s+|\s+$/g, '').replace(/\s{2,}/g, ' ').replace(/'/g, ''); ypr.__blockSpec__ = ypr.userSpec.replace(/%(\S+)/g, function (_, name) { var type = customBlockInputs[ypr.declarations.get(name)]; if (type === undefined) { if (ypr.declarations.get(name) === undefined) { type = '%s'; } else { console.warn('Missing input type ' + ypr.declarations.get(name)); } } return type; }); ypr.__types__ = (ypr.userSpec.match(/%\S+/g) || []).map(function (arg) { return ypr.declarations.get(arg.substr(1)) || 'any'; }); }); } function nsCustomBlocks(blocks, globals) { if (!blocks) return []; return (globals ? blocks : blocks.filter(function (b) { return !b.isGlobal })).map(function (ypr) { var args = (ypr.userSpec.match(/%\S+/g) || []).map(function (arg) { var name = arg.substr(1), type = customBlockInputs[ypr.declarations.get(name)]; if (type === undefined) { type = '%s'; } return n('input', { type: type }, [t(ypr.defaults.get(name))]); }), spec = ypr.userSpec.replace(/%(\S+)/g, '%\'$1\''); ypr.body = ypr.body || []; if (ypr.answer && ypr.answer.pop) { ypr.body.push(['byob', '', 'doAnswer', ypr.answer[0]]); } if (ypr.blockVars.length) { ypr.body.unshift(['byob', '', 'doDeclareVariables'].concat(ypr.blockVars.map(function (n) { return [0, 0, 0, n]; }))); } if (ypr.isAtomic) { ypr.body = [['byob', '', '_warp', ypr.body]]; } return n('block-definition', { s: spec, // type: ypr.isBoolean ? 'predicate' : ypr.isReporter ? 'reporter' : 'command', type: ypr.type === 'boolean' ? 'predicate' : ypr.answer !== null ? 'reporter' : 'command', category: ypr.category === 'none' ? 'other' : ypr.category === 'list' ? 'lists' : ypr.category }, [ n('inputs', {}, args), n('script', {}, nsScript(ypr.body)) ]); }); } function nsScript(script) { return script.map(function (block) { return nBlock(block); }); } function nBlock(block) { var node, a; node = n('block'); switch (block[2]) { case 'EventHatMorph': if (block[3] === 'Scratch-StartClicked') { node.attributes.s = 'receiveGo'; } else { node.attributes.s = 'receiveMessage'; node.children.push(n('l', {}, [t(block[3])])); } return node; case 'MouseClickEventHatMorph': node.attributes.s = 'receiveClick'; return node; case 'KeyEventHatMorph': node.attributes.s = 'receiveKey'; node.children.push(n('l', {}, [t(block[3])])); return node; case 'doRunBlockWithArgs': case 'doCallBlockWithArgs': case 'doForkBlockWithArgs': a = block.length - 5; while (a--) { if (block[5 + a].pop) { block[5 + a] = ['block', '', [block[5 + a]]]; } } a = { className: 'ScratchListMorph', complex: block.slice(5) }; block = block.slice(0, 4); block.push(a); break; case 'doRunBlockWithArgList': case 'doCallBlockWithArgList': case 'doForkBlockWithArgList': a = block[5]; block = block.slice(0, 4); block.push(a); break; case 'doDeclareVariables': block = ['byob', '', 'doDeclareVariables', { className: 'ScratchListMorph', complex: block.slice(3).map(function (block) { return block[3]; }) }]; break; case 'getLine:ofList:': case 'append:toList:': case 'deleteLine:ofList:': case 'setLine:ofList:to:': case 'getLine:ofList:': case 'insert:at:ofList:': case 'lineCountOfList:': case 'list:contains:': a = block[2] === 'insert:at:ofList:' ? 5 : block[2] === 'lineCountOfList:' || block[2] === 'list:contains:' ? 3 : 4; if (typeof block[a] === 'string') { block[a] = block[a] === '' ? null : ['byob', '', 'readVariable', block[a]]; } break; case 'readBlockVariable': case 'readVariable': case 'listNamed:': node.attributes['var'] = block[3]; return node; case 'function': case 'functionWithArgs': node.attributes.s = 'reifyReporter'; block.pop(); node.children.push(n('autolambda', {}, [nBlock(block[8])])); if (block[2] === 'functionWithArgs') { node.children.push(n('list', {}, block.slice(9).map(function (arg) { return n('l', {}, [t(arg[3])]); }))); } return node; case 'procedure': case 'procedureWithArgs': node.attributes.s = 'reifyScript'; node.children.push(n('script', {}, nsScript(block.pop()))); node.children.push(n('list', {}, block.slice(8).map(function (arg) { return n('l', {}, [t(arg[3])]); }))); return node; case 'autoBlock': case 'autoPredicate': node.attributes.s = block[2] === 'autoPredicate' ? 'reifyPredicate' : 'reifyReporter'; node.children.push(n('autolambda', {}, block[3] ? [nBlock(block[3][0])] : [])); return node; case 'concatenate:with:': block = block.slice(0, 3).concat({ className: 'ScratchListMorph', complex: block.slice(3) }); a = 2; while (a--) { if (block[3].complex[a].pop) { block[3].complex[a] = ['block', '', [block[3].complex[a]]]; } } break; case 'loopLambda': return n('script', {}, block[8] ? nsScript(block[8]) : []); case 'autoLambda': // node.attributes.s = block[2] === 'loopLambda' ? 'reifyScript' : 'reifyReporter'; node.attributes.s = 'reifyScript'; node.children.push(n('script', {}, block[8] ? nsScript(block[8]) : [])); return node; case 'changeVariable': case 'changeBlockVariable': block = [0, 0, block[4], block[3], block[block[2] === 'changeBlockVariable' ? 6 : 5]]; break; case 'distanceTo:': case 'gotoSpriteOrMouse:': case 'pointTowards:': case 'touching:': if (block[3] === 'mouse') { block[3] = 'mouse-pointer'; } else { block[3] = block[3].objName; } break; case 'doForeverIf': block = [0, 0, 'doForever', [[0, 0, 'doIf', block[3], block[4]]]] break; case '\\\\': case '+': case '-': case '*': case '/': if (block[3] === ' ') { block[3] = ''; } if (block[4] === ' ') { block[4] = ''; } break; case 'setPenHueTo:': case 'changePenHueBy:': block[3] = ['byob', 0, '/', block[3], '2']; break; } if (block[2] === 'doCustomBlock') { a = customBlocks[block[1] === 'Stage' ? '__global__' : block[1]][block[3]]; node = n('custom-block', { s: a.__blockSpec__ }); if (block[1] !== 'Stage' && !a.isGlobal) { node.attributes.scope = block[1]; } a.__types__.forEach(function (type, i) { if (type === 'template') { block[4 + i] = block[4 + i][3]; } }); block.shift(); } else { if ((node.attributes.s = blockSelectors[block[2]]) === undefined) { console.warn('Missing selector mapping for "' + block[2] + '"', block); } } block.slice(3).forEach(function (arg) { if (arg && arg.pop) { if (typeof arg[0] === 'string') { node.children.push(nBlock(arg)); } else { node.children.push(n('script', {}, nsScript(arg))); } } else { node.children.push(nValue(arg)); } }); return node; } function nsVariables(vars, lists) { return vars.keys.map(function (key, i) { return n('variable', { name: key }, [nValue(vars.values[i], true)]); }).concat(lists.keys.map(function (key, i) { return n('variable', { name: key }, [nValue(lists.values[i], true)]); })); } function nValue(value, listHasItems) { return typeof value === 'string' || typeof value === 'number' ? n('l', {}, [t(value)]) : value == null || typeof value === 'boolean' ? n('l') : value.className === 'ScratchListMorph' ? id(n('list', {}, value.complex.map(function (object, i) { var item = object == null ? n('l', {}, [t((item = value.strings[i]) === 'nil' ? '' : item)]) : typeof object === 'string' ? nValue(object) : object && object[0] === 'block' ? nsScript(object[2])[0] : n('l'); return listHasItems ? n('item', {}, [item]) : item; }))) : // (function () { throw 'Unimplemented' })(); value.constructor === sb.Color ? n('color', {}, [t([value.r * 255, value.g * 255, value.b * 255, value.a * 255])]) : value.constructor === sb.Dictionary ? id(n('list')) : (function () { console.warn('No serializer for', value); return n('__undefined__') })(); } function nsCostumes(media) { return [id(n('list', {}, media.filter(function (m) { return m.className === 'ImageMedia' }).map(function (c) { return n('item', {}, [ n('costume', { name: c.mediaName, 'center-x': c.rotationCenter.x, 'center-y': c.rotationCenter.y, image: c.form.image.toDataURL() }) ]); })))]; } function nsSounds(media) { if (media.filter(function (m) { return m.className === 'SoundMedia' }).length > 0) { console.warn('Sounds are unimplemented'); } return [id(n('list'))]; } function nsScripts(scripts) { return scripts.map(function (script) { if (script[1][0] && script[1][0][2] === 'scratchComment') { return n('comment', { x: script[0].x, y: script[0].y, w: script[1][0][5], collapsed: !script[1][0][4] }, [t(script[1][0][3])]); } return n('script', { x: script[0].x, y: script[0].y }, nsScript(script[1])); }); } function nsSprites(sprites, vars, lists) { return sprites.map(function (sprite) { return id(n('sprite', { name: sprite.objName, x: (sprite.bounds.origin.x + sprite.bounds.corner.x) / 2 - 240, y: 180 - (sprite.bounds.origin.y + sprite.bounds.corner.y) / 2, heading: sprite.rotationDegrees + 90, scale: sprite.scalePoint.x, rotation: rotationStyles[sprite.rotationStyle], draggable: sprite.draggable, costume: costumeIndex(sprite), color: (sprite.color.r * 255 | 0) + ',' + (sprite.color.g * 255 | 0) + ',' + (sprite.color.b * 255 | 0) }, [ n('variables', {}, nsVariables(sprite.vars, sprite.lists)), n('costumes', {}, nsCostumes(sprite.media)), n('sounds', {}, nsSounds(sprite.media)), n('blocks', {}, nsCustomBlocks(sprite.customBlocks, false)), n('scripts', {}, nsScripts(sprite.blocksBin)) ])); }).concat(vars.keys.concat(lists.keys).map(function (name) { return n('watcher', { 'var': name, style: 'normal', x: 10, y: 10, color: '243,118,29', hidden: true }); })); } preCustomBlocks('__global__', project.stage.customBlocks); project.stage.sprites.forEach(function (sprite) { preCustomBlocks(sprite.objName, sprite.customBlocks); }); xml = n('project', { name: projectName, app: 'Snap! 4.0, http://snap.berkeley.edu; serialized by yprxml, http://dl.dropbox.com/u/10715865/Web/sb/yprxml.html', version: '1' }, [ n('notes', {}, [t(project.info.get('comment') || '')]), n('thumbnail', {}, [t((a = project.info.get('thumbnail')) ? a.image.toDataURL() : '')]), id(n('stage', { costume: costumeIndex(project.stage), threadsafe: 'false' }, [ n('pentrails', {}, [t((a = project.info.get('penTrails')) ? a.image.toDataURL() : '')]), n('costumes', {}, nsCostumes(project.stage.media)), n('sounds', {}, nsSounds(project.stage.media)), n('blocks', {}, []), n('variables', {}, []), n('scripts', {}, nsScripts(project.stage.blocksBin)), n('sprites', {}, nsSprites(project.stage.sprites, project.stage.vars, project.stage.lists)) ])), n('variables', {}, nsVariables(project.stage.vars, project.stage.lists)), n('blocks', {}, nsCustomBlocks(project.stage.customBlocks, true)) ]); xml.toXML(out = []); return out.join(''); }; })(sb.XMLWriter = function () {}, sb.XMLWriter.prototype); return sb; })({});