scratch-gui
Version:
GraphicaL User Interface for creating and running Scratch 3.0 projects
377 lines (358 loc) • 14.4 kB
JSX
import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import {defineMessages, intlShape, injectIntl} from 'react-intl';
import VM from 'scratch-vm';
import AssetPanel from '../components/asset-panel/asset-panel.jsx';
import PaintEditorWrapper from './paint-editor-wrapper.jsx';
import CostumeLibrary from './costume-library.jsx';
import BackdropLibrary from './backdrop-library.jsx';
import CameraModal from './camera-modal.jsx';
import {connect} from 'react-redux';
import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js';
import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
import {
closeCameraCapture,
closeCostumeLibrary,
closeBackdropLibrary,
openCameraCapture,
openCostumeLibrary,
openBackdropLibrary
} from '../reducers/modals';
import addLibraryBackdropIcon from '../components/asset-panel/icon--add-backdrop-lib.svg';
import addLibraryCostumeIcon from '../components/asset-panel/icon--add-costume-lib.svg';
import fileUploadIcon from '../components/action-menu/icon--file-upload.svg';
import paintIcon from '../components/action-menu/icon--paint.svg';
import cameraIcon from '../components/action-menu/icon--camera.svg';
import surpriseIcon from '../components/action-menu/icon--surprise.svg';
import costumeLibraryContent from '../lib/libraries/costumes.json';
import backdropLibraryContent from '../lib/libraries/backdrops.json';
const messages = defineMessages({
addLibraryBackdropMsg: {
defaultMessage: 'Choose a Backdrop',
description: 'Button to add a backdrop in the editor tab',
id: 'gui.costumeTab.addBackdropFromLibrary'
},
addLibraryCostumeMsg: {
defaultMessage: 'Choose a Costume',
description: 'Button to add a costume in the editor tab',
id: 'gui.costumeTab.addCostumeFromLibrary'
},
addBlankCostumeMsg: {
defaultMessage: 'Paint',
description: 'Button to add a blank costume in the editor tab',
id: 'gui.costumeTab.addBlankCostume'
},
addSurpriseCostumeMsg: {
defaultMessage: 'Surprise',
description: 'Button to add a surprise costume in the editor tab',
id: 'gui.costumeTab.addSurpriseCostume'
},
addFileBackdropMsg: {
defaultMessage: 'Upload Backdrop',
description: 'Button to add a backdrop by uploading a file in the editor tab',
id: 'gui.costumeTab.addFileBackdrop'
},
addFileCostumeMsg: {
defaultMessage: 'Upload Costume',
description: 'Button to add a costume by uploading a file in the editor tab',
id: 'gui.costumeTab.addFileCostume'
},
addCameraCostumeMsg: {
defaultMessage: 'Camera',
description: 'Button to use the camera to create a costume costume in the editor tab',
id: 'gui.costumeTab.addCameraCostume'
}
});
class CostumeTab extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleSelectCostume',
'handleDeleteCostume',
'handleDuplicateCostume',
'handleNewCostume',
'handleNewBlankCostume',
'handleSurpriseCostume',
'handleSurpriseBackdrop',
'handleFileUploadClick',
'handleCostumeUpload',
'handleCameraBuffer',
'setFileInput'
]);
const {
editingTarget,
sprites,
stage
} = props;
const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
if (target && target.currentCostume) {
this.state = {selectedCostumeIndex: target.currentCostume};
} else {
this.state = {selectedCostumeIndex: 0};
}
}
componentWillReceiveProps (nextProps) {
const {
editingTarget,
sprites,
stage
} = nextProps;
const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
if (!target || !target.costumes) {
return;
}
if (this.props.editingTarget === editingTarget) {
// If costumes have been added or removed, change costumes to the editing target's
// current costume.
const oldTarget = this.props.sprites[editingTarget] ?
this.props.sprites[editingTarget] : this.props.stage;
// @todo: Find and switch to the index of the costume that is new. This is blocked by
// https://github.com/LLK/scratch-vm/issues/967
// Right now, you can land on the wrong costume if a costume changing script is running.
if (oldTarget.costumeCount !== target.costumeCount) {
this.setState({selectedCostumeIndex: target.currentCostume});
}
} else {
// If switching editing targets, update the costume index
this.setState({selectedCostumeIndex: target.currentCostume});
}
}
handleSelectCostume (costumeIndex) {
this.props.vm.editingTarget.setCostume(costumeIndex);
this.setState({selectedCostumeIndex: costumeIndex});
}
handleDeleteCostume (costumeIndex) {
this.props.vm.deleteCostume(costumeIndex);
}
handleDuplicateCostume (costumeIndex) {
this.props.vm.duplicateCostume(costumeIndex);
}
handleNewCostume (costume) {
this.props.vm.addCostume(costume.md5, costume);
}
handleNewBlankCostume () {
const emptyItem = costumeLibraryContent.find(item => (
item.name === 'Empty'
));
const name = this.props.vm.editingTarget.isStage ? `backdrop1` : `costume1`;
const vmCostume = {
name: name,
md5: emptyItem.md5,
rotationCenterX: emptyItem.info[0],
rotationCenterY: emptyItem.info[1],
bitmapResolution: emptyItem.info.length > 2 ? emptyItem.info[2] : 1,
skinId: null
};
this.handleNewCostume(vmCostume);
}
handleSurpriseCostume () {
const item = costumeLibraryContent[Math.floor(Math.random() * costumeLibraryContent.length)];
const vmCostume = {
name: item.name,
md5: item.md5,
rotationCenterX: item.info[0],
rotationCenterY: item.info[1],
bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
skinId: null
};
this.handleNewCostume(vmCostume);
}
handleSurpriseBackdrop () {
const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
const vmCostume = {
name: item.name,
md5: item.md5,
rotationCenterX: item.info[0] && item.info[0] / 2,
rotationCenterY: item.info[1] && item.info[1] / 2,
bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
skinId: null
};
this.handleNewCostume(vmCostume);
}
handleCostumeUpload (e) {
const storage = this.props.vm.runtime.storage;
handleFileUpload(e.target, (buffer, fileType, fileName) => {
costumeUpload(buffer, fileType, fileName, storage, this.handleNewCostume);
});
}
handleCameraBuffer (buffer) {
const storage = this.props.vm.runtime.storage;
costumeUpload(buffer, 'image/png', 'costume1', storage, this.handleNewCostume);
}
handleFileUploadClick () {
this.fileInput.click();
}
setFileInput (input) {
this.fileInput = input;
}
formatCostumeDetails (size, optResolution) {
// If no resolution is given, assume that the costume is an SVG
const resolution = optResolution ? optResolution : 1;
// Convert size to stage units by dividing by resolution
// Round up width and height for scratch-flash compatibility
// https://github.com/LLK/scratch-flash/blob/9fbac92ef3d09ceca0c0782f8a08deaa79e4df69/src/ui/media/MediaInfo.as#L224-L237
return `${Math.ceil(size[0] / resolution)} x ${Math.ceil(size[1] / resolution)}`;
}
render () {
const {
intl,
onNewCostumeFromCameraClick,
onNewLibraryBackdropClick,
onNewLibraryCostumeClick,
backdropLibraryVisible,
cameraModalVisible,
costumeLibraryVisible,
onRequestCloseBackdropLibrary,
onRequestCloseCameraModal,
onRequestCloseCostumeLibrary,
editingTarget,
sprites,
stage,
vm
} = this.props;
const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
if (!target) {
return null;
}
const addLibraryMessage = target.isStage ? messages.addLibraryBackdropMsg : messages.addLibraryCostumeMsg;
const addFileMessage = target.isStage ? messages.addFileBackdropMsg : messages.addFileCostumeMsg;
const addSurpriseFunc = target.isStage ? this.handleSurpriseBackdrop : this.handleSurpriseCostume;
const addLibraryFunc = target.isStage ? onNewLibraryBackdropClick : onNewLibraryCostumeClick;
const addLibraryIcon = target.isStage ? addLibraryBackdropIcon : addLibraryCostumeIcon;
const costumeData = (target.costumes || []).map(costume => ({
name: costume.name,
assetId: costume.assetId,
details: costume.size ? this.formatCostumeDetails(costume.size, costume.bitmapResolution) : null
}));
return (
<AssetPanel
buttons={[
{
title: intl.formatMessage(addLibraryMessage),
img: addLibraryIcon,
onClick: addLibraryFunc
},
{
title: intl.formatMessage(messages.addCameraCostumeMsg),
img: cameraIcon,
onClick: onNewCostumeFromCameraClick
},
{
title: intl.formatMessage(addFileMessage),
img: fileUploadIcon,
onClick: this.handleFileUploadClick,
fileAccept: '.svg, .png, .jpg, .jpeg',
fileChange: this.handleCostumeUpload,
fileInput: this.setFileInput
},
{
title: intl.formatMessage(messages.addSurpriseCostumeMsg),
img: surpriseIcon,
onClick: addSurpriseFunc
},
{
title: intl.formatMessage(messages.addBlankCostumeMsg),
img: paintIcon,
onClick: this.handleNewBlankCostume
}
]}
items={costumeData}
selectedItemIndex={this.state.selectedCostumeIndex}
onDeleteClick={target && target.costumes && target.costumes.length > 1 ?
this.handleDeleteCostume : null}
onDuplicateClick={this.handleDuplicateCostume}
onItemClick={this.handleSelectCostume}
>
{target.costumes ?
<PaintEditorWrapper
selectedCostumeIndex={this.state.selectedCostumeIndex}
/> :
null
}
{costumeLibraryVisible ? (
<CostumeLibrary
vm={vm}
onRequestClose={onRequestCloseCostumeLibrary}
/>
) : null}
{backdropLibraryVisible ? (
<BackdropLibrary
vm={vm}
onRequestClose={onRequestCloseBackdropLibrary}
/>
) : null}
{cameraModalVisible ? (
<CameraModal
onClose={onRequestCloseCameraModal}
onNewCostume={this.handleCameraBuffer}
/>
) : null}
</AssetPanel>
);
}
}
CostumeTab.propTypes = {
backdropLibraryVisible: PropTypes.bool,
cameraModalVisible: PropTypes.bool,
costumeLibraryVisible: PropTypes.bool,
editingTarget: PropTypes.string,
intl: intlShape,
onNewCostumeFromCameraClick: PropTypes.func.isRequired,
onNewLibraryBackdropClick: PropTypes.func.isRequired,
onNewLibraryCostumeClick: PropTypes.func.isRequired,
onRequestCloseBackdropLibrary: PropTypes.func.isRequired,
onRequestCloseCameraModal: PropTypes.func.isRequired,
onRequestCloseCostumeLibrary: PropTypes.func.isRequired,
sprites: PropTypes.shape({
id: PropTypes.shape({
costumes: PropTypes.arrayOf(PropTypes.shape({
url: PropTypes.string,
name: PropTypes.string.isRequired,
skinId: PropTypes.number
}))
})
}),
stage: PropTypes.shape({
sounds: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired
}))
}),
vm: PropTypes.instanceOf(VM)
};
const mapStateToProps = state => ({
editingTarget: state.targets.editingTarget,
sprites: state.targets.sprites,
stage: state.targets.stage,
cameraModalVisible: state.modals.cameraCapture,
costumeLibraryVisible: state.modals.costumeLibrary,
backdropLibraryVisible: state.modals.backdropLibrary
});
const mapDispatchToProps = dispatch => ({
onNewLibraryBackdropClick: e => {
e.preventDefault();
dispatch(openBackdropLibrary());
},
onNewLibraryCostumeClick: e => {
e.preventDefault();
dispatch(openCostumeLibrary());
},
onNewCostumeFromCameraClick: () => {
dispatch(openCameraCapture());
},
onRequestCloseBackdropLibrary: () => {
dispatch(closeBackdropLibrary());
},
onRequestCloseCostumeLibrary: () => {
dispatch(closeCostumeLibrary());
},
onRequestCloseCameraModal: () => {
dispatch(closeCameraCapture());
}
});
export default errorBoundaryHOC('Costume Tab')(
injectIntl(connect(
mapStateToProps,
mapDispatchToProps
)(CostumeTab))
);