mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Merge pull request #9 from fsih/importExport
Support import and export of costumes in the paint editor
This commit is contained in:
commit
16f337df47
8 changed files with 131 additions and 27 deletions
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||||
import PaperCanvas from '../containers/paper-canvas.jsx';
|
import PaperCanvas from '../containers/paper-canvas.jsx';
|
||||||
import BrushMode from '../containers/brush-mode.jsx';
|
import BrushMode from '../containers/brush-mode.jsx';
|
||||||
import EraserMode from '../containers/eraser-mode.jsx';
|
import EraserMode from '../containers/eraser-mode.jsx';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import LineMode from '../containers/line-mode.jsx';
|
import LineMode from '../containers/line-mode.jsx';
|
||||||
|
|
||||||
class PaintEditorComponent extends React.Component {
|
class PaintEditorComponent extends React.Component {
|
||||||
|
@ -21,10 +22,24 @@ class PaintEditorComponent extends React.Component {
|
||||||
if (this.state.canvas) {
|
if (this.state.canvas) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PaperCanvas canvasRef={this.setCanvas} />
|
<PaperCanvas
|
||||||
<BrushMode canvas={this.state.canvas} />
|
canvasRef={this.setCanvas}
|
||||||
<EraserMode canvas={this.state.canvas} />
|
rotationCenterX={this.props.rotationCenterX}
|
||||||
<LineMode canvas={this.state.canvas} />
|
rotationCenterY={this.props.rotationCenterY}
|
||||||
|
svg={this.props.svg}
|
||||||
|
/>
|
||||||
|
<BrushMode
|
||||||
|
canvas={this.state.canvas}
|
||||||
|
onUpdateSvg={this.props.onUpdateSvg}
|
||||||
|
/>
|
||||||
|
<EraserMode
|
||||||
|
canvas={this.state.canvas}
|
||||||
|
onUpdateSvg={this.props.onUpdateSvg}
|
||||||
|
/>
|
||||||
|
<LineMode
|
||||||
|
canvas={this.state.canvas}
|
||||||
|
onUpdateSvg={this.props.onUpdateSvg}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -36,4 +51,11 @@ class PaintEditorComponent extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PaintEditorComponent.propTypes = {
|
||||||
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
|
rotationCenterX: PropTypes.number,
|
||||||
|
rotationCenterY: PropTypes.number,
|
||||||
|
svg: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
export default PaintEditorComponent;
|
export default PaintEditorComponent;
|
||||||
|
|
|
@ -24,9 +24,13 @@ class Blobbiness {
|
||||||
return 9;
|
return 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor () {
|
/**
|
||||||
|
* @param {function} updateCallback call when the drawing has changed to let listeners know
|
||||||
|
*/
|
||||||
|
constructor (updateCallback) {
|
||||||
this.broadBrushHelper = new BroadBrushHelper();
|
this.broadBrushHelper = new BroadBrushHelper();
|
||||||
this.segmentBrushHelper = new SegmentBrushHelper();
|
this.segmentBrushHelper = new SegmentBrushHelper();
|
||||||
|
this.updateCallback = updateCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -113,6 +117,9 @@ class Blobbiness {
|
||||||
blob.mergeBrush(lastPath);
|
blob.mergeBrush(lastPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blob.cursorPreview.visible = false;
|
||||||
|
blob.updateCallback();
|
||||||
|
blob.cursorPreview.visible = true;
|
||||||
blob.cursorPreview.bringToFront();
|
blob.cursorPreview.bringToFront();
|
||||||
blob.cursorPreview.position = event.point;
|
blob.cursorPreview.position = event.point;
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ class BrushMode extends React.Component {
|
||||||
'deactivateTool',
|
'deactivateTool',
|
||||||
'onScroll'
|
'onScroll'
|
||||||
]);
|
]);
|
||||||
this.blob = new Blobbiness();
|
this.blob = new Blobbiness(this.props.onUpdateSvg);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
if (this.props.isBrushModeActive) {
|
if (this.props.isBrushModeActive) {
|
||||||
|
@ -70,7 +70,8 @@ BrushMode.propTypes = {
|
||||||
canvas: PropTypes.instanceOf(Element).isRequired,
|
canvas: PropTypes.instanceOf(Element).isRequired,
|
||||||
changeBrushSize: PropTypes.func.isRequired,
|
changeBrushSize: PropTypes.func.isRequired,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
isBrushModeActive: PropTypes.bool.isRequired
|
isBrushModeActive: PropTypes.bool.isRequired,
|
||||||
|
onUpdateSvg: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
|
@ -16,7 +16,7 @@ class EraserMode extends React.Component {
|
||||||
'deactivateTool',
|
'deactivateTool',
|
||||||
'onScroll'
|
'onScroll'
|
||||||
]);
|
]);
|
||||||
this.blob = new Blobbiness();
|
this.blob = new Blobbiness(this.props.onUpdateSvg);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
if (this.props.isEraserModeActive) {
|
if (this.props.isEraserModeActive) {
|
||||||
|
@ -66,7 +66,8 @@ EraserMode.propTypes = {
|
||||||
brushSize: PropTypes.number.isRequired
|
brushSize: PropTypes.number.isRequired
|
||||||
}),
|
}),
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
isEraserModeActive: PropTypes.bool.isRequired
|
isEraserModeActive: PropTypes.bool.isRequired,
|
||||||
|
onUpdateSvg: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
|
@ -209,6 +209,7 @@ class LineMode extends React.Component {
|
||||||
}
|
}
|
||||||
this.hitResult = null;
|
this.hitResult = null;
|
||||||
}
|
}
|
||||||
|
this.props.onUpdateSvg();
|
||||||
|
|
||||||
// TODO add back undo
|
// TODO add back undo
|
||||||
// if (this.path) {
|
// if (this.path) {
|
||||||
|
@ -280,7 +281,8 @@ LineMode.propTypes = {
|
||||||
isLineModeActive: PropTypes.bool.isRequired,
|
isLineModeActive: PropTypes.bool.isRequired,
|
||||||
lineModeState: PropTypes.shape({
|
lineModeState: PropTypes.shape({
|
||||||
lineWidth: PropTypes.number.isRequired
|
lineWidth: PropTypes.number.isRequired
|
||||||
})
|
}),
|
||||||
|
onUpdateSvg: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
|
@ -4,23 +4,50 @@ import PaintEditorComponent from '../components/paint-editor.jsx';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import Modes from '../modes/modes';
|
import Modes from '../modes/modes';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
import bindAll from 'lodash.bindall';
|
||||||
|
import paper from 'paper';
|
||||||
|
|
||||||
class PaintEditor extends React.Component {
|
class PaintEditor extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleUpdateSvg'
|
||||||
|
]);
|
||||||
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
document.addEventListener('keydown', this.props.onKeyPress);
|
document.addEventListener('keydown', this.props.onKeyPress);
|
||||||
}
|
}
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
document.removeEventListener('keydown', this.props.onKeyPress);
|
document.removeEventListener('keydown', this.props.onKeyPress);
|
||||||
}
|
}
|
||||||
|
handleUpdateSvg () {
|
||||||
|
const bounds = paper.project.activeLayer.bounds;
|
||||||
|
this.props.onUpdateSvg(
|
||||||
|
paper.project.exportSVG({
|
||||||
|
asString: true,
|
||||||
|
matrix: new paper.Matrix().translate(-bounds.x, -bounds.y)
|
||||||
|
}),
|
||||||
|
paper.project.view.center.x - bounds.x,
|
||||||
|
paper.project.view.center.y - bounds.y);
|
||||||
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<PaintEditorComponent />
|
<PaintEditorComponent
|
||||||
|
rotationCenterX={this.props.rotationCenterX}
|
||||||
|
rotationCenterY={this.props.rotationCenterY}
|
||||||
|
svg={this.props.svg}
|
||||||
|
onUpdateSvg={this.handleUpdateSvg}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PaintEditor.propTypes = {
|
PaintEditor.propTypes = {
|
||||||
onKeyPress: PropTypes.func.isRequired
|
onKeyPress: PropTypes.func.isRequired,
|
||||||
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
|
rotationCenterX: PropTypes.number,
|
||||||
|
rotationCenterY: PropTypes.number,
|
||||||
|
svg: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -7,27 +7,50 @@ class PaperCanvas extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'setCanvas'
|
'setCanvas',
|
||||||
|
'importSvg'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
paper.setup(this.canvas);
|
paper.setup(this.canvas);
|
||||||
// Create a Paper.js Path to draw a line into it:
|
if (this.props.svg) {
|
||||||
const path = new paper.Path();
|
this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY);
|
||||||
// Give the stroke a color
|
}
|
||||||
path.strokeColor = 'black';
|
}
|
||||||
const start = new paper.Point(100, 100);
|
componentWillReceiveProps (newProps) {
|
||||||
// Move to start and draw a line from there
|
paper.project.activeLayer.removeChildren();
|
||||||
path.moveTo(start);
|
this.importSvg(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY);
|
||||||
// Note that the plus operator on Point objects does not work
|
|
||||||
// in JavaScript. Instead, we need to call the add() function:
|
|
||||||
path.lineTo(start.add([200, -50]));
|
|
||||||
// Draw the view now:
|
|
||||||
paper.view.draw();
|
|
||||||
}
|
}
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
paper.remove();
|
paper.remove();
|
||||||
}
|
}
|
||||||
|
importSvg (svg, rotationCenterX, rotationCenterY) {
|
||||||
|
const imported = paper.project.importSVG(svg,
|
||||||
|
{
|
||||||
|
expandShapes: true,
|
||||||
|
onLoad: function (item) {
|
||||||
|
// Remove viewbox
|
||||||
|
if (item.clipped) {
|
||||||
|
item.clipped = false;
|
||||||
|
// Consider removing clip mask here?
|
||||||
|
}
|
||||||
|
while (item.reduce() !== item) {
|
||||||
|
item = item.reduce();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (typeof rotationCenterX !== 'undefined' && typeof rotationCenterY !== 'undefined') {
|
||||||
|
imported.position =
|
||||||
|
paper.project.view.center
|
||||||
|
.add(imported.bounds.width / 2, imported.bounds.height / 2)
|
||||||
|
.subtract(rotationCenterX, rotationCenterY);
|
||||||
|
} else {
|
||||||
|
// Center
|
||||||
|
imported.position = paper.project.view.center;
|
||||||
|
}
|
||||||
|
|
||||||
|
paper.project.view.update();
|
||||||
|
}
|
||||||
setCanvas (canvas) {
|
setCanvas (canvas) {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
if (this.props.canvasRef) {
|
if (this.props.canvasRef) {
|
||||||
|
@ -44,7 +67,10 @@ class PaperCanvas extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
PaperCanvas.propTypes = {
|
PaperCanvas.propTypes = {
|
||||||
canvasRef: PropTypes.func
|
canvasRef: PropTypes.func,
|
||||||
|
rotationCenterX: PropTypes.number,
|
||||||
|
rotationCenterY: PropTypes.number,
|
||||||
|
svg: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PaperCanvas;
|
export default PaperCanvas;
|
||||||
|
|
|
@ -13,10 +13,28 @@ const store = createStore(
|
||||||
intlInitialState,
|
intlInitialState,
|
||||||
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||||
);
|
);
|
||||||
|
const svgString =
|
||||||
|
'<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"' +
|
||||||
|
' x="0px" y="0px" width="32px" height="32px" viewBox="0.5 384.5 32 32"' +
|
||||||
|
' enable-background="new 0.5 384.5 32 32" xml:space="preserve">' +
|
||||||
|
'<path fill="none" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M7.5,392.241h7.269' +
|
||||||
|
'c4.571,0,8.231,5.555,8.231,10.123v7.377"/>' +
|
||||||
|
'<polyline points="10.689,399.492 3.193,391.997 10.689,384.5 "/>' +
|
||||||
|
'<polyline points="30.185,405.995 22.689,413.491 15.192,405.995 "/>' +
|
||||||
|
'</svg>';
|
||||||
|
const onUpdateSvg = function (newSvgString, rotationCenterX, rotationCenterY) {
|
||||||
|
console.log(newSvgString);
|
||||||
|
console.log(`rotationCenterX: ${rotationCenterX} rotationCenterY: ${rotationCenterY}`);
|
||||||
|
};
|
||||||
ReactDOM.render((
|
ReactDOM.render((
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<IntlProvider>
|
<IntlProvider>
|
||||||
<PaintEditor />
|
<PaintEditor
|
||||||
|
rotationCenterX={0}
|
||||||
|
rotationCenterY={0}
|
||||||
|
svg={svgString}
|
||||||
|
onUpdateSvg={onUpdateSvg}
|
||||||
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
), appTarget);
|
), appTarget);
|
||||||
|
|
Loading…
Reference in a new issue