Merge pull request #7 from tmickel/feature/libraries

Initial rough sprite/costume/backdrop libraries
This commit is contained in:
Tim Mickel authored and GitHub committed 2016-10-21 09:54:41 -04:00
commit 13efe77bcb
12 files changed
+623 -2

No files matched your search

+2
View File
@@ -45,9 +45,11 @@
"opt-cli": "1.5.1",
"react": "15.x.x",
"react-dom": "15.x.x",
"react-modal": "1.5.2",
"scratch-blocks": "latest",
"scratch-render": "latest",
"scratch-vm": "latest",
"svg-to-image": "1.1.3",
"travis-after-all": "jamesarosen/travis-after-all#override-api-urls",
"webpack": "1.13.2",
"webpack-dev-server": "1.15.2",
+117
View File
@@ -0,0 +1,117 @@
const React = require('react');
const svgToImage = require('svg-to-image');
const xhr = require('xhr');
/**
* @fileoverview
* A component for rendering Scratch costume URLs to canvases.
* Use for sprite library, costume library, sprite selector, etc.
* Props include width, height, and direction (direction in Scratch value).
*/
class CostumeCanvas extends React.Component {
componentDidMount () {
this.load();
}
componentDidUpdate (prevProps) {
if (prevProps.url !== this.props.url) {
this.load();
} else {
if (prevProps.width !== this.props.width ||
prevProps.height !== this.props.height ||
prevProps.direction !== this.props.direction) {
this.draw();
}
}
}
draw () {
if (!this.refs.costumeCanvas) {
return;
}
// Draw the costume to the rendered canvas.
const img = this.img;
const context = this.refs.costumeCanvas.getContext('2d');
// Scale to fit.
let scale;
// Choose the larger dimension to scale by.
if (img.width > img.height) {
scale = this.refs.costumeCanvas.width / img.width;
} else {
scale = this.refs.costumeCanvas.height / img.height;
}
// Rotate by the Scratch-value direction.
const angle = (-90 + this.props.direction) * Math.PI / 180;
// Rotation origin point will be center of the canvas.
const contextTranslateX = this.refs.costumeCanvas.width / 2;
const contextTranslateY = this.refs.costumeCanvas.height / 2;
// First, clear the canvas.
context.clearRect(0, 0,
this.refs.costumeCanvas.width, this.refs.costumeCanvas.height);
// Translate the context to the center of the canvas,
// then rotate canvas drawing by `angle`.
context.translate(contextTranslateX, contextTranslateY);
context.rotate(angle);
context.drawImage(img,
0, 0, img.width, img.height,
-(scale * img.width / 2), -(scale * img.height / 2),
scale * img.width,
scale * img.height);
// Reset the canvas rotation and translation to 0, (0, 0).
context.rotate(-angle);
context.translate(-contextTranslateX, -contextTranslateY);
}
load () {
// Draw the icon on our canvas.
const url = this.props.url;
if (url.indexOf('.svg') > -1) {
// Vector graphics: need to download with XDR and rasterize.
// Queue request asynchronously.
setTimeout(() => {
xhr.get({
useXDR: true,
url: url
}, (err, response, body) => {
if (!err) {
svgToImage(body, (err, img) => {
if (!err) {
this.img = img;
this.draw();
}
});
}
});
}, 0);
} else {
// Raster graphics: create Image and draw it.
let img = new Image();
img.src = url;
img.onload = () => {
this.img = img;
this.draw();
};
}
}
render () {
return <canvas
ref='costumeCanvas'
width={this.props.width}
height={this.props.height}
/>;
}
}
CostumeCanvas.defaultProps = {
width: 100,
height: 100,
direction: 90
};
CostumeCanvas.propTypes = {
url: React.PropTypes.string,
width: React.PropTypes.number,
height: React.PropTypes.number,
direction: React.PropTypes.number
};
module.exports = CostumeCanvas;
+49
View File
@@ -0,0 +1,49 @@
const React = require('react');
const CostumeCanvas = require('./costume-canvas');
class LibraryItem extends React.Component {
render () {
let style = (this.props.selected) ?
this.props.selectedGridTileStyle : this.props.gridTileStyle;
return (
<div style={style} onClick={() => this.props.onSelect(this.props.id)}>
<CostumeCanvas url={this.props.iconURL} />
<p>{this.props.name}</p>
</div>
);
}
}
LibraryItem.defaultProps = {
gridTileStyle: {
float: 'left',
width: '140px',
marginLeft: '5px',
marginRight: '5px',
textAlign: 'center',
cursor: 'pointer'
},
selectedGridTileStyle: {
float: 'left',
width: '140px',
marginLeft: '5px',
marginRight: '5px',
textAlign: 'center',
cursor: 'pointer',
background: '#aaa',
borderRadius: '6px'
}
};
LibraryItem.propTypes = {
name: React.PropTypes.string,
iconURL: React.PropTypes.string,
gridTileStyle: React.PropTypes.object,
selectedGridTileStyle: React.PropTypes.object,
selected: React.PropTypes.bool,
onSelect: React.PropTypes.func,
id: React.PropTypes.number
};
module.exports = LibraryItem;
+69
View File
@@ -0,0 +1,69 @@
const bindAll = require('lodash.bindall');
const React = require('react');
const LibraryItem = require('./library-item');
const ModalComponent = require('./modal');
class LibraryComponent extends React.Component {
constructor (props) {
super(props);
bindAll(this, ['onSelect']);
this.state = {selectedItem: null};
}
onSelect (id) {
if (this.state.selectedItem == id) {
// Double select: select as the library's value.
this.props.onRequestClose();
this.props.onItemSelected(this.props.data[id]);
}
this.setState({selectedItem: id});
}
render () {
let itemId = 0;
let gridItems = this.props.data.map((dataItem) => {
let id = itemId;
itemId++;
const scratchURL = (dataItem.md5) ? 'https://cdn.assets.scratch.mit.edu/internalapi/asset/' +
dataItem.md5 + '/get/' : dataItem.rawURL;
return <LibraryItem
name={dataItem.name}
iconURL={scratchURL}
key={'item_' + id}
selected={this.state.selectedItem == id}
onSelect={this.onSelect}
id={id}
/>;
});
const scrollGridStyle = {
overflow: 'scroll',
position: 'absolute',
top: '70px',
bottom: '20px',
left: '30px',
right: '30px'
};
return (
<ModalComponent
onRequestClose={this.props.onRequestClose}
visible={this.props.visible}
>
<h1>{this.props.title}</h1>
<div style={scrollGridStyle}>
{gridItems}
</div>
</ModalComponent>
);
}
}
LibraryComponent.propTypes = {
title: React.PropTypes.string,
data: React.PropTypes.array,
visible: React.PropTypes.bool,
onRequestClose: React.PropTypes.func,
onItemSelected: React.PropTypes.func
};
module.exports = LibraryComponent;
+70
View File
@@ -0,0 +1,70 @@
const React = require('react');
const ReactModal = require('react-modal');
class ModalComponent extends React.Component {
render () {
return (
<ReactModal
ref="modal"
style={this.props.modalStyle}
isOpen={this.props.visible}
onRequestClose={this.props.onRequestClose}
>
<div
onClick={this.props.onRequestClose}
style={this.props.closeButtonStyle}
>
x
</div>
{this.props.children}
</ReactModal>
);
}
}
const modalStyle = {
overlay: {
zIndex: 1000,
backgroundColor: 'rgba(0, 0, 0, .75)'
},
content: {
position: 'absolute',
overflow: 'visible',
borderRadius: '6px',
padding: 0,
top: '5%',
bottom: '5%',
left: '5%',
right: '5%',
background: '#fcfcfc'
}
};
const closeButtonStyle = {
color: 'rgb(255, 255, 255)',
background: 'rgb(50, 50, 50)',
borderRadius: '15px',
width: '30px',
height: '25px',
textAlign: 'center',
paddingTop: '5px',
position: 'absolute',
right: '3px',
top: '3px',
cursor: 'pointer'
};
ModalComponent.defaultProps = {
modalStyle: modalStyle,
closeButtonStyle: closeButtonStyle
};
ModalComponent.propTypes = {
children: React.PropTypes.node,
modalStyle: React.PropTypes.object,
closeButtonStyle: React.PropTypes.object,
onRequestClose: React.PropTypes.func,
visible: React.PropTypes.bool
};
module.exports = ModalComponent;
+12 -1
View File
@@ -6,6 +6,9 @@ class SpriteSelectorComponent extends React.Component {
onChange,
sprites,
value,
openNewSprite,
openNewCostume,
openNewBackdrop,
...props
} = this.props;
return (
@@ -28,6 +31,11 @@ class SpriteSelectorComponent extends React.Component {
</option>
))}
</select>
<p>
<button onClick={openNewSprite}>New sprite</button>
<button onClick={openNewCostume}>New costume</button>
<button onClick={openNewBackdrop}>New backdrop</button>
</p>
</div>
);
}
@@ -41,7 +49,10 @@ SpriteSelectorComponent.propTypes = {
name: React.PropTypes.string
})
),
value: React.PropTypes.arrayOf(React.PropTypes.string)
value: React.PropTypes.arrayOf(React.PropTypes.string),
openNewSprite: React.PropTypes.func,
openNewCostume: React.PropTypes.func,
openNewBackdrop: React.PropTypes.func
};
module.exports = SpriteSelectorComponent;
+53
View File
@@ -0,0 +1,53 @@
const bindAll = require('lodash.bindall');
const React = require('react');
const VM = require('scratch-vm');
const MediaLibrary = require('../lib/media-library');
const LibaryComponent = require('../components/library');
class BackdropLibrary extends React.Component {
constructor (props) {
super(props);
bindAll(this, ['setData', 'selectItem']);
this.state = {backdropData: []};
}
componentWillReceiveProps (nextProps) {
if (nextProps.visible && this.state.backdropData.length === 0) {
this.props.mediaLibrary.getMediaLibrary('backdrop', this.setData);
}
}
setData (data) {
this.setState({backdropData: data});
}
selectItem (item) {
var vmBackdrop = {
skin: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/' + item.md5 + '/get/',
name: item.name,
rotationCenterX: item.info[0],
rotationCenterY: item.info[1]
};
if (item.info.length > 2) {
vmBackdrop.bitmapResolution = item.info[2];
}
this.props.vm.addBackdrop(vmBackdrop);
}
render () {
return <LibaryComponent
title="Backdrop Library"
visible={this.props.visible}
data={this.state.backdropData}
onRequestClose={this.props.onRequestClose}
onItemSelected={this.selectItem}
/>;
}
}
BackdropLibrary.propTypes = {
vm: React.PropTypes.instanceOf(VM).isRequired,
mediaLibrary: React.PropTypes.instanceOf(MediaLibrary),
visible: React.PropTypes.bool,
onRequestClose: React.PropTypes.func
};
module.exports = BackdropLibrary;
+53
View File
@@ -0,0 +1,53 @@
const bindAll = require('lodash.bindall');
const React = require('react');
const VM = require('scratch-vm');
const MediaLibrary = require('../lib/media-library');
const LibaryComponent = require('../components/library');
class CostumeLibrary extends React.Component {
constructor (props) {
super(props);
bindAll(this, ['setData', 'selectItem']);
this.state = {costumeData: []};
}
componentWillReceiveProps (nextProps) {
if (nextProps.visible && this.state.costumeData.length === 0) {
this.props.mediaLibrary.getMediaLibrary('costume', this.setData);
}
}
setData (data) {
this.setState({costumeData: data});
}
selectItem (item) {
var vmCostume = {
skin: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/' + item.md5 + '/get/',
name: item.name,
rotationCenterX: item.info[0],
rotationCenterY: item.info[1]
};
if (item.info.length > 2) {
vmCostume.bitmapResolution = item.info[2];
}
this.props.vm.addCostume(vmCostume);
}
render () {
return <LibaryComponent
title="Costume Library"
visible={this.props.visible}
data={this.state.costumeData}
onRequestClose={this.props.onRequestClose}
onItemSelected={this.selectItem}
/>;
}
}
CostumeLibrary.propTypes = {
vm: React.PropTypes.instanceOf(VM).isRequired,
mediaLibrary: React.PropTypes.instanceOf(MediaLibrary),
visible: React.PropTypes.bool,
onRequestClose: React.PropTypes.func
};
module.exports = CostumeLibrary;
+47
View File
@@ -1,8 +1,10 @@
const bindAll = require('lodash.bindall');
const defaultsDeep = require('lodash.defaultsdeep');
const React = require('react');
const VM = require('scratch-vm');
const VMManager = require('../lib/vm-manager');
const MediaLibrary = require('../lib/media-library');
const Blocks = require('./blocks');
const GUIComponent = require('../components/gui');
@@ -11,10 +13,17 @@ const SpriteSelector = require('./sprite-selector');
const Stage = require('./stage');
const StopAll = require('./stop-all');
const SpriteLibrary = require('./sprite-library');
const CostumeLibrary = require('./costume-library');
const BackdropLibrary = require('./backdrop-library');
class GUI extends React.Component {
constructor (props) {
super(props);
bindAll(this, ['closeModal']);
this.vmManager = new VMManager(this.props.vm);
this.mediaLibrary = new MediaLibrary();
this.state = {currentModal: null};
}
componentDidMount () {
this.vmManager.attachKeyboardEvents();
@@ -30,12 +39,21 @@ class GUI extends React.Component {
this.props.vm.loadProject(nextProps.projectData);
}
}
openModal (modalName) {
this.setState({currentModal: modalName});
}
closeModal () {
this.setState({currentModal: null});
}
render () {
let {
backdropLibraryProps,
basePath,
blocksProps,
costumeLibraryProps,
greenFlagProps,
projectData, // eslint-disable-line no-unused-vars
spriteLibraryProps,
spriteSelectorProps,
stageProps,
stopAllProps,
@@ -47,6 +65,26 @@ class GUI extends React.Component {
media: basePath + 'static/blocks-media/'
}
});
spriteSelectorProps = defaultsDeep({}, spriteSelectorProps, {
openNewBackdrop: () => this.openModal('backdrop-library'),
openNewCostume: () => this.openModal('costume-library'),
openNewSprite: () => this.openModal('sprite-library')
});
spriteLibraryProps = defaultsDeep({}, spriteLibraryProps, {
mediaLibrary: this.mediaLibrary,
onRequestClose: this.closeModal,
visible: this.state.currentModal == 'sprite-library'
});
costumeLibraryProps = defaultsDeep({}, costumeLibraryProps, {
mediaLibrary: this.mediaLibrary,
onRequestClose: this.closeModal,
visible: this.state.currentModal == 'costume-library'
});
backdropLibraryProps = defaultsDeep({}, backdropLibraryProps, {
mediaLibrary: this.mediaLibrary,
onRequestClose: this.closeModal,
visible: this.state.currentModal == 'backdrop-library'
});
if (this.props.children) {
return (
<GUIComponent {... guiProps}>
@@ -61,6 +99,9 @@ class GUI extends React.Component {
<Stage vm={vm} {...stageProps} />
<SpriteSelector vm={vm} {... spriteSelectorProps} />
<Blocks vm={vm} {... blocksProps} />
<SpriteLibrary vm={vm} {...spriteLibraryProps} />
<CostumeLibrary vm={vm} {...costumeLibraryProps} />
<BackdropLibrary vm={vm} {...backdropLibraryProps} />
</GUIComponent>
);
}
@@ -68,11 +109,14 @@ class GUI extends React.Component {
}
GUI.propTypes = {
backdropLibraryProps: React.PropTypes.object,
basePath: React.PropTypes.string,
blocksProps: React.PropTypes.object,
costumeLibraryProps: React.PropTypes.object,
children: React.PropTypes.node,
greenFlagProps: React.PropTypes.object,
projectData: React.PropTypes.string,
spriteLibraryProps: React.PropTypes.object,
spriteSelectorProps: React.PropTypes.object,
stageProps: React.PropTypes.object,
stopAllProps: React.PropTypes.object,
@@ -80,10 +124,13 @@ GUI.propTypes = {
};
GUI.defaultProps = {
backdropLibraryProps: {},
basePath: '/',
blocksProps: {},
costumeLibraryProps: {},
greenFlagProps: {},
spriteSelectorProps: {},
spriteLibraryProps: {},
stageProps: {},
stopAllProps: {},
vm: new VM()
+61
View File
@@ -0,0 +1,61 @@
const bindAll = require('lodash.bindall');
const React = require('react');
const VM = require('scratch-vm');
const MediaLibrary = require('../lib/media-library');
const LibaryComponent = require('../components/library');
class SpriteLibrary extends React.Component {
constructor (props) {
super(props);
bindAll(this, ['setData', 'selectItem', 'setSpriteData']);
this.state = {data: [], spriteData: {}};
}
componentWillReceiveProps (nextProps) {
if (nextProps.visible && this.state.data.length === 0) {
this.props.mediaLibrary.getMediaLibrary('sprite', this.setData);
}
}
setData (data) {
this.setState({data: data});
for (let sprite of data) {
this.props.mediaLibrary.getSprite(sprite.md5, this.setSpriteData);
}
}
setSpriteData (md5, data) {
let spriteData = this.state.spriteData;
spriteData[md5] = data;
this.setState({spriteData: spriteData});
}
selectItem (item) {
var spriteData = JSON.stringify(this.state.spriteData[item.json]);
this.props.vm.addSprite2(spriteData);
}
render () {
let libraryData = Object.keys(this.state.spriteData).map((libraryKey) => {
let libraryItem = this.state.spriteData[libraryKey];
return {
name: libraryItem.objName,
md5: libraryItem.costumes[0].baseLayerMD5,
json: libraryKey
};
});
return <LibaryComponent
title="Sprite Library"
visible={this.props.visible}
data={libraryData}
mediaLibrary={this.props.mediaLibrary}
onRequestClose={this.props.onRequestClose}
onItemSelected={this.selectItem}
/>;
}
}
SpriteLibrary.propTypes = {
vm: React.PropTypes.instanceOf(VM).isRequired,
mediaLibrary: React.PropTypes.instanceOf(MediaLibrary),
visible: React.PropTypes.bool,
onRequestClose: React.PropTypes.func
};
module.exports = SpriteLibrary;
+10 -1
View File
@@ -25,12 +25,18 @@ class SpriteSelector extends React.Component {
render () {
const {
vm, // eslint-disable-line no-unused-vars
openNewSprite,
openNewCostume,
openNewBackdrop,
...props
} = this.props;
return (
<SpriteSelectorComponent
value={this.state.targets.editingTarget && [this.state.targets.editingTarget]}
onChange={this.onChange}
openNewSprite={openNewSprite}
openNewCostume={openNewCostume}
openNewBackdrop={openNewBackdrop}
sprites={this.state.targets.targetList.map(target => (
{
id: target[0],
@@ -44,7 +50,10 @@ class SpriteSelector extends React.Component {
}
SpriteSelector.propTypes = {
vm: React.PropTypes.object.isRequired
vm: React.PropTypes.object.isRequired,
openNewSprite: React.PropTypes.func,
openNewCostume: React.PropTypes.func,
openNewBackdrop: React.PropTypes.func
};
module.exports = SpriteSelector;
+80
View File
@@ -0,0 +1,80 @@
const xhr = require('xhr');
const LIBRARY_PREFIX = 'https://cdn.scratch.mit.edu/scratchr2/static/' +
'__8d9c95eb5aa1272a311775ca32568417__/medialibraries/';
const LIBRARY_URL = {
sprite: LIBRARY_PREFIX + 'spriteLibrary.json',
costume: LIBRARY_PREFIX + 'costumeLibrary.json',
backdrop: LIBRARY_PREFIX + 'backdropLibrary.json'
};
const SPRITE_OBJECT_PREFIX = 'https://cdn.assets.scratch.mit.edu/internalapi/asset/';
const SPRITE_OBJECT_SUFFIX = '/get/';
class MediaLibrary {
constructor () {
/*
* Cached library data, from JSON.
* @type {Object}
*/
this._libraryData = {};
/**
* Cached sprite data, from JSON.
* @type {Object.<!string, Object>}
*/
this._spriteData = {};
}
/**
* Get the media library data for a particular scratchr2 library.
* In the future, load this from `scratch-storage` asset manager,
* e.g., for offline support.
* @param {string} libraryType Type of library, i.e., sprite, costume, sound, backdrop.
* @param {!Function} callback Callback, called with list of data.
*/
getMediaLibrary (libraryType, callback) {
if (!this._libraryData.hasOwnProperty(libraryType)) {
this._libraryData[libraryType] = null;
}
if (this._libraryData[libraryType]) {
callback(this._libraryData[libraryType]);
} else {
xhr.get({
useXDR: true,
url: LIBRARY_URL[libraryType]
}, (err, response, body) => {
if (!err) {
let data = JSON.parse(body);
this._libraryData[libraryType] = data;
callback(this._libraryData[libraryType]);
}
});
}
}
/**
* Get media library info for a specific scratchr2 sprite.
* In the future, load this from `scratch-storage` asset manager,
* e.g., for offline support.
* @param {string} url URL to sprite (md5.json).
* @param {!Function} callback Callback, called with sprite data.
*/
getSprite (url, callback) {
if (this._spriteData.hasOwnProperty(url)) {
callback(url, this._spriteData[url]);
} else {
xhr.get({
useXDR: true,
url: SPRITE_OBJECT_PREFIX + url + SPRITE_OBJECT_SUFFIX
}, (err, response, body) => {
if (!err) {
let data = JSON.parse(body);
this._spriteData[url] = data;
callback(url, data);
}
});
}
}
}
module.exports = MediaLibrary;