Merge pull request #975 from fsih/fillSpace

Stretch to fill available space
This commit is contained in:
DD Liu 2020-05-14 14:25:12 -04:00 committed by GitHub
commit 616ed9b4d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 463 additions and 137 deletions

View file

@ -1,6 +1,7 @@
@import "../../css/units"; @import "../../css/units";
.button-group { .button-group {
display: flex; display: inline-flex;
flex-direction: row;
padding: 0 $grid-unit; padding: 0 $grid-unit;
} }

View file

@ -9,8 +9,6 @@ $arrow-border-width: 14px;
min-width: 3.5rem; min-width: 3.5rem;
color: $motion-primary; color: $motion-primary;
padding: .5rem; padding: .5rem;
display: flex;
align-items: center;
} }
.mod-open { .mod-open {

View file

@ -4,12 +4,7 @@ See https://github.com/LLK/scratch-paint/issues/13 */
@import "../../css/units.css"; @import "../../css/units.css";
@import "../../css/colors.css"; @import "../../css/colors.css";
@import "../input-group/input-group.css";
.input-group {
display: inline-flex;
flex-direction: row;
align-items: center;
}
.input-label, .input-label-secondary { .input-label, .input-label-secondary {
font-size: 0.625rem; font-size: 0.625rem;

View file

@ -1,5 +1,11 @@
@import '../../css/units.css'; @import '../../css/units.css';
.input-group {
display: inline-flex;
flex-direction: row;
align-items: center;
}
[dir="ltr"] .input-group + .input-group { [dir="ltr"] .input-group + .input-group {
margin-left: calc(2 * $grid-unit); margin-left: calc(2 * $grid-unit);
} }
@ -8,10 +14,6 @@
margin-right: calc(2 * $grid-unit); margin-right: calc(2 * $grid-unit);
} }
.input-group {
display:flex;
}
.disabled { .disabled {
opacity: 0.3; opacity: 0.3;
/* Prevent any user actions */ /* Prevent any user actions */

View file

@ -2,6 +2,8 @@
@import "../../css/units.css"; @import "../../css/units.css";
.editor-container { .editor-container {
width: 100%;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: calc(3 * $grid-unit); padding: calc(3 * $grid-unit);
@ -20,8 +22,10 @@
.top-align-row { .top-align-row {
display: flex; display: flex;
padding-top: calc(5 * $grid-unit);
flex-direction: row; flex-direction: row;
height: 100%;
padding-top: calc(5 * $grid-unit);
min-width: 524px;
} }
.row + .row { .row + .row {
@ -114,9 +118,19 @@ $border-radius: 0.25rem;
margin-left: calc(2 * $grid-unit); margin-left: calc(2 * $grid-unit);
} }
.controls-container {
width: 100%;
display: flex;
flex-flow: column;
flex-grow: 1;
margin-left: calc(2 * $grid-unit);
margin-right: calc(2 * $grid-unit);
}
.canvas-container { .canvas-container {
width: 480px; width: 100%;
height: 360px; flex-grow: 1;
min-width: 402px; /* Leave room for the border */
box-sizing: content-box; box-sizing: content-box;
border: 1px solid #e8edf1; border: 1px solid #e8edf1;
border-radius: .25rem; border-radius: .25rem;
@ -126,7 +140,7 @@ $border-radius: 0.25rem;
.mode-selector { .mode-selector {
display: flex; display: flex;
max-width: 6rem; max-width: 7.5rem;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
@ -134,14 +148,6 @@ $border-radius: 0.25rem;
justify-content: space-between; justify-content: space-between;
} }
[dir="ltr"] .mode-selector {
margin-right: calc(2 * $grid-unit);
}
[dir="rtl"] .mode-selector {
margin-left: calc(2 * $grid-unit);
}
.zoom-controls { .zoom-controls {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
@ -158,7 +164,8 @@ $border-radius: 0.25rem;
.canvas-controls { .canvas-controls {
display: flex; display: flex;
margin-top: .25rem; height: 36px;
margin-top: $grid-unit;
justify-content: space-between; justify-content: space-between;
} }
@ -188,10 +195,14 @@ $border-radius: 0.25rem;
} }
.mode-selector { .mode-selector {
margin-right: $grid-unit;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
} }
.controls-container {
margin-right: $grid-unit;
margin-left: $grid-unit;
}
} }
.text-area { .text-area {

View file

@ -196,7 +196,7 @@ const PaintEditorComponent = props => (
</div> </div>
) : null} ) : null}
<div> <div className={styles.controlsContainer}>
{/* Canvas */} {/* Canvas */}
<ScrollableCanvas <ScrollableCanvas
canvas={props.canvas} canvas={props.canvas}

View file

@ -1,5 +1,5 @@
$scrollbar-size: 8px; $scrollbar-size: 8px;
$scrollbar-padding: 1px; $scrollbar-padding: 4px;
.vertical-scrollbar, .horizontal-scrollbar { .vertical-scrollbar, .horizontal-scrollbar {
position: absolute; position: absolute;
@ -10,17 +10,17 @@ $scrollbar-padding: 1px;
.vertical-scrollbar-wrapper { .vertical-scrollbar-wrapper {
position: absolute; position: absolute;
width: $scrollbar-size; width: $scrollbar-size;
right: 0; right: $scrollbar-padding;
top: $scrollbar-padding; top: $scrollbar-padding;
height: calc(100% - $scrollbar-size - $scrollbar-padding); height: calc(100% - $scrollbar-size - 2 * $scrollbar-padding);
} }
.horizontal-scrollbar-wrapper { .horizontal-scrollbar-wrapper {
position: absolute; position: absolute;
height: $scrollbar-size; height: $scrollbar-size;
left: $scrollbar-padding; left: $scrollbar-padding;
bottom: 0; bottom: $scrollbar-padding;
width: calc(100% - $scrollbar-size - $scrollbar-padding); width: calc(100% - $scrollbar-size - 2 * $scrollbar-padding);
} }
.vertical-scrollbar { .vertical-scrollbar {

View file

@ -131,6 +131,7 @@ const mapStateToProps = state => ({
shouldShowGradientTools: state.scratchPaint.mode === Modes.SELECT || shouldShowGradientTools: state.scratchPaint.mode === Modes.SELECT ||
state.scratchPaint.mode === Modes.RESHAPE || state.scratchPaint.mode === Modes.RESHAPE ||
state.scratchPaint.mode === Modes.FILL || state.scratchPaint.mode === Modes.FILL ||
state.scratchPaint.mode === Modes.BIT_SELECT ||
state.scratchPaint.mode === Modes.BIT_FILL, state.scratchPaint.mode === Modes.BIT_FILL,
textEditTarget: state.scratchPaint.textEditTarget textEditTarget: state.scratchPaint.textEditTarget
}); });

View file

@ -20,7 +20,7 @@ import {setLayout} from '../reducers/layout';
import {getSelectedLeafItems} from '../helper/selection'; import {getSelectedLeafItems} from '../helper/selection';
import {convertToBitmap, convertToVector} from '../helper/bitmap'; import {convertToBitmap, convertToVector} from '../helper/bitmap';
import {resetZoom, zoomOnSelection} from '../helper/view'; import {resetZoom, zoomOnSelection, OUTERMOST_ZOOM_LEVEL} from '../helper/view';
import EyeDropperTool from '../helper/tools/eye-dropper'; import EyeDropperTool from '../helper/tools/eye-dropper';
import Modes from '../lib/modes'; import Modes from '../lib/modes';
@ -211,7 +211,13 @@ class PaintEditor extends React.Component {
} }
} }
handleZoomIn () { handleZoomIn () {
zoomOnSelection(PaintEditor.ZOOM_INCREMENT); // Make the "next step" after the outermost zoom level be the default
// zoom level (0.5)
let zoomIncrement = PaintEditor.ZOOM_INCREMENT;
if (paper.view.zoom === OUTERMOST_ZOOM_LEVEL) {
zoomIncrement = 0.5 - OUTERMOST_ZOOM_LEVEL;
}
zoomOnSelection(zoomIncrement);
this.props.updateViewBounds(paper.view.matrix); this.props.updateViewBounds(paper.view.matrix);
this.handleSetSelectedItems(); this.handleSetSelectedItems();
} }

View file

@ -1,7 +1,9 @@
.paper-canvas { .paper-canvas {
width: 480px; top: 1px; /* leave room for the border */
height: 360px; left: 1px;
width: calc(100% - 2px);
height: calc(100% - 2px);
margin: auto; margin: auto;
position: absolute; position: absolute;
background-color: #fff; background-color: #D9E3F2;
} }

View file

@ -9,9 +9,10 @@ import log from '../log/log';
import {performSnapshot} from '../helper/undo'; import {performSnapshot} from '../helper/undo';
import {undoSnapshot, clearUndoState} from '../reducers/undo'; import {undoSnapshot, clearUndoState} from '../reducers/undo';
import {isGroup, ungroupItems} from '../helper/group'; import {isGroup, ungroupItems} from '../helper/group';
import {clearRaster, getRaster, setupLayers} from '../helper/layer'; import {clearRaster, convertBackgroundGuideLayer, getRaster, setupLayers} from '../helper/layer';
import {clearSelectedItems} from '../reducers/selected-items'; import {clearSelectedItems} from '../reducers/selected-items';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, resetZoom, resizeCrosshair, zoomToFit} from '../helper/view'; import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER, MAX_WORKSPACE_BOUNDS} from '../helper/view';
import {clampViewBounds, resetZoom, setWorkspaceBounds, zoomToFit, resizeCrosshair} from '../helper/view';
import {ensureClockwise, scaleWithStrokes} from '../helper/math'; import {ensureClockwise, scaleWithStrokes} from '../helper/math';
import {clearHoveredItem} from '../reducers/hover'; import {clearHoveredItem} from '../reducers/hover';
import {clearPasteOffset} from '../reducers/clipboard'; import {clearPasteOffset} from '../reducers/clipboard';
@ -30,13 +31,15 @@ class PaperCanvas extends React.Component {
'importSvg', 'importSvg',
'initializeSvg', 'initializeSvg',
'maybeZoomToFit', 'maybeZoomToFit',
'switchCostume' 'switchCostume',
'onViewResize',
'recalibrateSize'
]); ]);
} }
componentDidMount () { componentDidMount () {
paper.setup(this.canvas); paper.setup(this.canvas);
paper.view.on('resize', this.onViewResize);
resetZoom(); resetZoom();
this.props.updateViewBounds(paper.view.matrix);
if (this.props.zoomLevelId) { if (this.props.zoomLevelId) {
this.props.setZoomLevelId(this.props.zoomLevelId); this.props.setZoomLevelId(this.props.zoomLevelId);
if (this.props.zoomLevels[this.props.zoomLevelId]) { if (this.props.zoomLevels[this.props.zoomLevelId]) {
@ -46,6 +49,8 @@ class PaperCanvas extends React.Component {
// Zoom to fit true means find a comfortable zoom level for viewing the costume // Zoom to fit true means find a comfortable zoom level for viewing the costume
this.shouldZoomToFit = true; this.shouldZoomToFit = true;
} }
} else {
this.props.updateViewBounds(paper.view.matrix);
} }
const context = this.canvas.getContext('2d'); const context = this.canvas.getContext('2d');
@ -55,7 +60,7 @@ class PaperCanvas extends React.Component {
// Don't show handles by default // Don't show handles by default
paper.settings.handleSize = 0; paper.settings.handleSize = 0;
// Make layers. // Make layers.
setupLayers(); setupLayers(this.props.format);
this.importImage( this.importImage(
this.props.imageFormat, this.props.image, this.props.rotationCenterX, this.props.rotationCenterY); this.props.imageFormat, this.props.image, this.props.rotationCenterX, this.props.rotationCenterY);
} }
@ -65,10 +70,17 @@ class PaperCanvas extends React.Component {
newProps.rotationCenterX, newProps.rotationCenterY, newProps.rotationCenterX, newProps.rotationCenterY,
this.props.zoomLevelId, newProps.zoomLevelId); this.props.zoomLevelId, newProps.zoomLevelId);
} }
if (this.props.format !== newProps.format) {
this.recalibrateSize();
convertBackgroundGuideLayer(newProps.format);
}
} }
componentWillUnmount () { componentWillUnmount () {
this.clearQueuedImport(); this.clearQueuedImport();
this.props.saveZoomLevel(); // shouldZoomToFit means the zoom level hasn't been initialized yet
if (!this.shouldZoomToFit) {
this.props.saveZoomLevel();
}
paper.remove(); paper.remove();
} }
clearQueuedImport () { clearQueuedImport () {
@ -97,7 +109,9 @@ class PaperCanvas extends React.Component {
for (const layer of paper.project.layers) { for (const layer of paper.project.layers) {
if (layer.data.isRasterLayer) { if (layer.data.isRasterLayer) {
clearRaster(); clearRaster();
} else if (!layer.data.isBackgroundGuideLayer && !layer.data.isDragCrosshairLayer) { } else if (!layer.data.isBackgroundGuideLayer &&
!layer.data.isDragCrosshairLayer &&
!layer.data.isOutlineLayer) {
layer.removeChildren(); layer.removeChildren();
} }
} }
@ -114,12 +128,20 @@ class PaperCanvas extends React.Component {
if (!image) { if (!image) {
this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT); this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT);
performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT); performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT);
this.recalibrateSize();
return; return;
} }
if (format === 'jpg' || format === 'png') { if (format === 'jpg' || format === 'png') {
// import bitmap // import bitmap
this.props.changeFormat(Formats.BITMAP_SKIP_CONVERT); this.props.changeFormat(Formats.BITMAP_SKIP_CONVERT);
const mask = new paper.Shape.Rectangle(getRaster().getBounds());
mask.guide = true;
mask.locked = true;
mask.setPosition(CENTER);
mask.clipMask = true;
const imgElement = new Image(); const imgElement = new Image();
this.queuedImageToLoad = imgElement; this.queuedImageToLoad = imgElement;
imgElement.onload = () => { imgElement.onload = () => {
@ -134,8 +156,10 @@ class PaperCanvas extends React.Component {
imgElement, imgElement,
(ART_BOARD_WIDTH / 2) - rotationCenterX, (ART_BOARD_WIDTH / 2) - rotationCenterX,
(ART_BOARD_HEIGHT / 2) - rotationCenterY); (ART_BOARD_HEIGHT / 2) - rotationCenterY);
this.maybeZoomToFit(true /* isBitmap */); this.maybeZoomToFit(true /* isBitmap */);
performSnapshot(this.props.undoSnapshot, Formats.BITMAP_SKIP_CONVERT); performSnapshot(this.props.undoSnapshot, Formats.BITMAP_SKIP_CONVERT);
this.recalibrateSize();
}; };
imgElement.src = image; imgElement.src = image;
} else if (format === 'svg') { } else if (format === 'svg') {
@ -145,6 +169,7 @@ class PaperCanvas extends React.Component {
log.error(`Didn't recognize format: ${format}. Use 'jpg', 'png' or 'svg'.`); log.error(`Didn't recognize format: ${format}. Use 'jpg', 'png' or 'svg'.`);
this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT); this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT);
performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT); performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT);
this.recalibrateSize();
} }
} }
maybeZoomToFit (isBitmapMode) { maybeZoomToFit (isBitmapMode) {
@ -154,9 +179,10 @@ class PaperCanvas extends React.Component {
resizeCrosshair(); resizeCrosshair();
} else if (this.shouldZoomToFit === true) { } else if (this.shouldZoomToFit === true) {
zoomToFit(isBitmapMode); zoomToFit(isBitmapMode);
this.props.updateViewBounds(paper.view.matrix);
} }
this.shouldZoomToFit = false; this.shouldZoomToFit = false;
setWorkspaceBounds();
this.props.updateViewBounds(paper.view.matrix);
} }
importSvg (svg, rotationCenterX, rotationCenterY) { importSvg (svg, rotationCenterX, rotationCenterY) {
const paperCanvas = this; const paperCanvas = this;
@ -200,6 +226,15 @@ class PaperCanvas extends React.Component {
// positioned incorrectly // positioned incorrectly
paperCanvas.queuedImport = paperCanvas.queuedImport =
window.setTimeout(() => { window.setTimeout(() => {
// Detached
if (!paper.view) return;
// Prevent blurriness caused if the "CSS size" of the element is a float--
// setting canvas dimensions to floats floors them, but we need to round instead
const elemSize = paper.DomElement.getSize(paper.view.element);
elemSize.width = Math.round(elemSize.width);
elemSize.height = Math.round(elemSize.height);
paper.view.setViewSize(elemSize);
paperCanvas.props.updateViewBounds(paper.view.matrix);
paperCanvas.initializeSvg(item, rotationCenterX, rotationCenterY, viewBox); paperCanvas.initializeSvg(item, rotationCenterX, rotationCenterY, viewBox);
}, 0); }, 0);
} }
@ -210,18 +245,28 @@ class PaperCanvas extends React.Component {
const itemWidth = item.bounds.width; const itemWidth = item.bounds.width;
const itemHeight = item.bounds.height; const itemHeight = item.bounds.height;
// Remove viewbox // Get reference to viewbox
let mask;
if (item.clipped) { if (item.clipped) {
let mask;
for (const child of item.children) { for (const child of item.children) {
if (child.isClipMask()) { if (child.isClipMask()) {
mask = child; mask = child;
break; break;
} }
} }
item.clipped = false; mask.clipMask = false;
mask.remove(); } else {
mask = new paper.Shape.Rectangle(item.bounds);
} }
mask.guide = true;
mask.locked = true;
mask.matrix = new paper.Matrix(); // Identity
// Set the artwork to get clipped at the max costume size
mask.size.height = MAX_WORKSPACE_BOUNDS.height;
mask.size.width = MAX_WORKSPACE_BOUNDS.width;
mask.setPosition(CENTER);
paper.project.activeLayer.addChild(mask);
mask.clipMask = true;
// Reduce single item nested in groups // Reduce single item nested in groups
if (item instanceof paper.Group && item.children.length === 1) { if (item instanceof paper.Group && item.children.length === 1) {
@ -231,18 +276,18 @@ class PaperCanvas extends React.Component {
ensureClockwise(item); ensureClockwise(item);
scaleWithStrokes(item, 2, new paper.Point()); // Import at 2x scaleWithStrokes(item, 2, new paper.Point()); // Import at 2x
// Apply rotation center
if (typeof rotationCenterX !== 'undefined' && typeof rotationCenterY !== 'undefined') { if (typeof rotationCenterX !== 'undefined' && typeof rotationCenterY !== 'undefined') {
let rotationPoint = new paper.Point(rotationCenterX, rotationCenterY); let rotationPoint = new paper.Point(rotationCenterX, rotationCenterY);
if (viewBox && viewBox.length >= 2 && !isNaN(viewBox[0]) && !isNaN(viewBox[1])) { if (viewBox && viewBox.length >= 2 && !isNaN(viewBox[0]) && !isNaN(viewBox[1])) {
rotationPoint = rotationPoint.subtract(viewBox[0], viewBox[1]); rotationPoint = rotationPoint.subtract(viewBox[0], viewBox[1]);
} }
item.translate(new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2) item.translate(CENTER.subtract(rotationPoint.multiply(2)));
.subtract(rotationPoint.multiply(2)));
} else { } else {
// Center // Center
item.translate(new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2) item.translate(CENTER.subtract(itemWidth, itemHeight));
.subtract(itemWidth, itemHeight));
} }
paper.project.activeLayer.insertChild(0, item); paper.project.activeLayer.insertChild(0, item);
if (isGroup(item)) { if (isGroup(item)) {
// Fixes an issue where we may export empty groups // Fixes an issue where we may export empty groups
@ -253,9 +298,25 @@ class PaperCanvas extends React.Component {
} }
ungroupItems([item]); ungroupItems([item]);
} }
performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT); performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT);
this.maybeZoomToFit(); this.maybeZoomToFit();
} }
onViewResize () {
setWorkspaceBounds(true /* clipEmpty */);
clampViewBounds();
// Fix incorrect paper canvas scale on browser zoom reset
this.recalibrateSize();
this.props.updateViewBounds(paper.view.matrix);
}
recalibrateSize () {
// Sets the size that Paper thinks the canvas is to the size the canvas element actually is.
// When these are out of sync, the mouse events in the paint editor don't line up correctly.
window.setTimeout(() => {
if (!paper.view) return;
paper.view.setViewSize(paper.DomElement.getSize(paper.view.element));
});
}
setCanvas (canvas) { setCanvas (canvas) {
this.canvas = canvas; this.canvas = canvas;
if (this.props.canvasRef) { if (this.props.canvasRef) {
@ -266,10 +327,9 @@ class PaperCanvas extends React.Component {
return ( return (
<canvas <canvas
className={styles.paperCanvas} className={styles.paperCanvas}
height="360px"
ref={this.setCanvas} ref={this.setCanvas}
style={{cursor: this.props.cursor}} style={{cursor: this.props.cursor}}
width="480px" resize="true"
/> />
); );
} }
@ -283,6 +343,7 @@ PaperCanvas.propTypes = {
clearSelectedItems: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired,
clearUndo: PropTypes.func.isRequired, clearUndo: PropTypes.func.isRequired,
cursor: PropTypes.string, cursor: PropTypes.string,
format: PropTypes.oneOf(Object.keys(Formats)),
image: PropTypes.oneOfType([ image: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.instanceOf(HTMLImageElement) PropTypes.instanceOf(HTMLImageElement)

View file

@ -5,7 +5,7 @@ import React from 'react';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import ScrollableCanvasComponent from '../components/scrollable-canvas/scrollable-canvas.jsx'; import ScrollableCanvasComponent from '../components/scrollable-canvas/scrollable-canvas.jsx';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, clampViewBounds, pan, zoomOnFixedPoint} from '../helper/view'; import {clampViewBounds, pan, zoomOnFixedPoint, getWorkspaceBounds} from '../helper/view';
import {updateViewBounds} from '../reducers/view-bounds'; import {updateViewBounds} from '../reducers/view-bounds';
import {redrawSelectionBox} from '../reducers/selected-items'; import {redrawSelectionBox} from '../reducers/selected-items';
@ -132,11 +132,12 @@ class ScrollableCanvas extends React.Component {
let topPercent = 0; let topPercent = 0;
let leftPercent = 0; let leftPercent = 0;
if (paper.project) { if (paper.project) {
const bounds = getWorkspaceBounds();
const {x, y, width, height} = paper.view.bounds; const {x, y, width, height} = paper.view.bounds;
widthPercent = Math.min(100, 100 * width / ART_BOARD_WIDTH); widthPercent = Math.min(100, 100 * width / bounds.width);
heightPercent = Math.min(100, 100 * height / ART_BOARD_HEIGHT); heightPercent = Math.min(100, 100 * height / bounds.height);
const centerX = (x + (width / 2)) / ART_BOARD_WIDTH; const centerX = (x + (width / 2) - bounds.x) / bounds.width;
const centerY = (y + (height / 2)) / ART_BOARD_HEIGHT; const centerY = (y + (height / 2) - bounds.y) / bounds.height;
topPercent = Math.max(0, (100 * centerY) - (heightPercent / 2)); topPercent = Math.max(0, (100 * centerY) - (heightPercent / 2));
leftPercent = Math.max(0, (100 * centerX) - (widthPercent / 2)); leftPercent = Math.max(0, (100 * centerX) - (widthPercent / 2));
} }

View file

@ -32,7 +32,7 @@ class OvalTool extends paper.Tool {
setCursor, setCursor,
onUpdateImage onUpdateImage
); );
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); const nudgeTool = new NudgeTool(Modes.BIT_OVAL, this.boundingBoxTool, onUpdateImage);
// We have to set these functions instead of just declaring them because // We have to set these functions instead of just declaring them because
// paper.js tools hook up the listeners in the setter functions. // paper.js tools hook up the listeners in the setter functions.

View file

@ -32,7 +32,7 @@ class RectTool extends paper.Tool {
setCursor, setCursor,
onUpdateImage onUpdateImage
); );
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); const nudgeTool = new NudgeTool(Modes.BIT_RECT, this.boundingBoxTool, onUpdateImage);
// We have to set these functions instead of just declaring them because // We have to set these functions instead of just declaring them because
// paper.js tools hook up the listeners in the setter functions. // paper.js tools hook up the listeners in the setter functions.

View file

@ -30,14 +30,14 @@ class SelectTool extends paper.Tool {
super(); super();
this.onUpdateImage = onUpdateImage; this.onUpdateImage = onUpdateImage;
this.boundingBoxTool = new BoundingBoxTool( this.boundingBoxTool = new BoundingBoxTool(
Modes.SELECT, Modes.BIT_SELECT,
setSelectedItems, setSelectedItems,
clearSelectedItems, clearSelectedItems,
setCursor, setCursor,
onUpdateImage onUpdateImage
); );
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); const nudgeTool = new NudgeTool(Modes.BIT_SELECT, this.boundingBoxTool, onUpdateImage);
this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems); this.selectionBoxTool = new SelectionBoxTool(Modes.BIT_SELECT, setSelectedItems, clearSelectedItems);
this.selectionBoxMode = false; this.selectionBoxMode = false;
this.selection = null; this.selection = null;
this.active = false; this.active = false;

View file

@ -2,6 +2,7 @@ import paper from '@scratch/paper';
import {createCanvas, clearRaster, getRaster, hideGuideLayers, showGuideLayers} from './layer'; import {createCanvas, clearRaster, getRaster, hideGuideLayers, showGuideLayers} from './layer';
import {getGuideColor} from './guides'; import {getGuideColor} from './guides';
import {clearSelection} from './selection'; import {clearSelection} from './selection';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER, MAX_WORKSPACE_BOUNDS} from './view';
import {inlineSvgFonts} from 'scratch-svg-renderer'; import {inlineSvgFonts} from 'scratch-svg-renderer';
import Formats from '../lib/format'; import Formats from '../lib/format';
@ -399,7 +400,17 @@ const convertToBitmap = function (clearSelectedItems, onUpdateImage) {
img, img,
new paper.Point(Math.floor(bounds.topLeft.x), Math.floor(bounds.topLeft.y))); new paper.Point(Math.floor(bounds.topLeft.x), Math.floor(bounds.topLeft.y)));
} }
paper.project.activeLayer.removeChildren(); for (let i = paper.project.activeLayer.children.length - 1; i >= 0; i--) {
const item = paper.project.activeLayer.children[i];
if (item.clipMask === false) {
item.remove();
} else {
// Resize mask for bitmap bounds
item.size.height = ART_BOARD_HEIGHT;
item.size.width = ART_BOARD_WIDTH;
item.setPosition(CENTER);
}
}
onUpdateImage(false /* skipSnapshot */, Formats.BITMAP /* formatOverride */); onUpdateImage(false /* skipSnapshot */, Formats.BITMAP /* formatOverride */);
}; };
img.onerror = () => { img.onerror = () => {
@ -420,7 +431,16 @@ const convertToBitmap = function (clearSelectedItems, onUpdateImage) {
const convertToVector = function (clearSelectedItems, onUpdateImage) { const convertToVector = function (clearSelectedItems, onUpdateImage) {
clearSelection(clearSelectedItems); clearSelection(clearSelectedItems);
for (const item of paper.project.activeLayer.children) {
if (item.clipMask === true) {
// Resize mask for vector bounds
item.size.height = MAX_WORKSPACE_BOUNDS.height;
item.size.width = MAX_WORKSPACE_BOUNDS.width;
item.setPosition(CENTER);
}
}
getTrimmedRaster(true /* shouldInsert */); getTrimmedRaster(true /* shouldInsert */);
clearRaster(); clearRaster();
onUpdateImage(false /* skipSnapshot */, Formats.VECTOR /* formatOverride */); onUpdateImage(false /* skipSnapshot */, Formats.VECTOR /* formatOverride */);
}; };

View file

@ -4,7 +4,7 @@ import BroadBrushHelper from './broad-brush-helper';
import SegmentBrushHelper from './segment-brush-helper'; import SegmentBrushHelper from './segment-brush-helper';
import {MIXED, styleCursorPreview} from '../../helper/style-path'; import {MIXED, styleCursorPreview} from '../../helper/style-path';
import {clearSelection, getItems} from '../../helper/selection'; import {clearSelection, getItems} from '../../helper/selection';
import {getGuideLayer} from '../../helper/layer'; import {getGuideLayer, setGuideItem} from '../../helper/layer';
import {isCompoundPathChild} from '../compound-path'; import {isCompoundPathChild} from '../compound-path';
/** /**
@ -182,6 +182,7 @@ class Blobbiness {
}); });
this.cursorPreview.parent = getGuideLayer(); this.cursorPreview.parent = getGuideLayer();
this.cursorPreview.data.isHelperItem = true; this.cursorPreview.data.isHelperItem = true;
setGuideItem(this.cursorPreview);
} }
this.cursorPreview.position = this.cursorPreviewLastPoint; this.cursorPreview.position = this.cursorPreviewLastPoint;
this.cursorPreview.radius = this.options.brushSize / 2; this.cursorPreview.radius = this.options.brushSize / 2;

View file

@ -1,8 +1,10 @@
import paper from '@scratch/paper'; import paper from '@scratch/paper';
import log from '../log/log'; import log from '../log/log';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER} from './view'; import {ART_BOARD_BOUNDS, ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER, MAX_WORKSPACE_BOUNDS} from './view';
import {isGroupItem} from './item'; import {isGroupItem} from './item';
import {isBitmap, isVector} from '../lib/format';
const CHECKERBOARD_SIZE = 8;
const CROSSHAIR_SIZE = 16; const CROSSHAIR_SIZE = 16;
const CROSSHAIR_FULL_OPACITY = 0.75; const CROSSHAIR_FULL_OPACITY = 0.75;
@ -42,7 +44,7 @@ const clearRaster = function () {
raster.parent = layer; raster.parent = layer;
raster.guide = true; raster.guide = true;
raster.locked = true; raster.locked = true;
raster.position = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); raster.position = CENTER;
}; };
const getRaster = function () { const getRaster = function () {
@ -62,6 +64,15 @@ const getBackgroundGuideLayer = function () {
return _getLayer('isBackgroundGuideLayer'); return _getLayer('isBackgroundGuideLayer');
}; };
const _convertLayer = function (layer, format) {
layer.bitmapBackground.visible = isBitmap(format);
layer.vectorBackground.visible = isVector(format);
};
const convertBackgroundGuideLayer = function (format) {
_convertLayer(getBackgroundGuideLayer(), format);
};
const _makeGuideLayer = function () { const _makeGuideLayer = function () {
const guideLayer = new paper.Layer(); const guideLayer = new paper.Layer();
guideLayer.data.isGuideLayer = true; guideLayer.data.isGuideLayer = true;
@ -95,8 +106,10 @@ const setGuideItem = function (item) {
const hideGuideLayers = function (includeRaster) { const hideGuideLayers = function (includeRaster) {
const backgroundGuideLayer = getBackgroundGuideLayer(); const backgroundGuideLayer = getBackgroundGuideLayer();
const dragCrosshairLayer = getDragCrosshairLayer(); const dragCrosshairLayer = getDragCrosshairLayer();
const outlineLayer = _getLayer('isOutlineLayer');
const guideLayer = getGuideLayer(); const guideLayer = getGuideLayer();
dragCrosshairLayer.remove(); dragCrosshairLayer.remove();
outlineLayer.remove();
guideLayer.remove(); guideLayer.remove();
backgroundGuideLayer.remove(); backgroundGuideLayer.remove();
let rasterLayer; let rasterLayer;
@ -106,6 +119,7 @@ const hideGuideLayers = function (includeRaster) {
} }
return { return {
dragCrosshairLayer: dragCrosshairLayer, dragCrosshairLayer: dragCrosshairLayer,
outlineLayer: outlineLayer,
guideLayer: guideLayer, guideLayer: guideLayer,
backgroundGuideLayer: backgroundGuideLayer, backgroundGuideLayer: backgroundGuideLayer,
rasterLayer: rasterLayer rasterLayer: rasterLayer
@ -120,6 +134,7 @@ const hideGuideLayers = function (includeRaster) {
const showGuideLayers = function (guideLayers) { const showGuideLayers = function (guideLayers) {
const backgroundGuideLayer = guideLayers.backgroundGuideLayer; const backgroundGuideLayer = guideLayers.backgroundGuideLayer;
const dragCrosshairLayer = guideLayers.dragCrosshairLayer; const dragCrosshairLayer = guideLayers.dragCrosshairLayer;
const outlineLayer = guideLayers.outlineLayer;
const guideLayer = guideLayers.guideLayer; const guideLayer = guideLayers.guideLayer;
const rasterLayer = guideLayers.rasterLayer; const rasterLayer = guideLayers.rasterLayer;
if (rasterLayer && !rasterLayer.index) { if (rasterLayer && !rasterLayer.index) {
@ -134,6 +149,10 @@ const showGuideLayers = function (guideLayers) {
paper.project.addLayer(dragCrosshairLayer); paper.project.addLayer(dragCrosshairLayer);
dragCrosshairLayer.bringToFront(); dragCrosshairLayer.bringToFront();
} }
if (!outlineLayer.index) {
paper.project.addLayer(outlineLayer);
outlineLayer.bringToFront();
}
if (!guideLayer.index) { if (!guideLayer.index) {
paper.project.addLayer(guideLayer); paper.project.addLayer(guideLayer);
guideLayer.bringToFront(); guideLayer.bringToFront();
@ -157,7 +176,7 @@ const _makeRasterLayer = function () {
return rasterLayer; return rasterLayer;
}; };
const _makeBackgroundPaper = function (width, height, color) { const _makeBackgroundPaper = function (width, height, color, opacity) {
// creates a checkerboard path of width * height squares in color on white // creates a checkerboard path of width * height squares in color on white
let x = 0; let x = 0;
let y = 0; let y = 0;
@ -176,16 +195,27 @@ const _makeBackgroundPaper = function (width, height, color) {
pathPoints.push(new paper.Point(x, y)); pathPoints.push(new paper.Point(x, y));
y--; y--;
} }
const vRect = new paper.Shape.Rectangle(new paper.Point(0, 0), new paper.Point(120, 90)); const vRect = new paper.Shape.Rectangle(
new paper.Point(0, 0),
new paper.Point(ART_BOARD_WIDTH / CHECKERBOARD_SIZE, ART_BOARD_HEIGHT / CHECKERBOARD_SIZE));
vRect.fillColor = '#fff'; vRect.fillColor = '#fff';
vRect.guide = true; vRect.guide = true;
vRect.locked = true; vRect.locked = true;
vRect.position = CENTER;
const vPath = new paper.Path(pathPoints); const vPath = new paper.Path(pathPoints);
vPath.fillRule = 'evenodd'; vPath.fillRule = 'evenodd';
vPath.fillColor = color; vPath.fillColor = color;
vPath.opacity = opacity;
vPath.guide = true; vPath.guide = true;
vPath.locked = true; vPath.locked = true;
const vGroup = new paper.Group([vRect, vPath]); vPath.position = CENTER;
const mask = new paper.Shape.Rectangle(MAX_WORKSPACE_BOUNDS);
mask.position = CENTER;
mask.guide = true;
mask.locked = true;
mask.scale(1 / CHECKERBOARD_SIZE);
const vGroup = new paper.Group([vRect, vPath, mask]);
mask.clipMask = true;
return vGroup; return vGroup;
}; };
@ -230,7 +260,6 @@ const _makeCrosshair = function (opacity, parent) {
crosshair.applyMatrix = false; crosshair.applyMatrix = false;
parent.dragCrosshair = crosshair; parent.dragCrosshair = crosshair;
crosshair.scale(CROSSHAIR_SIZE / crosshair.bounds.width / paper.view.zoom); crosshair.scale(CROSSHAIR_SIZE / crosshair.bounds.width / paper.view.zoom);
}; };
const _makeDragCrosshairLayer = function () { const _makeDragCrosshairLayer = function () {
@ -241,15 +270,55 @@ const _makeDragCrosshairLayer = function () {
return dragCrosshairLayer; return dragCrosshairLayer;
}; };
const _makeBackgroundGuideLayer = function () { const _makeOutlineLayer = function () {
const outlineLayer = new paper.Layer();
const whiteRect = new paper.Shape.Rectangle(ART_BOARD_BOUNDS.expand(1));
whiteRect.strokeWidth = 2;
whiteRect.strokeColor = 'white';
setGuideItem(whiteRect);
const blueRect = new paper.Shape.Rectangle(ART_BOARD_BOUNDS.expand(5));
blueRect.strokeWidth = 2;
blueRect.strokeColor = '#4280D7';
blueRect.opacity = 0.25;
setGuideItem(blueRect);
outlineLayer.data.isOutlineLayer = true;
return outlineLayer;
};
const _makeBackgroundGuideLayer = function (format) {
const guideLayer = new paper.Layer(); const guideLayer = new paper.Layer();
guideLayer.locked = true; guideLayer.locked = true;
const vBackground = _makeBackgroundPaper(120, 90, '#E5E5E5'); const vWorkspaceBounds = new paper.Shape.Rectangle(MAX_WORKSPACE_BOUNDS);
vBackground.position = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); vWorkspaceBounds.fillColor = '#ECF1F9';
vBackground.scaling = new paper.Point(8, 8); vWorkspaceBounds.position = CENTER;
vBackground.guide = true;
vBackground.locked = true; // Add 1 to the height because it's an odd number otherwise, and we want it to be even
// so the corner of the checkerboard to line up with the center crosshair
const vBackground = _makeBackgroundPaper(
MAX_WORKSPACE_BOUNDS.width / CHECKERBOARD_SIZE,
(MAX_WORKSPACE_BOUNDS.height / CHECKERBOARD_SIZE) + 1,
'#0062ff', 0.05);
vBackground.position = CENTER;
vBackground.scaling = new paper.Point(CHECKERBOARD_SIZE, CHECKERBOARD_SIZE);
const vectorBackground = new paper.Group();
vectorBackground.addChild(vWorkspaceBounds);
vectorBackground.addChild(vBackground);
setGuideItem(vectorBackground);
guideLayer.vectorBackground = vectorBackground;
const bitmapBackground = _makeBackgroundPaper(
ART_BOARD_WIDTH / CHECKERBOARD_SIZE,
ART_BOARD_HEIGHT / CHECKERBOARD_SIZE,
'#0062ff', 0.05);
bitmapBackground.position = CENTER;
bitmapBackground.scaling = new paper.Point(CHECKERBOARD_SIZE, CHECKERBOARD_SIZE);
bitmapBackground.guide = true;
bitmapBackground.locked = true;
guideLayer.bitmapBackground = bitmapBackground;
_convertLayer(guideLayer, format);
_makeCrosshair(0.16, guideLayer); _makeCrosshair(0.16, guideLayer);
@ -257,14 +326,16 @@ const _makeBackgroundGuideLayer = function () {
return guideLayer; return guideLayer;
}; };
const setupLayers = function () { const setupLayers = function (format) {
const backgroundGuideLayer = _makeBackgroundGuideLayer(); const backgroundGuideLayer = _makeBackgroundGuideLayer(format);
_makeRasterLayer(); _makeRasterLayer();
const paintLayer = _makePaintingLayer(); const paintLayer = _makePaintingLayer();
const dragCrosshairLayer = _makeDragCrosshairLayer(); const dragCrosshairLayer = _makeDragCrosshairLayer();
const outlineLayer = _makeOutlineLayer();
const guideLayer = _makeGuideLayer(); const guideLayer = _makeGuideLayer();
backgroundGuideLayer.sendToBack(); backgroundGuideLayer.sendToBack();
dragCrosshairLayer.bringToFront(); dragCrosshairLayer.bringToFront();
outlineLayer.bringToFront();
guideLayer.bringToFront(); guideLayer.bringToFront();
paintLayer.activate(); paintLayer.activate();
}; };
@ -278,6 +349,7 @@ export {
getDragCrosshairLayer, getDragCrosshairLayer,
getGuideLayer, getGuideLayer,
getBackgroundGuideLayer, getBackgroundGuideLayer,
convertBackgroundGuideLayer,
clearRaster, clearRaster,
getRaster, getRaster,
setGuideItem, setGuideItem,

View file

@ -49,7 +49,7 @@ class BoundingBoxTool {
this.boundsScaleHandles = []; this.boundsScaleHandles = [];
this.boundsRotHandles = []; this.boundsRotHandles = [];
this._modeMap = {}; this._modeMap = {};
this._modeMap[BoundingBoxModes.SCALE] = new ScaleTool(onUpdateImage); this._modeMap[BoundingBoxModes.SCALE] = new ScaleTool(mode, onUpdateImage);
this._modeMap[BoundingBoxModes.ROTATE] = new RotateTool(onUpdateImage); this._modeMap[BoundingBoxModes.ROTATE] = new RotateTool(onUpdateImage);
this._modeMap[BoundingBoxModes.MOVE] = this._modeMap[BoundingBoxModes.MOVE] =
new MoveTool(mode, setSelectedItems, clearSelectedItems, onUpdateImage, switchToTextTool); new MoveTool(mode, setSelectedItems, clearSelectedItems, onUpdateImage, switchToTextTool);

View file

@ -1,9 +1,10 @@
import paper from '@scratch/paper'; import paper from '@scratch/paper';
import Modes from '../../lib/modes'; import Modes from '../../lib/modes';
import {BitmapModes} from '../../lib/modes';
import {isGroup} from '../group'; import {isGroup} from '../group';
import {isCompoundPathItem, getRootItem} from '../item'; import {isCompoundPathItem, getRootItem} from '../item';
import {checkPointsClose, snapDeltaToAngle} from '../math'; import {checkPointsClose, snapDeltaToAngle} from '../math';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, CENTER} from '../view'; import {getActionBounds, CENTER} from '../view';
import {clearSelection, cloneSelection, getSelectedLeafItems, getSelectedRootItems, setItemSelection} import {clearSelection, cloneSelection, getSelectedLeafItems, getSelectedRootItems, setItemSelection}
from '../selection'; from '../selection';
import {getDragCrosshairLayer, CROSSHAIR_FULL_OPACITY} from '../layer'; import {getDragCrosshairLayer, CROSSHAIR_FULL_OPACITY} from '../layer';
@ -123,8 +124,10 @@ class MoveTool {
} }
onMouseDrag (event) { onMouseDrag (event) {
const point = event.point; const point = event.point;
point.x = Math.max(0, Math.min(point.x, ART_BOARD_WIDTH)); const actionBounds = getActionBounds(this.mode in BitmapModes);
point.y = Math.max(0, Math.min(point.y, ART_BOARD_HEIGHT));
point.x = Math.max(actionBounds.left, Math.min(point.x, actionBounds.right));
point.y = Math.max(actionBounds.top, Math.min(point.y, actionBounds.bottom));
const dragVector = point.subtract(event.downPoint); const dragVector = point.subtract(event.downPoint);
let snapVector; let snapVector;
@ -180,6 +183,7 @@ class MoveTool {
(CENTER.y > bounds.bottom && CENTER.x < bounds.left) || (CENTER.y > bounds.bottom && CENTER.x < bounds.left) ||
(CENTER.y < bounds.top && CENTER.x > bounds.right) || (CENTER.y < bounds.top && CENTER.x > bounds.right) ||
(CENTER.y > bounds.bottom && CENTER.x > bounds.right)) { (CENTER.y > bounds.bottom && CENTER.x > bounds.right)) {
// rotation center is to one of the 4 corners of the selection bounding box // rotation center is to one of the 4 corners of the selection bounding box
const distX = Math.max(CENTER.x - bounds.right, bounds.left - CENTER.x); const distX = Math.max(CENTER.x - bounds.right, bounds.left - CENTER.x);
const distY = Math.max(CENTER.y - bounds.bottom, bounds.top - CENTER.y); const distY = Math.max(CENTER.y - bounds.bottom, bounds.top - CENTER.y);
@ -196,7 +200,6 @@ class MoveTool {
(1 - ((Math.abs(CENTER.x - newCenter.x) - (bounds.width / 2)) / (FADE_DISTANCE / paper.view.zoom)))); (1 - ((Math.abs(CENTER.x - newCenter.x) - (bounds.width / 2)) / (FADE_DISTANCE / paper.view.zoom))));
} // else the rotation center is within selection bounds, always show drag crosshair at full opacity } // else the rotation center is within selection bounds, always show drag crosshair at full opacity
getDragCrosshairLayer().opacity = CROSSHAIR_FULL_OPACITY * opacityMultiplier; getDragCrosshairLayer().opacity = CROSSHAIR_FULL_OPACITY * opacityMultiplier;
} }
onMouseUp () { onMouseUp () {
this.firstDrag = false; this.firstDrag = false;

View file

@ -1,6 +1,7 @@
import paper from '@scratch/paper'; import paper from '@scratch/paper';
import {getSelectedRootItems} from '../selection'; import {getSelectedRootItems} from '../selection';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; import {getActionBounds} from '../view';
import {BitmapModes} from '../../lib/modes';
const NUDGE_MORE_MULTIPLIER = 15; const NUDGE_MORE_MULTIPLIER = 15;
@ -10,12 +11,14 @@ const NUDGE_MORE_MULTIPLIER = 15;
*/ */
class NudgeTool { class NudgeTool {
/** /**
* @param {Mode} mode Paint editor mode
* @param {function} boundingBoxTool to control the bounding box * @param {function} boundingBoxTool to control the bounding box
* @param {!function} onUpdateImage A callback to call when the image visibly changes * @param {!function} onUpdateImage A callback to call when the image visibly changes
*/ */
constructor (boundingBoxTool, onUpdateImage) { constructor (mode, boundingBoxTool, onUpdateImage) {
this.boundingBoxTool = boundingBoxTool; this.boundingBoxTool = boundingBoxTool;
this.onUpdateImage = onUpdateImage; this.onUpdateImage = onUpdateImage;
this.boundingBoxTool.isBitmap = mode in BitmapModes;
} }
onKeyDown (event) { onKeyDown (event) {
if (event.event.target instanceof HTMLInputElement) { if (event.event.target instanceof HTMLInputElement) {
@ -38,16 +41,21 @@ class NudgeTool {
rect = item.bounds; rect = item.bounds;
} }
} }
const bounds = getActionBounds(this.boundingBoxTool.isBitmap);
const bottom = bounds.bottom - rect.top - 1;
const top = bounds.top - rect.bottom + 1;
const left = bounds.left - rect.right + 1;
const right = bounds.right - rect.left - 1;
let translation; let translation;
if (event.key === 'up') { if (event.key === 'up') {
translation = new paper.Point(0, -Math.min(nudgeAmount, rect.bottom - 1)); translation = new paper.Point(0, Math.min(bottom, Math.max(-nudgeAmount, top)));
} else if (event.key === 'down') { } else if (event.key === 'down') {
translation = new paper.Point(0, Math.min(nudgeAmount, ART_BOARD_HEIGHT - rect.top - 1)); translation = new paper.Point(0, Math.max(top, Math.min(nudgeAmount, bottom)));
} else if (event.key === 'left') { } else if (event.key === 'left') {
translation = new paper.Point(-Math.min(nudgeAmount, rect.right - 1), 0); translation = new paper.Point(Math.min(right, Math.max(-nudgeAmount, left)), 0);
} else if (event.key === 'right') { } else if (event.key === 'right') {
translation = new paper.Point(Math.min(nudgeAmount, ART_BOARD_WIDTH - rect.left - 1), 0); translation = new paper.Point(Math.max(left, Math.min(nudgeAmount, right)), 0);
} }
if (translation) { if (translation) {

View file

@ -1,6 +1,6 @@
import paper from '@scratch/paper'; import paper from '@scratch/paper';
import {HANDLE_RATIO, snapDeltaToAngle} from '../math'; import {HANDLE_RATIO, snapDeltaToAngle} from '../math';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; import {getActionBounds} from '../view';
import {clearSelection, getSelectedLeafItems, getSelectedSegments} from '../selection'; import {clearSelection, getSelectedLeafItems, getSelectedSegments} from '../selection';
/** Subtool of ReshapeTool for moving control points. */ /** Subtool of ReshapeTool for moving control points. */
@ -146,8 +146,9 @@ class PointTool {
this.deleteOnMouseUp = null; this.deleteOnMouseUp = null;
const point = event.point; const point = event.point;
point.x = Math.max(0, Math.min(point.x, ART_BOARD_WIDTH)); const bounds = getActionBounds();
point.y = Math.max(0, Math.min(point.y, ART_BOARD_HEIGHT)); point.x = Math.max(bounds.left, Math.min(point.x, bounds.right));
point.y = Math.max(bounds.top, Math.min(point.y, bounds.bottom));
if (!this.lastPoint) this.lastPoint = event.lastPoint; if (!this.lastPoint) this.lastPoint = event.lastPoint;
const dragVector = point.subtract(event.downPoint); const dragVector = point.subtract(event.downPoint);

View file

@ -1,6 +1,7 @@
import paper from '@scratch/paper'; import paper from '@scratch/paper';
import {getItems} from '../selection'; import {getItems} from '../selection';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; import {getActionBounds} from '../view';
import {BitmapModes} from '../../lib/modes';
/** /**
* Tool to handle scaling items by pulling on the handles around the edges of the bounding * Tool to handle scaling items by pulling on the handles around the edges of the bounding
@ -8,9 +9,11 @@ import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view';
*/ */
class ScaleTool { class ScaleTool {
/** /**
* @param {Mode} mode Paint editor mode
* @param {!function} onUpdateImage A callback to call when the image visibly changes * @param {!function} onUpdateImage A callback to call when the image visibly changes
*/ */
constructor (onUpdateImage) { constructor (mode, onUpdateImage) {
this.isBitmap = mode in BitmapModes;
this.active = false; this.active = false;
this.boundsPath = null; this.boundsPath = null;
this.pivot = null; this.pivot = null;
@ -71,8 +74,9 @@ class ScaleTool {
onMouseDrag (event) { onMouseDrag (event) {
if (!this.active) return; if (!this.active) return;
const point = event.point; const point = event.point;
point.x = Math.max(0, Math.min(point.x, ART_BOARD_WIDTH)); const bounds = getActionBounds(this.isBitmap);
point.y = Math.max(0, Math.min(point.y, ART_BOARD_HEIGHT)); point.x = Math.max(bounds.left, Math.min(point.x, bounds.right));
point.y = Math.max(bounds.top, Math.min(point.y, bounds.bottom));
if (!this.lastPoint) this.lastPoint = event.lastPoint; if (!this.lastPoint) this.lastPoint = event.lastPoint;
const delta = point.subtract(this.lastPoint); const delta = point.subtract(this.lastPoint);

View file

@ -46,7 +46,7 @@ class SelectTool extends paper.Tool {
onUpdateImage, onUpdateImage,
switchToTextTool switchToTextTool
); );
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); const nudgeTool = new NudgeTool(Modes.SELECT, this.boundingBoxTool, onUpdateImage);
this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems); this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems);
this.selectionBoxMode = false; this.selectionBoxMode = false;
this.prevHoveredItemId = null; this.prevHoveredItemId = null;

View file

@ -44,7 +44,7 @@ class SelectionBoxTool {
} }
onMouseUpBitmap (event) { onMouseUpBitmap (event) {
if (event.event.button > 0) return; // only first mouse button if (event.event.button > 0) return; // only first mouse button
if (this.selectionRect) { if (this.selectionRect && this.selectionRect.bounds.intersects(getRaster().bounds)) {
const rect = new paper.Rectangle({ const rect = new paper.Rectangle({
from: new paper.Point( from: new paper.Point(
Math.max(0, Math.round(this.selectionRect.bounds.topLeft.x)), Math.max(0, Math.round(this.selectionRect.bounds.topLeft.x)),
@ -54,10 +54,6 @@ class SelectionBoxTool {
Math.min(ART_BOARD_HEIGHT, Math.round(this.selectionRect.bounds.bottomRight.y))) Math.min(ART_BOARD_HEIGHT, Math.round(this.selectionRect.bounds.bottomRight.y)))
}); });
// Remove dotted rectangle
this.selectionRect.remove();
this.selectionRect = null;
if (rect.area) { if (rect.area) {
// Pull selected raster to active layer // Pull selected raster to active layer
const raster = getRaster().getSubRaster(rect); const raster = getRaster().getSubRaster(rect);
@ -75,6 +71,11 @@ class SelectionBoxTool {
this.setSelectedItems(); this.setSelectedItems();
} }
} }
if (this.selectionRect) {
// Remove dotted rectangle
this.selectionRect.remove();
this.selectionRect = null;
}
} }
} }

View file

@ -31,7 +31,7 @@ class OvalTool extends paper.Tool {
setCursor, setCursor,
onUpdateImage onUpdateImage
); );
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); const nudgeTool = new NudgeTool(Modes.OVAL, this.boundingBoxTool, onUpdateImage);
// We have to set these functions instead of just declaring them because // We have to set these functions instead of just declaring them because
// paper.js tools hook up the listeners in the setter functions. // paper.js tools hook up the listeners in the setter functions.

View file

@ -31,7 +31,7 @@ class RectTool extends paper.Tool {
setCursor, setCursor,
onUpdateImage onUpdateImage
); );
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); const nudgeTool = new NudgeTool(Modes.RECT, this.boundingBoxTool, onUpdateImage);
// We have to set these functions instead of just declaring them because // We have to set these functions instead of just declaring them because
// paper.js tools hook up the listeners in the setter functions. // paper.js tools hook up the listeners in the setter functions.

View file

@ -50,14 +50,15 @@ class TextTool extends paper.Tool {
this.onUpdateImage = onUpdateImage; this.onUpdateImage = onUpdateImage;
this.setTextEditTarget = setTextEditTarget; this.setTextEditTarget = setTextEditTarget;
this.changeFont = changeFont; this.changeFont = changeFont;
const paintMode = isBitmap ? Modes.BIT_TEXT : Modes.TEXT;
this.boundingBoxTool = new BoundingBoxTool( this.boundingBoxTool = new BoundingBoxTool(
Modes.TEXT, paintMode,
setSelectedItems, setSelectedItems,
clearSelectedItems, clearSelectedItems,
setCursor, setCursor,
onUpdateImage onUpdateImage
); );
this.nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); this.nudgeTool = new NudgeTool(paintMode, this.boundingBoxTool, onUpdateImage);
this.isBitmap = isBitmap; this.isBitmap = isBitmap;
// We have to set these functions instead of just declaring them because // We have to set these functions instead of just declaring them because

View file

@ -27,7 +27,9 @@ const performSnapshot = function (dispatchPerformSnapshot, format) {
const _restore = function (entry, setSelectedItems, onUpdateImage, isBitmapMode) { const _restore = function (entry, setSelectedItems, onUpdateImage, isBitmapMode) {
for (let i = paper.project.layers.length - 1; i >= 0; i--) { for (let i = paper.project.layers.length - 1; i >= 0; i--) {
const layer = paper.project.layers[i]; const layer = paper.project.layers[i];
if (!layer.data.isBackgroundGuideLayer && !layer.data.isDragCrosshairLayer) { if (!layer.data.isBackgroundGuideLayer &&
!layer.data.isDragCrosshairLayer &&
!layer.data.isOutlineLayer) {
layer.removeChildren(); layer.removeChildren();
layer.remove(); layer.remove();
} }

View file

@ -1,34 +1,96 @@
import paper from '@scratch/paper'; import paper from '@scratch/paper';
import {getSelectedRootItems} from './selection';
import {CROSSHAIR_SIZE, getBackgroundGuideLayer, getDragCrosshairLayer, getRaster} from './layer'; import {CROSSHAIR_SIZE, getBackgroundGuideLayer, getDragCrosshairLayer, getRaster} from './layer';
import {getAllRootItems, getSelectedRootItems} from './selection';
import {getHitBounds} from './bitmap'; import {getHitBounds} from './bitmap';
import log from '../log/log';
// Vectors are imported and exported at SVG_ART_BOARD size. // Vectors are imported and exported at SVG_ART_BOARD size.
// Once they are imported however, both SVGs and bitmaps are on // Once they are imported however, both SVGs and bitmaps are on
// canvases of ART_BOARD size. // canvases of ART_BOARD size.
// (This is for backwards compatibility, to handle both assets
// designed for 480 x 360, and bitmap resolution 2 bitmaps)
const SVG_ART_BOARD_WIDTH = 480; const SVG_ART_BOARD_WIDTH = 480;
const SVG_ART_BOARD_HEIGHT = 360; const SVG_ART_BOARD_HEIGHT = 360;
const ART_BOARD_WIDTH = 480 * 2; const ART_BOARD_WIDTH = SVG_ART_BOARD_WIDTH * 2;
const ART_BOARD_HEIGHT = 360 * 2; const ART_BOARD_HEIGHT = SVG_ART_BOARD_HEIGHT * 2;
const CENTER = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); const CENTER = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2);
const PADDING_PERCENT = 25; // Padding as a percent of the max of width/height of the sprite const PADDING_PERCENT = 25; // Padding as a percent of the max of width/height of the sprite
const BUFFER = 50; // Number of pixels of allowance around objects at the edges of the workspace
const MIN_RATIO = .125; // Zoom in to at least 1/8 of the screen. This way you don't end up incredibly const MIN_RATIO = .125; // Zoom in to at least 1/8 of the screen. This way you don't end up incredibly
// zoomed in for tiny costumes. // zoomed in for tiny costumes.
const OUTERMOST_ZOOM_LEVEL = 0.333;
const ART_BOARD_BOUNDS = new paper.Rectangle(0, 0, ART_BOARD_WIDTH, ART_BOARD_HEIGHT);
const MAX_WORKSPACE_BOUNDS = new paper.Rectangle(
-ART_BOARD_WIDTH / 4,
-ART_BOARD_HEIGHT / 4,
ART_BOARD_WIDTH * 1.5,
ART_BOARD_HEIGHT * 1.5);
let _workspaceBounds = ART_BOARD_BOUNDS;
const getWorkspaceBounds = () => _workspaceBounds;
/**
* The workspace bounds define the areas that the scroll bars can access.
* They include at minimum the artboard, and extend to a bit beyond the
* farthest item off tne edge in any given direction (so items can't be
* "lost" off the edge)
*
* @param {boolean} clipEmpty Clip empty space from bounds, even if it
* means discontinuously jumping the viewport. This should probably be
* false unless the viewport is going to move discontinuously anyway
* (such as in a zoom button click)
*/
const setWorkspaceBounds = clipEmpty => {
const items = getAllRootItems();
// Include the artboard and what's visible in the viewport
let bounds = ART_BOARD_BOUNDS;
if (!clipEmpty) {
bounds = bounds.unite(paper.view.bounds);
}
// Include everything the user has drawn and a buffer around it
for (const item of items) {
bounds = bounds.unite(item.bounds.expand(BUFFER));
}
// Limit to max workspace bounds
bounds = bounds.intersect(MAX_WORKSPACE_BOUNDS.expand(BUFFER));
let top = bounds.top;
let left = bounds.left;
let bottom = bounds.bottom;
let right = bounds.right;
// Center in view if viewport is larger than workspace
let hDiff = 0;
let vDiff = 0;
if (bounds.width < paper.view.bounds.width) {
hDiff = (paper.view.bounds.width - bounds.width) / 2;
left -= hDiff;
right += hDiff;
}
if (bounds.height < paper.view.bounds.height) {
vDiff = (paper.view.bounds.height - bounds.height) / 2;
top -= vDiff;
bottom += vDiff;
}
_workspaceBounds = new paper.Rectangle(left, top, right - left, bottom - top);
};
const clampViewBounds = () => { const clampViewBounds = () => {
const {left, right, top, bottom} = paper.project.view.bounds; const {left, right, top, bottom} = paper.project.view.bounds;
if (left < 0) { if (left < _workspaceBounds.left) {
paper.project.view.scrollBy(new paper.Point(-left, 0)); paper.project.view.scrollBy(new paper.Point(_workspaceBounds.left - left, 0));
} }
if (top < 0) { if (top < _workspaceBounds.top) {
paper.project.view.scrollBy(new paper.Point(0, -top)); paper.project.view.scrollBy(new paper.Point(0, _workspaceBounds.top - top));
} }
if (bottom > ART_BOARD_HEIGHT) { if (bottom > _workspaceBounds.bottom) {
paper.project.view.scrollBy(new paper.Point(0, ART_BOARD_HEIGHT - bottom)); paper.project.view.scrollBy(new paper.Point(0, _workspaceBounds.bottom - bottom));
} }
if (right > ART_BOARD_WIDTH) { if (right > _workspaceBounds.right) {
paper.project.view.scrollBy(new paper.Point(ART_BOARD_WIDTH - right, 0)); paper.project.view.scrollBy(new paper.Point(_workspaceBounds.right - right, 0));
} }
setWorkspaceBounds();
}; };
const resizeCrosshair = () => { const resizeCrosshair = () => {
@ -47,13 +109,15 @@ const resizeCrosshair = () => {
const zoomOnFixedPoint = (deltaZoom, fixedPoint) => { const zoomOnFixedPoint = (deltaZoom, fixedPoint) => {
const view = paper.view; const view = paper.view;
const preZoomCenter = view.center; const preZoomCenter = view.center;
const newZoom = Math.max(0.5, view.zoom + deltaZoom); const newZoom = Math.max(OUTERMOST_ZOOM_LEVEL, view.zoom + deltaZoom);
const scaling = view.zoom / newZoom; const scaling = view.zoom / newZoom;
const preZoomOffset = fixedPoint.subtract(preZoomCenter); const preZoomOffset = fixedPoint.subtract(preZoomCenter);
const postZoomOffset = fixedPoint.subtract(preZoomOffset.multiply(scaling)) const postZoomOffset = fixedPoint.subtract(preZoomOffset.multiply(scaling))
.subtract(preZoomCenter); .subtract(preZoomCenter);
view.zoom = newZoom; view.zoom = newZoom;
view.translate(postZoomOffset.multiply(-1)); view.translate(postZoomOffset.multiply(-1));
setWorkspaceBounds(true /* clipEmpty */);
clampViewBounds(); clampViewBounds();
resizeCrosshair(); resizeCrosshair();
}; };
@ -80,6 +144,7 @@ const zoomOnSelection = deltaZoom => {
const resetZoom = () => { const resetZoom = () => {
paper.project.view.zoom = .5; paper.project.view.zoom = .5;
setWorkspaceBounds(true /* clipEmpty */);
resizeCrosshair(); resizeCrosshair();
clampViewBounds(); clampViewBounds();
}; };
@ -89,18 +154,40 @@ const pan = (dx, dy) => {
clampViewBounds(); clampViewBounds();
}; };
/**
* Mouse actions are clamped to action bounds
* @param {boolean} isBitmap True if the editor is in bitmap mode, false if it is in vector mode
* @returns {paper.Rectangle} the bounds within which mouse events should work in the paint editor
*/
const getActionBounds = isBitmap => {
if (isBitmap) {
return ART_BOARD_BOUNDS;
}
return paper.view.bounds.unite(ART_BOARD_BOUNDS).intersect(MAX_WORKSPACE_BOUNDS);
};
const zoomToFit = isBitmap => { const zoomToFit = isBitmap => {
resetZoom(); resetZoom();
let bounds; let bounds;
if (isBitmap) { if (isBitmap) {
bounds = getHitBounds(getRaster()); bounds = getHitBounds(getRaster()).expand(BUFFER);
} else { } else {
bounds = paper.project.activeLayer.bounds; const items = getAllRootItems();
for (const item of items) {
if (bounds) {
bounds = bounds.unite(item.bounds);
} else {
bounds = item.bounds;
}
}
} }
if (bounds && bounds.width && bounds.height) { if (bounds && bounds.width && bounds.height) {
// Ratio of (sprite length plus padding on all sides) to art board length. const canvas = paper.view.element;
let ratio = Math.max(bounds.width * (1 + (2 * PADDING_PERCENT / 100)) / ART_BOARD_WIDTH, // Ratio of (sprite length plus padding on all sides) to viewport length.
bounds.height * (1 + (2 * PADDING_PERCENT / 100)) / ART_BOARD_HEIGHT); let ratio = paper.view.zoom *
Math.max(
bounds.width * (1 + (2 * PADDING_PERCENT / 100)) / canvas.clientWidth,
bounds.height * (1 + (2 * PADDING_PERCENT / 100)) / canvas.clientHeight);
// Clamp ratio // Clamp ratio
ratio = Math.max(Math.min(1, ratio), MIN_RATIO); ratio = Math.max(Math.min(1, ratio), MIN_RATIO);
if (ratio < 1) { if (ratio < 1) {
@ -109,18 +196,26 @@ const zoomToFit = isBitmap => {
resizeCrosshair(); resizeCrosshair();
clampViewBounds(); clampViewBounds();
} }
} else {
log.warn('No bounds!');
} }
}; };
export { export {
ART_BOARD_BOUNDS,
ART_BOARD_HEIGHT, ART_BOARD_HEIGHT,
ART_BOARD_WIDTH, ART_BOARD_WIDTH,
CENTER, CENTER,
OUTERMOST_ZOOM_LEVEL,
SVG_ART_BOARD_WIDTH, SVG_ART_BOARD_WIDTH,
SVG_ART_BOARD_HEIGHT, SVG_ART_BOARD_HEIGHT,
MAX_WORKSPACE_BOUNDS,
clampViewBounds, clampViewBounds,
getActionBounds,
pan, pan,
resetZoom, resetZoom,
setWorkspaceBounds,
getWorkspaceBounds,
resizeCrosshair, resizeCrosshair,
zoomOnSelection, zoomOnSelection,
zoomOnFixedPoint, zoomOnFixedPoint,

View file

@ -8,13 +8,16 @@ import {connect} from 'react-redux';
import {undoSnapshot} from '../reducers/undo'; import {undoSnapshot} from '../reducers/undo';
import {setSelectedItems} from '../reducers/selected-items'; import {setSelectedItems} from '../reducers/selected-items';
import {updateViewBounds} from '../reducers/view-bounds';
import {getSelectedLeafItems} from '../helper/selection'; import {getSelectedLeafItems} from '../helper/selection';
import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer'; import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer';
import {commitRectToBitmap, commitOvalToBitmap, commitSelectionToBitmap, getHitBounds} from '../helper/bitmap'; import {commitRectToBitmap, commitOvalToBitmap, commitSelectionToBitmap, getHitBounds} from '../helper/bitmap';
import {performSnapshot} from '../helper/undo'; import {performSnapshot} from '../helper/undo';
import {scaleWithStrokes} from '../helper/math'; import {scaleWithStrokes} from '../helper/math';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, SVG_ART_BOARD_WIDTH, SVG_ART_BOARD_HEIGHT} from '../helper/view'; import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, SVG_ART_BOARD_WIDTH, SVG_ART_BOARD_HEIGHT} from '../helper/view';
import {setWorkspaceBounds} from '../helper/view';
import Modes from '../lib/modes'; import Modes from '../lib/modes';
import {BitmapModes} from '../lib/modes'; import {BitmapModes} from '../lib/modes';
@ -47,6 +50,9 @@ const UpdateImageHOC = function (WrappedComponent) {
} else if (isVector(actualFormat)) { } else if (isVector(actualFormat)) {
this.handleUpdateVector(skipSnapshot); this.handleUpdateVector(skipSnapshot);
} }
// Any time an image update is made, recalculate the bounds of the artwork
setWorkspaceBounds();
this.props.updateViewBounds(paper.view.matrix);
} }
handleUpdateBitmap (skipSnapshot) { handleUpdateBitmap (skipSnapshot) {
if (!getRaster().loaded) { if (!getRaster().loaded) {
@ -110,12 +116,24 @@ const UpdateImageHOC = function (WrappedComponent) {
} }
} }
handleUpdateVector (skipSnapshot) { handleUpdateVector (skipSnapshot) {
// Remove viewbox (this would make it export at MAX_WORKSPACE_BOUNDS)
let workspaceMask;
if (paper.project.activeLayer.clipped) {
for (const child of paper.project.activeLayer.children) {
if (child.isClipMask()) {
workspaceMask = child;
break;
}
}
paper.project.activeLayer.clipped = false;
workspaceMask.remove();
}
const guideLayers = hideGuideLayers(true /* includeRaster */); const guideLayers = hideGuideLayers(true /* includeRaster */);
// Export at 0.5x // Export at 0.5x
scaleWithStrokes(paper.project.activeLayer, .5, new paper.Point()); scaleWithStrokes(paper.project.activeLayer, .5, new paper.Point());
const bounds = paper.project.activeLayer.drawnBounds; const bounds = paper.project.activeLayer.drawnBounds;
// @todo (https://github.com/LLK/scratch-paint/issues/445) generate view box
this.props.onUpdateImage( this.props.onUpdateImage(
true /* isVector */, true /* isVector */,
paper.project.exportSVG({ paper.project.exportSVG({
@ -130,6 +148,12 @@ const UpdateImageHOC = function (WrappedComponent) {
showGuideLayers(guideLayers); showGuideLayers(guideLayers);
// Add back viewbox
if (workspaceMask) {
paper.project.activeLayer.addChild(workspaceMask);
workspaceMask.clipMask = true;
}
if (!skipSnapshot) { if (!skipSnapshot) {
performSnapshot(this.props.undoSnapshot, Formats.VECTOR); performSnapshot(this.props.undoSnapshot, Formats.VECTOR);
} }
@ -153,7 +177,8 @@ const UpdateImageHOC = function (WrappedComponent) {
format: PropTypes.oneOf(Object.keys(Formats)), format: PropTypes.oneOf(Object.keys(Formats)),
mode: PropTypes.oneOf(Object.keys(Modes)).isRequired, mode: PropTypes.oneOf(Object.keys(Modes)).isRequired,
onUpdateImage: PropTypes.func.isRequired, onUpdateImage: PropTypes.func.isRequired,
undoSnapshot: PropTypes.func.isRequired undoSnapshot: PropTypes.func.isRequired,
updateViewBounds: PropTypes.func.isRequired
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -167,6 +192,9 @@ const UpdateImageHOC = function (WrappedComponent) {
}, },
undoSnapshot: snapshot => { undoSnapshot: snapshot => {
dispatch(undoSnapshot(snapshot)); dispatch(undoSnapshot(snapshot));
},
updateViewBounds: matrix => {
dispatch(updateViewBounds(matrix));
} }
}); });

View file

@ -4,11 +4,6 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
</style>
</head> </head>
<body> <body>
</body> </body>

View file

@ -0,0 +1,15 @@
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: 0px;
}
body, html {
height: 100%
}
.playgroundContainer{
height: 90%;
width: 90%;
margin: auto;
}

View file

@ -6,8 +6,10 @@ import {Provider} from 'react-redux';
import {createStore} from 'redux'; import {createStore} from 'redux';
import reducer from './reducers/combine-reducers'; import reducer from './reducers/combine-reducers';
import {intlInitialState, IntlProvider} from './reducers/intl.js'; import {intlInitialState, IntlProvider} from './reducers/intl.js';
import styles from './playground.css';
const appTarget = document.createElement('div'); const appTarget = document.createElement('div');
appTarget.setAttribute('class', styles.playgroundContainer);
document.body.appendChild(appTarget); document.body.appendChild(appTarget);
const store = createStore( const store = createStore(
reducer, reducer,