UNPKG

scratch-sb1-converter

Version:

Scratch 1 (.sb) to Scratch 2 (.sb2) conversion library for Scratch 3.0

519 lines (443 loc) 10.9 kB
import {CRC32} from '../coders/crc32'; import {SqueakImage} from '../coders/squeak-image'; import {SqueakSound} from '../coders/squeak-sound'; import {WAVFile} from '../coders/wav-file'; import {FieldObject} from './field-object'; import {value as valueOf} from './fields'; import {TYPES} from './ids'; import md5 from 'js-md5'; /** * @extends FieldObject */ class PointData extends FieldObject.define({ /** * @memberof PointData# * @type {Value} */ X: 0, /** * @memberof PointData# * @type {Value} */ Y: 1 }) {} export {PointData}; class RectangleData extends FieldObject.define({ X: 0, Y: 1, X2: 2, Y2: 3 }) { get width () { return this.x2 - this.x; } get height () { return this.y2 - this.y; } } export {RectangleData}; const _bgra2rgbaInPlace = uint8a => { for (let i = 0; i < uint8a.length; i += 4) { const r = uint8a[i + 2]; const b = uint8a[i + 0]; uint8a[i + 2] = b; uint8a[i + 0] = r; } return uint8a; }; /** * @extends FieldObject */ class ImageData extends FieldObject.define({ /** * @memberof ImageData# * @type {Value} */ WIDTH: 0, /** * @memberof ImageData# * @type {Value} */ HEIGHT: 1, /** * @memberof ImageData# * @type {Value} */ DEPTH: 2, /** * @memberof ImageData# * @type {Value} */ BYTES: 4, /** * @memberof ImageData# * @type {Value} */ COLORMAP: 5 }) { /** * @type {Uint8Array} */ get decoded () { if (!this._decoded) { this._decoded = _bgra2rgbaInPlace(new Uint8Array( new SqueakImage().decode( this.width.value, this.height.value, this.depth.value, this.bytes.value, this.colormap && this.colormap.map(color => color.valueOf()) ).buffer )); } return this._decoded; } /** * @type {string} */ get extension () { return 'uncompressed'; } } export {ImageData}; class StageData extends FieldObject.define({ STAGE_CONTENTS: 2, OBJ_NAME: 6, VARS: 7, BLOCKS_BIN: 8, IS_CLONE: 9, MEDIA: 10, CURRENT_COSTUME: 11, ZOOM: 12, H_PAN: 13, V_PAN: 14, OBSOLETE_SAVED_STATE: 15, SPRITE_ORDER_IN_LIBRARY: 16, VOLUME: 17, TEMPO_BPM: 18, SCENE_STATES: 19, LISTS: 20 }) { get spriteOrderInLibrary () { return this.fields[this.FIELDS.SPRITE_ORDER_IN_LIBRARY] || null; } get tempoBPM () { return this.fields[this.FIELDS.TEMPO_BPM] || 0; } get lists () { return this.fields[this.FIELDS.LISTS] || []; } } export {StageData}; class SpriteData extends FieldObject.define({ BOX: 0, PARENT: 1, COLOR: 3, VISIBLE: 4, OBJ_NAME: 6, VARS: 7, BLOCKS_BIN: 8, IS_CLONE: 9, MEDIA: 10, CURRENT_COSTUME: 11, VISIBILITY: 12, SCALE_POINT: 13, ROTATION_DEGREES: 14, ROTATION_STYLE: 15, VOLUME: 16, TEMPO_BPM: 17, DRAGGABLE: 18, SCENE_STATES: 19, LISTS: 20 }) { get scratchX () { return this.box.x + this.currentCostume.rotationCenter.x - 240; } get scratchY () { return 180 - (this.box.y + this.currentCostume.rotationCenter.y); } get visible () { return (this.fields[this.FIELDS.VISIBLE] & 1) === 0; } get tempoBPM () { return this.fields[this.FIELDS.TEMPO_BPM] || 0; } get lists () { return this.fields[this.FIELDS.LISTS] || []; } } export {SpriteData}; class TextDetailsData extends FieldObject.define({ RECTANGLE: 0, FONT: 8, COLOR: 9, LINES: 11 }) {} export {TextDetailsData}; class ImageMediaData extends FieldObject.define({ COSTUME_NAME: 0, BITMAP: 1, ROTATION_CENTER: 2, TEXT_DETAILS: 3, BASE_LAYER_DATA: 4, OLD_COMPOSITE: 5 }) { get image () { if (this.oldComposite instanceof ImageData) { return this.oldComposite; } if (this.baseLayerData.value) { return null; } return this.bitmap; } get width () { if (this.image === null) { return -1; } return this.image.width; } get height () { if (this.image === null) { return -1; } return this.image.height; } get rawBytes () { if (this.image === null) { return this.baseLayerData.value.slice(); } return this.image.bytes.value; } get decoded () { if (this.image === null) { return this.baseLayerData.value.slice(); } return this.image.decoded; } get crc () { if (!this._crc) { const crc = new CRC32() .update(new Uint8Array(new Uint32Array([this.bitmap.width]).buffer)) .update(new Uint8Array(new Uint32Array([this.bitmap.height]).buffer)) .update(new Uint8Array(new Uint32Array([this.bitmap.depth]).buffer)) .update(this.rawBytes); this._crc = crc.digest; } return this._crc; } get extension () { if (this.oldComposite instanceof ImageData) return 'uncompressed'; if (this.baseLayerData.value) return 'jpg'; return 'uncompressed'; } toString () { return `ImageMediaData "${this.costumeName}"`; } } export {ImageMediaData}; class UncompressedData extends FieldObject.define({ DATA: 3, RATE: 4 }) {} export {UncompressedData}; const reverseBytes16 = input => { const uint8a = new Uint8Array(input); for (let i = 0; i < uint8a.length; i += 2) { uint8a[i] = input[i + 1]; uint8a[i + 1] = input[i]; } return uint8a; }; class SoundMediaData extends FieldObject.define({ NAME: 0, UNCOMPRESSED: 1, RATE: 4, BITS_PER_SAMPLE: 5, DATA: 6 }) { get rate () { if (this.uncompressed.data.value.length !== 0) { return this.uncompressed.rate; } return this.fields[this.FIELDS.RATE]; } get rawBytes () { if (this.data && this.data.value) { return this.data.value; } return this.uncompressed.data.value; } get decoded () { if (!this._decoded) { if (this.data && this.data.value) { this._decoded = new SqueakSound(this.bitsPerSample.value).decode( this.data.value ); } else { this._decoded = new Int16Array(reverseBytes16(this.uncompressed.data.value.slice()).buffer); } } return this._decoded; } get crc () { if (!this._crc) { this._crc = new CRC32() .update(new Uint32Array([this.rate])) .update(this.rawBytes) .digest; } return this._crc; } get sampleCount () { if (this.data && this.data.value) { return SqueakSound.samples(this.bitsPerSample.value, this.data.value); } return this.uncompressed.data.value.length / 2; } get extension () { return 'pcm'; } get wavEncodedData () { if (!this._wavEncodedData) { this._wavEncodedData = new Uint8Array(WAVFile.encode(this.decoded, { sampleRate: this.rate && this.rate.value })); } return this._wavEncodedData; } get md5 () { if (!this._md5) { this._md5 = md5(this.wavEncodedData); } return this._md5; } toString () { return `SoundMediaData "${this.name}"`; } } export {SoundMediaData}; class ListWatcherData extends FieldObject.define({ BOX: 0, HIDDEN_WHEN_NULL: 1, LIST_NAME: 8, CONTENTS: 9, TARGET: 10 }) { get x () { if (valueOf(this.hiddenWhenNull) === null) return 5; return this.box.x + 1; } get y () { if (valueOf(this.hiddenWhenNull) === null) return 5; return this.box.y + 1; } get width () { return this.box.width - 2; } get height () { return this.box.height - 2; } } export {ListWatcherData}; class AlignmentData extends FieldObject.define({ BOX: 0, PARENT: 1, FRAMES: 2, COLOR: 3, DIRECTION: 8, ALIGNMENT: 9 }) {} export {AlignmentData}; class MorphData extends FieldObject.define({ BOX: 0, PARENT: 1, COLOR: 3 }) {} export {MorphData}; class StaticStringData extends FieldObject.define({ BOX: 0, COLOR: 3, VALUE: 8 }) {} export {StaticStringData}; class UpdatingStringData extends FieldObject.define({ BOX: 0, READOUT_FRAME: 1, COLOR: 3, FONT: 6, VALUE: 8, TARGET: 10, CMD: 11, PARAM: 13 }) {} export {UpdatingStringData}; class WatcherReadoutFrameData extends FieldObject.define({ BOX: 0 }) {} export {WatcherReadoutFrameData}; const WATCHER_MODES = { NORMAL: 1, LARGE: 2, SLIDER: 3, TEXT: 4 }; export {WATCHER_MODES}; class WatcherData extends FieldObject.define({ BOX: 0, TARGET: 1, SHAPE: 2, READOUT: 14, READOUT_FRAME: 15, SLIDER: 16, ALIGNMENT: 17, SLIDER_MIN: 20, SLIDER_MAX: 21 }) { get x () { return this.box.x; } get y () { return this.box.y; } get mode () { if (valueOf(this.slider) === null) { if (this.readoutFrame.box.height <= 14) { return WATCHER_MODES.NORMAL; } return WATCHER_MODES.LARGE; } return WATCHER_MODES.SLIDER; } get isDiscrete () { return ( Math.floor(this.sliderMin) === this.sliderMin && Math.floor(this.sliderMax) === this.sliderMax && Math.floor(this.readout.value) === this.readout.value ); } } export {WatcherData}; const FIELD_OBJECT_CONTRUCTOR_PROTOS = { [TYPES.POINT]: PointData, [TYPES.RECTANGLE]: RectangleData, [TYPES.FORM]: ImageData, [TYPES.SQUEAK]: ImageData, [TYPES.SAMPLED_SOUND]: UncompressedData, [TYPES.SPRITE]: SpriteData, [TYPES.STAGE]: StageData, [TYPES.IMAGE_MEDIA]: ImageMediaData, [TYPES.SOUND_MEDIA]: SoundMediaData, [TYPES.ALIGNMENT]: AlignmentData, [TYPES.MORPH]: MorphData, [TYPES.WATCHER_READOUT_FRAME]: WatcherReadoutFrameData, [TYPES.STATIC_STRING]: StaticStringData, [TYPES.UPDATING_STRING]: UpdatingStringData, [TYPES.WATCHER]: WatcherData, [TYPES.LIST_WATCHER]: ListWatcherData }; const FIELD_OBJECT_CONTRUCTORS = Array.from( {length: 256}, (_, i) => (FIELD_OBJECT_CONTRUCTOR_PROTOS[i] || null) ); export {FIELD_OBJECT_CONTRUCTORS};