Merge branch 'feature/on-screen-text'

This commit is contained in:
casperlamboo 2018-01-10 12:33:55 +01:00
commit 9ce8594856
22 changed files with 369 additions and 83 deletions

BIN
img/contextmenu/btnFont.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

60
package-lock.json generated
View File

@ -1932,6 +1932,11 @@
"isarray": "1.0.0"
}
},
"buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
},
"buffer-from": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.1.tgz",
@ -4286,6 +4291,14 @@
}
}
},
"fd-slicer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
"integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
"requires": {
"pend": "1.2.0"
}
},
"figures": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
@ -5666,6 +5679,33 @@
"pinkie-promise": "2.0.1"
}
},
"google-fonts-webpack-plugin": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/google-fonts-webpack-plugin/-/google-fonts-webpack-plugin-0.4.4.tgz",
"integrity": "sha512-+e2D9/DVBG9EDydRovzoqMZ658SsTBGbC0c65GyZqkwNvdj8vRSYQKXqbz7/yt7QaXsCPT1MpH45r3ivWOitcw==",
"requires": {
"lodash": "4.17.4",
"node-fetch": "1.7.3",
"webpack-sources": "0.2.3",
"yauzl": "2.9.1"
},
"dependencies": {
"source-list-map": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-1.1.2.tgz",
"integrity": "sha1-mIkBnRAkzOVc3AaUmDN+9hhqEaE="
},
"webpack-sources": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-0.2.3.tgz",
"integrity": "sha1-F8Yr+vE8cH+dAsR54Nzd6DgGl/s=",
"requires": {
"source-list-map": "1.1.2",
"source-map": "0.5.7"
}
}
}
},
"got": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/got/-/got-3.3.1.tgz",
@ -10550,6 +10590,11 @@
"integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0=",
"dev": true
},
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
},
"pepjs": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/pepjs/-/pepjs-0.4.3.tgz",
@ -11747,12 +11792,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",
@ -14370,6 +14409,15 @@
}
}
},
"yauzl": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz",
"integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8=",
"requires": {
"buffer-crc32": "0.2.13",
"fd-slicer": "1.0.1"
}
},
"yml-loader": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/yml-loader/-/yml-loader-2.1.0.tgz",

View File

@ -27,6 +27,7 @@
"blueimp-canvas-to-blob": "^3.14.0",
"bowser": "^1.8.1",
"fit-curve": "^0.1.6",
"google-fonts-webpack-plugin": "^0.4.4",
"imports-loader": "^0.7.1",
"jss": "^9.4.0",
"keycode": "^2.1.9",
@ -82,7 +83,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,14 +388,10 @@ 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 };
export function d2textInputChange(text) {
return { type: D2_TEXT_INPUT_CHANGE, text };
}
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>
);
}

101
src/components/InputText.js Normal file
View File

