Skip to content
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
117 changes: 117 additions & 0 deletions src/components/costume-canvas.js
Original file line number Diff line number Diff line change
@@ -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);

This comment was marked as abuse.

This comment was marked as abuse.

}
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 changes: 49 additions & 0 deletions src/components/library-item.js
Original file line number Diff line number Diff line change
@@ -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 changes: 69 additions & 0 deletions src/components/library.js
Original file line number Diff line number Diff line change
@@ -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 changes: 70 additions & 0 deletions src/components/modal.js
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 12 additions & 1 deletion src/components/sprite-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class SpriteSelectorComponent extends React.Component {
onChange,
sprites,
value,
openNewSprite,
openNewCostume,
openNewBackdrop,
...props
} = this.props;
return (
Expand All @@ -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>
);
}
Expand All @@ -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;
Loading