mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 06:23:37 -05:00
Merge pull request #676 from paulkaplan/duplicate
Add sprite duplication method
This commit is contained in:
commit
0f04c99e67
9 changed files with 176 additions and 67 deletions
|
@ -2,6 +2,7 @@ const adapter = require('./adapter');
|
||||||
const mutationAdapter = require('./mutation-adapter');
|
const mutationAdapter = require('./mutation-adapter');
|
||||||
const xmlEscape = require('../util/xml-escape');
|
const xmlEscape = require('../util/xml-escape');
|
||||||
const MonitorRecord = require('./monitor-record');
|
const MonitorRecord = require('./monitor-record');
|
||||||
|
const Clone = require('../util/clone');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @fileoverview
|
* @fileoverview
|
||||||
|
@ -178,6 +179,12 @@ class Blocks {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duplicate () {
|
||||||
|
const newBlocks = new Blocks();
|
||||||
|
newBlocks._blocks = Clone.simple(this._blocks);
|
||||||
|
newBlocks._scripts = Clone.simple(this._scripts);
|
||||||
|
return newBlocks;
|
||||||
|
}
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,59 @@
|
||||||
const StringUtil = require('../util/string-util');
|
const StringUtil = require('../util/string-util');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a costume from an asset asynchronously.
|
||||||
|
* Do not call this unless there is a renderer attached.
|
||||||
|
* @param {!object} costume - the Scratch costume object.
|
||||||
|
* @property {int} skinId - the ID of the costume's render skin, once installed.
|
||||||
|
* @property {number} rotationCenterX - the X component of the costume's origin.
|
||||||
|
* @property {number} rotationCenterY - the Y component of the costume's origin.
|
||||||
|
* @property {number} [bitmapResolution] - the resolution scale for a bitmap costume.
|
||||||
|
* @param {!Asset} costumeAsset - the asset of the costume loaded from storage.
|
||||||
|
* @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
|
||||||
|
* @returns {?Promise} - a promise which will resolve after skinId is set, or null on error.
|
||||||
|
*/
|
||||||
|
const loadCostumeFromAsset = function (costume, costumeAsset, runtime) {
|
||||||
|
costume.assetId = costumeAsset.assetId;
|
||||||
|
if (!runtime.renderer) {
|
||||||
|
log.error('No rendering module present; cannot load costume: ', costume.name);
|
||||||
|
return costume;
|
||||||
|
}
|
||||||
|
const AssetType = runtime.storage.AssetType;
|
||||||
|
const rotationCenter = [
|
||||||
|
costume.rotationCenterX / costume.bitmapResolution,
|
||||||
|
costume.rotationCenterY / costume.bitmapResolution
|
||||||
|
];
|
||||||
|
if (costumeAsset.assetType === AssetType.ImageVector) {
|
||||||
|
costume.skinId = runtime.renderer.createSVGSkin(costumeAsset.decodeText(), rotationCenter);
|
||||||
|
return costume;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const imageElement = new Image();
|
||||||
|
const onError = function () {
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
removeEventListeners();
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
const onLoad = function () {
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
removeEventListeners();
|
||||||
|
resolve(imageElement);
|
||||||
|
};
|
||||||
|
const removeEventListeners = function () {
|
||||||
|
imageElement.removeEventListener('error', onError);
|
||||||
|
imageElement.removeEventListener('load', onLoad);
|
||||||
|
};
|
||||||
|
imageElement.addEventListener('error', onError);
|
||||||
|
imageElement.addEventListener('load', onLoad);
|
||||||
|
imageElement.src = costumeAsset.encodeDataURI();
|
||||||
|
}).then(imageElement => {
|
||||||
|
costume.skinId = runtime.renderer.createBitmapSkin(imageElement, costume.bitmapResolution, rotationCenter);
|
||||||
|
return costume;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a costume's asset into memory asynchronously.
|
* Load a costume's asset into memory asynchronously.
|
||||||
* Do not call this unless there is a renderer attached.
|
* Do not call this unless there is a renderer attached.
|
||||||
|
@ -25,55 +78,13 @@ const loadCostume = function (md5ext, costume, runtime) {
|
||||||
const ext = idParts[1].toLowerCase();
|
const ext = idParts[1].toLowerCase();
|
||||||
const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap;
|
const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap;
|
||||||
|
|
||||||
const rotationCenter = [
|
return runtime.storage.load(assetType, md5, ext).then(costumeAsset => {
|
||||||
costume.rotationCenterX / costume.bitmapResolution,
|
|
||||||
costume.rotationCenterY / costume.bitmapResolution
|
|
||||||
];
|
|
||||||
|
|
||||||
let promise = runtime.storage.load(assetType, md5, ext).then(costumeAsset => {
|
|
||||||
costume.assetId = costumeAsset.assetId;
|
|
||||||
costume.dataFormat = ext;
|
costume.dataFormat = ext;
|
||||||
return costumeAsset;
|
return loadCostumeFromAsset(costume, costumeAsset, runtime);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!runtime.renderer) {
|
|
||||||
log.error('No rendering module present; cannot load costume asset: ', md5ext);
|
|
||||||
return promise.then(() => costume);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assetType === AssetType.ImageVector) {
|
|
||||||
promise = promise.then(costumeAsset => {
|
|
||||||
costume.skinId = runtime.renderer.createSVGSkin(costumeAsset.decodeText(), rotationCenter);
|
|
||||||
return costume;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
promise = promise.then(costumeAsset => (
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const imageElement = new Image();
|
|
||||||
const onError = function () {
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
removeEventListeners();
|
|
||||||
reject();
|
|
||||||
};
|
|
||||||
const onLoad = function () {
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
removeEventListeners();
|
|
||||||
resolve(imageElement);
|
|
||||||
};
|
|
||||||
const removeEventListeners = function () {
|
|
||||||
imageElement.removeEventListener('error', onError);
|
|
||||||
imageElement.removeEventListener('load', onLoad);
|
|
||||||
};
|
|
||||||
imageElement.addEventListener('error', onError);
|
|
||||||
imageElement.addEventListener('load', onLoad);
|
|
||||||
imageElement.src = costumeAsset.encodeDataURI();
|
|
||||||
})
|
|
||||||
)).then(imageElement => {
|
|
||||||
costume.skinId = runtime.renderer.createBitmapSkin(imageElement, costume.bitmapResolution, rotationCenter);
|
|
||||||
return costume;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return promise;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = loadCostume;
|
module.exports = {
|
||||||
|
loadCostume,
|
||||||
|
loadCostumeFromAsset
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,27 @@
|
||||||
const StringUtil = require('../util/string-util');
|
const StringUtil = require('../util/string-util');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a sound from an asset asynchronously.
|
||||||
|
* @param {!object} sound - the Scratch sound object.
|
||||||
|
* @property {string} md5 - the MD5 and extension of the sound to be loaded.
|
||||||
|
* @property {Buffer} data - sound data will be written here once loaded.
|
||||||
|
* @param {!Asset} soundAsset - the asset loaded from storage.
|
||||||
|
* @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
|
||||||
|
* @returns {!Promise} - a promise which will resolve to the sound when ready.
|
||||||
|
*/
|
||||||
|
const loadSoundFromAsset = function (sound, soundAsset, runtime) {
|
||||||
|
sound.assetId = soundAsset.assetId;
|
||||||
|
return runtime.audioEngine.decodeSound(Object.assign(
|
||||||
|
{},
|
||||||
|
sound,
|
||||||
|
{data: soundAsset.data}
|
||||||
|
)).then(soundId => {
|
||||||
|
sound.soundId = soundId;
|
||||||
|
return sound;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a sound's asset into memory asynchronously.
|
* Load a sound's asset into memory asynchronously.
|
||||||
* @param {!object} sound - the Scratch sound object.
|
* @param {!object} sound - the Scratch sound object.
|
||||||
|
@ -23,18 +44,12 @@ const loadSound = function (sound, runtime) {
|
||||||
const ext = idParts[1].toLowerCase();
|
const ext = idParts[1].toLowerCase();
|
||||||
return runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext)
|
return runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext)
|
||||||
.then(soundAsset => {
|
.then(soundAsset => {
|
||||||
sound.assetId = soundAsset.assetId;
|
|
||||||
sound.dataFormat = ext;
|
sound.dataFormat = ext;
|
||||||
return runtime.audioEngine.decodeSound(Object.assign(
|
return loadSoundFromAsset(sound, soundAsset, runtime);
|
||||||
{},
|
|
||||||
sound,
|
|
||||||
{data: soundAsset.data}
|
|
||||||
));
|
|
||||||
})
|
|
||||||
.then(soundId => {
|
|
||||||
sound.soundId = soundId;
|
|
||||||
return sound;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = loadSound;
|
module.exports = {
|
||||||
|
loadSound,
|
||||||
|
loadSoundFromAsset
|
||||||
|
};
|
||||||
|
|
|
@ -15,8 +15,8 @@ const specMap = require('./sb2_specmap');
|
||||||
const Variable = require('../engine/variable');
|
const Variable = require('../engine/variable');
|
||||||
const List = require('../engine/list');
|
const List = require('../engine/list');
|
||||||
|
|
||||||
const loadCostume = require('../import/load-costume.js');
|
const {loadCostume} = require('../import/load-costume.js');
|
||||||
const loadSound = require('../import/load-sound.js');
|
const {loadSound} = require('../import/load-sound.js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n")
|
* Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n")
|
||||||
|
|
|
@ -10,8 +10,8 @@ const Sprite = require('../sprites/sprite');
|
||||||
const Variable = require('../engine/variable');
|
const Variable = require('../engine/variable');
|
||||||
const List = require('../engine/list');
|
const List = require('../engine/list');
|
||||||
|
|
||||||
const loadCostume = require('../import/load-costume.js');
|
const {loadCostume} = require('../import/load-costume.js');
|
||||||
const loadSound = require('../import/load-sound.js');
|
const {loadSound} = require('../import/load-sound.js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the specified VM runtime.
|
* Serializes the specified VM runtime.
|
||||||
|
|
|
@ -787,6 +787,33 @@ class RenderedTarget extends Target {
|
||||||
return newClone;
|
return newClone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a duplicate using a duplicate sprite.
|
||||||
|
* @return {RenderedTarget} New clone.
|
||||||
|
*/
|
||||||
|
duplicate () {
|
||||||
|
return this.sprite.duplicate().then(newSprite => {
|
||||||
|
const newTarget = newSprite.createClone();
|
||||||
|
// Copy all properties.
|
||||||
|
// @todo refactor with clone methods
|
||||||
|
newTarget.x = Math.random() * 400 / 2;
|
||||||
|
newTarget.y = Math.random() * 300 / 2;
|
||||||
|
newTarget.direction = this.direction;
|
||||||
|
newTarget.draggable = this.draggable;
|
||||||
|
newTarget.visible = this.visible;
|
||||||
|
newTarget.size = this.size;
|
||||||
|
newTarget.currentCostume = this.currentCostume;
|
||||||
|
newTarget.rotationStyle = this.rotationStyle;
|
||||||
|
newTarget.effects = JSON.parse(JSON.stringify(this.effects));
|
||||||
|
newTarget.variables = JSON.parse(JSON.stringify(this.variables));
|
||||||
|
newTarget.lists = JSON.parse(JSON.stringify(this.lists));
|
||||||
|
newTarget.initDrawable();
|
||||||
|
newTarget.updateAllDrawableProperties();
|
||||||
|
newTarget.goBehindOther(this);
|
||||||
|
return newTarget;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the project receives a "green flag."
|
* Called when the project receives a "green flag."
|
||||||
* For a rendered target, this clears graphic effects.
|
* For a rendered target, this clears graphic effects.
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
const RenderedTarget = require('./rendered-target');
|
const RenderedTarget = require('./rendered-target');
|
||||||
const Blocks = require('../engine/blocks');
|
const Blocks = require('../engine/blocks');
|
||||||
|
const {loadSoundFromAsset} = require('../import/load-sound');
|
||||||
|
const {loadCostumeFromAsset} = require('../import/load-costume');
|
||||||
|
const StringUtil = require('../util/string-util');
|
||||||
|
|
||||||
class Sprite {
|
class Sprite {
|
||||||
/**
|
/**
|
||||||
|
@ -73,6 +76,33 @@ class Sprite {
|
||||||
this.clones.splice(cloneIndex, 1);
|
this.clones.splice(cloneIndex, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duplicate () {
|
||||||
|
const newSprite = new Sprite(null, this.runtime);
|
||||||
|
|
||||||
|
newSprite.blocks = this.blocks.duplicate();
|
||||||
|
|
||||||
|
const allNames = this.runtime.targets.map(t => t.name);
|
||||||
|
newSprite.name = StringUtil.unusedName(this.name, allNames);
|
||||||
|
|
||||||
|
const assetPromises = [];
|
||||||
|
|
||||||
|
newSprite.costumes = this.costumes.map(costume => {
|
||||||
|
const newCostume = Object.assign({}, costume);
|
||||||
|
const costumeAsset = this.runtime.storage.get(costume.assetId);
|
||||||
|
assetPromises.push(loadCostumeFromAsset(newCostume, costumeAsset, this.runtime));
|
||||||
|
return newCostume;
|
||||||
|
});
|
||||||
|
|
||||||
|
newSprite.sounds = this.sounds.map(sound => {
|
||||||
|
const newSound = Object.assign({}, sound);
|
||||||
|
const soundAsset = this.runtime.storage.get(sound.assetId);
|
||||||
|
assetPromises.push(loadSoundFromAsset(newSound, soundAsset, this.runtime));
|
||||||
|
return newSound;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(assetPromises).then(() => newSprite);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Sprite;
|
module.exports = Sprite;
|
||||||
|
|
|
@ -6,8 +6,8 @@ const sb2 = require('./serialization/sb2');
|
||||||
const sb3 = require('./serialization/sb3');
|
const sb3 = require('./serialization/sb3');
|
||||||
const StringUtil = require('./util/string-util');
|
const StringUtil = require('./util/string-util');
|
||||||
|
|
||||||
const loadCostume = require('./import/load-costume.js');
|
const {loadCostume} = require('./import/load-costume.js');
|
||||||
const loadSound = require('./import/load-sound.js');
|
const {loadSound} = require('./import/load-sound.js');
|
||||||
|
|
||||||
const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_'];
|
const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_'];
|
||||||
|
|
||||||
|
@ -472,6 +472,25 @@ class VirtualMachine extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate a sprite.
|
||||||
|
* @param {string} targetId ID of a target whose sprite to duplicate.
|
||||||
|
*/
|
||||||
|
duplicateSprite (targetId) {
|
||||||
|
const target = this.runtime.getTargetById(targetId);
|
||||||
|
if (!target) {
|
||||||
|
throw new Error('No target with the provided id.');
|
||||||
|
} else if (!target.isSprite()) {
|
||||||
|
throw new Error('Cannot duplicate non-sprite targets.');
|
||||||
|
} else if (!target.sprite) {
|
||||||
|
throw new Error('No sprite associated with this target.');
|
||||||
|
}
|
||||||
|
target.duplicate().then(newTarget => {
|
||||||
|
this.runtime.targets.push(newTarget);
|
||||||
|
this.setEditingTarget(newTarget.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the audio engine for the VM/runtime
|
* Set the audio engine for the VM/runtime
|
||||||
* @param {!AudioEngine} audioEngine The audio engine to attach
|
* @param {!AudioEngine} audioEngine The audio engine to attach
|
||||||
|
|
|
@ -2,8 +2,8 @@ const test = require('tap').test;
|
||||||
|
|
||||||
const Blocks = require('../../src/engine/blocks');
|
const Blocks = require('../../src/engine/blocks');
|
||||||
const Clone = require('../../src/util/clone');
|
const Clone = require('../../src/util/clone');
|
||||||
const loadCostume = require('../../src/import/load-costume');
|
const {loadCostume} = require('../../src/import/load-costume');
|
||||||
const loadSound = require('../../src/import/load-sound');
|
const {loadSound} = require('../../src/import/load-sound');
|
||||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
const Runtime = require('../../src/engine/runtime');
|
const Runtime = require('../../src/engine/runtime');
|
||||||
const sb3 = require('../../src/serialization/sb3');
|
const sb3 = require('../../src/serialization/sb3');
|
||||||
|
|
Loading…
Reference in a new issue