@ -0,0 +1,101 @@
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';
const CONTEXT = document.createElement('canvas').getContext('2d');
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 text = this.refs.text.value;
changeText(text);
};
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);
const { family, style, weight, text } = shapeData.text;
CONTEXT.font = `${style} normal ${weight} ${TEXT_TOOL_FONT_SIZE}px ${family}`;
const width = Math.max(10, CONTEXT.measureText(shapeData.text.text).width);
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={{
fontFamily: family,
fontStyle: style,
fontWeight: weight,
width: `${width}px`
}}
value={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

@ -9,6 +9,8 @@ import * as contextTools from '../constants/contextTools.js';
import * as d2Tools from '../constants/d2Tools.js';
import { createSelector } from 'reselect';
import injectSheet from 'react-jss';
import { FONT_TOOLS } from '../constants/contextTools.js';
import { FONT_FACE } from '../constants/general.js';
// TODO move this to jss instead of css
import '../../styles/styles.css';
// import createDebug from 'debug';
@ -120,6 +122,7 @@ function renderChildren(children) {
for (const child of children) {
let component;
if (child.children.length > 0) {
component = (
<SubMenu
@ -135,6 +138,18 @@ function renderChildren(children) {
{renderChildren(child.children)}
</SubMenu>
);
} else if (FONT_TOOLS.includes(child.value)) {
component = (
<MenuItem
id={child.value}
value={child.value}
key={child.value}
svg={child.svg}
disabled={child.disabled}
>
<p style={{ fontFamily: FONT_FACE[child.value] }}>{FONT_FACE[child.value]}</p>
</MenuItem>
);
} else {
component = (
<MenuItem
@ -151,7 +166,7 @@ function renderChildren(children) {
return components;
}
function filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, menus) {
function filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, selectionIncludesText, menus) {
const showUnion = activeTool === d2Tools.TRANSFORM && numFilledObjects && numSelectedObjects >= 2;
const showIntersect = activeTool === d2Tools.TRANSFORM && numSelectedObjects >= 1;
return {
@ -191,11 +206,14 @@ function filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidO
case contextTools.HOLE_TOGGLE:
return numSelectedObjects > 0;
case contextTools.FONT:
return selectionIncludesText || activeTool === d2Tools.TEXT;
default:
return true;
}
}).map(child => {
return filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, child);
return filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, selectionIncludesText, child);
})
};
}
@ -217,11 +235,12 @@ const getMenus = createSelector([
state => state.sketcher.present.d2.tool,
state => state.sketcher.present.selection.objects.length,
state => state.sketcher.present.selection.objects.filter(({ id }) => state.sketcher.present.objectsById[id].fill).length,
state => state.sketcher.present.selection.objects.filter(({ id }) => state.sketcher.present.objectsById[id].solid).length
], (menus, activeTool, numSelectedObjects, numFilledObjects, numSolidObjects) => ({
toolbar2d: filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, nestChildren(menus, menus[TOOLBAR2D])),
toolbar3d: filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, nestChildren(menus, menus[TOOLBAR3D])),
context: filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, nestChildren(menus, menus[CONTEXT]))
state => state.sketcher.present.selection.objects.filter(({ id }) => state.sketcher.present.objectsById[id].solid).length,
state => state.sketcher.present.selection.objects.some(({ id }) => state.sketcher.present.objectsById[id].type === 'TEXT')
], (menus, activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, selectionIncludesText) => ({
toolbar2d: filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, selectionIncludesText, nestChildren(menus, menus[TOOLBAR2D])),
toolbar3d: filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, selectionIncludesText, nestChildren(menus, menus[TOOLBAR3D])),
context: filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, selectionIncludesText, nestChildren(menus, menus[CONTEXT]))
}));
export default injectSheet(styles)(connect(getMenus)(SketcherToolbars));

View File

@ -8,6 +8,7 @@ export const HOLE_TOGGLE = 'hole-toggle-tool';
export const ALIGN = 'align-tool';
export const UNION = 'union-tool';
export const INTERSECT = 'intersect-tool';
export const FONT = 'font-tool';
export const LIGHT_BLUE_A = 'color-light-blue-a';
export const LIGHT_BLUE_B = 'color-light-blue-b';
@ -79,3 +80,23 @@ export const ALIGN_TOOLS = [
ALIGN_VERTICAL,
ALIGN_BOTTOM
];
export const OSWALD = 'oswald';
export const RANGA = 'ranga';
export const JOTI_ONE = 'joti-one';
export const BELLEFAIR = 'bellefair';
export const LOBSTER = 'lobster';
export const ABRIL_FATFACE = 'abril-fatface';
export const PLAY = 'play';
export const FASCINATE = 'fascinate';
export const FONT_TOOLS = [
OSWALD,
RANGA,
JOTI_ONE,
BELLEFAIR,
LOBSTER,
ABRIL_FATFACE,
PLAY,
FASCINATE
];

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

@ -37,3 +37,14 @@ export const COLOR_STRING_TO_HEX = {
[contextTools.BLACK_B]: 0xAAAAAA,
[contextTools.BLACK_C]: 0x444444
};
export const FONT_FACE = {
[contextTools.OSWALD]: 'Oswald',
[contextTools.RANGA]: 'Ranga',
[contextTools.JOTI_ONE]: 'Joti One',
[contextTools.BELLEFAIR]: 'Bellefair',
[contextTools.LOBSTER]: 'Lobster',
[contextTools.ABRIL_FATFACE]: 'Abril Fatface',
[contextTools.PLAY]: 'Play',
[contextTools.FASCINATE]: 'Fascinate'
};

