mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-07-07 02:25:03 -04:00
Add gradients to bitmap shape tools
This commit is contained in:
parent
9f77faf5c1
commit
a304dea338
9 changed files with 179 additions and 113 deletions
|
@ -4,9 +4,10 @@ import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import ColorStyleProptype from '../lib/color-style-proptype';
|
||||||
import {MIXED} from '../helper/style-path';
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
|
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
import {setCursor} from '../reducers/cursor';
|
import {setCursor} from '../reducers/cursor';
|
||||||
|
@ -61,9 +62,8 @@ class BitOvalMode extends React.Component {
|
||||||
}
|
}
|
||||||
activateTool () {
|
activateTool () {
|
||||||
clearSelection(this.props.clearSelectedItems);
|
clearSelection(this.props.clearSelectedItems);
|
||||||
this.props.clearGradient();
|
|
||||||
// Force the default brush color if fill is MIXED or transparent
|
// Force the default brush color if fill is MIXED or transparent
|
||||||
const fillColorPresent = this.props.color !== MIXED && this.props.color !== null;
|
const fillColorPresent = this.props.color.primary !== MIXED && this.props.color.primary !== null;
|
||||||
if (!fillColorPresent) {
|
if (!fillColorPresent) {
|
||||||
this.props.onChangeFillColor(DEFAULT_COLOR);
|
this.props.onChangeFillColor(DEFAULT_COLOR);
|
||||||
}
|
}
|
||||||
|
@ -94,9 +94,8 @@ class BitOvalMode extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
BitOvalMode.propTypes = {
|
BitOvalMode.propTypes = {
|
||||||
clearGradient: PropTypes.func.isRequired,
|
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
color: PropTypes.string,
|
color: ColorStyleProptype,
|
||||||
filled: PropTypes.bool,
|
filled: PropTypes.bool,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
isOvalModeActive: PropTypes.bool.isRequired,
|
isOvalModeActive: PropTypes.bool.isRequired,
|
||||||
|
@ -110,7 +109,7 @@ BitOvalMode.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
color: state.scratchPaint.color.fillColor.primary,
|
color: state.scratchPaint.color.fillColor,
|
||||||
filled: state.scratchPaint.fillBitmapShapes,
|
filled: state.scratchPaint.fillBitmapShapes,
|
||||||
isOvalModeActive: state.scratchPaint.mode === Modes.BIT_OVAL,
|
isOvalModeActive: state.scratchPaint.mode === Modes.BIT_OVAL,
|
||||||
selectedItems: state.scratchPaint.selectedItems,
|
selectedItems: state.scratchPaint.selectedItems,
|
||||||
|
@ -121,9 +120,6 @@ const mapDispatchToProps = dispatch => ({
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
clearGradient: () => {
|
|
||||||
dispatch(clearFillGradient());
|
|
||||||
},
|
|
||||||
setCursor: cursorString => {
|
setCursor: cursorString => {
|
||||||
dispatch(setCursor(cursorString));
|
dispatch(setCursor(cursorString));
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,9 +4,10 @@ import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import ColorStyleProptype from '../lib/color-style-proptype';
|
||||||
import {MIXED} from '../helper/style-path';
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
|
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
import {setCursor} from '../reducers/cursor';
|
import {setCursor} from '../reducers/cursor';
|
||||||
|
@ -61,9 +62,8 @@ class BitRectMode extends React.Component {
|
||||||
}
|
}
|
||||||
activateTool () {
|
activateTool () {
|
||||||
clearSelection(this.props.clearSelectedItems);
|
clearSelection(this.props.clearSelectedItems);
|
||||||
this.props.clearGradient();
|
|
||||||
// Force the default brush color if fill is MIXED or transparent
|
// Force the default brush color if fill is MIXED or transparent
|
||||||
const fillColorPresent = this.props.color !== MIXED && this.props.color !== null;
|
const fillColorPresent = this.props.color.primary !== MIXED && this.props.color.primary !== null;
|
||||||
if (!fillColorPresent) {
|
if (!fillColorPresent) {
|
||||||
this.props.onChangeFillColor(DEFAULT_COLOR);
|
this.props.onChangeFillColor(DEFAULT_COLOR);
|
||||||
}
|
}
|
||||||
|
@ -94,9 +94,8 @@ class BitRectMode extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
BitRectMode.propTypes = {
|
BitRectMode.propTypes = {
|
||||||
clearGradient: PropTypes.func.isRequired,
|
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
color: PropTypes.string,
|
color: ColorStyleProptype,
|
||||||
filled: PropTypes.bool,
|
filled: PropTypes.bool,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
isRectModeActive: PropTypes.bool.isRequired,
|
isRectModeActive: PropTypes.bool.isRequired,
|
||||||
|
@ -110,7 +109,7 @@ BitRectMode.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
color: state.scratchPaint.color.fillColor.primary,
|
color: state.scratchPaint.color.fillColor,
|
||||||
filled: state.scratchPaint.fillBitmapShapes,
|
filled: state.scratchPaint.fillBitmapShapes,
|
||||||
isRectModeActive: state.scratchPaint.mode === Modes.BIT_RECT,
|
isRectModeActive: state.scratchPaint.mode === Modes.BIT_RECT,
|
||||||
selectedItems: state.scratchPaint.selectedItems,
|
selectedItems: state.scratchPaint.selectedItems,
|
||||||
|
@ -121,9 +120,6 @@ const mapDispatchToProps = dispatch => ({
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
clearGradient: () => {
|
|
||||||
dispatch(clearFillGradient());
|
|
||||||
},
|
|
||||||
setCursor: cursorString => {
|
setCursor: cursorString => {
|
||||||
dispatch(setCursor(cursorString));
|
dispatch(setCursor(cursorString));
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,30 +52,34 @@ const makeColorIndicator = (label, isStroke) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatIsBitmap = isBitmap(this.props.format);
|
||||||
// Apply color and update redux, but do not update svg until picker closes.
|
// Apply color and update redux, but do not update svg until picker closes.
|
||||||
const isDifferent = applyColorToSelection(
|
const isDifferent = applyColorToSelection(
|
||||||
newColor,
|
newColor,
|
||||||
this.props.colorIndex,
|
this.props.colorIndex,
|
||||||
this.props.gradientType === GradientTypes.SOLID,
|
this.props.gradientType === GradientTypes.SOLID,
|
||||||
isBitmap(this.props.format),
|
formatIsBitmap,
|
||||||
isStroke,
|
// In bitmap mode, only the fill color selector is used, but it applies to stroke if fillBitmapShapes
|
||||||
|
// is set to true via the "Fill"/"Outline" selector button
|
||||||
|
isStroke || (formatIsBitmap && !this.props.fillBitmapShapes),
|
||||||
this.props.textEditTarget);
|
this.props.textEditTarget);
|
||||||
this._hasChanged = this._hasChanged || isDifferent;
|
this._hasChanged = this._hasChanged || isDifferent;
|
||||||
this.props.onChangeColor(newColor, this.props.colorIndex);
|
this.props.onChangeColor(newColor, this.props.colorIndex);
|
||||||
}
|
}
|
||||||
handleChangeGradientType (gradientType) {
|
handleChangeGradientType (gradientType) {
|
||||||
|
const formatIsBitmap = isBitmap(this.props.format);
|
||||||
// Apply color and update redux, but do not update svg until picker closes.
|
// Apply color and update redux, but do not update svg until picker closes.
|
||||||
const isDifferent = applyGradientTypeToSelection(
|
const isDifferent = applyGradientTypeToSelection(
|
||||||
gradientType,
|
gradientType,
|
||||||
isBitmap(this.props.format),
|
formatIsBitmap,
|
||||||
isStroke,
|
isStroke || (formatIsBitmap && !this.props.fillBitmapShapes),
|
||||||
this.props.textEditTarget);
|
this.props.textEditTarget);
|
||||||
this._hasChanged = this._hasChanged || isDifferent;
|
this._hasChanged = this._hasChanged || isDifferent;
|
||||||
const hasSelectedItems = getSelectedLeafItems().length > 0;
|
const hasSelectedItems = getSelectedLeafItems().length > 0;
|
||||||
if (hasSelectedItems) {
|
if (hasSelectedItems) {
|
||||||
if (isDifferent) {
|
if (isDifferent) {
|
||||||
// Recalculates the swatch colors
|
// Recalculates the swatch colors
|
||||||
this.props.setSelectedItems();
|
this.props.setSelectedItems(this.props.format);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.props.gradientType === GradientTypes.SOLID && gradientType !== GradientTypes.SOLID) {
|
if (this.props.gradientType === GradientTypes.SOLID && gradientType !== GradientTypes.SOLID) {
|
||||||
|
@ -100,11 +104,12 @@ const makeColorIndicator = (label, isStroke) => {
|
||||||
}
|
}
|
||||||
handleSwap () {
|
handleSwap () {
|
||||||
if (getSelectedLeafItems().length) {
|
if (getSelectedLeafItems().length) {
|
||||||
|
const formatIsBitmap = isBitmap(this.props.format);
|
||||||
const isDifferent = swapColorsInSelection(
|
const isDifferent = swapColorsInSelection(
|
||||||
isBitmap(this.props.format),
|
formatIsBitmap,
|
||||||
isStroke,
|
isStroke || (formatIsBitmap && !this.props.fillBitmapShapes),
|
||||||
this.props.textEditTarget);
|
this.props.textEditTarget);
|
||||||
this.props.setSelectedItems();
|
this.props.setSelectedItems(this.props.format);
|
||||||
this._hasChanged = this._hasChanged || isDifferent;
|
this._hasChanged = this._hasChanged || isDifferent;
|
||||||
} else {
|
} else {
|
||||||
let color1 = this.props.color;
|
let color1 = this.props.color;
|
||||||
|
@ -136,6 +141,7 @@ const makeColorIndicator = (label, isStroke) => {
|
||||||
color: PropTypes.string,
|
color: PropTypes.string,
|
||||||
color2: PropTypes.string,
|
color2: PropTypes.string,
|
||||||
colorModalVisible: PropTypes.bool.isRequired,
|
colorModalVisible: PropTypes.bool.isRequired,
|
||||||
|
fillBitmapShapes: PropTypes.bool.isRequired,
|
||||||
format: PropTypes.oneOf(Object.keys(Formats)),
|
format: PropTypes.oneOf(Object.keys(Formats)),
|
||||||
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
|
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
|
|
|
@ -28,6 +28,7 @@ const mapStateToProps = state => ({
|
||||||
color: state.scratchPaint.color.fillColor.primary,
|
color: state.scratchPaint.color.fillColor.primary,
|
||||||
color2: state.scratchPaint.color.fillColor.secondary,
|
color2: state.scratchPaint.color.fillColor.secondary,
|
||||||
colorModalVisible: state.scratchPaint.modals.fillColor,
|
colorModalVisible: state.scratchPaint.modals.fillColor,
|
||||||
|
fillBitmapShapes: state.scratchPaint.fillBitmapShapes,
|
||||||
format: state.scratchPaint.format,
|
format: state.scratchPaint.format,
|
||||||
gradientType: state.scratchPaint.color.fillColor.gradientType,
|
gradientType: state.scratchPaint.color.fillColor.gradientType,
|
||||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
||||||
|
@ -38,6 +39,8 @@ const mapStateToProps = state => ({
|
||||||
state.scratchPaint.mode === Modes.RECT ||
|
state.scratchPaint.mode === Modes.RECT ||
|
||||||
state.scratchPaint.mode === Modes.OVAL ||
|
state.scratchPaint.mode === Modes.OVAL ||
|
||||||
state.scratchPaint.mode === Modes.BIT_SELECT ||
|
state.scratchPaint.mode === Modes.BIT_SELECT ||
|
||||||
|
state.scratchPaint.mode === Modes.BIT_RECT ||
|
||||||
|
state.scratchPaint.mode === Modes.BIT_OVAL ||
|
||||||
state.scratchPaint.mode === Modes.BIT_FILL,
|
state.scratchPaint.mode === Modes.BIT_FILL,
|
||||||
textEditTarget: state.scratchPaint.textEditTarget
|
textEditTarget: state.scratchPaint.textEditTarget
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,6 +30,7 @@ const mapStateToProps = state => ({
|
||||||
state.scratchPaint.mode === Modes.FILL,
|
state.scratchPaint.mode === Modes.FILL,
|
||||||
color: state.scratchPaint.color.strokeColor.primary,
|
color: state.scratchPaint.color.strokeColor.primary,
|
||||||
color2: state.scratchPaint.color.strokeColor.secondary,
|
color2: state.scratchPaint.color.strokeColor.secondary,
|
||||||
|
fillBitmapShapes: state.scratchPaint.fillBitmapShapes,
|
||||||
colorModalVisible: state.scratchPaint.modals.strokeColor,
|
colorModalVisible: state.scratchPaint.modals.strokeColor,
|
||||||
format: state.scratchPaint.format,
|
format: state.scratchPaint.format,
|
||||||
gradientType: state.scratchPaint.color.strokeColor.gradientType,
|
gradientType: state.scratchPaint.color.strokeColor.gradientType,
|
||||||
|
@ -38,7 +39,9 @@ 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.RECT ||
|
state.scratchPaint.mode === Modes.RECT ||
|
||||||
state.scratchPaint.mode === Modes.OVAL,
|
state.scratchPaint.mode === Modes.OVAL ||
|
||||||
|
state.scratchPaint.mode === Modes.BIT_RECT ||
|
||||||
|
state.scratchPaint.mode === Modes.BIT_OVAL,
|
||||||
textEditTarget: state.scratchPaint.textEditTarget
|
textEditTarget: state.scratchPaint.textEditTarget
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import Modes from '../../lib/modes';
|
import Modes from '../../lib/modes';
|
||||||
|
import {styleShape} from '../style-path';
|
||||||
import {commitOvalToBitmap} from '../bitmap';
|
import {commitOvalToBitmap} from '../bitmap';
|
||||||
import {getRaster} from '../layer';
|
import {getRaster} from '../layer';
|
||||||
import {clearSelection} from '../selection';
|
import {clearSelection} from '../selection';
|
||||||
|
@ -83,29 +84,22 @@ class OvalTool extends paper.Tool {
|
||||||
this.commitOval();
|
this.commitOval();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
styleOval () {
|
||||||
|
styleShape(this.oval, {
|
||||||
|
fillColor: this.filled ? this.color : null,
|
||||||
|
strokeColor: this.filled ? null : this.color,
|
||||||
|
strokeWidth: this.filled ? 0 : this.thickness
|
||||||
|
});
|
||||||
|
}
|
||||||
setColor (color) {
|
setColor (color) {
|
||||||
this.color = color;
|
this.color = color;
|
||||||
if (this.oval) {
|
if (this.oval) this.styleOval();
|
||||||
if (this.filled) {
|
|
||||||
this.oval.fillColor = this.color;
|
|
||||||
} else {
|
|
||||||
this.oval.strokeColor = this.color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setFilled (filled) {
|
setFilled (filled) {
|
||||||
if (this.filled === filled) return;
|
if (this.filled === filled) return;
|
||||||
this.filled = filled;
|
this.filled = filled;
|
||||||
if (this.oval && this.oval.isInserted()) {
|
if (this.oval && this.oval.isInserted()) {
|
||||||
if (this.filled) {
|
this.styleOval();
|
||||||
this.oval.fillColor = this.color;
|
|
||||||
this.oval.strokeWidth = 0;
|
|
||||||
this.oval.strokeColor = null;
|
|
||||||
} else {
|
|
||||||
this.oval.fillColor = null;
|
|
||||||
this.oval.strokeWidth = this.thickness;
|
|
||||||
this.oval.strokeColor = this.color;
|
|
||||||
}
|
|
||||||
this.onUpdateImage();
|
this.onUpdateImage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,23 +125,12 @@ class OvalTool extends paper.Tool {
|
||||||
this.isBoundingBoxMode = false;
|
this.isBoundingBoxMode = false;
|
||||||
clearSelection(this.clearSelectedItems);
|
clearSelection(this.clearSelectedItems);
|
||||||
this.commitOval();
|
this.commitOval();
|
||||||
if (this.filled) {
|
this.oval = new paper.Shape.Ellipse({
|
||||||
this.oval = new paper.Shape.Ellipse({
|
point: event.downPoint,
|
||||||
fillColor: this.color,
|
size: 0,
|
||||||
point: event.downPoint,
|
strokeScaling: false
|
||||||
strokeWidth: 0,
|
});
|
||||||
strokeScaling: false,
|
this.styleOval();
|
||||||
size: 0
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.oval = new paper.Shape.Ellipse({
|
|
||||||
strokeColor: this.color,
|
|
||||||
strokeWidth: this.thickness,
|
|
||||||
point: event.downPoint,
|
|
||||||
strokeScaling: false,
|
|
||||||
size: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.oval.data = {zoomLevel: paper.view.zoom};
|
this.oval.data = {zoomLevel: paper.view.zoom};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,6 +158,7 @@ class OvalTool extends paper.Tool {
|
||||||
} else {
|
} else {
|
||||||
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
|
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
|
||||||
}
|
}
|
||||||
|
this.styleOval();
|
||||||
}
|
}
|
||||||
handleMouseMove (event) {
|
handleMouseMove (event) {
|
||||||
this.boundingBoxTool.onMouseMove(event, this.getHitOptions());
|
this.boundingBoxTool.onMouseMove(event, this.getHitOptions());
|
||||||
|
@ -197,6 +181,7 @@ class OvalTool extends paper.Tool {
|
||||||
// Hit testing does not work correctly unless the width and height are positive
|
// Hit testing does not work correctly unless the width and height are positive
|
||||||
this.oval.size = new paper.Point(Math.abs(this.oval.size.width), Math.abs(this.oval.size.height));
|
this.oval.size = new paper.Point(Math.abs(this.oval.size.width), Math.abs(this.oval.size.height));
|
||||||
this.oval.selected = true;
|
this.oval.selected = true;
|
||||||
|
this.styleOval();
|
||||||
this.setSelectedItems();
|
this.setSelectedItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import Modes from '../../lib/modes';
|
import Modes from '../../lib/modes';
|
||||||
|
import {styleShape} from '../../helper/style-path';
|
||||||
import {commitRectToBitmap} from '../bitmap';
|
import {commitRectToBitmap} from '../bitmap';
|
||||||
import {getRaster} from '../layer';
|
import {getRaster} from '../layer';
|
||||||
import {clearSelection} from '../selection';
|
import {clearSelection} from '../selection';
|
||||||
|
@ -81,29 +82,22 @@ class RectTool extends paper.Tool {
|
||||||
this.commitRect();
|
this.commitRect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
styleRect () {
|
||||||
|
styleShape(this.rect, {
|
||||||
|
fillColor: this.filled ? this.color : null,
|
||||||
|
strokeColor: this.filled ? null : this.color,
|
||||||
|
strokeWidth: this.filled ? 0 : this.thickness
|
||||||
|
});
|
||||||
|
}
|
||||||
setColor (color) {
|
setColor (color) {
|
||||||
this.color = color;
|
this.color = color;
|
||||||
if (this.rect) {
|
if (this.rect) this.styleRect();
|
||||||
if (this.filled) {
|
|
||||||
this.rect.fillColor = this.color;
|
|
||||||
} else {
|
|
||||||
this.rect.strokeColor = this.color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setFilled (filled) {
|
setFilled (filled) {
|
||||||
if (this.filled === filled) return;
|
if (this.filled === filled) return;
|
||||||
this.filled = filled;
|
this.filled = filled;
|
||||||
if (this.rect && this.rect.isInserted()) {
|
if (this.rect && this.rect.isInserted()) {
|
||||||
if (this.filled) {
|
this.styleRect();
|
||||||
this.rect.fillColor = this.color;
|
|
||||||
this.rect.strokeWidth = 0;
|
|
||||||
this.rect.strokeColor = null;
|
|
||||||
} else {
|
|
||||||
this.rect.fillColor = null;
|
|
||||||
this.rect.strokeWidth = this.thickness;
|
|
||||||
this.rect.strokeColor = this.color;
|
|
||||||
}
|
|
||||||
this.onUpdateImage();
|
this.onUpdateImage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,16 +142,10 @@ class RectTool extends paper.Tool {
|
||||||
|
|
||||||
if (this.rect) this.rect.remove();
|
if (this.rect) this.rect.remove();
|
||||||
this.rect = new paper.Shape.Rectangle(baseRect);
|
this.rect = new paper.Shape.Rectangle(baseRect);
|
||||||
if (this.filled) {
|
|
||||||
this.rect.fillColor = this.color;
|
|
||||||
this.rect.strokeWidth = 0;
|
|
||||||
} else {
|
|
||||||
this.rect.strokeColor = this.color;
|
|
||||||
this.rect.strokeWidth = this.thickness;
|
|
||||||
}
|
|
||||||
this.rect.strokeJoin = 'round';
|
this.rect.strokeJoin = 'round';
|
||||||
this.rect.strokeScaling = false;
|
this.rect.strokeScaling = false;
|
||||||
this.rect.data = {zoomLevel: paper.view.zoom};
|
this.rect.data = {zoomLevel: paper.view.zoom};
|
||||||
|
this.styleRect();
|
||||||
|
|
||||||
if (event.modifiers.alt) {
|
if (event.modifiers.alt) {
|
||||||
this.rect.position = event.downPoint;
|
this.rect.position = event.downPoint;
|
||||||
|
@ -188,6 +176,7 @@ class RectTool extends paper.Tool {
|
||||||
// Hit testing does not work correctly unless the width and height are positive
|
// Hit testing does not work correctly unless the width and height are positive
|
||||||
this.rect.size = new paper.Point(Math.abs(this.rect.size.width), Math.abs(this.rect.size.height));
|
this.rect.size = new paper.Point(Math.abs(this.rect.size.width), Math.abs(this.rect.size.height));
|
||||||
this.rect.selected = true;
|
this.rect.selected = true;
|
||||||
|
this.styleRect();
|
||||||
this.setSelectedItems();
|
this.setSelectedItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,8 +275,24 @@ const drawEllipse = function (options, context) {
|
||||||
if (!matrix.isInvertible()) return false;
|
if (!matrix.isInvertible()) return false;
|
||||||
const inverse = matrix.clone().invert();
|
const inverse = matrix.clone().invert();
|
||||||
|
|
||||||
|
const isGradient = context.fillStyle instanceof CanvasGradient;
|
||||||
|
|
||||||
|
// If drawing a gradient, we need to draw the shape onto a temporary canvas, then draw the gradient atop that canvas
|
||||||
|
// only where the shape appears. drawShearedEllipse draws some pixels twice, which would be a problem if the
|
||||||
|
// gradient fades to transparent as those pixels would end up looking more opaque. Instead, mask in the gradient.
|
||||||
|
// https://github.com/LLK/scratch-paint/issues/1152
|
||||||
|
// Outlines are drawn as a series of brush mark images and as such can't be drawn as gradients in the first place.
|
||||||
|
let origContext;
|
||||||
|
let tmpCanvas;
|
||||||
|
const {width: canvasWidth, height: canvasHeight} = context.canvas;
|
||||||
|
if (isGradient) {
|
||||||
|
tmpCanvas = createCanvas(canvasWidth, canvasHeight);
|
||||||
|
origContext = context;
|
||||||
|
context = tmpCanvas.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
if (!isFilled) {
|
if (!isFilled) {
|
||||||
const brushMark = getBrushMark(thickness, context.fillStyle);
|
const brushMark = getBrushMark(thickness, isGradient ? 'black' : context.fillStyle);
|
||||||
const roundedUpRadius = Math.ceil(thickness / 2);
|
const roundedUpRadius = Math.ceil(thickness / 2);
|
||||||
drawFn = (x, y) => {
|
drawFn = (x, y) => {
|
||||||
context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius);
|
context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius);
|
||||||
|
@ -295,7 +311,7 @@ const drawEllipse = function (options, context) {
|
||||||
const radiusA = Math.sqrt(-4 * C / ((B * B) - (4 * A * C)));
|
const radiusA = Math.sqrt(-4 * C / ((B * B) - (4 * A * C)));
|
||||||
const slope = B / 2 / C;
|
const slope = B / 2 / C;
|
||||||
|
|
||||||
return drawShearedEllipse_({
|
const wasDrawn = drawShearedEllipse_({
|
||||||
centerX: positionX,
|
centerX: positionX,
|
||||||
centerY: positionY,
|
centerY: positionY,
|
||||||
radiusX: radiusA,
|
radiusX: radiusA,
|
||||||
|
@ -304,6 +320,17 @@ const drawEllipse = function (options, context) {
|
||||||
isFilled: isFilled,
|
isFilled: isFilled,
|
||||||
drawFn: drawFn
|
drawFn: drawFn
|
||||||
}, context);
|
}, context);
|
||||||
|
|
||||||
|
// Mask in the gradient only where the shape was drawn, and draw it. Then draw the gradientified shape onto the
|
||||||
|
// original canvas normally.
|
||||||
|
if (isGradient && wasDrawn) {
|
||||||
|
context.globalCompositeOperation = 'source-in';
|
||||||
|
context.fillStyle = origContext.fillStyle;
|
||||||
|
context.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
origContext.drawImage(tmpCanvas, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasDrawn;
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowBlank_ = function (imageData, width, y) {
|
const rowBlank_ = function (imageData, width, y) {
|
||||||
|
@ -658,6 +685,20 @@ const outlineRect = function (rect, thickness, context) {
|
||||||
context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius);
|
context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isGradient = context.fillStyle instanceof CanvasGradient;
|
||||||
|
|
||||||
|
// If drawing a gradient, we need to draw the shape onto a temporary canvas, then draw the gradient atop that canvas
|
||||||
|
// only where the shape appears. Outlines are drawn as a series of brush mark images and as such can't be drawn as
|
||||||
|
// gradients.
|
||||||
|
let origContext;
|
||||||
|
let tmpCanvas;
|
||||||
|
const {width: canvasWidth, height: canvasHeight} = context.canvas;
|
||||||
|
if (isGradient) {
|
||||||
|
tmpCanvas = createCanvas(canvasWidth, canvasHeight);
|
||||||
|
origContext = context;
|
||||||
|
context = tmpCanvas.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
const startPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, -rect.size.height / 2));
|
const startPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, -rect.size.height / 2));
|
||||||
const widthPoint = rect.matrix.transform(new paper.Point(rect.size.width / 2, -rect.size.height / 2));
|
const widthPoint = rect.matrix.transform(new paper.Point(rect.size.width / 2, -rect.size.height / 2));
|
||||||
const heightPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, rect.size.height / 2));
|
const heightPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, rect.size.height / 2));
|
||||||
|
@ -667,6 +708,16 @@ const outlineRect = function (rect, thickness, context) {
|
||||||
forEachLinePoint(startPoint, heightPoint, drawFn);
|
forEachLinePoint(startPoint, heightPoint, drawFn);
|
||||||
forEachLinePoint(endPoint, widthPoint, drawFn);
|
forEachLinePoint(endPoint, widthPoint, drawFn);
|
||||||
forEachLinePoint(endPoint, heightPoint, drawFn);
|
forEachLinePoint(endPoint, heightPoint, drawFn);
|
||||||
|
|
||||||
|
// Mask in the gradient only where the shape was drawn, and draw it. Then draw the gradientified shape onto the
|
||||||
|
// original canvas normally.
|
||||||
|
if (isGradient) {
|
||||||
|
context.globalCompositeOperation = 'source-in';
|
||||||
|
context.fillStyle = origContext.fillStyle;
|
||||||
|
context.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
origContext.drawImage(tmpCanvas, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const flipBitmapHorizontal = function (canvas) {
|
const flipBitmapHorizontal = function (canvas) {
|
||||||
|
@ -773,6 +824,62 @@ const commitSelectionToBitmap = function (selection, bitmap) {
|
||||||
commitArbitraryTransformation_(selection, bitmap);
|
commitArbitraryTransformation_(selection, bitmap);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Paper.js color style (an item's fillColor or strokeColor) into a canvas-applicable color style.
|
||||||
|
* Note that a "color style" as applied to an item is different from a plain paper.Color or paper.Gradient.
|
||||||
|
* For instance, a gradient "color style" has origin and destination points whereas an unattached paper.Gradient
|
||||||
|
* does not.
|
||||||
|
* @param {paper.Color} color The color to convert to a canvas color/gradient
|
||||||
|
* @param {CanvasRenderingContext2D} context The rendering context on which the style will be used
|
||||||
|
* @returns {string|CanvasGradient} The canvas fill/stroke style.
|
||||||
|
*/
|
||||||
|
const _paperColorToCanvasStyle = function (color, context) {
|
||||||
|
if (!color) return null;
|
||||||
|
if (color.type === 'gradient') {
|
||||||
|
let canvasGradient;
|
||||||
|
const {origin, destination} = color;
|
||||||
|
if (color.gradient.radial) {
|
||||||
|
// Adapted from:
|
||||||
|
// https://github.com/paperjs/paper.js/blob/b081fd72c72cd61331313c3961edb48f3dfaffbd/src/style/Color.js#L926-L935
|
||||||
|
let {highlight} = color;
|
||||||
|
const start = highlight || origin;
|
||||||
|
const radius = destination.getDistance(origin);
|
||||||
|
if (highlight) {
|
||||||
|
const vector = highlight.subtract(origin);
|
||||||
|
if (vector.getLength() > radius) {
|
||||||
|
// Paper ¯\_(ツ)_/¯
|
||||||
|
highlight = origin.add(vector.normalize(radius - 0.1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvasGradient = context.createRadialGradient(
|
||||||
|
start.x, start.y,
|
||||||
|
0,
|
||||||
|
origin.x, origin.y,
|
||||||
|
radius
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
canvasGradient = context.createLinearGradient(
|
||||||
|
origin.x, origin.y,
|
||||||
|
destination.x, destination.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {stops} = color.gradient;
|
||||||
|
// Adapted from:
|
||||||
|
// https://github.com/paperjs/paper.js/blob/b081fd72c72cd61331313c3961edb48f3dfaffbd/src/style/Color.js#L940-L950
|
||||||
|
for (let i = 0, len = stops.length; i < len; i++) {
|
||||||
|
const stop = stops[i];
|
||||||
|
const offset = stop.offset;
|
||||||
|
canvasGradient.addColorStop(
|
||||||
|
offset || i / (len - 1),
|
||||||
|
stop.color.toCSS()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return canvasGradient;
|
||||||
|
}
|
||||||
|
return color.toCSS();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {paper.Shape.Ellipse} oval Vector oval to convert
|
* @param {paper.Shape.Ellipse} oval Vector oval to convert
|
||||||
* @param {paper.Raster} bitmap raster to draw selection
|
* @param {paper.Raster} bitmap raster to draw selection
|
||||||
|
@ -784,12 +891,12 @@ const commitOvalToBitmap = function (oval, bitmap) {
|
||||||
const context = bitmap.getContext('2d');
|
const context = bitmap.getContext('2d');
|
||||||
const filled = oval.strokeWidth === 0;
|
const filled = oval.strokeWidth === 0;
|
||||||
|
|
||||||
const canvasColor = filled ? oval.fillColor : oval.strokeColor;
|
const canvasColor = _paperColorToCanvasStyle(filled ? oval.fillColor : oval.strokeColor, context);
|
||||||
// If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything,
|
// If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything
|
||||||
// and especially don't try calling `toCSS` on it
|
|
||||||
if (!canvasColor) return;
|
if (!canvasColor) return;
|
||||||
|
|
||||||
context.fillStyle = canvasColor.toCSS();
|
context.fillStyle = canvasColor;
|
||||||
|
|
||||||
const drew = drawEllipse({
|
const drew = drawEllipse({
|
||||||
position: oval.position,
|
position: oval.position,
|
||||||
radiusX,
|
radiusX,
|
||||||
|
@ -811,12 +918,12 @@ const commitRectToBitmap = function (rect, bitmap) {
|
||||||
const context = tmpCanvas.getContext('2d');
|
const context = tmpCanvas.getContext('2d');
|
||||||
const filled = rect.strokeWidth === 0;
|
const filled = rect.strokeWidth === 0;
|
||||||
|
|
||||||
const canvasColor = filled ? rect.fillColor : rect.strokeColor;
|
const canvasColor = _paperColorToCanvasStyle(filled ? rect.fillColor : rect.strokeColor, context);
|
||||||
// If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything,
|
// If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything
|
||||||
// and especially don't try calling `toCSS` on it
|
|
||||||
if (!canvasColor) return;
|
if (!canvasColor) return;
|
||||||
|
|
||||||
context.fillStyle = canvasColor.toCSS();
|
context.fillStyle = canvasColor;
|
||||||
|
|
||||||
if (filled) {
|
if (filled) {
|
||||||
fillRect(rect, context);
|
fillRect(rect, context);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -120,20 +120,6 @@ const applyColorToSelection = function (
|
||||||
item = item.parent;
|
item = item.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In bitmap mode, fill color applies to the stroke if there is a stroke
|
|
||||||
if (
|
|
||||||
bitmapMode &&
|
|
||||||
!applyToStroke &&
|
|
||||||
item.strokeColor !== null &&
|
|
||||||
item.strokeWidth
|
|
||||||
) {
|
|
||||||
if (!_colorMatch(item.strokeColor, colorString)) {
|
|
||||||
changed = true;
|
|
||||||
item.strokeColor = colorString;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor';
|
const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor';
|
||||||
const itemColor = item[itemColorProp];
|
const itemColor = item[itemColorProp];
|
||||||
|
|
||||||
|
@ -179,8 +165,6 @@ const applyColorToSelection = function (
|
||||||
* @return {boolean} Whether the color application actually changed visibly.
|
* @return {boolean} Whether the color application actually changed visibly.
|
||||||
*/
|
*/
|
||||||
const swapColorsInSelection = function (bitmapMode, applyToStroke, textEditTargetId) {
|
const swapColorsInSelection = function (bitmapMode, applyToStroke, textEditTargetId) {
|
||||||
if (bitmapMode) return; // @todo
|
|
||||||
|
|
||||||
const items = _getColorStateListeners(textEditTargetId);
|
const items = _getColorStateListeners(textEditTargetId);
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
@ -255,10 +239,7 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, applyTo
|
||||||
itemColor2 = itemColor.gradient.stops[1].color.toCSS();
|
itemColor2 = itemColor.gradient.stops[1].color.toCSS();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bitmapMode) {
|
if (gradientType === GradientTypes.SOLID) {
|
||||||
// @todo Add when we apply gradients to selections in bitmap mode
|
|
||||||
continue;
|
|
||||||
} else if (gradientType === GradientTypes.SOLID) {
|
|
||||||
if (itemColor && itemColor.gradient) {
|
if (itemColor && itemColor.gradient) {
|
||||||
changed = true;
|
changed = true;
|
||||||
item[itemColorProp] = itemColor1;
|
item[itemColorProp] = itemColor1;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue