Text can now be edited directly on the canvas

This commit is contained in:
casperlamboo 2018-01-09 11:21:26 +01:00
parent 834ca531fd
commit 04186fd56c
8 changed files with 131 additions and 40 deletions

6
package-lock.json generated
View File

@ -11747,12 +11747,6 @@
"prop-types": "15.6.0"
}
},
"react-router-redux": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/react-router-redux/-/react-router-redux-4.0.8.tgz",
"integrity": "sha1-InQDWWtRUeGCN32rg1tdRfD4BU4=",
"dev": true
},
"react-svg-inline": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/react-svg-inline/-/react-svg-inline-2.0.1.tgz",

View File

@ -82,7 +82,6 @@
"normalize-jss": "^4.0.0",
"raw-loader": "^0.5.1",
"react-dom": "^16.1.1",
"react-router-redux": "^4.0.8",
"react-tap-event-plugin": "^3.0.2",
"redux": "^3.7.2",
"redux-action-wrapper": "^1.0.1",

View File

@ -1,6 +1,5 @@
import { ActionCreators as undo } from 'redux-undo';
import * as notification from 'react-notification-system-redux';
import { routerActions as router } from 'react-router-redux';
import * as selectionUtils from '../utils/selectionUtils.js';
import { calculatePointInImage, decomposeMatrix } from '../utils/matrixUtils.js';
import { loadImage, prepareImage } from '../utils/imageUtils.js';
@ -74,7 +73,6 @@ export const ALIGN = 'ALIGN';
export const ADD_IMAGE = 'ADD_IMAGE';
export const D2_TEXT_INIT = 'D2_TEXT_INIT';
export const D2_TEXT_INPUT_CHANGE = 'D2_TEXT_INPUT_CHANGE';
export const D2_TEXT_ADD = 'D2_TEXT_ADD';
export const UNION = 'UNION';
export const INTERSECT = 'INTERSECT';
export const MOVE_SELECTION = 'MOVE_SELECTION';
@ -390,15 +388,11 @@ export function addImage(file) {
export function d2textInit(position, textId, screenMatrixContainer, screenMatrixZoom) {
return (dispatch) => {
dispatch({ type: D2_TEXT_INIT, position, textId, screenMatrixContainer, screenMatrixZoom });
dispatch(router.push('/sketch/inputtext'));
};
}
export function d2textInputChange(text, family, weight, style, fill) {
return { type: D2_TEXT_INPUT_CHANGE, text, family, weight, style, fill };
}
export function d2textAdd() {
return { type: D2_TEXT_ADD };
}
const traceDragThrottle = createThrottle();

View File

@ -23,6 +23,7 @@ import ShapesManager from '../d2/ShapesManager.js';
import EventGroup from '../d2/EventGroup.js';
import ReactResizeDetector from 'react-resize-detector';
import { load as loadPattern } from '../d2/Shape.js';
import InputText from './InputText.js';
// import createDebug from 'debug';
// const debug = createDebug('d3d:d2');
@ -63,6 +64,9 @@ class D2Panel extends React.Component {
dispatch: PropTypes.func.isRequired,
classes: PropTypes.objectOf(PropTypes.string)
};
state = {
screenMatrix: new CAL.Matrix()
};
activeNeedRender = false;
inactiveNeedRender = false;
@ -117,7 +121,7 @@ class D2Panel extends React.Component {
}
switchTool(toolName) {
if (this.state && toolName === this.state.d2.tool) return;
if (this.state.state && toolName === this.state.state.d2.tool) return;
// cleanup & remove previous tool
if (this.tool) {
this.tool.destroy();
@ -135,18 +139,18 @@ class D2Panel extends React.Component {
this.objectContainerActive.add(this.tool);
}
update(newState) {
if (this.state === newState) return;
update(state) {
if (this.state.state === state) return;
this.updateTool(newState);
this.updateTool(state);
const shapesNeedRender = this.shapesManager.update(newState);
const shapesNeedRender = this.shapesManager.update(state);
if (shapesNeedRender.active) this.activeNeedRender = true;
if (shapesNeedRender.inactive) this.inactiveNeedRender = true;
// Update Objects Container Space with zoom & panning
const newCanvasMatrix = newState.d2.canvasMatrix;
if (this.state && newCanvasMatrix !== this.state.d2.canvasMatrix) {
const newCanvasMatrix = state.d2.canvasMatrix;
if (this.state.state && newCanvasMatrix !== this.state.state.d2.canvasMatrix) {
this.objectContainerActive.copyMatrix(newCanvasMatrix);
this.objectContainerInactive.copyMatrix(newCanvasMatrix);
@ -154,9 +158,9 @@ class D2Panel extends React.Component {
this.inactiveNeedRender = true;
}
const selection = (this.state) ? this.state.selection : null;
const newSelection = newState.selection;
if (!this.state || newSelection !== selection) {
const selection = (this.state.state) ? this.state.state.selection : null;
const newSelection = state.selection;
if (!this.state.state || newSelection !== selection) {
const newSelectedObjects = newSelection.objects;
if (!selection || selection.objects !== newSelectedObjects) {
const selected = newSelectedObjects.map((object) => object.id);
@ -167,25 +171,29 @@ class D2Panel extends React.Component {
}
}
const dragSelect = (this.state) ? this.state.d2.transform.dragSelect : null;
const newDragSelect = newState.d2.transform.dragSelect;
const dragSelect = (this.state.state) ? this.state.state.d2.transform.dragSelect : null;
const newDragSelect = state.d2.transform.dragSelect;
if (!dragSelect || dragSelect !== newDragSelect) {
this.activeNeedRender = true;
}
this.state = newState;
this.setState({ state });
}
resizeHandler = (width, height) => {
this.sceneActive.setSize(width, height, PIXEL_RATIO);
this.sceneInactive.setSize(width, height, PIXEL_RATIO);
this.sceneInactive.x = this.sceneActive.x = Math.round(width / 2 * PIXEL_RATIO);
this.sceneInactive.y = this.sceneActive.y = Math.round(height / 2 * PIXEL_RATIO);
const x = Math.round(width / 2 * PIXEL_RATIO);
const y = Math.round(height / 2 * PIXEL_RATIO);
const scale = Math.min(width * PIXEL_RATIO / CANVAS_WIDTH, height * PIXEL_RATIO / CANVAS_HEIGHT);
this.sceneInactive.scale = this.sceneActive.scale = scale;
const screenMatrix = new CAL.Matrix({ sx: scale, sy: scale, x, y });
this.sceneInactive.copyMatrix(screenMatrix);
this.sceneActive.copyMatrix(screenMatrix);
this.setState({ screenMatrix });
this.inactiveNeedRender = this.activeNeedRender = true;
this.renderRequest();
@ -210,12 +218,14 @@ class D2Panel extends React.Component {
render() {
// debug('this.props.state: ', this.props.state);
const { state, classes } = this.props;
const { screenMatrix } = this.state;
this.update(state);
this.renderCanvas();
return (
<div className={classes.container}>
<ReactResizeDetector handleWidth handleHeight onResize={this.resizeHandler} />
<div className={classes.canvasContainer} ref="canvasContainer" />
<InputText screenMatrix={screenMatrix} />
</div>
);
}

View File

@ -0,0 +1,94 @@
import React from 'react';
import injectSheet from 'react-jss';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import * as actions from '../actions/index.js';
import * as CAL from 'cal';
import { TEXT_TOOL_FONT_SIZE } from '../constants/d2Constants.js';
document.createElement('canvas');
const styles = {
textInputContainer: {
position: 'absolute',
display: 'flex'
},
textInput: {
position: 'absolute',
fontSize: `${TEXT_TOOL_FONT_SIZE}px`,
background: 'transparent',
border: 'none',
color: 'black',
textFillColor: 'transparent',
outline: 'none'
}
};
class InputText extends React.Component {
static propTypes = {
state: PropTypes.object.isRequired,
classes: PropTypes.objectOf(PropTypes.string),
changeText: PropTypes.func.isRequired,
screenMatrix: PropTypes.instanceOf(CAL.Matrix).isRequired
};
onInputChange = () => {
const shapeData = this.getShapeData();
if (!shapeData) return;
const { changeText } = this.props;
const { family, weight, style } = shapeData.text;
const text = this.refs.text.value;
changeText(text, family, weight, style, true);
};
getShapeData = () => {
const { state } = this.props;
if (!state.d2.activeShape) return null;
const shapeData = state.objectsById[state.d2.activeShape];
if (shapeData.type !== 'TEXT') return null;
return shapeData;
}
componentWillUpdate() {
if (this.refs.text) this.refs.text.focus();
}
render() {
const { classes, state, screenMatrix } = this.props;
const shapeData = this.getShapeData();
if (shapeData) {
const { _matrix: m } = shapeData.transform
.multiplyMatrix(state.d2.canvasMatrix)
.multiplyMatrix(screenMatrix);
return (
<div
className={classes.textInputContainer}
style={{ transform: `matrix(${m[0]}, ${m[3]}, ${m[1]}, ${m[4]}, ${m[2]}, ${m[5]})` }}
>
<input
className={classes.textInput}
style={{
family: shapeData.text.family
}}
value={shapeData.text.text}
ref="text"
spellCheck="false"
autoFocus
onChange={this.onInputChange}
/>
</div>
);
}
return null;
}
}
export default injectSheet(styles)(connect(state => ({
state: state.sketcher.present
}), {
changeText: actions.d2textInputChange
})(InputText));

View File

@ -41,3 +41,4 @@ export const BRUSH_SIZES = {
};
export const CLIPPER_PRECISION = 100; // accurate to the hundredth
export const TEXT_TOOL_FONT_SIZE = 40;

View File

@ -8,6 +8,7 @@ import { MAX_ANGLE } from '../constants/d3Constants.js';
import { SHAPE_CACHE_LIMIT } from '../constants/general.js';
import { createText } from '../utils/textUtils.js';
import { segmentBezierPath } from '../utils/curveUtils.js';
import { TEXT_TOOL_FONT_SIZE } from '../constants/d2Constants.js';
const setDirection = (clockwise) => (path) => {
return (THREE.ShapeUtils.isClockWise(path) === clockwise) ? path : path.reverse();
@ -120,7 +121,7 @@ function shapeToPointsRaw(shapeData) {
}
case 'TEXT': {
const { text, family, style, weight } = shapeData.text;
const textShapes = createText(text, 400, family, style, weight)
const textShapes = createText(text, TEXT_TOOL_FONT_SIZE, 10, family, style, weight)
.map(([points, ...holes]) => ({ points, holes }));
shapes.push(...textShapes);

View File

@ -9,19 +9,17 @@ import memoize from 'memoizee';
const MARGIN = 200;
export const createText = memoize(createTextRaw, { max: SHAPE_CACHE_LIMIT });
export function createTextRaw(text, size, family, style, weight) {
export function createTextRaw(text, size, precision, family, style, weight) {
if (text === '') return [];
const { width, height, canvas } = createTextCanvas(text, size, family, style, weight);
const canvas = createTextCanvas(text, size * precision, family, style, weight);
// TODO merge with potrace in flood fill trace reducer
const paths = POTRACE.getPaths(POTRACE.traceCanvas(canvas, POTRACE_OPTIONS));
const halfWidth = width / 2;
const halfHeight = height / 2;
const pathsOffset = paths.map(path => path.map(({ x, y }) => ({
x: (x - halfWidth) / 10,
y: (y - halfHeight) / 10
x: (x - MARGIN) / precision,
y: (y - MARGIN) / precision
})));
const shapes = new ClipperShape(pathsOffset, true, true, false)
@ -33,7 +31,7 @@ export function createTextRaw(text, size, family, style, weight) {
return shapes;
}
const textContext = new Text();
const textContext = new Text({ baseline: 'top' });
export function createTextCanvas(text, size, family, style, weight) {
textContext.size = size;
textContext.family = family;
@ -52,7 +50,7 @@ export function createTextCanvas(text, size, family, style, weight) {
context.fillStyle = 'white';
context.fillRect(0, 0, width, height);
textContext.drawText(context, text, MARGIN, height / 2);
textContext.drawText(context, text, MARGIN, MARGIN);
return { width, height, canvas };
return canvas;
}