View File

@ -109,6 +109,11 @@ const context = {
selected: contextTools.ALIGN_HORIZONTAL,
children: contextTools.ALIGN_TOOLS.map(value => ({ value })),
...selectorBehavior
}, {
value: contextTools.FONT,
selected: contextTools.OSWALD,
children: contextTools.FONT_TOOLS.map(value => ({ value })),
...selectorBehavior
},
{ value: contextTools.UNION },
{ value: contextTools.INTERSECT }

View File

@ -1,6 +1,8 @@
import { Vector, Matrix } from '@doodle3d/cal';
import * as d2Tools from './d2Tools';
import * as d3Tools from './d3Tools';
import * as contextTools from './contextTools';
import { FONT_FACE } from '../constants/general.js';
const SHAPE = {
D3Visible: true,
@ -58,7 +60,7 @@ export const SHAPE_TYPE_PROPERTIES = {
...SHAPE,
defaultProperties: {
...defaultProperties,
text: { text: '', family: 'Arial', weight: 'normal', style: 'normal' },
text: { text: '', family: FONT_FACE[contextTools.OSWALD], weight: 'normal', style: 'normal' },
fill: true
}
},

View File

@ -8,7 +8,7 @@ import { shapeToPoints } from '../../shape/shapeToPoints';
import { LINE_COLLISION_MARGIN } from '../../constants/d2Constants';
import { LINE_WIDTH } from '../../constants/d2Constants';
import { PIXEL_RATIO } from '../../constants/general';
import { Matrix } from 'cal';
import { Matrix, Vector } from 'cal';
const HIT_ORDER = {
RECT: 0,
@ -132,8 +132,20 @@ export default class BaseTool extends EventGroup {
const objects = shapeDatas
.filter(shapeData => {
const shapeMatrix = shapeData.transform.multiplyMatrix(matrix);
let shapePoints = shapeToPoints(shapeData);
const isHit = shapeToPoints(shapeData)
if (shapeData.type === 'TEXT') {
if (shapeData.text.text === '') return false;
const { min, max } = getPointsBounds(shapePoints);
shapePoints = [{ points: [
new Vector(min.x, min.y),
new Vector(min.x, max.y),
new Vector(max.x, max.y),
new Vector(max.x, min.y)
], holes: [] }];
}
const isHit = shapePoints
.some(({ points, holes }) => {
const shape = applyMatrixOnShape([points, ...holes], shapeMatrix);
const clipperShape = new ClipperShape(shape, shapeData.fill, true, true);

View File

@ -1,6 +1,6 @@
import update from 'react-addons-update';
import * as contextTools from '../constants/contextTools.js';
import { COLOR_STRING_TO_HEX } from '../constants/general.js';
import { COLOR_STRING_TO_HEX, FONT_FACE } from '../constants/general.js';
import { ERASER_SIZES, BRUSH_SIZES } from '../constants/d2Constants.js';
import { SHAPE_TYPE_PROPERTIES } from '../constants/shapeTypeProperties.js';
import * as actions from '../actions/index.js';
@ -69,6 +69,33 @@ export default function (state, action) {
});
}
case contextTools.OSWALD:
case contextTools.RANGA:
case contextTools.JOTI_ONE:
case contextTools.BELLEFAIR:
case contextTools.LOBSTER:
case contextTools.ABRIL_FATFACE:
case contextTools.PLAY:
case contextTools.FASCINATE: {
const family = FONT_FACE[action.tool];
const { activeShape } = state.d2;
if (activeShape && state.objectsById[activeShape].type === 'TEXT') {
state = update(state, { objectsById: { [activeShape]: { text: { family: { $set: family } } } } });
}
return update(state, {
objectsById: state.selection.objects.reduce((updateObject, { id }) => {
if (state.objectsById[id].type === 'TEXT') {
updateObject[id] = { text: { family: { $set: FONT_FACE[action.tool] } } };
}
return updateObject;
}, {}),
context: {
font: { $set: FONT_FACE[action.tool] }
}
});
}
case contextTools.LIGHT_BLUE_A:
case contextTools.LIGHT_BLUE_B:
case contextTools.LIGHT_BLUE_C:
@ -94,6 +121,11 @@ export default function (state, action) {
case contextTools.BLACK_B:
case contextTools.BLACK_C: {
const color = COLOR_STRING_TO_HEX[action.tool];
const { activeShape } = state.d2;
if (activeShape) {
state = update(state, { objectsById: { [activeShape]: { color: { $set: color } } } });
}
return updateColor(state, color);
}

View File

@ -8,7 +8,7 @@ import starReducer from './tools/shapes/starReducer.js';
import triangleReducer from './tools/shapes/triangleReducer.js';
import bucketReducer from './tools/bucketReducer.js';
import penReducer from './tools/penReducer.js';
import textReducer from './tools/textReducer.js';
import textReducer, { removeEmptyText } from './tools/textReducer.js';
import photoGuideReducer from './tools/photoGuideReducer.js';
import { transformReducer } from './tools/transformReducer.js';
import eraserReducer from './tools/eraserReducer.js';
@ -44,6 +44,7 @@ export default function toolReducer(state, action) {
// change 2D tool after explicit tool change action or on some selection
if (action.type === actions.D2_CHANGE_TOOL) {
state = updateTool(state, action.tool);
state = removeEmptyText(state);
}
if (action.category === actions.CAT_SELECTION) {
state = updateTool(state, tools.TRANSFORM);

View File

@ -8,10 +8,10 @@ const debug = createDebug('d3d:reducer:text');
export default function textReducer(state, action) {
if (action.log !== false) debug(action.type);
const activeShape = state.d2.activeShape;
switch (action.type) {
case actions.D2_TEXT_INIT: {
state = removeEmptyText(state);
const { position, textId, screenMatrixZoom } = action;
const screenPosition = (position && screenMatrixZoom) ?
position.applyMatrix(screenMatrixZoom.inverseMatrix()) :
@ -22,37 +22,40 @@ export default function textReducer(state, action) {
} else {
return addObjectActive2D(state, {
transform: new Matrix({ x: screenPosition.x, y: screenPosition.y }),
type: 'TEXT'
type: 'TEXT',
text: {
text: '',
family: state.context.font,
weight: 'normal',
style: 'normal'
}
});
}
return state;
}
case actions.D2_TEXT_INPUT_CHANGE: {
const { text, family, style, weight, fill } = action;
const { text } = action;
const { activeShape } = state.d2;
return update(state, {
objectsById: {
[activeShape]: {
text: {
text: { $set: text },
family: { $set: family },
style: { $set: style },
weight: { $set: weight }
},
fill: { $set: fill }
text: { $set: text }
}
}
}
});
}
case actions.D2_TEXT_ADD: {
if (activeShape && state.objectsById[activeShape].text.text.length === 0) {
return setActive2D(removeObject(state, activeShape), null);
} else {
return setActive2D(state, null);
}
break;
}
default:
return state;
}
}
export function removeEmptyText(state) {
const { activeShape } = state.d2;
if (activeShape && state.objectsById[activeShape].text.text === '') {
return setActive2D(removeObject(state, activeShape), null);
}
return state;
}

View File

@ -4,7 +4,7 @@ import undoFilter from '../utils/undoFilter.js';
import * as actions from '../actions/index.js';
import * as d2Tools from '../constants/d2Tools.js';
import * as d3Tools from '../constants/d3Tools.js';
import { COLOR_STRING_TO_HEX } from '../constants/general.js';
import { COLOR_STRING_TO_HEX, FONT_FACE } from '../constants/general.js';
import * as contextTools from '../constants/contextTools.js';
import { ERASER_SIZES, BRUSH_SIZES } from '../constants/d2Constants.js';
import update from 'react-addons-update';
@ -36,7 +36,8 @@ const initialState = {
objectIdCounter: 0,
context: {
solid: true,
color: COLOR_STRING_TO_HEX[contextTools.LIGHT_BLUE_B]
color: COLOR_STRING_TO_HEX[contextTools.LIGHT_BLUE_B],
font: FONT_FACE[contextTools.OSWALD]
},
selection: {
transform: new Matrix(),
@ -142,7 +143,6 @@ function sketcherReducer(state = initialState, action) {
case actions.MULTITOUCH_TRANSFORM_END:
case actions.D2_TEXT_INIT:
case actions.D2_TEXT_INPUT_CHANGE:
case actions.D2_TEXT_ADD:
case actions.MOVE_SELECTION:
return d2ToolReducer(state, action);
@ -188,11 +188,11 @@ function sketcherReducer(state = initialState, action) {
return d3ToolReducer(state, action);
case actions.D2_CHANGE_TOOL:
state = setActive2D(state, null);
state = selectionReducer(state, action);
state = d2ToolReducer(state, action); // switch and initialize tool
state = updateMenus(state, action);
state = contextReducer(state, action);
state = setActive2D(state, null);
return state;
case actions.D3_CHANGE_TOOL:

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();
@ -64,7 +65,7 @@ function shapeToPointsRaw(shapeData) {
const points = [];
let even = false;
const numLines = rays * 2;
for (let i = 0, rad = 0; i <= numLines; i++, rad += Math.PI / rays) {
for (let i = 0, rad = 0; i <= numLines; i ++, rad += Math.PI / rays, even = !even) {
if (i === numLines) { // last line?
points.push(points[0].clone()); // go to first point
} else {
@ -72,7 +73,6 @@ function shapeToPointsRaw(shapeData) {
let x = Math.sin(rad) * radius;
let y = -Math.cos(rad) * radius;
points.push(new Vector(x, y));
even = !even;
}
}
shapes.push({ points, holes: [] });
@ -120,7 +120,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;
}

View File

@ -14,7 +14,6 @@ const INCLUDE = [
actions.DESELECT_ALL,
actions.STAMP,
actions.SELECT,
actions.D2_TEXT_ADD,
actions.CLEAR,
actions.TWIST_END,
actions.SCULPT_END,

View File

@ -249,6 +249,24 @@
flex-wrap: wrap;
}
#font-tool > .button {
background-image: url('../img/contextmenu/btnFont.png');
background-size: 40px auto;
width: 40px;
height: 40px;
}
#font-tool .menu {
flex-direction: column;
max-height: 170px;
width: 120px;
overflow-y: auto;
}
#font-tool .menu .menuitem {
text-align: center;
}
#color-light-blue-a { fill: #BCFFFF; }
#color-light-blue-b { fill: #68E1FD; }
#color-light-blue-c { fill: #01B8FF; }

View File

@ -3,6 +3,7 @@ const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HTMLWebpackPlugin = require('html-webpack-plugin');
const CordovaPlugin = require('webpack-cordova-plugin');
const GoogleFontsPlugin = require('google-fonts-webpack-plugin');
const devMode = process.env.NODE_ENV !== 'production';
const appMode = process.env.TARGET === 'app';
@ -75,9 +76,7 @@ module.exports = {
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
'TARGET': JSON.stringify(process.env.TARGET)
}
'process.env.TARGET': JSON.stringify(process.env.TARGET)
}),
new HTMLWebpackPlugin({
title: 'Doodle3D Core - Simple example',
@ -86,6 +85,18 @@ module.exports = {
scripts: appMode ? ['cordova.js'] : null,
appMountId: 'app'
}),
new GoogleFontsPlugin({
fonts: [
{ family: 'Oswald' },
{ family: 'Ranga' },
{ family: 'Joti One' },
{ family: 'Bellefair' },
{ family: 'Lobster' },
{ family: 'Abril Fatface' },
{ family: 'Play' },
{ family: 'Fascinate' }
]
}),
...(appMode ? [
new CordovaPlugin({
config: 'config.xml',
@ -95,7 +106,7 @@ module.exports = {
})
] : [])
],
devtool: "source-map",
devtool: 'source-map',
devServer: {
contentBase: 'dist'
}